From 6602afde38f0cff0628f398f818ee6857d154c10 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Sun, 24 May 2026 11:43:42 -0400 Subject: [PATCH] Backup-FormtracepakSettings: timeout-fence reg.exe + recursive walk WJF00052 / WJF00083 / WJF00084 / WJF00159 produced 0-byte ZIPs on the 2026-05-24 capture pass. Export-FormtracepakInventory was the primary culprit (Get-Service hang, fixed in d359563), but Backup has the same root failure mode for the same bays: reg.exe export via Start-Process -Wait and the recursive Get-ChildItem walk over HKLM:\SOFTWARE\ WOW6432Node\Mitutoyo (95k+ values) both lack timeouts, so a degraded SCM / antivirus interception / WMI repository on the bay can wedge the script and the operator kills the .bat - same outcome as the inventory hang. Two timeout fences: - reg.exe export: switch from Start-Process -PassThru -Wait to [System.Diagnostics.Process]::Start + WaitForExit(300_000). If the process hasn't exited after 5 minutes, Kill() it and log + count an error; the .reg file for that one root is skipped but other roots + the CSV fallback + the file/data captures continue. Bonus: the ProcessStartInfo path also gives reliable $proc.ExitCode access; the Start-Process -PassThru object sometimes returned a stub with unpopulated ExitCode, producing a false "reg.exe exit for ..." warning even on successful exports. - Recursive Get-ChildItem CSV walk: move into a Start-Job + Wait-Job -Timeout 600 (10 min). If the walk hangs, Stop-Job, log, increment error count, .reg file remains authoritative for that root. Also fixed a subtle Start-Job return-shape bug introduced in the same edit: emitting the rows via the pipeline inside the job + Receive-Job flattens correctly, whereas `return ,$list` wrapped the whole List in a single-element array, so the outer foreach was treating the list-of-95k as a single row. Net effect was Registry Values: 4 instead of 95152 in the manifest. Verified fixed via a full real-install run on the VM: 95152 values captured cleanly in 22 s, 0 errors. Net behaviour: the failed-bay re-runs (WJF00052/00083/00084/00159) should now either produce real ZIPs or print a clear warning naming the specific reg root that hung, instead of leaving an empty ZIP behind. --- .../scripts/Backup-FormtracepakSettings.ps1 | 88 +++++++++++++------ 1 file changed, 62 insertions(+), 26 deletions(-) diff --git a/playbook/shopfloor-setup/gea-shopfloor-waxtrace/scripts/Backup-FormtracepakSettings.ps1 b/playbook/shopfloor-setup/gea-shopfloor-waxtrace/scripts/Backup-FormtracepakSettings.ps1 index 8ccf370..1a0f842 100755 --- a/playbook/shopfloor-setup/gea-shopfloor-waxtrace/scripts/Backup-FormtracepakSettings.ps1 +++ b/playbook/shopfloor-setup/gea-shopfloor-waxtrace/scripts/Backup-FormtracepakSettings.ps1 @@ -333,12 +333,33 @@ foreach ($root in $RegistryRoots) { $nativeRoot = $root -replace '^HKLM:\\', 'HKEY_LOCAL_MACHINE\' ` -replace '^HKCU:\\', 'HKEY_CURRENT_USER\' ` -replace '^Registry::', '' + # reg.exe export wrapped in a 5-minute kill fence. On shopfloor PCs with + # a degraded SCM / antivirus interception / WMI repository corruption, + # reg.exe has been observed to wedge indefinitely; without this fence the + # whole Backup script hangs and the operator kills the .bat, leaving a + # 0-byte ZIP (root cause of the WJF00052 / 00083 / 00084 / 00159 backup + # failures on 2026-05-24). try { - $proc = Start-Process -FilePath 'reg.exe' ` - -ArgumentList "export `"$nativeRoot`" `"$regFilePath`" /y" ` - -Wait -PassThru -NoNewWindow -ErrorAction SilentlyContinue - if ($proc.ExitCode -eq 0) { + # System.Diagnostics.Process gives reliable ExitCode access; Start-Process + # -PassThru + WaitForExit(int) sometimes returns a stub object whose + # ExitCode is unset, causing a false "reg.exe exit for ..." warning. + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = 'reg.exe' + $psi.Arguments = "export `"$nativeRoot`" `"$regFilePath`" /y" + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $proc = [System.Diagnostics.Process]::Start($psi) + if (-not $proc.WaitForExit(300000)) { + try { $proc.Kill() } catch { } + Write-Warning " reg.exe export wedged >5min for ${root} - killed, skipping" + $counters.Errors++ + } elseif ($proc.ExitCode -eq 0) { Write-Host " Exported: $regFilePath" -ForegroundColor Green + } else { + $stderr = $proc.StandardError.ReadToEnd() + Write-Warning " reg.exe exit=$($proc.ExitCode) for ${root}: $stderr" } } catch { Write-Warning " reg.exe export failed for ${root}: $_" @@ -352,33 +373,48 @@ foreach ($root in $RegistryRoots) { # which then corrupts the registry if the CSV restore overwrites the # .reg-import result. Skip those types in the CSV (the .reg file is # authoritative for them). + # + # Recursive Get-ChildItem on a 95k-value HKLM tree takes ~25s on healthy + # bays but has been seen to wedge on degraded ones. Run it in a job with + # a 10-minute timeout so a single bad root cannot kill the whole backup. $csvSafeTypes = @('String', 'ExpandString', 'DWord', 'QWord') - try { - $allKeys = @(Get-Item -Path $root -ErrorAction SilentlyContinue) - $allKeys += Get-ChildItem -Path $root -Recurse -ErrorAction SilentlyContinue - - foreach ($key in $allKeys) { - foreach ($valName in $key.GetValueNames()) { - $kind = $key.GetValueKind($valName) - if ($csvSafeTypes -notcontains [string]$kind) { - # Skip Binary / MultiString / None / Unknown. The .reg - # export already captured them losslessly. - continue + $walkJob = Start-Job -ArgumentList $root -ScriptBlock { + param($rootArg) + $safeTypes = @('String', 'ExpandString', 'DWord', 'QWord') + try { + $allKeys = @(Get-Item -Path $rootArg -ErrorAction SilentlyContinue) + $allKeys += Get-ChildItem -Path $rootArg -Recurse -ErrorAction SilentlyContinue + foreach ($key in $allKeys) { + foreach ($valName in $key.GetValueNames()) { + $kind = $key.GetValueKind($valName) + if ($safeTypes -notcontains [string]$kind) { continue } + $val = $key.GetValue($valName) + # Emit each row to the pipeline so Receive-Job returns + # a flat array of PSObjects (not a List inside an array). + [PSCustomObject]@{ + Path = $key.PSPath + Name = $valName + Value = [string]$val + Type = [string]$kind + } } - $val = $key.GetValue($valName) - $regCsvRows.Add([PSCustomObject]@{ - Path = $key.PSPath - Name = $valName - Value = [string]$val - Type = [string]$kind - }) - $counters.RegKeys++ } - } - } catch { - Write-Warning " Registry read error at ${root}: $_" + } catch { } + } + $walked = @() + if (Wait-Job -Job $walkJob -Timeout 600) { + $walked = @(Receive-Job -Job $walkJob -ErrorAction SilentlyContinue) + } else { + Write-Warning " Registry walk wedged >10min for ${root} - aborting (.reg file is still authoritative)" + Stop-Job -Job $walkJob -ErrorAction SilentlyContinue $counters.Errors++ } + Remove-Job -Job $walkJob -Force -ErrorAction SilentlyContinue + + foreach ($row in $walked) { + $regCsvRows.Add($row) + $counters.RegKeys++ + } } if ($regCsvRows.Count -gt 0) {