Files
pxe-server/playbook/preinstall/opentext/Setup-OpenText.ps1
cproudlock 7c8eb6899d Shared machine-number helper, site-config for OpenText + PreInstall, placeholder type dirs
Three optimization batches from the pipeline audit:

1. Shared Update-MachineNumber.ps1 helper (lib/)
   Extracts duplicated machine-number update logic from Configure-PC.ps1,
   Check-MachineNumber.ps1, and Set-MachineNumber.ps1 into a shared
   dot-sourceable helper at Shopfloor/lib/Update-MachineNumber.ps1.

   Exports:
     Get-CurrentMachineNumber → @{ Udc = $string; Ednc = $string }
     Update-MachineNumber -NewNumber <n> [-Site <s>] → @{ UdcUpdated; EdncUpdated; Errors }

   All three consumers now dot-source the helper instead of duplicating
   ~50 lines each. Set-MachineNumber.ps1 also migrated from inline
   Get-SiteConfig to dot-sourcing Get-PCProfile.ps1 for consistency.

2. Site-config integration for remaining scripts
   Setup-OpenText.ps1: exclude lists (profiles + shortcuts) now read from
     site-config.json opentext section, falling back to West Jefferson
     defaults. Inline Get-SiteConfig since the script runs from
     C:\PreInstall\installers\opentext\ (can't dot-source Get-PCProfile).

   00-PreInstall-MachineApps.ps1: after parsing preinstall.json, scans
     InstallArgs for "West Jefferson" and replaces with site-config
     siteName if different. Inline Get-SiteConfig for same reason.

3. Placeholder type-specific directories
   Created skeleton 01-Setup-*.ps1 scripts for all PC types so the
   directory structure is in place and Run-ShopfloorSetup's type-specific
   loop has something to iterate over:
     Genspect/01-Setup-Genspect.ps1
     Keyence/01-Setup-Keyence.ps1
     WaxAndTrace/01-Setup-WaxAndTrace.ps1
     Lab/01-Setup-Lab.ps1
   Each logs a "no type-specific apps configured yet" banner and exits.
   Fill in app installs when details are finalized; for share-based
   installs, copy the CMM/01-Setup-CMM.ps1 pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:44:10 -04:00

352 lines
15 KiB
PowerShell

#!/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
}
# --- 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