Bring the share's common scripts under version control
Sixteen files that run on every shopfloor PC existed only on the SFLD share. The cost showed up while debugging the NTLARS backup: the script that posts to ShopDB could not be read, reviewed or diffed, so its behaviour was inferred from log output for most of a day. It turned out to hold a silent fallback that had been governing the whole fleet for months. Imported as-is from tsgwp00525-v2, no edits: lib/ShopdbBackupClient.psm1 the shared backup client scripts/Backup-NtlarsSettings.ps1 converted to use it scripts/Set-ShopdbCollectorKey.ps1 collector credential delivery scripts/Test-RegExport.ps1 exercises the .reg codec with mocks scripts/Set-EventSaver*.ps1 kiosk power / screensaver / disable scripts/Setup-OpenText.* OpenText install + toolbar scripts/Migrate-PCType.ps1, Select-KioskType.ps1, Set-FmsHostsEntry.ps1, scripts/ensure-vnc-firewall.ps1, Install-AcroReader.cmd, Install-Oracle11r2.cmd lib/Install-FromManifest.ps1 is also updated from the share, which was 37 lines AHEAD of this repo and purely additive: the Add-EnforceResult reporting added during the kiosk API cutover, done live and never committed back. Nothing was removed. Checked for embedded secrets before committing; there are none. Set-ShopdbCollectorKey deliberately reads its token from a sibling file on the share rather than holding it, so the script is safe to track. The share remains what actually runs. This makes it reviewable, and makes the next drift visible as a diff rather than a surprise.
This commit is contained in:
361
playbook/shopfloor-setup/common/scripts/Setup-OpenText.ps1
Executable file
361
playbook/shopfloor-setup/common/scripts/Setup-OpenText.ps1
Executable file
@@ -0,0 +1,361 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# Setup-OpenText.ps1 - OpenText HostExplorer 15 SP1 ShopFloor installer + profile
|
||||
# deployment, callable from both PXE PreInstall and Intune DSC paths.
|
||||
#
|
||||
# WHY THIS EXISTS:
|
||||
# The vendor-supplied OpenText.exe (Inno Setup wrapper built by WJDT) bundles
|
||||
# the install steps but its [Files] section deploys per-user content to
|
||||
# {userappdata} - which resolves to SYSTEM's profile under DSC and to a single
|
||||
# user under PreInstall. As a result the operator (logging in via Azure AD)
|
||||
# never sees the profiles, keymaps, menus, or macros - only the installed
|
||||
# binaries. This script replaces OpenText.exe entirely, doing the same install
|
||||
# steps via direct msiexec calls AND fanning the per-user content out to:
|
||||
# - %ProgramData%\Hummingbird\Connectivity\15.00\Shared\
|
||||
# - C:\Users\Default\AppData\Roaming\Hummingbird\Connectivity\15.00\
|
||||
# - Each existing user profile under C:\Users\
|
||||
#
|
||||
# INVOKED BY:
|
||||
# - PreInstall: Setup-OpenText.cmd wrapper (because the runner only knows MSI/EXE)
|
||||
# - DSC: Install-OpenText.ps1 downloads the bundled tree from blob, then
|
||||
# invokes this script with -SourceDir <temp dir>
|
||||
#
|
||||
# DETECTION:
|
||||
# Skips if HKLM:\SOFTWARE\GE\OpenText\Installed = $expectedVersion. Marker is
|
||||
# written at the end of a successful run, so the runner / DSC wrapper can
|
||||
# no-op on subsequent invocations.
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
# Override when invoked from a temp dir (DSC path) where the bundled files were
|
||||
# just downloaded. Defaults to $PSScriptRoot, but resolved INSIDE the script body
|
||||
# below - PowerShell evaluates `param([string]$X = $PSScriptRoot)` at parameter-
|
||||
# binding time, when $PSScriptRoot may not yet be populated, so the default winds
|
||||
# up as an empty string. Setting it in the body works because $PSScriptRoot is
|
||||
# reliably populated by then.
|
||||
[string]$SourceDir
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if (-not $SourceDir) {
|
||||
$SourceDir = $PSScriptRoot
|
||||
}
|
||||
|
||||
# Normalize $SourceDir to a canonical absolute path. The CMD shim passes
|
||||
# "%~dp0..\apps\opentext" which embeds a literal "..". msiexec / the Windows
|
||||
# Installer service fail to open the package with that unresolved segment
|
||||
# (exit 1619, ERROR_INSTALL_PACKAGE_OPEN_FAILED), even though every other
|
||||
# .NET / PowerShell API resolves it fine. Resolve-Path collapses ".." into
|
||||
# a clean drive-rooted path before any msiexec invocation.
|
||||
if (Test-Path -LiteralPath $SourceDir) {
|
||||
try { $SourceDir = (Resolve-Path -LiteralPath $SourceDir).ProviderPath } catch {}
|
||||
}
|
||||
|
||||
# --- Inline site-config reader (this script runs from C:\PreInstall\installers\opentext\,
|
||||
# NOT from C:\Enrollment\shopfloor-setup\, so it can't dot-source Get-PCProfile.ps1) ---
|
||||
function Get-SiteConfig {
|
||||
$configPath = 'C:\Enrollment\site-config.json'
|
||||
if (-not (Test-Path $configPath)) { return $null }
|
||||
try {
|
||||
return Get-Content $configPath -Raw | ConvertFrom-Json
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
# --- Logging (set up FIRST so any startup error - missing version.txt, broken
|
||||
# bundled file, etc. - lands in the log file instead of disappearing into the
|
||||
# runner's stdout void) ---
|
||||
$logDir = 'C:\Logs\PreInstall'
|
||||
$logFile = Join-Path $logDir 'Setup-OpenText.log'
|
||||
$msiLog = Join-Path $logDir 'Setup-OpenText-msi.log'
|
||||
$mspLog = Join-Path $logDir 'Setup-OpenText-msp.log'
|
||||
|
||||
if (-not (Test-Path $logDir)) {
|
||||
New-Item -Path $logDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
function Write-SetupLog {
|
||||
param([string]$Message)
|
||||
$line = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] $Message"
|
||||
Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue
|
||||
Write-Host $line
|
||||
}
|
||||
|
||||
Write-SetupLog "================================================================"
|
||||
Write-SetupLog "=== Setup-OpenText.ps1 starting ==="
|
||||
Write-SetupLog "================================================================"
|
||||
Write-SetupLog "SourceDir: $SourceDir"
|
||||
Write-SetupLog "PSScriptRoot: $PSScriptRoot"
|
||||
Write-SetupLog "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
|
||||
|
||||
# --- Config ---
|
||||
# Version is read from version.txt next to this script. ONE source of truth: bumping
|
||||
# version.txt is the only edit needed when shipping a new OpenText build. Setup-
|
||||
# OpenText.ps1 itself, Install-OpenText.ps1 (DSC wrapper), and the registry marker
|
||||
# all derive their notion of "expected version" from this file.
|
||||
$versionFile = Join-Path $SourceDir 'version.txt'
|
||||
Write-SetupLog "Looking for version.txt at: $versionFile"
|
||||
if (-not (Test-Path $versionFile)) {
|
||||
Write-SetupLog "ERROR: version.txt not found in $SourceDir - cannot determine expected version."
|
||||
Write-SetupLog "Directory listing of $SourceDir :"
|
||||
if (Test-Path $SourceDir) {
|
||||
Get-ChildItem $SourceDir -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Write-SetupLog " $($_.Name) $($_.Length) bytes"
|
||||
}
|
||||
} else {
|
||||
Write-SetupLog " (SourceDir does not exist)"
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
$expectedVersion = (Get-Content -Path $versionFile -Raw -ErrorAction Stop).Trim()
|
||||
if (-not $expectedVersion) {
|
||||
Write-SetupLog "ERROR: version.txt at $versionFile is empty."
|
||||
exit 1
|
||||
}
|
||||
Write-SetupLog "Expected version (from version.txt): $expectedVersion"
|
||||
|
||||
# --- Detection: skip if already deployed at expected version ---
|
||||
$markerKey = 'HKLM:\SOFTWARE\GE\OpenText'
|
||||
$markerVal = 'Installed'
|
||||
if (Test-Path $markerKey) {
|
||||
$installed = (Get-ItemProperty -Path $markerKey -Name $markerVal -ErrorAction SilentlyContinue).$markerVal
|
||||
if ($installed -eq $expectedVersion) {
|
||||
Write-SetupLog "OpenText $expectedVersion already deployed (marker present) - skipping."
|
||||
exit 0
|
||||
}
|
||||
Write-SetupLog "OpenText marker mismatch (found '$installed', expected '$expectedVersion') - re-deploying."
|
||||
}
|
||||
else {
|
||||
Write-SetupLog "OpenText marker not present - first install."
|
||||
}
|
||||
|
||||
# --- Verify bundled files are where we expect ---
|
||||
$msiPath = Join-Path $SourceDir 'OpenTextHostExplorer15x64.msi'
|
||||
$cabPath = Join-Path $SourceDir 'OpenTextHostExplorer15x64.cab'
|
||||
$mspPath = Join-Path $SourceDir 'OpenTextHostExplorer15x64_ServicePack1.msp'
|
||||
$mstPath = Join-Path $SourceDir 'ShopFloorx64.mst'
|
||||
|
||||
foreach ($f in @($msiPath, $cabPath, $mspPath, $mstPath)) {
|
||||
if (-not (Test-Path $f)) {
|
||||
Write-SetupLog "ERROR: required file not found: $f"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# --- Step 1: Install the base MSI with the ShopFloor transform ---
|
||||
# NOTE: We deliberately do NOT pass REBOOT=ReallySuppress here even though we do
|
||||
# for the VC++ MSIs. OpenText HostExplorer installs shell extensions that hook
|
||||
# explorer.exe, and the MSI uses Restart Manager to ask explorer to close so the
|
||||
# in-use shell DLLs can be replaced. With REBOOT=ReallySuppress, RM closes
|
||||
# explorer.exe but interprets "restart explorer" as a reboot action and refuses
|
||||
# to relaunch it - leaving the user without a desktop. /norestart on its own
|
||||
# prevents the actual Windows reboot but lets RM cleanly close-and-relaunch
|
||||
# explorer mid-install. msiexec still returns 3010 ("reboot would be needed"),
|
||||
# which we treat as success below.
|
||||
Write-SetupLog ""
|
||||
Write-SetupLog "Step 1: Installing OpenTextHostExplorer15x64.msi with ShopFloorx64.mst..."
|
||||
if (Test-Path $msiLog) { Remove-Item $msiLog -Force -ErrorAction SilentlyContinue }
|
||||
|
||||
$msiArgs = "/i `"$msiPath`" TRANSFORMS=`"$mstPath`" /qn /norestart /L*v `"$msiLog`""
|
||||
Write-SetupLog " msiexec.exe $msiArgs"
|
||||
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = 'msiexec.exe'
|
||||
$psi.Arguments = $msiArgs
|
||||
$psi.UseShellExecute = $false
|
||||
$psi.CreateNoWindow = $true
|
||||
$proc = [System.Diagnostics.Process]::Start($psi)
|
||||
$proc.WaitForExit()
|
||||
$msiExit = $proc.ExitCode
|
||||
Write-SetupLog " msiexec exit code: $msiExit"
|
||||
|
||||
if ($msiExit -ne 0 -and $msiExit -ne 3010) {
|
||||
Write-SetupLog "ERROR: base MSI install failed (exit $msiExit). See $msiLog"
|
||||
exit 1
|
||||
}
|
||||
if ($msiExit -eq 3010) {
|
||||
Write-SetupLog " (3010 = reboot needed but suppressed)"
|
||||
}
|
||||
|
||||
# --- Step 2: Apply Service Pack 1 patch ---
|
||||
# Same Restart Manager rationale as Step 1 - skip REBOOT=ReallySuppress so RM
|
||||
# can relaunch explorer.exe after replacing the patched shell extension DLLs.
|
||||
Write-SetupLog ""
|
||||
Write-SetupLog "Step 2: Applying SP1 patch..."
|
||||
if (Test-Path $mspLog) { Remove-Item $mspLog -Force -ErrorAction SilentlyContinue }
|
||||
|
||||
$mspArgs = "/p `"$mspPath`" /qn /norestart /L*v `"$mspLog`""
|
||||
Write-SetupLog " msiexec.exe $mspArgs"
|
||||
|
||||
$psi.Arguments = $mspArgs
|
||||
$proc = [System.Diagnostics.Process]::Start($psi)
|
||||
$proc.WaitForExit()
|
||||
$mspExit = $proc.ExitCode
|
||||
Write-SetupLog " msiexec exit code: $mspExit"
|
||||
|
||||
if ($mspExit -ne 0 -and $mspExit -ne 3010) {
|
||||
Write-SetupLog "ERROR: SP1 patch failed (exit $mspExit). See $mspLog"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Step 3: Deploy profiles, keymaps, menus, accessories ---
|
||||
# Source layout (bundled with this script):
|
||||
# $SourceDir\Profile\*.hep
|
||||
# $SourceDir\Accessories\EB\*.eb*
|
||||
# $SourceDir\HostExplorer\Keymap\*.kmv
|
||||
# $SourceDir\HostExplorer\Menu\*.hmv
|
||||
#
|
||||
# Target layouts (Hummingbird-canonical):
|
||||
# %ProgramData%\Hummingbird\Connectivity\15.00\Shared\Profile\
|
||||
# %ProgramData%\Hummingbird\Connectivity\15.00\Shared\Accessories\EB\
|
||||
# %ProgramData%\Hummingbird\Connectivity\15.00\Shared\HostExplorer\Keymap\
|
||||
# %ProgramData%\Hummingbird\Connectivity\15.00\Shared\HostExplorer\Menu\
|
||||
# <user>\AppData\Roaming\Hummingbird\Connectivity\15.00\Profile\
|
||||
# <user>\AppData\Roaming\Hummingbird\Connectivity\15.00\Accessories\EB\
|
||||
# <user>\AppData\Roaming\Hummingbird\Connectivity\15.00\HostExplorer\Keymap\
|
||||
# <user>\AppData\Roaming\Hummingbird\Connectivity\15.00\HostExplorer\Menu\
|
||||
#
|
||||
# We deploy to ProgramData\Shared (system-wide fallback), Default User (template
|
||||
# inherited by every NEW user profile), and every existing user profile (so
|
||||
# already-created accounts like SupportUser get them immediately).
|
||||
|
||||
Write-SetupLog ""
|
||||
Write-SetupLog "Step 3: Deploying profiles/keymaps/menus/macros..."
|
||||
|
||||
# --- Resolve exclude lists from site-config.json (falls back to West Jefferson defaults) ---
|
||||
$siteConfig = Get-SiteConfig
|
||||
$profileExcludes = if ($siteConfig -and $siteConfig.opentext -and $siteConfig.opentext.excludeProfiles) {
|
||||
@($siteConfig.opentext.excludeProfiles)
|
||||
} else {
|
||||
@('WJ_Office.hep', 'IBM_qks.hep', 'mmcs.hep') # West Jefferson defaults
|
||||
}
|
||||
$shortcutExcludes = if ($siteConfig -and $siteConfig.opentext -and $siteConfig.opentext.excludeShortcuts) {
|
||||
@($siteConfig.opentext.excludeShortcuts)
|
||||
} else {
|
||||
@('WJ_Office.lnk', 'IBM_qks.lnk', 'mmcs.lnk')
|
||||
}
|
||||
if ($siteConfig) {
|
||||
Write-SetupLog "Site config loaded - profile excludes: $($profileExcludes -join ', ')"
|
||||
Write-SetupLog "Site config loaded - shortcut excludes: $($shortcutExcludes -join ', ')"
|
||||
} else {
|
||||
Write-SetupLog "No site-config.json found - using West Jefferson defaults for excludes"
|
||||
}
|
||||
|
||||
# Map of source subdir -> destination subdir relative to the Hummingbird root.
|
||||
# Optional Exclude list drops specific filenames from both the source-to-dest
|
||||
# copy AND from the destination if they were left over from a prior install.
|
||||
$contentMap = @(
|
||||
@{
|
||||
Src = 'Profile'
|
||||
Dst = 'Profile'
|
||||
Exclude = $profileExcludes
|
||||
}
|
||||
@{ Src = 'Accessories\EB'; Dst = 'Accessories\EB' }
|
||||
@{ Src = 'HostExplorer\Keymap'; Dst = 'HostExplorer\Keymap' }
|
||||
@{ Src = 'HostExplorer\Menu'; Dst = 'HostExplorer\Menu' }
|
||||
)
|
||||
|
||||
function Copy-HummingbirdContent {
|
||||
param(
|
||||
[string]$RootDst, # Hummingbird root, e.g. C:\ProgramData\Hummingbird\Connectivity\15.00\Shared
|
||||
[string]$Label
|
||||
)
|
||||
foreach ($entry in $contentMap) {
|
||||
$srcPath = Join-Path $SourceDir $entry.Src
|
||||
if (-not (Test-Path $srcPath)) { continue }
|
||||
$dstPath = Join-Path $RootDst $entry.Dst
|
||||
New-Item -Path $dstPath -ItemType Directory -Force | Out-Null
|
||||
|
||||
# Remove any previously-deployed excluded files from the destination
|
||||
# - handles the case where a PC got them from an older install.
|
||||
if ($entry.Exclude) {
|
||||
foreach ($name in $entry.Exclude) {
|
||||
$stale = Join-Path $dstPath $name
|
||||
if (Test-Path -LiteralPath $stale) {
|
||||
try {
|
||||
Remove-Item -LiteralPath $stale -Force -ErrorAction Stop
|
||||
Write-SetupLog " $Label : removed stale $name"
|
||||
} catch {
|
||||
Write-SetupLog " $Label : failed to remove $stale : $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$files = Get-ChildItem -Path $srcPath -File -ErrorAction SilentlyContinue
|
||||
if ($entry.Exclude) {
|
||||
$files = @($files | Where-Object { $entry.Exclude -notcontains $_.Name })
|
||||
}
|
||||
foreach ($f in $files) {
|
||||
Copy-Item -Path $f.FullName -Destination $dstPath -Force
|
||||
}
|
||||
Write-SetupLog " $Label : $($entry.Src) -> $dstPath ($($files.Count) files)"
|
||||
}
|
||||
}
|
||||
|
||||
# 3a. ProgramData Shared
|
||||
$sharedRoot = Join-Path $env:ProgramData 'Hummingbird\Connectivity\15.00\Shared'
|
||||
Copy-HummingbirdContent -RootDst $sharedRoot -Label 'Shared'
|
||||
|
||||
# 3b. Default User (template for new user profiles)
|
||||
$defaultUserRoot = 'C:\Users\Default\AppData\Roaming\Hummingbird\Connectivity\15.00'
|
||||
Copy-HummingbirdContent -RootDst $defaultUserRoot -Label 'Default User'
|
||||
|
||||
# 3c. Every existing user profile under C:\Users\
|
||||
$skipNames = @('Default', 'Default User', 'Public', 'defaultuser0', 'All Users', 'WDAGUtilityAccount')
|
||||
$userDirs = Get-ChildItem 'C:\Users' -Directory -ErrorAction SilentlyContinue |
|
||||
Where-Object { $skipNames -notcontains $_.Name -and (Test-Path "$($_.FullName)\AppData\Roaming") }
|
||||
|
||||
foreach ($u in $userDirs) {
|
||||
$userRoot = Join-Path $u.FullName 'AppData\Roaming\Hummingbird\Connectivity\15.00'
|
||||
Copy-HummingbirdContent -RootDst $userRoot -Label $u.Name
|
||||
}
|
||||
|
||||
# --- Step 4: Public Desktop shortcuts ---
|
||||
# Uses $shortcutExcludes (resolved from site-config.json above) to skip
|
||||
# deploying unwanted .lnk files AND remove any a prior install left behind.
|
||||
Write-SetupLog ""
|
||||
Write-SetupLog "Step 4: Deploying public desktop shortcuts..."
|
||||
$shortcutSrc = Join-Path $SourceDir 'W10shortcuts'
|
||||
$publicDesktop = 'C:\Users\Public\Desktop'
|
||||
|
||||
# Clean up stale copies from prior installs first
|
||||
foreach ($name in $shortcutExcludes) {
|
||||
$stale = Join-Path $publicDesktop $name
|
||||
if (Test-Path -LiteralPath $stale) {
|
||||
try {
|
||||
Remove-Item -LiteralPath $stale -Force -ErrorAction Stop
|
||||
Write-SetupLog " removed stale desktop shortcut: $name"
|
||||
} catch {
|
||||
Write-SetupLog " failed to remove stale $stale : $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path $shortcutSrc) {
|
||||
$lnkFiles = Get-ChildItem -Path $shortcutSrc -Filter '*.lnk' -File -ErrorAction SilentlyContinue
|
||||
foreach ($l in $lnkFiles) {
|
||||
if ($shortcutExcludes -contains $l.Name) {
|
||||
Write-SetupLog " skip (excluded): $($l.Name)"
|
||||
continue
|
||||
}
|
||||
Copy-Item -Path $l.FullName -Destination $publicDesktop -Force
|
||||
Write-SetupLog " $($l.Name) -> $publicDesktop"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Step 5: Write registry marker ---
|
||||
Write-SetupLog ""
|
||||
Write-SetupLog "Step 5: Writing registry marker..."
|
||||
if (-not (Test-Path $markerKey)) {
|
||||
New-Item -Path $markerKey -Force | Out-Null
|
||||
}
|
||||
Set-ItemProperty -Path $markerKey -Name $markerVal -Value $expectedVersion -Force
|
||||
Set-ItemProperty -Path $markerKey -Name 'InstalledAt' -Value (Get-Date -Format 'o') -Force
|
||||
|
||||
Write-SetupLog ""
|
||||
Write-SetupLog "================================================================"
|
||||
Write-SetupLog "=== OpenText HostExplorer ShopFloor $expectedVersion deployed ==="
|
||||
Write-SetupLog "================================================================"
|
||||
exit 0
|
||||
Reference in New Issue
Block a user