Capture-MclCache: read files MCL still has open, skip any it cannot

This commit is contained in:
cproudlock
2026-10-18 09:21:28 -04:00
parent 3b3041173b
commit 902ce5780a
2 changed files with 100 additions and 4 deletions

View File

@@ -12,9 +12,11 @@ its own log (`AppVersion: 4.0.0.20`). Everything in this document was verified
against a 4.0.0.20 run on bay 579C144 that day: the media drive resolved to `Z:\`, against a 4.0.0.20 run on bay 579C144 that day: the media drive resolved to `Z:\`,
`W:` was created by `PrepareDisk` and used for every copy destination, the fallback `W:` was created by `PrepareDisk` and used for every copy destination, the fallback
unattend at `Deploy\FlatUnattendW10.xml` was the one loaded, and driver selection unattend at `Deploy\FlatUnattendW10.xml` was the one loaded, and driver selection
matched `win11_optiplexd13mlk7020_a09.zip` by model. The decompiled detail below has matched `win11_optiplexd13mlk7020_a09.zip` by model. Most of the decompiled detail
not been re-derived from the 4.0.0.20 binary, so treat exact line-level claims as below has not been re-derived from the 4.0.0.20 binary, so treat exact line-level
4.0.0.17 and the observed behaviour as current. claims as 4.0.0.17 and the observed behaviour as current. The exception is
"The Patch Tuesday freshness gate" under GATHER DATA, which was decompiled from
4.0.0.20 on 2026-08-21 and does NOT exist in 4.0.0.17.
## How to re-derive this ## How to re-derive this
@@ -110,6 +112,74 @@ Four operations, all automatic:
6. Otherwise `GetDriverByModel` (below). 6. Otherwise `GetDriverByModel` (below).
7. If `Control\skip.json` exists, package installation is skipped. 7. If `Control\skip.json` exists, package installation is skipped.
### The Patch Tuesday freshness gate (4.0.0.20, NOT in 4.0.0.17)
Added after this document was first written, and it is the last thing
`GatherDataSelection` does before it succeeds. Verified against the 4.0.0.20
binary on 2026-08-21.
```csharp
DateTime lastPatchTuesday = MiscHelpers.GetLastPatchTuesday();
DateTime dateTime = lastPatchTuesday.AddDays(10.0);
if (!(DateTime.Now > lastPatchTuesday) || !(DateTime.Now < dateTime))
{
bool flag = true;
if (!string.IsNullOrEmpty(osv.operatingSystemVersion.wim.creationDate)) {
DateTime d2 = DateTime.ParseExact(..., "yyyy-MM-dd", ...);
if (d2 > lastPatchTuesday) flag = false;
}
if (imageinfo.LastUpdate >= dateTime) flag = false;
if (flag) {
SetResult(Failed, "gatherdata_mediadrive_error_patchday");
imageinfo.IsUpdateRequired = true;
}
}
```
`GetLastPatchTuesday` returns the second Tuesday of this month, or of the
previous month when today is on or before it. That makes `DateTime.Now >
lastPatchTuesday` always true, so only the `DateTime.Now >= lastPatchTuesday +
10 days` half of the guard ever fires. In effect: **from Patch Tuesday + 10
onward, the media must prove it is at least that fresh, or imaging fails.**
Two things can satisfy it. Neither is a package check; both are dates.
1. `Control\OperatingSystem.json` -> `wim.creationDate`, format `yyyy-MM-dd`,
later than the last Patch Tuesday. Our OperatingSystem.json has no
`creationDate` field at all, so this branch never runs here.
2. `imageinfo.LastUpdate`, which is nothing more than the mtime of
`<media>\Deploy\Control\Media.tag` (set in `GatherDataMediaDrive.
CheckLastWriteTime`, the same read that drives the 30-day expiry).
So on this server the gate reduces to one question: is Media.tag's mtime on or
after the last Patch Tuesday + 10 days?
**The midnight trap.** `/etc/cron.d/media-tag-refresh` touches Media.tag at
00:00:01 server local time. WinPE renders SMB file times about an hour behind
the server, which drops a 00:00 stamp back onto the previous day. That is
invisible for 29 days a month and fatal on exactly one: the Patch Tuesday + 10
boundary day, when the gate compares against midnight of that very date. Hit on
2026-08-21 (PESetup log showed `Last Imaging Source Update: '2026-08-20'`
against `Last Patch Tuesday + 10 days: '2026-08-21'`), on a Dell Pro Tower Plus
QBT1250 that was otherwise fine: driver matched, secure boot on, media view
intact. It recurs every month on Patch Tuesday + 10.
Restamp Media.tag at a mid-day hour, never at midnight. It must also stay in the
PAST relative to the bay clock: a future mtime makes `CheckLastWriteTime` set
`isBiosTimeCorrect = false` and fail with the BIOS-time error instead.
The failure text itself is useless. `gatherdata_mediadrive_error_patchday`
renders as an empty `####ERROR:` line in the log, so the three INFO lines above
it are the only diagnosis:
```
Last Imaging Source Update: '<Media.tag mtime>'
Last Patch Tuesday: '<second Tuesday>'
Last Patch Tuesday + 10 days: '<deadline>'
Imaging Source Update required: True
```
### 2. SELECT OS ### 2. SELECT OS
Operator-facing. Warns when the media holds an LTSC image ("99% of the time only Operator-facing. Warns when the media holds an LTSC image ("99% of the time only

View File

@@ -74,13 +74,31 @@ $rows | Sort-Object Path | Export-Csv -LiteralPath (Join-Path $dest "tree.csv")
$copied = 0 $copied = 0
$redacted = 0 $redacted = 0
$skippedBig = New-Object System.Collections.Generic.List[string] $skippedBig = New-Object System.Collections.Generic.List[string]
$unreadable = New-Object System.Collections.Generic.List[string]
foreach ($f in $all) { foreach ($f in $all) {
if ($textExt -notcontains $f.Extension.ToLower()) { continue } if ($textExt -notcontains $f.Extension.ToLower()) { continue }
$rel = $f.FullName.Substring($Root.Length + 1) $rel = $f.FullName.Substring($Root.Length + 1)
if ($f.Length -gt $maxCopy) { $skippedBig.Add("$rel ($($f.Length) bytes)"); continue } if ($f.Length -gt $maxCopy) { $skippedBig.Add("$rel ($($f.Length) bytes)"); continue }
$target = Join-Path $filesDir $rel $target = Join-Path $filesDir $rel
New-Item -ItemType Directory -Path (Split-Path $target -Parent) -Force | Out-Null New-Item -ItemType Directory -Path (Split-Path $target -Parent) -Force | Out-Null
$bytes = [System.IO.File]::ReadAllBytes($f.FullName) # Share mode lets this read the log MCL still has open.
try {
$fs = New-Object System.IO.FileStream($f.FullName, [System.IO.FileMode]::Open,
[System.IO.FileAccess]::Read, ([System.IO.FileShare]::ReadWrite -bor [System.IO.FileShare]::Delete))
try {
$bytes = New-Object byte[] $fs.Length
$read = 0
while ($read -lt $bytes.Length) {
$n = $fs.Read($bytes, $read, $bytes.Length - $read)
if ($n -le 0) { break }
$read += $n
}
if ($read -lt $bytes.Length) { $bytes = $bytes[0..($read - 1)] }
} finally { $fs.Dispose() }
} catch {
$unreadable.Add("$rel ($($_.Exception.InnerException.Message))")
continue
}
$text = [System.Text.Encoding]::UTF8.GetString($bytes) $text = [System.Text.Encoding]::UTF8.GetString($bytes)
if ($text -match $sasPattern) { if ($text -match $sasPattern) {
$text = [regex]::Replace($text, $sasPattern, '${1}REDACTED') $text = [regex]::Replace($text, $sasPattern, '${1}REDACTED')
@@ -116,6 +134,11 @@ foreach ($g in ($rows | Group-Object { ($_.Path -split '\\')[0..1] -join '\' } |
$gb = [math]::Round((($g.Group | Measure-Object Length -Sum).Sum) / 1GB, 2) $gb = [math]::Round((($g.Group | Measure-Object Length -Sum).Sum) / 1GB, 2)
$sum.Add(" $($g.Name): $($g.Count) files, $gb GB") $sum.Add(" $($g.Name): $($g.Count) files, $gb GB")
} }
if ($unreadable.Count) {
$sum.Add("")
$sum.Add("Text files that could not be read, not copied:")
foreach ($s in $unreadable) { $sum.Add(" $s") }
}
if ($skippedBig.Count) { if ($skippedBig.Count) {
$sum.Add("") $sum.Add("")
$sum.Add("Text files over 5 MB, not copied:") $sum.Add("Text files over 5 MB, not copied:")
@@ -131,4 +154,7 @@ Write-Host "Captured $($all.Count) files ($copied text files copied)." -Foregrou
if ($tags.Count -eq 0) { if ($tags.Count -eq 0) {
Write-Host "WARNING: no .tag file. MCL did not finish this run." -ForegroundColor Yellow Write-Host "WARNING: no .tag file. MCL did not finish this run." -ForegroundColor Yellow
} }
if ($unreadable.Count) {
Write-Host "$($unreadable.Count) file(s) could not be read; listed in summary.txt." -ForegroundColor Yellow
}
Write-Host "Send back: $zip" Write-Host "Send back: $zip"