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>
352 lines
15 KiB
PowerShell
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
|