Enhances file backup and cleanup for current runs

Improves the current-run cleanup mechanism by tracking file downloads explicitly. This ensures that files downloaded via BITS or preserving older timestamps are correctly identified and removed during cleanup. Extends the run manifest schema to support file backups, allowing for safe restoration of pre-existing scripts and configuration files modified during a run. Additional cleanup logic now correctly prunes residual empty directories after tracked files are removed.
This commit is contained in:
rbalsleyMSFT
2026-03-05 19:10:17 -08:00
parent 96603f025a
commit 422bc33da7
3 changed files with 512 additions and 124 deletions
+352 -49
View File
@@ -2223,6 +2223,8 @@ function Get-Office {
# Run setup.exe with config.xml and modify xml file to download to $OfficePath # Run setup.exe with config.xml and modify xml file to download to $OfficePath
$xmlContent = [xml](Get-Content $OfficeDownloadXML) $xmlContent = [xml](Get-Content $OfficeDownloadXML)
$xmlContent.Configuration.Add.SourcePath = $OfficePath $xmlContent.Configuration.Add.SourcePath = $OfficePath
# Back up existing XML content before changing it so cancel cleanup can restore pre-run state.
Backup-RunFile -FFUDevelopmentPath $FFUDevelopmentPath -Path $OfficeDownloadXML
$xmlContent.Save($OfficeDownloadXML) $xmlContent.Save($OfficeDownloadXML)
Mark-DownloadInProgress -FFUDevelopmentPath $FFUDevelopmentPath -TargetPath $OfficePath Mark-DownloadInProgress -FFUDevelopmentPath $FFUDevelopmentPath -TargetPath $OfficePath
WriteLog "Downloading M365 Apps/Office to $OfficePath" WriteLog "Downloading M365 Apps/Office to $OfficePath"
@@ -2238,6 +2240,8 @@ function Get-Office {
$installOfficePath = Join-Path -Path $orchestrationpath -ChildPath "Install-Office.ps1" $installOfficePath = Join-Path -Path $orchestrationpath -ChildPath "Install-Office.ps1"
# Create the Install-Office.ps1 file # Create the Install-Office.ps1 file
$installOfficeCommand = "& d:\Office\setup.exe /configure d:\office\$OfficeInstallXML" $installOfficeCommand = "& d:\Office\setup.exe /configure d:\office\$OfficeInstallXML"
# Back up any pre-existing script with the same name before overwrite.
Backup-RunFile -FFUDevelopmentPath $FFUDevelopmentPath -Path $installOfficePath
Set-Content -Path $installOfficePath -Value $installOfficeCommand -Force Set-Content -Path $installOfficePath -Value $installOfficeCommand -Force
WriteLog "Install-Office.ps1 created successfully at $installOfficePath" WriteLog "Install-Office.ps1 created successfully at $installOfficePath"
@@ -4341,10 +4345,10 @@ function Get-FFUEnvironment {
WriteLog "Cleanup-CurrentRunDownloads failed: $($_.Exception.Message)" WriteLog "Cleanup-CurrentRunDownloads failed: $($_.Exception.Message)"
} }
try { try {
Restore-RunJsonBackups -FFUDevelopmentPath $FFUDevelopmentPath Restore-RunBackups -FFUDevelopmentPath $FFUDevelopmentPath
} }
catch { catch {
WriteLog "Restore-RunJsonBackups failed: $($_.Exception.Message)" WriteLog "Restore-RunBackups failed: $($_.Exception.Message)"
} }
} }
# Check for running VMs that start with '_FFU-' and are in the 'Off' state # Check for running VMs that start with '_FFU-' and are in the 'Off' state
@@ -4639,9 +4643,12 @@ function New-RunSession {
if (-not (Test-Path $inprogDir)) { New-Item -ItemType Directory -Path $inprogDir -Force | Out-Null } if (-not (Test-Path $inprogDir)) { New-Item -ItemType Directory -Path $inprogDir -Force | Out-Null }
$manifest = [ordered]@{ $manifest = [ordered]@{
SchemaVersion = 2
RunStartUtc = (Get-Date).ToUniversalTime().ToString('o') RunStartUtc = (Get-Date).ToUniversalTime().ToString('o')
JsonBackups = @() JsonBackups = @()
OfficeXmlBackups = @() OfficeXmlBackups = @()
FileBackups = @()
DownloadTargets = @()
} }
if ($DriversFolder) { if ($DriversFolder) {
@@ -4698,6 +4705,56 @@ function Save-RunManifest {
$manifestPath = Join-Path $FFUDevelopmentPath '.session\currentRun.json' $manifestPath = Join-Path $FFUDevelopmentPath '.session\currentRun.json'
$Manifest | ConvertTo-Json -Depth 5 | Set-Content -Path $manifestPath -Encoding UTF8 $Manifest | ConvertTo-Json -Depth 5 | Set-Content -Path $manifestPath -Encoding UTF8
} }
function Backup-RunFile {
param(
[string]$FFUDevelopmentPath,
[string]$Path
)
if ([string]::IsNullOrWhiteSpace($FFUDevelopmentPath) -or [string]::IsNullOrWhiteSpace($Path)) { return }
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return }
$manifest = Get-CurrentRunManifest -FFUDevelopmentPath $FFUDevelopmentPath
if ($null -eq $manifest) { return }
# Ensure FileBackups exists for newer schema while remaining compatible with older manifests.
if ($null -eq $manifest.PSObject.Properties['FileBackups']) {
Add-Member -InputObject $manifest -MemberType NoteProperty -Name FileBackups -Value @()
}
# Skip when this path is already backed up (legacy or current schema).
$alreadyBackedUp = $false
foreach ($entry in @($manifest.FileBackups) + @($manifest.JsonBackups) + @($manifest.OfficeXmlBackups)) {
if ($entry.Path -eq $Path) {
$alreadyBackedUp = $true
break
}
}
if ($alreadyBackedUp) { return }
try {
# Store a uniquely named backup file so restore can safely overwrite or recreate the original path.
$backupDir = Join-Path (Join-Path $FFUDevelopmentPath '.session') 'backups'
if (-not (Test-Path -Path $backupDir)) {
New-Item -ItemType Directory -Path $backupDir -Force | Out-Null
}
$safeLeaf = Split-Path -Path $Path -Leaf
$backupName = "{0}_{1}" -f ([guid]::NewGuid().ToString('N')), $safeLeaf
$backupPath = Join-Path $backupDir $backupName
Copy-Item -LiteralPath $Path -Destination $backupPath -Force
$fileBackups = @($manifest.FileBackups)
$fileBackups += @{ Path = $Path; Backup = $backupPath }
$manifest.FileBackups = $fileBackups
Save-RunManifest -FFUDevelopmentPath $FFUDevelopmentPath -Manifest $manifest
WriteLog "Backed up existing file before modification: $Path"
}
catch {
WriteLog "Backup-RunFile failed for $($Path): $($_.Exception.Message)"
}
}
function Mark-DownloadInProgress { function Mark-DownloadInProgress {
param([string]$FFUDevelopmentPath, [string]$TargetPath) param([string]$FFUDevelopmentPath, [string]$TargetPath)
if ([string]::IsNullOrWhiteSpace($FFUDevelopmentPath) -or [string]::IsNullOrWhiteSpace($TargetPath)) { return } if ([string]::IsNullOrWhiteSpace($FFUDevelopmentPath) -or [string]::IsNullOrWhiteSpace($TargetPath)) { return }
@@ -4706,6 +4763,30 @@ function Mark-DownloadInProgress {
$marker = Join-Path $sessionInprog ("{0}.marker" -f ([guid]::NewGuid())) $marker = Join-Path $sessionInprog ("{0}.marker" -f ([guid]::NewGuid()))
$payload = @{ TargetPath = $TargetPath; CreatedUtc = (Get-Date).ToUniversalTime().ToString('o') } $payload = @{ TargetPath = $TargetPath; CreatedUtc = (Get-Date).ToUniversalTime().ToString('o') }
$payload | ConvertTo-Json -Depth 3 | Set-Content -Path $marker -Encoding UTF8 $payload | ConvertTo-Json -Depth 3 | Set-Content -Path $marker -Encoding UTF8
# Track file download targets so cleanup can remove them even when BITS preserves old source timestamps.
try {
if ([System.IO.Path]::HasExtension($TargetPath)) {
$manifest = Get-CurrentRunManifest -FFUDevelopmentPath $FFUDevelopmentPath
if ($manifest) {
if ($null -eq $manifest.PSObject.Properties['DownloadTargets']) {
Add-Member -InputObject $manifest -MemberType NoteProperty -Name DownloadTargets -Value @()
}
$downloadTargets = @($manifest.DownloadTargets)
if ($TargetPath -notin $downloadTargets) {
$downloadTargets += $TargetPath
$manifest.DownloadTargets = $downloadTargets
Save-RunManifest -FFUDevelopmentPath $FFUDevelopmentPath -Manifest $manifest
WriteLog "Registered current-run download target: $TargetPath"
}
}
}
}
catch {
WriteLog "Failed registering current-run download target $($TargetPath): $($_.Exception.Message)"
}
WriteLog "Marked in-progress: $TargetPath" WriteLog "Marked in-progress: $TargetPath"
} }
function Clear-DownloadInProgress { function Clear-DownloadInProgress {
@@ -4830,13 +4911,59 @@ function Remove-InProgressItems {
WriteLog "Failed Remove-InProgressItems marker '$($_.FullName)': $($_.Exception.Message)" WriteLog "Failed Remove-InProgressItems marker '$($_.FullName)': $($_.Exception.Message)"
} }
} }
# Also clean up any driver content created this run (model folders and temp folders), # Also clean up any driver content created this run (model folders, temp folders, and files)
# even when broader current-run cleanup is not requested. # only when broader current-run cleanup is requested.
try { try {
if ($DriversFolder -and (Test-Path $DriversFolder)) { if ($DriversFolder -and (Test-Path $DriversFolder)) {
$manifest = Get-CurrentRunManifest -FFUDevelopmentPath $FFUDevelopmentPath $manifest = Get-CurrentRunManifest -FFUDevelopmentPath $FFUDevelopmentPath
if ($manifest -and $manifest.RunStartUtc) { $driverProcessingMarker = Join-Path (Join-Path $FFUDevelopmentPath '.session') 'driverProcessing.active'
$shouldCleanupDriverCurrentRun = ($CleanupCurrentRunDownloads -or (Test-Path -LiteralPath $driverProcessingMarker))
if ($manifest -and $manifest.RunStartUtc -and $shouldCleanupDriverCurrentRun) {
if (-not $CleanupCurrentRunDownloads) {
WriteLog 'Driver processing marker detected during cancel; running in-progress driver cleanup for current run.'
}
$runStart = [datetime]::Parse($manifest.RunStartUtc) $runStart = [datetime]::Parse($manifest.RunStartUtc)
$affectedMakeRoots = @{}
# Remove tracked driver download targets for this run.
# This handles BITS files whose timestamps can be inherited from source metadata.
# Preserve HP platform list artifacts because they are model-list cache assets, not per-model download artifacts.
if ($manifest.PSObject.Properties['DownloadTargets']) {
foreach ($downloadTarget in @($manifest.DownloadTargets)) {
if ([string]::IsNullOrWhiteSpace($downloadTarget)) { continue }
try {
$fullTarget = [System.IO.Path]::GetFullPath($downloadTarget).TrimEnd('\')
$driversRoot = [System.IO.Path]::GetFullPath($DriversFolder).TrimEnd('\')
if (-not $fullTarget.StartsWith($driversRoot, [System.StringComparison]::OrdinalIgnoreCase)) { continue }
$leafName = [System.IO.Path]::GetFileName($fullTarget)
if ($leafName -in @('platformList.cab', 'platformList.xml')) { continue }
if (Test-Path -LiteralPath $downloadTarget -PathType Leaf) {
WriteLog "Removing tracked driver file from current run: $downloadTarget"
$removed = $false
try { $removed = Remove-PathWithRetry -path $downloadTarget -isDirectory:$false } catch {}
if ($removed) {
$relative = $fullTarget.Substring($driversRoot.Length).TrimStart('\')
$parts = $relative -split '\\'
if ($parts.Length -ge 1 -and -not [string]::IsNullOrWhiteSpace($parts[0])) {
$makeRoot = Join-Path $DriversFolder $parts[0]
$affectedMakeRoots[$makeRoot] = $true
}
}
else {
WriteLog "Failed removing tracked driver file from current run after retries: $downloadTarget"
}
}
}
catch {
WriteLog "Failed removing tracked driver target '$($downloadTarget)': $($_.Exception.Message)"
}
}
}
# Remove OEM temp folders like _TEMP_* (safe to always remove) # Remove OEM temp folders like _TEMP_* (safe to always remove)
Get-ChildItem -Path $DriversFolder -Directory -Recurse -ErrorAction SilentlyContinue | Get-ChildItem -Path $DriversFolder -Directory -Recurse -ErrorAction SilentlyContinue |
@@ -4846,6 +4973,22 @@ function Remove-InProgressItems {
Remove-Item -Path $_.FullName -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -Path $_.FullName -Recurse -Force -ErrorAction SilentlyContinue
} }
# Remove residual BITS temp files under Drivers regardless of timestamp.
Get-ChildItem -Path $DriversFolder -Directory -ErrorAction SilentlyContinue | ForEach-Object {
$makeRoot = $_.FullName
Get-ChildItem -Path $makeRoot -File -Recurse -Force -Filter 'BIT*.tmp' -ErrorAction SilentlyContinue | ForEach-Object {
WriteLog "Removing residual BITS temp file: $($_.FullName)"
$removed = $false
try { $removed = Remove-PathWithRetry -path $_.FullName -isDirectory:$false } catch {}
if ($removed) {
$affectedMakeRoots[$makeRoot] = $true
}
else {
WriteLog "Failed removing residual BITS temp file after retries: $($_.FullName)"
}
}
}
# Remove model folders created/modified this run; never remove top-level make roots # Remove model folders created/modified this run; never remove top-level make roots
Get-ChildItem -Path $DriversFolder -Directory -ErrorAction SilentlyContinue | ForEach-Object { Get-ChildItem -Path $DriversFolder -Directory -ErrorAction SilentlyContinue | ForEach-Object {
$makeRoot = $_.FullName $makeRoot = $_.FullName
@@ -4854,21 +4997,40 @@ function Remove-InProgressItems {
Where-Object { $_.CreationTimeUtc -ge $runStart -or $_.LastWriteTimeUtc -ge $runStart } | Where-Object { $_.CreationTimeUtc -ge $runStart -or $_.LastWriteTimeUtc -ge $runStart } |
ForEach-Object { ForEach-Object {
WriteLog "Removing driver model folder from current run: $($_.FullName)" WriteLog "Removing driver model folder from current run: $($_.FullName)"
$affectedMakeRoots[$makeRoot] = $true
Remove-Item -Path $_.FullName -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -Path $_.FullName -Recurse -Force -ErrorAction SilentlyContinue
} }
} }
# Remove make root folders created this run (if empty) # Remove run-created driver files that can live outside model folders
Get-ChildItem -Path $DriversFolder -Directory -ErrorAction SilentlyContinue | # (for example BITS temp files and vendor index JSON files under make roots).
Where-Object { $_.CreationTimeUtc -ge $runStart -and $_.LastWriteTimeUtc -ge $runStart } | Get-ChildItem -Path $DriversFolder -Directory -ErrorAction SilentlyContinue | ForEach-Object {
$makeRoot = $_.FullName
Get-ChildItem -Path $makeRoot -File -Recurse -Force -ErrorAction SilentlyContinue |
Where-Object { $_.CreationTimeUtc -ge $runStart } |
ForEach-Object { ForEach-Object {
$any = Get-ChildItem -Path $_.FullName -Force -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 WriteLog "Removing driver file from current run: $($_.FullName)"
$affectedMakeRoots[$makeRoot] = $true
$removed = $false
try { $removed = Remove-PathWithRetry -path $_.FullName -isDirectory:$false } catch {}
if (-not $removed) {
WriteLog "Failed removing driver file from current run after retries: $($_.FullName)"
}
}
}
# Remove empty make root folders that were affected during this run.
Get-ChildItem -Path $DriversFolder -Directory -ErrorAction SilentlyContinue | ForEach-Object {
$makeRoot = $_.FullName
if ($affectedMakeRoots.ContainsKey($makeRoot)) {
$any = Get-ChildItem -Path $makeRoot -Force -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if ($null -eq $any) { if ($null -eq $any) {
WriteLog "Removing empty make root folder created this run: $($_.FullName)" WriteLog "Removing empty make root folder after current-run cleanup: $makeRoot"
Remove-Item -Path $_.FullName -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -Path $makeRoot -Recurse -Force -ErrorAction SilentlyContinue
} }
else { else {
WriteLog "Skipping non-empty make root folder: $($_.FullName)" WriteLog "Skipping non-empty affected make root folder: $makeRoot"
}
} }
} }
} }
@@ -4884,7 +5046,77 @@ function Cleanup-CurrentRunDownloads {
if ($null -eq $manifest) { WriteLog "No current run manifest; skipping current-run cleanup."; return } if ($null -eq $manifest) { WriteLog "No current run manifest; skipping current-run cleanup."; return }
$runStart = [datetime]::Parse($manifest.RunStartUtc) $runStart = [datetime]::Parse($manifest.RunStartUtc)
# 1) Generic current-run scrub across known roots (includes Orchestration now) # Remove tracked file download targets for this run.
# This handles BITS files that may keep old source timestamps and bypass time-based cleanup.
$affectedDriverMakeRoots = @{}
if ($manifest.PSObject.Properties['DownloadTargets']) {
foreach ($downloadTarget in @($manifest.DownloadTargets)) {
if ([string]::IsNullOrWhiteSpace($downloadTarget)) { continue }
try {
if (Test-Path -LiteralPath $downloadTarget -PathType Leaf) {
WriteLog "Removing tracked current-run download file: $downloadTarget"
$removed = $false
for ($i = 0; $i -lt 5; $i++) {
try {
try { (Get-Item -LiteralPath $downloadTarget -ErrorAction SilentlyContinue).Attributes = 'Normal' } catch {}
Remove-Item -LiteralPath $downloadTarget -Force -ErrorAction Stop
$removed = $true
break
}
catch {
Start-Sleep -Milliseconds 400
}
}
if ($removed) {
if ($DriversFolder) {
try {
$fullTarget = [System.IO.Path]::GetFullPath($downloadTarget).TrimEnd('\')
$driversRoot = [System.IO.Path]::GetFullPath($DriversFolder).TrimEnd('\')
if ($fullTarget.StartsWith($driversRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
$parent = Split-Path -Path $fullTarget -Parent
$parentParent = Split-Path -Path $parent -Parent
if ($parent -and $parentParent) {
$fullParentParent = [System.IO.Path]::GetFullPath($parentParent).TrimEnd('\')
if ($fullParentParent -ieq $driversRoot) {
$affectedDriverMakeRoots[$parent] = $true
}
}
}
}
catch {}
}
}
else {
WriteLog "Failed removing tracked current-run download file after retries: $downloadTarget"
}
}
}
catch {
WriteLog "Failed removing tracked current-run download file $($downloadTarget): $($_.Exception.Message)"
}
}
}
# Remove empty make roots affected by tracked download file removal.
foreach ($makeRoot in $affectedDriverMakeRoots.Keys) {
try {
if (Test-Path -LiteralPath $makeRoot -PathType Container) {
$any = Get-ChildItem -Path $makeRoot -Force -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if ($null -eq $any) {
WriteLog "Removing empty make root folder after tracked download cleanup: $makeRoot"
Remove-Item -LiteralPath $makeRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
}
catch {
WriteLog "Failed removing empty make root folder '$($makeRoot)': $($_.Exception.Message)"
}
}
# 1) Generic current-run scrub across known download roots.
$roots = @() $roots = @()
if ($AppsPath) { $roots += (Join-Path $AppsPath 'Win32'); $roots += (Join-Path $AppsPath 'MSStore') } if ($AppsPath) { $roots += (Join-Path $AppsPath 'Win32'); $roots += (Join-Path $AppsPath 'MSStore') }
if ($DefenderPath) { $roots += $DefenderPath } if ($DefenderPath) { $roots += $DefenderPath }
@@ -4893,7 +5125,6 @@ function Cleanup-CurrentRunDownloads {
if ($EdgePath) { $roots += $EdgePath } if ($EdgePath) { $roots += $EdgePath }
if ($KBPath) { $roots += $KBPath } if ($KBPath) { $roots += $KBPath }
if ($DriversFolder) { $roots += $DriversFolder } if ($DriversFolder) { $roots += $DriversFolder }
if ($orchestrationPath) { $roots += $orchestrationPath }
foreach ($root in $roots | Where-Object { $_ -and (Test-Path $_) }) { foreach ($root in $roots | Where-Object { $_ -and (Test-Path $_) }) {
$isDriversRoot = $false $isDriversRoot = $false
@@ -4931,7 +5162,7 @@ function Cleanup-CurrentRunDownloads {
} }
# Remove driver files created this run # Remove driver files created this run
Get-ChildItem -Path $root -File -Recurse -ErrorAction SilentlyContinue | Get-ChildItem -Path $root -File -Recurse -Force -ErrorAction SilentlyContinue |
Where-Object { $_.CreationTimeUtc -ge $runStart } | Where-Object { $_.CreationTimeUtc -ge $runStart } |
ForEach-Object { ForEach-Object {
try { try {
@@ -4941,6 +5172,16 @@ function Cleanup-CurrentRunDownloads {
catch { WriteLog "Failed removing driver file $($_.FullName): $($_.Exception.Message)" } catch { WriteLog "Failed removing driver file $($_.FullName): $($_.Exception.Message)" }
} }
# Remove residual BITS temp files under Drivers regardless of timestamp.
Get-ChildItem -Path $root -File -Recurse -Force -Filter 'BIT*.tmp' -ErrorAction SilentlyContinue |
ForEach-Object {
try {
WriteLog "Removing residual BITS temp file: $($_.FullName)"
Remove-Item -Path $_.FullName -Force -ErrorAction SilentlyContinue
}
catch { WriteLog "Failed removing residual BITS temp file $($_.FullName): $($_.Exception.Message)" }
}
# Prune empty driver folders (skip existing make roots) # Prune empty driver folders (skip existing make roots)
Get-ChildItem -Path $root -Directory -Recurse -ErrorAction SilentlyContinue | Get-ChildItem -Path $root -Directory -Recurse -ErrorAction SilentlyContinue |
Sort-Object FullName -Descending | ForEach-Object { Sort-Object FullName -Descending | ForEach-Object {
@@ -4967,9 +5208,9 @@ function Cleanup-CurrentRunDownloads {
} }
else { else {
WriteLog "Scanning for current-run items in $root" WriteLog "Scanning for current-run items in $root"
# Remove folders created/modified this run (legacy behavior for non-Drivers roots) # Remove only folders created this run so pre-existing modified files are not deleted.
Get-ChildItem -Path $root -Directory -Recurse -ErrorAction SilentlyContinue | Get-ChildItem -Path $root -Directory -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTimeUtc -ge $runStart } | Where-Object { $_.CreationTimeUtc -ge $runStart } |
Sort-Object FullName -Descending | ForEach-Object { Sort-Object FullName -Descending | ForEach-Object {
try { try {
WriteLog "Removing current-run folder: $($_.FullName)" WriteLog "Removing current-run folder: $($_.FullName)"
@@ -4977,9 +5218,9 @@ function Cleanup-CurrentRunDownloads {
} }
catch { WriteLog "Failed removing folder $($_.FullName): $($_.Exception.Message)" } catch { WriteLog "Failed removing folder $($_.FullName): $($_.Exception.Message)" }
} }
# Remove files created/modified this run (preserve Office XMLs) # Remove only files created this run so pre-existing modified files are restored instead of removed.
Get-ChildItem -Path $root -File -Recurse -ErrorAction SilentlyContinue | Get-ChildItem -Path $root -File -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTimeUtc -ge $runStart -and $_.Name -notin @('DeployFFU.xml', 'DownloadFFU.xml') } | Where-Object { $_.CreationTimeUtc -ge $runStart -and $_.Name -notin @('DeployFFU.xml', 'DownloadFFU.xml') } |
ForEach-Object { ForEach-Object {
try { try {
WriteLog "Removing current-run file: $($_.FullName)" WriteLog "Removing current-run file: $($_.FullName)"
@@ -4990,12 +5231,16 @@ function Cleanup-CurrentRunDownloads {
} }
} }
# 2) Office folder policy: keep XML configs, remove everything else # 2) Office folder policy: keep XML configs, remove only current-run Office content
if ($OfficePath -and (Test-Path $OfficePath)) { if ($OfficePath -and (Test-Path $OfficePath)) {
$preserve = @('DeployFFU.xml', 'DownloadFFU.xml') $preserve = @('DeployFFU.xml', 'DownloadFFU.xml')
WriteLog "Cleaning Office folder: preserving $($preserve -join ', ') and removing other content." $officeItemsToRemove = Get-ChildItem -Path $OfficePath -Force -ErrorAction SilentlyContinue | Where-Object {
Get-ChildItem -Path $OfficePath -Force | ForEach-Object { ($preserve -notcontains $_.Name) -and ($_.CreationTimeUtc -ge $runStart)
if ($preserve -notcontains $_.Name) { }
if ($officeItemsToRemove) {
WriteLog "Cleaning Office folder: preserving $($preserve -join ', ') and removing current-run content only."
$officeItemsToRemove | ForEach-Object {
try { try {
WriteLog "Removing Office item: $($_.FullName)" WriteLog "Removing Office item: $($_.FullName)"
if ($_.PSIsContainer) { if ($_.PSIsContainer) {
@@ -5008,28 +5253,40 @@ function Cleanup-CurrentRunDownloads {
catch { WriteLog "Failed removing Office item $($_.FullName): $($_.Exception.Message)" } catch { WriteLog "Failed removing Office item $($_.FullName): $($_.Exception.Message)" }
} }
} }
else {
WriteLog 'Skipping Office cleanup: no current-run Office content detected.'
}
} }
# 3) Remove generated update artifacts under Orchestration (Update-*.ps1) created this run # 3) Remove generated orchestration artifacts created this run when there was no pre-run backup.
if ($orchestrationPath -and (Test-Path $orchestrationPath)) { if ($orchestrationPath -and (Test-Path $orchestrationPath)) {
$generatedArtifacts = @(
'Update-Defender.ps1',
'Update-MSRT.ps1',
'Update-OneDrive.ps1',
'Update-Edge.ps1',
'Install-Office.ps1',
'Install-LTSCUpdate.ps1',
'AppsScriptVariables.json'
)
foreach ($artifactName in $generatedArtifacts) {
try { try {
Get-ChildItem -Path $orchestrationPath -Filter 'Update-*.ps1' -File -ErrorAction SilentlyContinue | $artifactPath = Join-Path $orchestrationPath $artifactName
Where-Object { $_.LastWriteTimeUtc -ge $runStart } | ForEach-Object { if (Test-Path -LiteralPath $artifactPath) {
WriteLog "Removing current-run artifact: $($_.FullName)" $hasBackup = (@($manifest.FileBackups) + @($manifest.JsonBackups) + @($manifest.OfficeXmlBackups)) | Where-Object { $_.Path -eq $artifactPath } | Select-Object -First 1
Remove-Item -Path $_.FullName -Force -ErrorAction SilentlyContinue if ($null -eq $hasBackup) {
} $fi = Get-Item -LiteralPath $artifactPath
}
catch { WriteLog "Failed removing Update-*.ps1 artifacts: $($_.Exception.Message)" }
# Also remove Install-Office.ps1 if created this run
$installOffice = Join-Path $orchestrationPath 'Install-Office.ps1'
if (Test-Path $installOffice) {
$fi = Get-Item $installOffice
if ($fi.LastWriteTimeUtc -ge $runStart) { if ($fi.LastWriteTimeUtc -ge $runStart) {
WriteLog "Removing current-run artifact: $installOffice" WriteLog "Removing current-run artifact: $artifactPath"
Remove-Item -Path $installOffice -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $artifactPath -Force -ErrorAction SilentlyContinue
} }
} }
} }
}
catch { WriteLog "Failed removing generated artifact $($artifactName): $($_.Exception.Message)" }
}
}
# 4) If Defender/OneDrive/Edge/MSRT folders exist, remove them entirely (they're session downloads) # 4) If Defender/OneDrive/Edge/MSRT folders exist, remove them entirely (they're session downloads)
foreach ($p in @($DefenderPath, $OneDrivePath, $EdgePath, $MSRTPath)) { foreach ($p in @($DefenderPath, $OneDrivePath, $EdgePath, $MSRTPath)) {
@@ -5065,43 +5322,51 @@ function Cleanup-CurrentRunDownloads {
} }
} }
} }
function Restore-RunJsonBackups { function Restore-RunBackups {
param([string]$FFUDevelopmentPath) param([string]$FFUDevelopmentPath)
$manifest = Get-CurrentRunManifest -FFUDevelopmentPath $FFUDevelopmentPath $manifest = Get-CurrentRunManifest -FFUDevelopmentPath $FFUDevelopmentPath
if ($null -eq $manifest) { return } if ($null -eq $manifest) { return }
$runStart = [datetime]::Parse($manifest.RunStartUtc) $runStart = [datetime]::Parse($manifest.RunStartUtc)
foreach ($entry in $manifest.JsonBackups) { # Build a unified backup list for compatibility across manifest versions.
$allBackups = @()
if ($manifest.PSObject.Properties['FileBackups']) { $allBackups += @($manifest.FileBackups) }
if ($manifest.PSObject.Properties['JsonBackups']) { $allBackups += @($manifest.JsonBackups) }
if ($manifest.PSObject.Properties['OfficeXmlBackups']) { $allBackups += @($manifest.OfficeXmlBackups) }
# Restore any pre-existing files that were backed up before in-run modification.
foreach ($entry in $allBackups) {
if ($null -eq $entry -or [string]::IsNullOrWhiteSpace($entry.Path) -or [string]::IsNullOrWhiteSpace($entry.Backup)) { continue }
$path = $entry.Path $path = $entry.Path
$backup = $entry.Backup $backup = $entry.Backup
try { try {
if (Test-Path $backup) { if (Test-Path -LiteralPath $backup) {
WriteLog "Restoring JSON from backup: $path" WriteLog "Restoring file from backup: $path"
Copy-Item -Path $backup -Destination $path -Force Copy-Item -LiteralPath $backup -Destination $path -Force
} }
} }
catch { WriteLog "Failed restoring backup for $($path): $($_.Exception.Message)" } catch { WriteLog "Failed restoring backup for $($path): $($_.Exception.Message)" }
} }
# Remove current-run JSON files that were generated this run and had no pre-run backup.
$candidateJsons = @() $candidateJsons = @()
if ($DriversFolder) { $candidateJsons += (Join-Path $DriversFolder 'DriverMapping.json') } if ($DriversFolder) { $candidateJsons += (Join-Path $DriversFolder 'DriverMapping.json') }
if ($orchestrationPath) { $candidateJsons += (Join-Path $orchestrationPath 'WinGetWin32Apps.json') } if ($orchestrationPath) { $candidateJsons += (Join-Path $orchestrationPath 'WinGetWin32Apps.json') }
foreach ($jp in $candidateJsons) { foreach ($jp in $candidateJsons) {
if (Test-Path $jp) { if (Test-Path -LiteralPath $jp) {
$hasBackup = $manifest.JsonBackups | Where-Object { $_.Path -eq $jp } $hasBackup = $allBackups | Where-Object { $_.Path -eq $jp } | Select-Object -First 1
if ($null -eq $hasBackup) { if ($null -eq $hasBackup) {
$fi = Get-Item $jp $fi = Get-Item -LiteralPath $jp
if ($fi.LastWriteTimeUtc -ge $runStart) { if ($fi.LastWriteTimeUtc -ge $runStart) {
WriteLog "Removing current-run JSON: $jp" WriteLog "Removing current-run JSON: $jp"
Remove-Item -Path $jp -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $jp -Force -ErrorAction SilentlyContinue
}
} }
} }
} }
} }
# Restore Office XML backups if present; ensure Office folder exists and only XMLs remain # Restore Office XML backups if present; ensure Office folder exists and only XMLs remain.
if ($manifest.OfficeXmlBackups -and $OfficePath) { if ($manifest.OfficeXmlBackups -and $OfficePath) {
if (-not (Test-Path $OfficePath)) { if (-not (Test-Path $OfficePath)) {
try { New-Item -ItemType Directory -Path $OfficePath -Force | Out-Null } catch {} try { New-Item -ItemType Directory -Path $OfficePath -Force | Out-Null } catch {}
@@ -5113,7 +5378,7 @@ if ($manifest.OfficeXmlBackups -and $OfficePath) {
} }
catch { WriteLog "Failed restoring Office XML $($ox.Path): $($_.Exception.Message)" } catch { WriteLog "Failed restoring Office XML $($ox.Path): $($_.Exception.Message)" }
} }
# Ensure only DeployFFU.xml and DownloadFFU.xml remain # Ensure only DeployFFU.xml and DownloadFFU.xml remain.
$preserve = @('DeployFFU.xml', 'DownloadFFU.xml') $preserve = @('DeployFFU.xml', 'DownloadFFU.xml')
Get-ChildItem -Path $OfficePath -Force -ErrorAction SilentlyContinue | ForEach-Object { Get-ChildItem -Path $OfficePath -Force -ErrorAction SilentlyContinue | ForEach-Object {
if ($preserve -notcontains $_.Name) { if ($preserve -notcontains $_.Name) {
@@ -5125,6 +5390,11 @@ if ($manifest.OfficeXmlBackups -and $OfficePath) {
} }
} }
} }
}
function Restore-RunJsonBackups {
param([string]$FFUDevelopmentPath)
Restore-RunBackups -FFUDevelopmentPath $FFUDevelopmentPath
}
###END FUNCTIONS ###END FUNCTIONS
@@ -5597,6 +5867,16 @@ if ($driversJsonPath -and (Test-Path $driversJsonPath) -and ($InstallDrivers -or
} }
WriteLog "Starting parallel driver processing using Invoke-ParallelProcessing..." WriteLog "Starting parallel driver processing using Invoke-ParallelProcessing..."
# Mark driver processing as active so Cancel+No can still clean partial driver artifacts.
$driverProcessingMarker = Join-Path (Join-Path $FFUDevelopmentPath '.session') 'driverProcessing.active'
try {
Set-Content -Path $driverProcessingMarker -Value ((Get-Date).ToUniversalTime().ToString('o')) -Encoding UTF8
}
catch {
WriteLog "Failed to create driver processing marker at $($driverProcessingMarker): $($_.Exception.Message)"
}
try {
# Use the configured Threads value to control driver download concurrency # Use the configured Threads value to control driver download concurrency
$parallelResults = Invoke-ParallelProcessing -ItemsToProcess $driversToProcess ` $parallelResults = Invoke-ParallelProcessing -ItemsToProcess $driversToProcess `
-TaskType 'DownloadDriverByMake' ` -TaskType 'DownloadDriverByMake' `
@@ -5606,6 +5886,17 @@ if ($driversJsonPath -and (Test-Path $driversJsonPath) -and ($InstallDrivers -or
-ListViewControl $null ` -ListViewControl $null `
-MainThreadLogPath $LogFile ` -MainThreadLogPath $LogFile `
-ThrottleLimit $Threads -ThrottleLimit $Threads
}
finally {
if (Test-Path -Path $driverProcessingMarker) {
try {
Remove-Item -Path $driverProcessingMarker -Force -ErrorAction SilentlyContinue
}
catch {
WriteLog "Failed to remove driver processing marker at $($driverProcessingMarker): $($_.Exception.Message)"
}
}
}
# After processing, update the driver mapping file and detect failures # After processing, update the driver mapping file and detect failures
$successfullyDownloaded = [System.Collections.Generic.List[PSCustomObject]]::new() $successfullyDownloaded = [System.Collections.Generic.List[PSCustomObject]]::new()
@@ -6005,6 +6296,8 @@ if ($InstallApps) {
# Create Update-Defender.ps1 # Create Update-Defender.ps1
WriteLog "Creating $installDefenderPath" WriteLog "Creating $installDefenderPath"
# Back up any pre-existing script with the same name before overwrite.
Backup-RunFile -FFUDevelopmentPath $FFUDevelopmentPath -Path $installDefenderPath
Set-Content -Path $installDefenderPath -Value $installDefenderCommand -Force Set-Content -Path $installDefenderPath -Value $installDefenderCommand -Force
if (Test-Path -Path $installDefenderPath) { if (Test-Path -Path $installDefenderPath) {
WriteLog "$installDefenderPath created successfully" WriteLog "$installDefenderPath created successfully"
@@ -6062,6 +6355,8 @@ if ($InstallApps) {
$installMSRTPath = Join-Path -Path $orchestrationPath -ChildPath "Update-MSRT.ps1" $installMSRTPath = Join-Path -Path $orchestrationPath -ChildPath "Update-MSRT.ps1"
WriteLog "Creating $installMSRTPath" WriteLog "Creating $installMSRTPath"
$installMSRTCommand = "& d:\MSRT\$MSRTFileName /quiet" $installMSRTCommand = "& d:\MSRT\$MSRTFileName /quiet"
# Back up any pre-existing script with the same name before overwrite.
Backup-RunFile -FFUDevelopmentPath $FFUDevelopmentPath -Path $installMSRTPath
Set-Content -Path $installMSRTPath -Value $installMSRTCommand -Force Set-Content -Path $installMSRTPath -Value $installMSRTCommand -Force
# Validate that the file created successfully # Validate that the file created successfully
if (Test-Path -Path $installMSRTPath) { if (Test-Path -Path $installMSRTPath) {
@@ -6113,6 +6408,8 @@ if ($InstallApps) {
$installODPath = Join-Path -Path $orchestrationPath -ChildPath "Update-OneDrive.ps1" $installODPath = Join-Path -Path $orchestrationPath -ChildPath "Update-OneDrive.ps1"
WriteLog "Creating $installODPath" WriteLog "Creating $installODPath"
$installODCommand = "& d:\OneDrive\OneDriveSetup.exe /allusers /silent" $installODCommand = "& d:\OneDrive\OneDriveSetup.exe /allusers /silent"
# Back up any pre-existing script with the same name before overwrite.
Backup-RunFile -FFUDevelopmentPath $FFUDevelopmentPath -Path $installODPath
Set-Content -Path $installODPath -Value $installODCommand -Force Set-Content -Path $installODPath -Value $installODCommand -Force
# Validate that the file created successfully # Validate that the file created successfully
if (Test-Path -Path $installODPath) { if (Test-Path -Path $installODPath) {
@@ -6167,6 +6464,8 @@ if ($InstallApps) {
$installEdgePath = Join-Path -Path $orchestrationPath -ChildPath "Update-Edge.ps1" $installEdgePath = Join-Path -Path $orchestrationPath -ChildPath "Update-Edge.ps1"
WriteLog "Creating $installEdgePath" WriteLog "Creating $installEdgePath"
$installEdgeCommand = "& d:\Edge\$EdgeMSIFileName /quiet /norestart" $installEdgeCommand = "& d:\Edge\$EdgeMSIFileName /quiet /norestart"
# Back up any pre-existing script with the same name before overwrite.
Backup-RunFile -FFUDevelopmentPath $FFUDevelopmentPath -Path $installEdgePath
Set-Content -Path $installEdgePath -Value $installEdgeCommand -Force Set-Content -Path $installEdgePath -Value $installEdgeCommand -Force
# Validate that the file created successfully # Validate that the file created successfully
if (Test-Path -Path $installEdgePath) { if (Test-Path -Path $installEdgePath) {
@@ -6182,6 +6481,8 @@ if ($InstallApps) {
# Process AppsScriptVariables - Create json file # Process AppsScriptVariables - Create json file
if ($AppsScriptVariables) { if ($AppsScriptVariables) {
# Back up any pre-existing JSON before overwrite.
Backup-RunFile -FFUDevelopmentPath $FFUDevelopmentPath -Path $appsScriptVarsJsonPath
$AppsScriptVariables | ConvertTo-Json | Out-File -FilePath $appsScriptVarsJsonPath -Encoding UTF8 $AppsScriptVariables | ConvertTo-Json | Out-File -FilePath $appsScriptVarsJsonPath -Encoding UTF8
WriteLog "AppsScriptVariables exported to $appsScriptVarsJsonPath for use during orchestration" WriteLog "AppsScriptVariables exported to $appsScriptVarsJsonPath for use during orchestration"
} }
@@ -7061,6 +7362,8 @@ if (`$wusaExitCode -eq 0 -or `$wusaExitCode -eq 2359302) {
throw "LTSC CU install failed with WUSA exit code `$wusaExitCode." throw "LTSC CU install failed with WUSA exit code `$wusaExitCode."
"@ "@
# Back up any pre-existing script with the same name before overwrite.
Backup-RunFile -FFUDevelopmentPath $FFUDevelopmentPath -Path $InstallLTSCUpdatePath
Set-Content -Path $InstallLTSCUpdatePath -Value $installLtscUpdateCommand -Force Set-Content -Path $InstallLTSCUpdatePath -Value $installLtscUpdateCommand -Force
if (-not (Test-Path -Path $InstallLTSCUpdatePath)) { if (-not (Test-Path -Path $InstallLTSCUpdatePath)) {
throw "Failed to create $InstallLTSCUpdatePath" throw "Failed to create $InstallLTSCUpdatePath"
@@ -157,6 +157,79 @@ function Invoke-Process {
return $cmd return $cmd
} }
function Get-RunManifestPathForDownloadTarget {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Destination
)
try {
$currentPath = Split-Path -Path $Destination -Parent
if ([string]::IsNullOrWhiteSpace($currentPath)) { return $null }
while ($currentPath) {
$manifestPath = Join-Path -Path $currentPath -ChildPath '.session\currentRun.json'
if (Test-Path -LiteralPath $manifestPath -PathType Leaf) {
return $manifestPath
}
$parentPath = Split-Path -Path $currentPath -Parent
if ([string]::IsNullOrWhiteSpace($parentPath) -or $parentPath -eq $currentPath) {
break
}
$currentPath = $parentPath
}
}
catch {
WriteLog "Get-RunManifestPathForDownloadTarget failed for '$Destination': $($_.Exception.Message)"
}
return $null
}
function Register-CurrentRunDownloadTarget {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Destination
)
if ([string]::IsNullOrWhiteSpace($Destination)) { return }
$manifestPath = Get-RunManifestPathForDownloadTarget -Destination $Destination
if ([string]::IsNullOrWhiteSpace($manifestPath)) { return }
$mutexName = 'Global\FFUCurrentRunDownloadTargetsMutex'
$mutex = New-Object System.Threading.Mutex($false, $mutexName)
try {
$null = $mutex.WaitOne()
$manifest = Get-Content -LiteralPath $manifestPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
if ($null -eq $manifest) { return }
if ($null -eq $manifest.PSObject.Properties['DownloadTargets']) {
Add-Member -InputObject $manifest -MemberType NoteProperty -Name DownloadTargets -Value @()
}
$downloadTargets = @($manifest.DownloadTargets)
if ($Destination -notin $downloadTargets) {
$downloadTargets += $Destination
$manifest.DownloadTargets = $downloadTargets
$manifest | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
WriteLog "Registered current-run download target: $Destination"
}
}
catch {
WriteLog "Register-CurrentRunDownloadTarget failed for '$Destination': $($_.Exception.Message)"
}
finally {
try { $mutex.ReleaseMutex() | Out-Null } catch {}
$mutex.Dispose()
}
}
# Function to download a file using BITS with retry and error handling # Function to download a file using BITS with retry and error handling
function Start-BitsTransferWithRetry { function Start-BitsTransferWithRetry {
param ( param (
@@ -181,6 +254,10 @@ function Start-BitsTransferWithRetry {
} }
} }
# Register destination so cancel cleanup can remove this run's downloaded files
# even when file timestamps are inherited from the source.
Register-CurrentRunDownloadTarget -Destination $Destination
$attempt = 0 $attempt = 0
$lastError = $null $lastError = $null
$notLoggedOnHResult = [int]0x800704dd $notLoggedOnHResult = [int]0x800704dd
@@ -212,6 +212,14 @@ function Save-DellDriversTask {
Remove-Item $modelCabPath -Force -ErrorAction SilentlyContinue Remove-Item $modelCabPath -Force -ErrorAction SilentlyContinue
if (-not (Test-Path $modelXmlPath)) { throw "Model XML not found after extraction: $modelXmlPath" } if (-not (Test-Path $modelXmlPath)) { throw "Model XML not found after extraction: $modelXmlPath" }
# Track extracted model XML so cancel cleanup can remove it even if file timestamps are preserved from source metadata.
try {
Register-CurrentRunDownloadTarget -Destination $modelXmlPath
}
catch {
WriteLog "Failed to register Dell model XML for current-run cleanup ($modelXmlPath): $($_.Exception.Message)"
}
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status 'Selecting latest drivers...' } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status 'Selecting latest drivers...' }
$packages = Get-DellLatestDriverPackages -ModelXmlPath $modelXmlPath -WindowsArch $WindowsArch -WindowsRelease $WindowsRelease $packages = Get-DellLatestDriverPackages -ModelXmlPath $modelXmlPath -WindowsArch $WindowsArch -WindowsRelease $WindowsRelease
} }