Two related fixes from a debugging round on the test PC:
1. PreInstall runner: detection-during-install kill is now opt-in via
"KillAfterDetection: true" on JSON entries that need it. Old behavior
killed any installer as soon as its detection passed - which broke
Oracle: Oracle creates its registry key partway through install,
the runner detected it at the 25s poll, killed msiexec mid-install,
and msiserver was still doing rollback when the next install (VC++
2008) started - so VC++ 2008 hit ERROR_INSTALL_ALREADY_RUNNING
(1618). Only UDC needs the detection-kill (its installer spawns a
hidden WPF window and never exits). Other installers exit cleanly
on their own and shouldn't be killed.
2. Track Setup-OpenText scripts in git. The bundled OpenText install
scripts (Setup-OpenText.ps1, Setup-OpenText.cmd, version.txt) live
at runtime in /home/camp/pxe-images/main/dependencies/opentext/
alongside the binary install files (~106 MB of MSI/CAB/MSP/MST plus
profile content). The binaries stay outside git but the script
logic and version stamp are mirrored into playbook/preinstall/
opentext/ here so git history captures changes to the install
logic and version bumps. README.md explains the workflow.
Latest Setup-OpenText.ps1 includes:
- $SourceDir default moved into script body (PowerShell evaluates
param([string]$X = $PSScriptRoot) defaults at parameter-binding
time, when $PSScriptRoot may not yet be populated, so the
default came out as empty string and Join-Path crashed)
- Logging set up FIRST so any startup error gets captured
- REBOOT=ReallySuppress dropped from both msiexec calls (base MSI
and SP1 patch) - OpenText installs shell extensions that hook
explorer.exe, and Restart Manager closes explorer to replace
the shell DLLs. With REBOOT=ReallySuppress, RM closed explorer
but interpreted the relaunch as a "reboot action" and refused
to do it, leaving the user with no desktop. /norestart on its
own prevents the actual Windows reboot but lets RM cleanly
close-and-relaunch explorer mid-install.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
275 lines
12 KiB
PowerShell
275 lines
12 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
|
|
}
|
|
|
|
# --- 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..."
|
|
|
|
# Map of source subdir -> destination subdir relative to the Hummingbird root
|
|
$contentMap = @(
|
|
@{ Src = 'Profile'; Dst = 'Profile' }
|
|
@{ 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
|
|
$files = Get-ChildItem -Path $srcPath -File -ErrorAction SilentlyContinue
|
|
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 ---
|
|
Write-SetupLog ""
|
|
Write-SetupLog "Step 4: Deploying public desktop shortcuts..."
|
|
$shortcutSrc = Join-Path $SourceDir 'W10shortcuts'
|
|
$publicDesktop = 'C:\Users\Public\Desktop'
|
|
if (Test-Path $shortcutSrc) {
|
|
$lnkFiles = Get-ChildItem -Path $shortcutSrc -Filter '*.lnk' -File -ErrorAction SilentlyContinue
|
|
foreach ($l in $lnkFiles) {
|
|
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
|