heal: verify file completeness (size/timestamp), not just presence
The shallow present-check passed a file that merely existed, so a partially transferred payload (e.g. a truncated PC-DMIS MSI) looked PRESENT and was never re-pulled - then failed to install because it was incomplete. Replace it with a per-item robocopy that compares size + timestamp on every file and re-pulls anything missing OR partial, skipping ones already complete. VerifyOnly uses /L to report INCOMPLETE without changing anything. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,8 +17,10 @@ Designed to be:
|
|||||||
- called from the pre-install phase before 00-PreInstall-MachineApps so a bay is
|
- called from the pre-install phase before 00-PreInstall-MachineApps so a bay is
|
||||||
never left under-provisioned.
|
never left under-provisioned.
|
||||||
|
|
||||||
Idempotent. Verifies first; only heals what is actually missing/empty. Heals use
|
Idempotent. Uses robocopy per item, which compares size + timestamp on every
|
||||||
/R:3 /W:5 (resilient), not the WinPE fail-fast /R:1 /W:1.
|
file, so it re-pulls anything MISSING or PARTIAL (e.g. a truncated MSI that
|
||||||
|
"exists" but is incomplete and would fail to install) and skips files already
|
||||||
|
complete. Heals use /R:3 /W:5 (resilient), not the WinPE fail-fast /R:1 /W:1.
|
||||||
|
|
||||||
Share + creds: read from C:\Enrollment\fetch-source.txt (line1=UNC, line2=user,
|
Share + creds: read from C:\Enrollment\fetch-source.txt (line1=UNC, line2=user,
|
||||||
line3=pass) - same file Fetch-StagingPayload uses - else the defaults below.
|
line3=pass) - same file Fetch-StagingPayload uses - else the defaults below.
|
||||||
@@ -88,39 +90,28 @@ if ($pcType -eq 'gea-shopfloor-cmm') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- present check -------------------------------------------------------------
|
# --- robocopy-based verify/heal -----------------------------------------------
|
||||||
function Test-Present($it){
|
# Presence alone is NOT trusted: a partially transferred file (e.g. a truncated
|
||||||
if (-not (Test-Path -LiteralPath $it.Verify)) { return $false }
|
# MSI) exists but is incomplete and breaks install. Instead robocopy runs per
|
||||||
if ($it.Mode -eq 'Dir') {
|
# item and compares size + timestamp on EVERY file, re-pulling any that are
|
||||||
$n = @(Get-ChildItem -LiteralPath $it.Verify -Recurse -File -EA SilentlyContinue).Count
|
# missing OR differ (partial/truncated) and skipping ones already complete (a
|
||||||
return ($n -gt 0)
|
# cheap metadata scan). So it scans all files, not just checks a folder is
|
||||||
}
|
# non-empty. VerifyOnly adds /L (list-only): it reports what WOULD be re-pulled
|
||||||
return $true
|
# without changing anything.
|
||||||
}
|
|
||||||
|
|
||||||
# --- mount share fresh (only if we will heal) ---------------------------------
|
|
||||||
$drive='Z:'; $mounted=$false
|
$drive='Z:'; $mounted=$false
|
||||||
function Mount-Share { & net use $drive /delete /y 2>$null | Out-Null; & net use $drive $ShareUnc /user:$ShareUser $SharePass /persistent:no 2>&1 | Out-Null; return ($LASTEXITCODE -eq 0) }
|
function Mount-Share { & net use $drive /delete /y 2>$null | Out-Null; & net use $drive $ShareUnc /user:$ShareUser $SharePass /persistent:no 2>&1 | Out-Null; return ($LASTEXITCODE -eq 0) }
|
||||||
|
|
||||||
# --- first pass: verify --------------------------------------------------------
|
|
||||||
$report = New-Object System.Collections.Generic.List[object]
|
$report = New-Object System.Collections.Generic.List[object]
|
||||||
$missing = New-Object System.Collections.Generic.List[object]
|
for ($a=1; $a -le 5 -and -not $mounted; $a++){ if (Mount-Share){$mounted=$true;Log "Mounted $ShareUnc as $drive"} else {Log "mount attempt $a/5 failed - 10s" 'WARN'; Start-Sleep 10} }
|
||||||
|
if (-not $mounted) {
|
||||||
|
Log "Could not mount $ShareUnc after 5 attempts - cannot verify/heal. Bay may be under-provisioned; re-run once the share is reachable." 'ERROR'
|
||||||
|
foreach ($it in $items) { $report.Add([pscustomobject]@{Item=$it.Label;Status='NO-MOUNT'}) }
|
||||||
|
} else {
|
||||||
foreach ($it in $items) {
|
foreach ($it in $items) {
|
||||||
if (Test-Present $it) { $report.Add([pscustomobject]@{Item=$it.Label;Status='PRESENT'}) }
|
|
||||||
else { $missing.Add($it); $report.Add([pscustomobject]@{Item=$it.Label;Status=$(if($it.Optional){'MISSING(opt)'}else{'MISSING'})}) }
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- heal missing --------------------------------------------------------------
|
|
||||||
if ($missing.Count -gt 0 -and -not $VerifyOnly) {
|
|
||||||
Log "$($missing.Count) item(s) missing - mounting share to heal"
|
|
||||||
for ($a=1;$a -le 5 -and -not $mounted;$a++){ if (Mount-Share){$mounted=$true;Log "Mounted $ShareUnc as $drive"} else {Log "mount attempt $a failed - 10s" 'WARN'; Start-Sleep 10} }
|
|
||||||
if (-not $mounted) { Log "Could not mount share - cannot heal. $($missing.Count) item(s) remain missing." 'ERROR' }
|
|
||||||
else {
|
|
||||||
foreach ($it in $missing) {
|
|
||||||
$src = Join-Path $drive $it.Src
|
$src = Join-Path $drive $it.Src
|
||||||
if (-not (Test-Path -LiteralPath $src)) {
|
if (-not (Test-Path -LiteralPath $src)) {
|
||||||
Log "[heal SKIP] $($it.Label): not on share ($src)$(if($it.Optional){' - optional'})" $(if($it.Optional){'INFO'}else{'WARN'})
|
$report.Add([pscustomobject]@{Item=$it.Label;Status=$(if($it.Optional){'ABSENT(opt)'}else{'NO-SOURCE'})})
|
||||||
($report | Where-Object { $_.Item -eq $it.Label })[0].Status = $(if($it.Optional){'ABSENT(opt)'}else{'NO-SOURCE'})
|
Log "[$($it.Label)] source not on share ($src)$(if($it.Optional){' - optional'})" $(if($it.Optional){'INFO'}else{'WARN'})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (-not (Test-Path -LiteralPath $it.Dst)) { New-Item -ItemType Directory -Path $it.Dst -Force | Out-Null }
|
if (-not (Test-Path -LiteralPath $it.Dst)) { New-Item -ItemType Directory -Path $it.Dst -Force | Out-Null }
|
||||||
@@ -128,28 +119,32 @@ if ($missing.Count -gt 0 -and -not $VerifyOnly) {
|
|||||||
if ($it.Mode -eq 'Dir') { $args+='/E' } else { $args+=$it.Files }
|
if ($it.Mode -eq 'Dir') { $args+='/E' } else { $args+=$it.Files }
|
||||||
if ($it.Xd) { $args+=@('/XD',(Join-Path $src $it.Xd)) }
|
if ($it.Xd) { $args+=@('/XD',(Join-Path $src $it.Xd)) }
|
||||||
$args+=@('/R:3','/W:5','/NFL','/NDL','/NP')
|
$args+=@('/R:3','/W:5','/NFL','/NDL','/NP')
|
||||||
Log "[heal] $($it.Label): robocopy $src -> $($it.Dst)"
|
if ($VerifyOnly) { $args+='/L' } # list-only: detect missing/partial, change nothing
|
||||||
& robocopy @args | Out-Null
|
$out = & robocopy @args 2>&1
|
||||||
$rc = $LASTEXITCODE
|
$rc = $LASTEXITCODE
|
||||||
Start-Sleep 1
|
# robocopy exit bits: 1=copied, 2=extra, 4=mismatch, 8+=failure (<8 success).
|
||||||
$ok = Test-Present $it
|
$copied = (($rc -band 1) -ne 0) -or (($rc -band 4) -ne 0)
|
||||||
($report | Where-Object { $_.Item -eq $it.Label })[0].Status = $(if($ok){'HEALED'}elseif($rc -ge 8){'HEAL-FAIL'}else{'STILL-MISSING'})
|
$files = ($out | Select-String -Pattern '^\s*Files :' | Select-Object -First 1)
|
||||||
Log "[heal $(if($ok){'OK'}else{'FAIL'})] $($it.Label) robocopy exit=$rc present=$ok"
|
if ($rc -ge 8) { $status='HEAL-FAIL' }
|
||||||
|
elseif (-not $copied) { $status='COMPLETE' } # in sync, nothing to do
|
||||||
|
elseif ($VerifyOnly) { $status='INCOMPLETE' } # would re-pull (missing/partial)
|
||||||
|
else { $status='HEALED' } # actually re-pulled missing/partial
|
||||||
|
$report.Add([pscustomobject]@{Item=$it.Label;Status=$status})
|
||||||
|
Log "[$($it.Label)] robocopy rc=$rc -> $status $(("$files").Trim())"
|
||||||
}
|
}
|
||||||
& net use $drive /delete /y 2>$null | Out-Null
|
& net use $drive /delete /y 2>$null | Out-Null
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
# --- report --------------------------------------------------------------------
|
# --- report --------------------------------------------------------------------
|
||||||
Log '================ STAGING VERIFY/HEAL REPORT ================'
|
Log '================ STAGING VERIFY/HEAL REPORT ================'
|
||||||
foreach ($r in $report) { Log (" {0,-26} {1}" -f $r.Item, $r.Status) }
|
foreach ($r in $report) { Log (" {0,-26} {1}" -f $r.Item, $r.Status) }
|
||||||
$bad = @($report | Where-Object { $_.Status -in @('MISSING','NO-SOURCE','HEAL-FAIL','STILL-MISSING') })
|
$bad = @($report | Where-Object { $_.Status -in @('NO-SOURCE','HEAL-FAIL','NO-MOUNT','INCOMPLETE') })
|
||||||
if ($bad.Count -gt 0) {
|
if ($bad.Count -gt 0) {
|
||||||
Log "RESULT: $($bad.Count) REQUIRED item(s) still missing: $(($bad|ForEach-Object{$_.Item}) -join ', ')" 'ERROR'
|
Log "RESULT: $($bad.Count) item(s) need attention: $(($bad|ForEach-Object{$_.Item+'='+$_.Status}) -join ', ')" 'ERROR'
|
||||||
Log "Log: $log"
|
Log "Log: $log"
|
||||||
exit 1
|
exit 1
|
||||||
} else {
|
} else {
|
||||||
Log 'RESULT: all required payloads present (or healed).'
|
Log 'RESULT: all required payloads complete (or healed).'
|
||||||
Log "Log: $log"
|
Log "Log: $log"
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user