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) {