Capture-MclCache: read files MCL still has open, skip any it cannot
This commit is contained in:
@@ -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:\`,
|
||||
`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
|
||||
matched `win11_optiplexd13mlk7020_a09.zip` by model. The decompiled detail below has
|
||||
not been re-derived from the 4.0.0.20 binary, so treat exact line-level claims as
|
||||
4.0.0.17 and the observed behaviour as current.
|
||||
matched `win11_optiplexd13mlk7020_a09.zip` by model. Most of the decompiled detail
|
||||
below has not been re-derived from the 4.0.0.20 binary, so treat exact line-level
|
||||
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
|
||||
|
||||
@@ -110,6 +112,74 @@ Four operations, all automatic:
|
||||
6. Otherwise `GetDriverByModel` (below).
|
||||
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
|
||||
|
||||
Operator-facing. Warns when the media holds an LTSC image ("99% of the time only
|
||||
|
||||
@@ -74,13 +74,31 @@ $rows | Sort-Object Path | Export-Csv -LiteralPath (Join-Path $dest "tree.csv")
|
||||
$copied = 0
|
||||
$redacted = 0
|
||||
$skippedBig = New-Object System.Collections.Generic.List[string]
|
||||
$unreadable = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($f in $all) {
|
||||
if ($textExt -notcontains $f.Extension.ToLower()) { continue }
|
||||
$rel = $f.FullName.Substring($Root.Length + 1)
|
||||
if ($f.Length -gt $maxCopy) { $skippedBig.Add("$rel ($($f.Length) bytes)"); continue }
|
||||
$target = Join-Path $filesDir $rel
|
||||
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)
|
||||
if ($text -match $sasPattern) {
|
||||
$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)
|
||||
$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) {
|
||||
$sum.Add("")
|
||||
$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) {
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user