New tools:
Configure-PC.bat/.ps1 - Interactive desktop tool for SupportUser to
configure a shopfloor PC after imaging. Two sections:
1. Machine number: if UDC/eDNC are still at placeholder 9999, prompt
to set the real number right now (updates UDC JSON + eDNC registry,
restarts UDC.exe with new args).
2. Auto-startup toggle: pick which apps start at user logon from a
numbered list (UDC, eDNC, Defect Tracker, WJ Shopfloor, Plant Apps).
Creates/removes .lnk files in AllUsers Startup folder. Toggle UI
shows [ON]/[ ] state, safe to re-run anytime. Plant Apps URL
resolved from .url file at runtime with hardcoded fallback to
https://mes-wjefferson.apps.lr.geaerospace.net/run/...
3. Item 6 in the toggle list: register/unregister a "Check Machine
Number" logon task for standard (non-admin) users. When enabled,
the task fires at every logon, checks for 9999, pops an InputBox
if found, updates both apps, then unregisters itself on success.
Check-MachineNumber.ps1 - The logon task script. Runs as the logged-in
user (needs GUI for InputBox), not SYSTEM. Writing to ProgramData + HKLM
is possible because 02-MachineNumberACLs.ps1 pre-grants BUILTIN\Users
write access on the two specific targets during imaging.
02-MachineNumberACLs.ps1 - Standard type-specific script (runs after
01-eDNC.ps1). Opens C:\ProgramData\UDC\udc_settings.json for Users:Modify
and HKLM:\...\GE Aircraft Engines\DNC\General for Users:SetValue. Narrow
scope, not blanket admin.
Execution order fixes in Run-ShopfloorSetup.ps1:
The dispatcher now has two lists: $skipInBaseline (scripts NOT run in the
alphabetical baseline loop) and $runAfterTypeSpecific (scripts run
explicitly after type-specific scripts complete). This fixes the bug where
06/07 ran before 01-eDNC.ps1 installed DnC, so eDNC/NTLARS shortcuts were
silently skipped.
New execution order:
Baseline: 00-PreInstall, 04-NetworkAndWinRM (skipping 05-08 + tools)
Type-specific: 01-eDNC, 02-MachineNumberACLs
Finalization: 06-OrganizeDesktop, 07-TaskbarLayout
06 internally calls 05 (Office shortcuts, Phase 0) and 08 (Edge config,
Phase 4) as sub-phases, so they also benefit from running late. Office
isn't installed until after the first reboot (ppkg streams C2R), so 05
no-ops at imaging time but succeeds when 06's SYSTEM logon task re-runs
it on the second boot. 08 resolves startup-tab URLs from .url files
delivered by DSC (even later); same self-heal via the logon task.
Other fixes in this commit:
- OpenText Setup-OpenText.ps1 Step 4: exclude WJ_Office.lnk, IBM_qks.lnk,
mmcs.lnk desktop shortcuts (matching the Step 3 .hep profile exclusion
from the previous commit). Removes stale copies from prior installs.
- 05-OfficeShortcuts.ps1: widened Office detection to 6 path variants
covering C2R + MSI + Office15/16, with diagnostic output on miss.
- 06-OrganizeDesktop.ps1: removed Phase 3 (desktop-root pin copies for
eDNC/NTLARS) so shortcuts live in Shopfloor Tools only, not duplicated
at root. Emptied $keepAtRoot. Added Phase 0 (call 05) and Phase 4
(call 08). Lazy folder creation + empty-folder cleanup. Scheduled task
now runs as SYSTEM (was BUILTIN\Users with Limited which failed the
admin check). Added NTLARS to 07's taskbar pin list.
- 08-EdgeDefaultBrowser.ps1: Plant Apps URL fallback hardcoded from
device-config.yaml.
- All new scripts have Start-Transcript logging to C:\Logs\SFLD\ with
timestamps and running-as identity.
- Run-ShopfloorSetup.ps1: Start-Transcript + Stop-Transcript wrapping
entire dispatcher run, writes to C:\Logs\SFLD\shopfloor-setup.log.
Configure-PC.bat added to SupportUser desktop copy list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
336 lines
14 KiB
PowerShell
336 lines
14 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.
|
|
# 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'
|
|
# West Jefferson site-specific: these three .hep sessions aren't used
|
|
# on shopfloor PCs and just clutter the HostExplorer session picker.
|
|
# Leaving the .hep files in the bundled source for rollback, just
|
|
# skipping the deploy step.
|
|
Exclude = @(
|
|
'WJ_Office.hep',
|
|
'IBM_qks.hep',
|
|
'mmcs.hep'
|
|
)
|
|
}
|
|
@{ 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 ---
|
|
# Same exclusion list as the Profile step: these three sessions aren't
|
|
# used on shopfloor PCs, so we skip deploying their .lnk files AND
|
|
# remove any that 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'
|
|
|
|
$excludeShortcuts = @(
|
|
'WJ_Office.lnk',
|
|
'IBM_qks.lnk',
|
|
'mmcs.lnk'
|
|
)
|
|
|
|
# Clean up stale copies from prior installs first
|
|
foreach ($name in $excludeShortcuts) {
|
|
$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 ($excludeShortcuts -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
|