Files
pxe-server/playbook/shopfloor-setup/gea-shopfloor-waxtrace/09-Setup-WaxAndTrace.ps1
cproudlock de7d41f5e5 Wax/Trace: defer HKEY_USERS per-user prefs restore to first ShopFloor logon via SYSTEM scheduled task
Bay's ShopFloor user account exists but has never logged in at imaging
time, so its NTUSER.DAT doesn't exist yet and we can't reg-load its
hive to remap source SID -> ShopFloor SID. The in-line restore at
09-Setup Step 3b handles HKLM (controller config, device-map) + files,
but per-user prefs (LouteditS Layout, Page margins, Recent Files, ~2700
rows in a typical WJF capture) get skipped.

Fix: register a SYSTEM-context scheduled task at imaging time that
fires AtLogOn UserId=ShopFloor. When ShopFloor first logs in, Windows
loads their NTUSER.DAT automatically; task fires (running as SYSTEM
so lockdown policies on ShopFloor's user-context don't block HKLM
writes via the same Install script); SID-remap path finds the live
hive and writes prefs into HKEY_USERS\<ShopFloor-sid>. Task writes a
flag file + unregisters itself after one successful run.

Pieces:
- Install-FormtracepakSettings.ps1: new -HKEYUsersOnly switch that
  skips the HKLM .reg files + HKLM CSV rows (already restored at
  imaging time). Fallback user chain ShopFloor->SupportUser->$USERNAME.
- Schedule-WaxTracePerUserRestore.ps1: registers the task, writes
  C:\WaxTrace-Install\Run-WaxTracePerUserRestore.ps1 task action which
  invokes Install with -HKEYUsersOnly and self-cleans on success.
- 09-Setup-WaxAndTrace.ps1 Step 3b: in-line restore now uses
  -RestoreRegistry -RestoreData -RestoreConfig (HKLM + files now);
  calls Schedule-WaxTracePerUserRestore.ps1 to queue HKEY_USERS for
  first ShopFloor logon.
- sync-waxtrace.sh: pushes Schedule-WaxTracePerUserRestore.ps1 to
  PXE share alongside Install-FormtracepakSettings.ps1.

Smoke tested on win11 VM partially: task registration works, manual
trigger fires + self-unregisters cleanly, flag file lands. Real per-
user SID-remap happens at first ShopFloor logon (can't simulate from
qga without an interactive ShopFloor session).
2026-05-24 16:19:45 -04:00

418 lines
22 KiB
PowerShell

# 09-Setup-WaxAndTrace.ps1 - Wax and Trace pc-type setup (Mitutoyo FormTracePak v6.213)
#
# Imaging-time install. Installs prereqs from manifest, then runs the
# Mitutoyo FormTracePak v6.213 vendor installer from a mounted ISO. The
# VB6 Setup.exe wrapper checks GetDriveType() == DRIVE_CDROM; Mount-DiskImage
# satisfies that (a real CD drive isn't required). Per-asset calibration ISO
# applies last.
#
# Layout at C:\WaxTrace-Install\ (xcopied by startnet.cmd):
# waxtrace-manifest.json
# 09-Setup-WaxAndTrace.ps1 (this file)
# prereqs\vcredist_x86.exe (VC++ 2008 x86)
# prereqs\vcredist_x64.exe (VC++ 2008 x64)
# prereqs\vc_redist.x86.exe (VC++ 2017 x86)
# prereqs\vc_redist.x64.exe (VC++ 2017 x64)
# prereqs\HASPUserSetup.exe (Sentinel Runtime / HASP dongle driver)
# formtracepak\FORMTRACEPAK-V6.213.iso (vendor installer ISO, ~2 GB)
# calibrations\CAL-<asset>_serial-<sn>_probe-<probe>.iso (per-machine)
#
# Per-machine calibration is applied separately (mount cal ISO + run its
# Setup.exe), keyed by C:\Enrollment\machine-number.txt.
#
# Legacy captured/ replay path (V6.0 zip + reg) retired 2026-05-21. The
# captured-binary/ payload is still in the repo as a fallback - if the
# v6.213 vendor install fails on a bay, fall back to the captured path
# manually. See git history for the prior shape.
#
# Log: C:\Logs\WaxTrace\09-Setup-WaxAndTrace.log
# C:\Logs\WaxTrace\install.log (written by Install-FromManifest)
$ErrorActionPreference = 'Continue'
# Mirror the CMM pattern (09-Setup-CMM.ps1): the script itself lives in the
# shopfloor-setup tree (xcopied during WinPE), but the bulky bootstrap bundle
# (prereqs + captured master + cal ISOs) lives at C:\WaxTrace-Install\, put
# there by startnet.cmd from Y:\installers-post\waxtrace\ at imaging time.
$stagingRoot = 'C:\WaxTrace-Install'
$manifestPath = Join-Path $stagingRoot 'waxtrace-manifest.json'
$libSource = Join-Path $PSScriptRoot '..\common\lib\Install-FromManifest.ps1'
$logDir = 'C:\Logs\WaxTrace'
$installLog = Join-Path $logDir 'install.log'
$transcriptLog = Join-Path $logDir '09-Setup-WaxAndTrace.log'
if (-not (Test-Path $logDir)) {
New-Item -Path $logDir -ItemType Directory -Force | Out-Null
}
try { Start-Transcript -Path $transcriptLog -Append -Force | Out-Null } catch {}
function Write-WTLog {
param([string]$Message, [string]$Level = 'INFO')
$stamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Write-Host "[$stamp] [$Level] $Message"
}
Write-WTLog "================================================================"
Write-WTLog "=== WaxTrace Setup (imaging-time) session start (PID $PID) ==="
Write-WTLog "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
Write-WTLog "Script root: $PSScriptRoot"
Write-WTLog "================================================================"
# Status push - best-effort.
$pxeStatusLib = Join-Path $PSScriptRoot '..\Shopfloor\lib\Send-PxeStatus.ps1'
if (Test-Path $pxeStatusLib) {
try { . $pxeStatusLib; Send-PxeStatus -Stage '09-Setup-WaxAndTrace: starting' -StageIndex 3 -StageTotal 8 } catch { }
}
foreach ($file in @('pc-type.txt','machine-number.txt')) {
$path = "C:\Enrollment\$file"
if (Test-Path -LiteralPath $path) {
$content = (Get-Content -LiteralPath $path -First 1 -ErrorAction SilentlyContinue).Trim()
Write-WTLog " $file = $content"
} else {
Write-WTLog " $file = (not present)"
}
}
# ============================================================================
# Step 1: Prereqs via manifest (VC++ 2008 + 2017, HASP/Sentinel)
# ============================================================================
if (-not (Test-Path $manifestPath)) {
Write-WTLog "waxtrace-manifest.json not found at $manifestPath" 'ERROR'
} elseif (-not (Test-Path $libSource)) {
Write-WTLog "Install-FromManifest.ps1 not found at $libSource" 'ERROR'
} else {
Write-WTLog "Running Install-FromManifest (InstallerRoot=$PSScriptRoot)"
& $libSource -ManifestPath $manifestPath -InstallerRoot $stagingRoot -LogFile $installLog
$rc = $LASTEXITCODE
Write-WTLog "Install-FromManifest returned $rc"
}
# ============================================================================
# Step 2: FormTracePak vendor install - per-bay version + sub-version
# ============================================================================
# Detection: skip if Formtracepak already present (re-run safe).
$ftpakExe = 'C:\Program Files (x86)\MitutoyoApp\Formtracepak\Formtracepak.exe'
if (Test-Path -LiteralPath $ftpakExe) {
Write-WTLog "Formtracepak.exe already present - skipping vendor install"
} else {
# Per-bay config resolved by resolve-bay-config.ps1 at WinPE time + written
# to C:\Enrollment\waxtrace\. Drives BOTH which FTPak ISO to mount AND what
# to relay to the tech in the pre-install banner + desktop reference file.
$waxCfgDir = 'C:\Enrollment\waxtrace'
$bayAsset = $null
$bayVersion = $null
$bayModel = $null
$bayUserId = $null
foreach ($pair in @(
@{ Var='bayAsset'; File='C:\Enrollment\machine-number.txt' },
@{ Var='bayVersion'; File=(Join-Path $waxCfgDir 'version.txt') },
@{ Var='bayModel'; File=(Join-Path $waxCfgDir 'model.txt') },
@{ Var='bayUserId'; File=(Join-Path $waxCfgDir 'userid.txt') }
)) {
if (Test-Path -LiteralPath $pair.File) {
$val = (Get-Content -LiteralPath $pair.File -First 1 -ErrorAction SilentlyContinue)
if ($val) { Set-Variable -Name $pair.Var -Value $val.Trim() }
}
}
Write-WTLog "Bay config: asset=$bayAsset version=$bayVersion model=$bayModel userid=$bayUserId"
if (-not $bayVersion) {
Write-WTLog "No FormTracePak version in $waxCfgDir\version.txt - bay-config did not resolve (asset_tag not in bay-config.csv?). Manual install required." 'ERROR'
} else {
$ftpakIso = Join-Path $stagingRoot ("formtracepak\FORMTRACEPAK-V$bayVersion.iso")
if (-not (Test-Path -LiteralPath $ftpakIso)) {
Write-WTLog "FormTracePak ISO missing: $ftpakIso" 'ERROR'
Write-WTLog " Bay needs V$bayVersion but no matching ISO at the expected path. Either bay-config.csv has the wrong version, or sync-waxtrace.sh did not push the right ISO. Manual install required." 'ERROR'
} else {
# Drop a reference text file on the SupportUser Desktop so the tech
# always has the model + USER ID in front of them when the Setup.exe
# dialogs come up. File persists post-imaging.
try {
$desktop = 'C:\Users\SupportUser\Desktop'
if (-not (Test-Path -LiteralPath $desktop)) { $desktop = 'C:\Users\Public\Desktop' }
$deskFile = Join-Path $desktop ("$bayAsset-FTPak-install-info.txt")
$deskContent = @(
'========================================',
'FormTracePak install info for this bay',
'========================================',
'',
"Bay: $bayAsset",
"FormTracePak: V$bayVersion",
"Sub-version: $bayModel <- SELECT THIS IN THE Setup.exe DIALOG",
"USER ID: $bayUserId <- ENTER THIS AT THE LICENSING PROMPT",
'',
'Reference file. Open this whenever you need to recall the USER ID',
'during the install dialogs.',
'',
"Generated at imaging time by 09-Setup-WaxAndTrace.ps1 ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'))"
) -join "`r`n"
Set-Content -LiteralPath $deskFile -Value $deskContent -Force
Write-WTLog "Wrote reference file to $deskFile"
} catch {
Write-WTLog "Failed to write desktop reference file: $_" 'WARN'
}
# Big banner in the PS host window. Operator sees this BEFORE Setup.exe
# opens so the model + USER ID are top-of-mind when the dialogs come up.
$bannerLines = @(
'',
'================================================================',
" BAY: $bayAsset",
" FORMTRACEPAK: V$bayVersion",
" SELECT MODEL: $bayModel",
" USER ID: $bayUserId",
'================================================================',
' Setup.exe is about to launch. In its dialogs:',
" - Pick the MODEL '$bayModel' on the language / model page.",
" - Enter USER ID '$bayUserId' at the licensing prompt.",
" - A reference text file with these values has been placed on",
" SupportUser's desktop ($bayAsset-FTPak-install-info.txt).",
'================================================================',
''
)
foreach ($l in $bannerLines) { Write-WTLog $l }
Write-WTLog "Mounting FormTracePak ISO: $ftpakIso"
try {
$img = Mount-DiskImage -ImagePath $ftpakIso -PassThru -ErrorAction Stop
Start-Sleep -Seconds 8
$vol = Get-DiskImage -ImagePath $ftpakIso | Get-Volume
$ftpakDrive = $vol.DriveLetter
if (-not $ftpakDrive) {
Write-WTLog " Mount succeeded but no drive letter assigned" 'ERROR'
} else {
Write-WTLog " mounted at ${ftpakDrive}: (volume label=$($vol.FileSystemLabel))"
$setupExe = "${ftpakDrive}:\Setup.exe"
if (-not [System.IO.File]::Exists($setupExe)) {
Write-WTLog " Setup.exe not found at $setupExe" 'ERROR'
} else {
Write-WTLog " running $setupExe (VB6 wrapper - requires DRIVE_CDROM)"
try {
$p = Start-Process -FilePath $setupExe `
-WorkingDirectory "${ftpakDrive}:\" `
-ArgumentList '/silent' `
-Wait -PassThru
Write-WTLog " Setup.exe exit $($p.ExitCode)"
} catch {
Write-WTLog " Setup.exe failed: $_" 'ERROR'
}
}
}
Dismount-DiskImage -ImagePath $ftpakIso -ErrorAction SilentlyContinue | Out-Null
Write-WTLog " FormTracePak ISO dismounted"
} catch {
Write-WTLog " Mount-DiskImage failed: $_" 'ERROR'
}
}
}
}
# ============================================================================
# Step 3: Per-machine calibration ISO (mount + apply via cal Setup.exe)
# ============================================================================
# Cal ISOs are keyed by asset_tag. Read machine-number.txt to pick the right
# ISO. Each cal ISO is ~1.7MB and contains a tiny Mitutoyo wrapper Setup.exe
# plus data/*.txt compensation tables for the bay's specific probe + serial.
$mnPath = 'C:\Enrollment\machine-number.txt'
$asset = $null
if (Test-Path -LiteralPath $mnPath) {
$asset = (Get-Content -LiteralPath $mnPath -First 1 -ErrorAction SilentlyContinue).Trim()
}
if (-not $asset) {
Write-WTLog "machine-number.txt missing or empty - skipping calibration apply" 'WARN'
} else {
$calDir = Join-Path $stagingRoot 'calibrations'
$candidate = Get-ChildItem $calDir -Filter "CAL-${asset}_*.iso" -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $candidate) {
Write-WTLog "No cal ISO matched CAL-${asset}_*.iso in $calDir - skipping" 'WARN'
} else {
Write-WTLog "Mounting cal ISO: $($candidate.FullName)"
try {
$img = Mount-DiskImage -ImagePath $candidate.FullName -PassThru -ErrorAction Stop
Start-Sleep -Seconds 5
$calDrive = (Get-DiskImage -ImagePath $candidate.FullName | Get-Volume).DriveLetter
$calRoot = "${calDrive}:\"
Write-WTLog " mounted at $calRoot"
# Disc layout differs by probe series. We have observed two:
#
# Older 218-458A: E:\App.ini, E:\Setup.exe, E:\data\CVIFDLL.ini,
# E:\data\Cvif_Correction.exe, E:\data\<files>.txt.
# Vendor Setup.exe works (VB6, may prompt).
#
# Newer 218-378-13: E:\setup.bat, E:\setup.exe (.NET 4.0),
# E:\iniStatus\<png>, E:\data\cvifdll.ini,
# E:\data\<files>.txt - BUT the filenames have a
# bug: probe ID ends with a TRAILING SPACE
# ("218-378-13 _100072210.txt"). The vendor's
# .NET Setup.exe calls FileSystemInfo.set_Attributes
# on the source path and throws System.ArgumentException
# because the path contains an embedded space
# component, crashing every time
# (exit -532462766 = 0xE0434352, .NET unhandled
# exception). See diag-cal.log, WER Event 1026.
#
# We bypass the vendor wrapper entirely when the disc carries
# the buggy filenames and do a direct file copy into FormTracePak's
# data dir, renaming each file to strip the trailing space.
$srcDataDir = Join-Path $calRoot 'data'
$dstDataDir = 'C:\Program Files (x86)\MitutoyoApp\Formtracepak\data'
$hasBrokenFilenames = $false
if (Test-Path -LiteralPath $srcDataDir) {
# Get-ChildItem -Filter uses Win32 filtering and does NOT
# honor PowerShell wildcards / character classes - '[0-9]'
# would match literal bracketed text. Use -Include +
# Where-Object regex instead. The bug signature is
# ' _' (space-underscore) inside the filename, e.g.
# 'Linear_X_218-378-13 _100072210.txt'.
$hasBrokenFilenames = [bool](Get-ChildItem -LiteralPath $srcDataDir -File -ErrorAction SilentlyContinue |
Where-Object { $_.Name -match ' _\d+\.txt$' } |
Select-Object -First 1)
}
if ($hasBrokenFilenames) {
Write-WTLog " disc has trailing-space probe-ID filenames (218-378-13 series) - bypassing buggy vendor Setup.exe, doing direct copy"
if (-not (Test-Path -LiteralPath $dstDataDir)) {
Write-WTLog " destination data dir does not exist: $dstDataDir" 'ERROR'
Write-WTLog " (FormTracePak install may not be complete; cal apply aborted)" 'ERROR'
} else {
$copied = 0
Get-ChildItem -LiteralPath $srcDataDir -File -ErrorAction SilentlyContinue | ForEach-Object {
# Strip ' _' (space-underscore) -> '_' inside the filename to
# heal the Mitutoyo burn-time typo. cvifdll.ini etc unaffected.
$cleanName = $_.Name -replace ' _', '_'
$dst = Join-Path $dstDataDir $cleanName
try {
Copy-Item -LiteralPath $_.FullName -Destination $dst -Force -ErrorAction Stop
# Clear read-only on the destination so future overwrites work.
try { (Get-Item -LiteralPath $dst).Attributes = 'Normal' } catch {}
Write-WTLog " copied $($_.Name) -> $cleanName"
$copied++
} catch {
Write-WTLog " copy failed for $($_.Name): $_" 'ERROR'
}
}
Write-WTLog " direct copy complete: $copied file(s) into $dstDataDir"
}
} else {
# Older-style disc (clean filenames). Vendor Setup.exe is reliable.
$calSetup = "${calDrive}:\Setup.exe"
if (Test-Path -LiteralPath $calSetup) {
Write-WTLog " running cal Setup.exe (may prompt - VB6 wrapper, same vintage as main installer)"
$p = Start-Process -FilePath $calSetup -WorkingDirectory $calRoot -Wait -PassThru
Write-WTLog " cal Setup.exe exit $($p.ExitCode)"
} else {
Write-WTLog " cal Setup.exe not found at $calSetup" 'WARN'
}
}
Dismount-DiskImage -ImagePath $candidate.FullName -ErrorAction SilentlyContinue | Out-Null
Write-WTLog " cal ISO dismounted"
} catch {
Write-WTLog " Mount-DiskImage failed: $_" 'ERROR'
}
}
}
# ============================================================================
# Step 3b: Restore captured layouts / prefs / data from per-asset backup ZIP
# ============================================================================
# startnet.cmd stages C:\WaxTrace-Install\backup\<asset>.zip iff the PXE
# share has a backup ZIP for this asset. The ZIP is the output of
# Backup-FormtracepakSettings.ps1 captured on the LIVE bay BEFORE re-imaging.
# Restore is data + config only - skip -RestoreRegistry until the HKEY_USERS
# SID rewriting is built (captured .reg carries source bay's user SID which
# does not exist on the freshly imaged bay). HKLM controller config / model
# device-map / etc. came from the vendor MSI install in Step 2 already.
$backupZip = $null
# startnet.cmd robocopy's the whole installers-post\waxtrace\backups\ dir
# to C:\WaxTrace-Install\backups\ (plural). Try plural first, then singular
# for backward-compat with any older boot.wim that used the cherry-pick path.
$backupDirCandidates = @(
'C:\WaxTrace-Install\backups',
'C:\WaxTrace-Install\backup'
)
foreach ($bd in $backupDirCandidates) {
if (-not (Test-Path -LiteralPath $bd) -or -not $bayAsset) { continue }
$candidate = Join-Path $bd ("$bayAsset.zip")
if (Test-Path -LiteralPath $candidate) {
$backupZip = $candidate
break
}
$newest = Get-ChildItem -LiteralPath $bd -Filter "${bayAsset}*.zip" -File -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($newest) {
$backupZip = $newest.FullName
break
}
}
if ($backupZip) {
$installPs1 = Join-Path $stagingRoot 'Install-FormtracepakSettings.ps1'
if (Test-Path -LiteralPath $installPs1) {
Write-WTLog "Restoring per-asset backup from $backupZip (HKLM + files; HKEY_USERS deferred to first ShopFloor logon)"
try {
# In-line restore: HKLM (controller config, device-map) + files
# (layouts, prefs in install dir). HKEY_USERS per-user prefs would
# need ShopFloor's hive loaded, which it isn't at imaging time -
# deferred to a scheduled SYSTEM task that fires on first ShopFloor
# logon (see Schedule-WaxTracePerUserRestore call below).
& $installPs1 -BackupPath $backupZip -RestoreRegistry -RestoreData -RestoreConfig -Force
Write-WTLog " Restore call returned (see restore log + Install-FormtracepakSettings output above)"
} catch {
Write-WTLog " Restore call threw: $_" 'WARN'
}
# Schedule the per-user prefs restore as a SYSTEM task that fires on
# first ShopFloor logon. The task self-removes after one successful
# run via flag file at C:\WaxTrace-Install\per-user-restore-ShopFloor.flag.
$schedScript = Join-Path $stagingRoot 'Schedule-WaxTracePerUserRestore.ps1'
if (-not (Test-Path -LiteralPath $schedScript)) {
# Fall back: pull from shopfloor-setup scripts/ sibling tree
$alt = Join-Path $PSScriptRoot 'scripts\Schedule-WaxTracePerUserRestore.ps1'
if (Test-Path -LiteralPath $alt) { $schedScript = $alt }
}
if (Test-Path -LiteralPath $schedScript) {
Write-WTLog "Registering deferred ShopFloor per-user restore task ($schedScript)"
try {
& $schedScript -BackupPath $backupZip -InstallScript $installPs1 -TargetUser 'ShopFloor' -AssetNumber $bayAsset
Write-WTLog " Scheduled task 'WaxTrace-PerUser-Restore' registered"
} catch {
Write-WTLog " Schedule task call threw: $_" 'WARN'
}
} else {
Write-WTLog "Schedule-WaxTracePerUserRestore.ps1 not found - per-user prefs will NOT auto-restore at first ShopFloor logon" 'WARN'
}
} else {
Write-WTLog "Install-FormtracepakSettings.ps1 not found at $installPs1 - skipping restore" 'WARN'
}
} else {
Write-WTLog "No per-asset backup ZIP at $backupDir\$bayAsset.zip - skipping restore (clean vendor install only)"
}
# ============================================================================
# Step 4: OpenText auto-start at login (HostExplorer "WJ Shopfloor" session)
# ============================================================================
$autoStartLib = Join-Path $PSScriptRoot '..\Shopfloor\lib\Set-OpenTextAutoStart.ps1'
if (Test-Path -LiteralPath $autoStartLib) {
Write-WTLog "Calling $autoStartLib"
& $autoStartLib
} else {
Write-WTLog "Set-OpenTextAutoStart.ps1 not found at $autoStartLib - OpenText auto-start NOT configured" 'WARN'
}
if (Get-Command Send-PxeStatus -ErrorAction SilentlyContinue) {
$finalStatus = if ($rc -eq 0) { 'in_progress' } else { 'failed' }
$finalErr = if ($rc -ne 0) { "Install-FromManifest exit $rc" } else { '' }
Send-PxeStatus -Stage '09-Setup-WaxAndTrace: complete' -StageIndex 4 -StageTotal 8 -Status $finalStatus -Error_ $finalErr
}
Write-WTLog "================================================================"
Write-WTLog "=== WaxTrace Setup session end ==="
Write-WTLog "================================================================"
try { Stop-Transcript | Out-Null } catch {}