Files
pxe-server/scripts/diagnostics/Capture-MclCache.ps1

217 lines
9.5 KiB
PowerShell

<#
.SYNOPSIS
Captures the layout of a Media Creator Lite cache, plus its small
control files, so the PXE import can be checked against what the
current MCL actually writes.
.DESCRIPTION
Run on the PC where Media Creator Lite just FINISHED a run. Run it once
after the Windows 11 Pro run and once after the Windows 11 LTSC run,
before starting the next MCL run (MCL deletes the cache when it starts).
.\Capture-MclCache.ps1 -Label pro
.\Capture-MclCache.ps1 -Label ltsc
It writes, under the Desktop:
mcl-capture-<label>-<timestamp>\
tree.csv every file: path, size, last write time (UTC)
summary.txt tag files and their contents, file counts, totals
files\ copies of the small text files (*.json, *.xml,
*.tag, *.txt, *.ini, *.log, *.cmd, *.ps1, *.bat)
under 5 MB, same relative paths
SHA256SUMS hash of every file above, for checking the copy
Then it robocopies that folder to -CopyTo (default Z:\mcl-capture, the
dev box pxe-images share), hashes every copied file back through the
share and compares with SHA256SUMS. No zip: archives arrive empty on
that share. The dev box can re-check with: sha256sum -c SHA256SUMS
Nothing large is copied: no WIM, driver, MSU or installer. Text copies
have any Azure SAS signature (sig=...) blanked, so the capture carries
no storage credential. Read-only on the cache; needs no admin rights.
.PARAMETER Label
Short name for this run, for example pro or ltsc.
.PARAMETER Root
The Media Creator folder. Default C:\ProgramData\GEAerospace\MediaCreator.
.PARAMETER OutDir
Where to write the capture. Default: the current user's Desktop, or
this script's folder when there is no Desktop.
.PARAMETER CopyTo
Folder to copy the capture into and verify. Default Z:\mcl-capture.
Pass -CopyTo none to keep it on this PC only.
#>
param(
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9_-]{1,20}$')][string]$Label,
[string]$Root = "C:\ProgramData\GEAerospace\MediaCreator",
[string]$OutDir = [Environment]::GetFolderPath("Desktop"),
[string]$CopyTo = "Z:\mcl-capture"
)
$ErrorActionPreference = "Stop"
if (-not $OutDir) { $OutDir = $PSScriptRoot } # no Desktop (e.g. run as SYSTEM)
if (-not (Test-Path -LiteralPath $Root -PathType Container)) {
Write-Host "Media Creator folder not found: $Root" -ForegroundColor Red
exit 1
}
$Root = (Resolve-Path -LiteralPath $Root).ProviderPath.TrimEnd('\')
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
$dest = Join-Path $OutDir "mcl-capture-$Label-$stamp"
$filesDir = Join-Path $dest "files"
New-Item -ItemType Directory -Path $filesDir -Force | Out-Null
$textExt = @(".json", ".xml", ".tag", ".txt", ".ini", ".log", ".cmd", ".ps1", ".bat")
$maxCopy = 5MB
# Azure SAS signature in a URL or connection string.
$sasPattern = '(?i)(sig=)[^&"''\s<]+'
Write-Host "Reading $Root ..."
$all = @(Get-ChildItem -LiteralPath $Root -Recurse -Force -File -ErrorAction SilentlyContinue)
$rows = foreach ($f in $all) {
[pscustomobject]@{
Path = $f.FullName.Substring($Root.Length + 1)
Length = $f.Length
LastWriteUtc = $f.LastWriteTimeUtc.ToString("yyyy-MM-ddTHH:mm:ssZ")
}
}
$rows | Sort-Object Path | Export-Csv -LiteralPath (Join-Path $dest "tree.csv") -NoTypeInformation -Encoding UTF8
$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
# 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')
$bytes = [System.Text.Encoding]::UTF8.GetBytes($text)
$redacted++
}
[System.IO.File]::WriteAllBytes($target, $bytes)
$copied++
}
$tags = @($all | Where-Object { $_.Extension -eq ".tag" })
$sum = New-Object System.Collections.Generic.List[string]
$sum.Add("Media Creator Lite cache capture")
$sum.Add("Label: $Label")
$sum.Add("Captured: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss zzz')")
$sum.Add("Computer: $env:COMPUTERNAME")
$sum.Add("Root: $Root")
$sum.Add("PowerShell $($PSVersionTable.PSVersion)")
$sum.Add("")
$sum.Add("Files: $($all.Count), total $([math]::Round((($all | Measure-Object Length -Sum).Sum) / 1GB, 2)) GB")
$sum.Add("Text files copied: $copied (SAS signatures blanked in $redacted)")
$sum.Add("")
$sum.Add("Tag files ($($tags.Count)):")
foreach ($t in $tags) {
$content = (Get-Content -LiteralPath $t.FullName -Raw -ErrorAction SilentlyContinue)
if ($content) { $content = $content.Trim() }
$sum.Add(" $($t.FullName.Substring($Root.Length + 1)) [$content] written $($t.LastWriteTime)")
}
if ($tags.Count -eq 0) { $sum.Add(" NONE - the MCL run did not finish") }
$sum.Add("")
$sum.Add("Top-level folders:")
foreach ($g in ($rows | Group-Object { ($_.Path -split '\\')[0..1] -join '\' } | Sort-Object Name)) {
$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:")
foreach ($s in $skippedBig) { $sum.Add(" $s") }
}
$sum | Set-Content -LiteralPath (Join-Path $dest "summary.txt") -Encoding UTF8
# Relative path with forward slashes, the way sha256sum -c reads it.
function Get-RelPath($full, $base) { $full.Substring($base.Length + 1).Replace('\', '/') }
$sumLines = foreach ($f in (Get-ChildItem -LiteralPath $dest -Recurse -File | Sort-Object FullName)) {
"$((Get-FileHash -LiteralPath $f.FullName -Algorithm SHA256).Hash.ToLower()) $(Get-RelPath $f.FullName $dest)"
}
[System.IO.File]::WriteAllText((Join-Path $dest "SHA256SUMS"), (($sumLines -join "`n") + "`n"))
Write-Host ""
Write-Host "Captured $($all.Count) files ($copied text files copied)." -ForegroundColor Green
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 "Capture folder: $dest"
if (-not $CopyTo -or $CopyTo -eq "none") { exit 0 }
$drive = Split-Path -Qualifier $CopyTo -ErrorAction SilentlyContinue
if (($drive -and -not (Test-Path -LiteralPath "$drive\")) -or -not (Test-Path -LiteralPath (Split-Path $CopyTo -Parent))) {
Write-Host ""
Write-Host "$CopyTo is not reachable, so nothing was copied." -ForegroundColor Yellow
Write-Host "Map Z: to the dev box pxe-images share and run again, or copy the folder above by hand."
exit 1
}
$target = Join-Path $CopyTo (Split-Path $dest -Leaf)
Write-Host ""
Write-Host "Copying to $target ..."
& robocopy.exe $dest $target /E /COPY:DT /R:2 /W:5 /NP /NFL /NDL /NJH /NJS | Out-Null
$rc = $LASTEXITCODE
if ($rc -ge 8) {
Write-Host "Copy FAILED (robocopy exit $rc)." -ForegroundColor Red
exit 2
}
# Read every file back through the share and compare with SHA256SUMS.
$bad = New-Object System.Collections.Generic.List[string]
foreach ($line in $sumLines) {
$hash = $line.Substring(0, 64)
$rel = $line.Substring(66)
$copy = Join-Path $target ($rel.Replace('/', '\'))
if (-not (Test-Path -LiteralPath $copy -PathType Leaf)) { $bad.Add("missing $rel"); continue }
$got = (Get-FileHash -LiteralPath $copy -Algorithm SHA256).Hash.ToLower()
if ($got -ne $hash) { $bad.Add("differs $rel ($((Get-Item -LiteralPath $copy).Length) bytes)") }
}
if ((Get-FileHash -LiteralPath (Join-Path $target "SHA256SUMS") -Algorithm SHA256).Hash -ne
(Get-FileHash -LiteralPath (Join-Path $dest "SHA256SUMS") -Algorithm SHA256).Hash) {
$bad.Add("differs SHA256SUMS")
}
if ($bad.Count) {
Write-Host "Copy NOT verified: $($bad.Count) of $($sumLines.Count + 1) files wrong." -ForegroundColor Red
foreach ($b in $bad) { Write-Host " $b" }
exit 2
}
Write-Host "Copied and verified: all $($sumLines.Count + 1) files match." -ForegroundColor Green
Write-Host "On the share: $target"