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:
@@ -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')
|
||||
$walkJob = Start-Job -ArgumentList $root -ScriptBlock {
|
||||
param($rootArg)
|
||||
$safeTypes = @('String', 'ExpandString', 'DWord', 'QWord')
|
||||
try {
|
||||
$allKeys = @(Get-Item -Path $root -ErrorAction SilentlyContinue)
|
||||
$allKeys += Get-ChildItem -Path $root -Recurse -ErrorAction SilentlyContinue
|
||||
|
||||
$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 ($csvSafeTypes -notcontains [string]$kind) {
|
||||
# Skip Binary / MultiString / None / Unknown. The .reg
|
||||
# export already captured them losslessly.
|
||||
continue
|
||||
}
|
||||
if ($safeTypes -notcontains [string]$kind) { continue }
|
||||
$val = $key.GetValue($valName)
|
||||
$regCsvRows.Add([PSCustomObject]@{
|
||||
# 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
|
||||
})
|
||||
$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) {
|
||||
|
||||
Reference in New Issue
Block a user