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.
This commit is contained in:
cproudlock
2026-05-24 11:43:42 -04:00
parent d359563a4c
commit 6602afde38

View File

@@ -333,12 +333,33 @@ foreach ($root in $RegistryRoots) {
$nativeRoot = $root -replace '^HKLM:\\', 'HKEY_LOCAL_MACHINE\' ` $nativeRoot = $root -replace '^HKLM:\\', 'HKEY_LOCAL_MACHINE\' `
-replace '^HKCU:\\', 'HKEY_CURRENT_USER\' ` -replace '^HKCU:\\', 'HKEY_CURRENT_USER\' `
-replace '^Registry::', '' -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 { try {
$proc = Start-Process -FilePath 'reg.exe' ` # System.Diagnostics.Process gives reliable ExitCode access; Start-Process
-ArgumentList "export `"$nativeRoot`" `"$regFilePath`" /y" ` # -PassThru + WaitForExit(int) sometimes returns a stub object whose
-Wait -PassThru -NoNewWindow -ErrorAction SilentlyContinue # ExitCode is unset, causing a false "reg.exe exit for ..." warning.
if ($proc.ExitCode -eq 0) { $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 Write-Host " Exported: $regFilePath" -ForegroundColor Green
} else {
$stderr = $proc.StandardError.ReadToEnd()
Write-Warning " reg.exe exit=$($proc.ExitCode) for ${root}: $stderr"
} }
} catch { } catch {
Write-Warning " reg.exe export failed for ${root}: $_" 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 # which then corrupts the registry if the CSV restore overwrites the
# .reg-import result. Skip those types in the CSV (the .reg file is # .reg-import result. Skip those types in the CSV (the .reg file is
# authoritative for them). # 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') $csvSafeTypes = @('String', 'ExpandString', 'DWord', 'QWord')
try { $walkJob = Start-Job -ArgumentList $root -ScriptBlock {
$allKeys = @(Get-Item -Path $root -ErrorAction SilentlyContinue) param($rootArg)
$allKeys += Get-ChildItem -Path $root -Recurse -ErrorAction SilentlyContinue $safeTypes = @('String', 'ExpandString', 'DWord', 'QWord')
try {
foreach ($key in $allKeys) { $allKeys = @(Get-Item -Path $rootArg -ErrorAction SilentlyContinue)
foreach ($valName in $key.GetValueNames()) { $allKeys += Get-ChildItem -Path $rootArg -Recurse -ErrorAction SilentlyContinue
$kind = $key.GetValueKind($valName) foreach ($key in $allKeys) {
if ($csvSafeTypes -notcontains [string]$kind) { foreach ($valName in $key.GetValueNames()) {
# Skip Binary / MultiString / None / Unknown. The .reg $kind = $key.GetValueKind($valName)
# export already captured them losslessly. if ($safeTypes -notcontains [string]$kind) { continue }
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 { }
} catch { }
Write-Warning " Registry read error at ${root}: $_" $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++ $counters.Errors++
} }
Remove-Job -Job $walkJob -Force -ErrorAction SilentlyContinue
foreach ($row in $walked) {
$regCsvRows.Add($row)
$counters.RegKeys++
}
} }
if ($regCsvRows.Count -gt 0) { if ($regCsvRows.Count -gt 0) {