feat: Add progress reporting to FFU build process

Introduces a progress reporting system to provide real-time feedback during the FFU build. This includes adding a progress bar and status messages to the UI, which are updated at key stages of the build process.

- Adds a new `Set-Progress` function to log progress updates.
- Integrates `Set-Progress` calls throughout the main build script.
- Updates the UI to parse progress logs and update the progress bar and status text.
- Improves error reporting in the UI to display more detailed failure reasons.
- Corrects a typo in the `LogicalSectorSizeBytes` parameter name in documentation and log messages.
This commit is contained in:
rbalsleyMSFT
2025-07-11 16:43:42 -07:00
parent 4a719b6c9a
commit 08c9d5a0e3
7 changed files with 100 additions and 26 deletions
@@ -11,7 +11,7 @@ Get-ChildItem -Path $rootKey | ForEach-Object {
} }
# Run the disk cleanup tool with the specified flags # Run the disk cleanup tool with the specified flags
Start-Process -FilePath "cleanmgr.exe" -ArgumentList "/sagerun:0" -Wait Start-Process -FilePath "cleanmgr.exe" -ArgumentList "/sagerun:0"
# Remove the StateFlags0000 registry values that were added # Remove the StateFlags0000 registry values that were added
Get-ChildItem -Path $rootKey | ForEach-Object { Get-ChildItem -Path $rootKey | ForEach-Object {
+26 -2
View File
@@ -2807,6 +2807,7 @@ function New-FFU {
WriteLog "Configuring VM complete" WriteLog "Configuring VM complete"
#Start VM #Start VM
Set-Progress -Percentage 68 -Message "Capturing FFU from VM..."
WriteLog "Starting VM" WriteLog "Starting VM"
Start-VM -Name $VMName Start-VM -Name $VMName
@@ -2844,6 +2845,7 @@ function New-FFU {
WriteLog "FFU file name: $FFUFileName" WriteLog "FFU file name: $FFUFileName"
$FFUFile = "$FFUCaptureLocation\$FFUFileName" $FFUFile = "$FFUCaptureLocation\$FFUFileName"
#Capture the FFU #Capture the FFU
Set-Progress -Percentage 68 -Message "Capturing FFU from VHDX..."
WriteLog 'Capturing FFU' WriteLog 'Capturing FFU'
Invoke-Process cmd "/c ""$DandIEnv"" && dism /Capture-FFU /ImageFile:$FFUFile /CaptureDrive:\\.\PhysicalDrive$($vhdxDisk.DiskNumber) /Name:$($winverinfo.Name)$($winverinfo.DisplayVersion)$($shortenedWindowsSKU) /Compress:Default" | Out-Null Invoke-Process cmd "/c ""$DandIEnv"" && dism /Capture-FFU /ImageFile:$FFUFile /CaptureDrive:\\.\PhysicalDrive$($vhdxDisk.DiskNumber) /Name:$($winverinfo.Name)$($winverinfo.DisplayVersion)$($shortenedWindowsSKU) /Compress:Default" | Out-Null
WriteLog 'FFU Capture complete' WriteLog 'FFU Capture complete'
@@ -2883,6 +2885,7 @@ function New-FFU {
#Add drivers #Add drivers
If ($InstallDrivers) { If ($InstallDrivers) {
Set-Progress -Percentage 75 -Message "Injecting drivers into FFU..."
WriteLog 'Adding drivers' WriteLog 'Adding drivers'
WriteLog "Creating $FFUDevelopmentPath\Mount directory" WriteLog "Creating $FFUDevelopmentPath\Mount directory"
New-Item -Path "$FFUDevelopmentPath\Mount" -ItemType Directory -Force | Out-Null New-Item -Path "$FFUDevelopmentPath\Mount" -ItemType Directory -Force | Out-Null
@@ -2907,11 +2910,13 @@ function New-FFU {
} }
#Optimize FFU #Optimize FFU
if ($Optimize -eq $true) { if ($Optimize -eq $true) {
Set-Progress -Percentage 85 -Message "Optimizing FFU..."
WriteLog 'Optimizing FFU - This will take a few minutes, please be patient' WriteLog 'Optimizing FFU - This will take a few minutes, please be patient'
#Need to use ADK version of DISM to address bug in DISM - perhaps Windows 11 24H2 will fix this #Need to use ADK version of DISM to address bug in DISM - perhaps Windows 11 24H2 will fix this
Invoke-Process cmd "/c ""$DandIEnv"" && dism /optimize-ffu /imagefile:$FFUFile" | Out-Null Invoke-Process cmd "/c ""$DandIEnv"" && dism /optimize-ffu /imagefile:$FFUFile" | Out-Null
#Invoke-Process cmd "/c dism /optimize-ffu /imagefile:$FFUFile" | Out-Null #Invoke-Process cmd "/c dism /optimize-ffu /imagefile:$FFUFile" | Out-Null
WriteLog 'Optimizing FFU complete' WriteLog 'Optimizing FFU complete'
Set-Progress -Percentage 90 -Message "FFU post-processing complete."
} }
@@ -3692,6 +3697,7 @@ Write-Host "This process can take 20 minutes or more. Please do not close this w
Write-Host "To track progress, please open the log file $Logfile or use the -Verbose parameter next time" Write-Host "To track progress, please open the log file $Logfile or use the -Verbose parameter next time"
WriteLog 'Begin Logging' WriteLog 'Begin Logging'
Set-Progress -Percentage 1 -Message "FFU build process started..."
####### Generate Config File ####### ####### Generate Config File #######
@@ -3727,6 +3733,7 @@ if ($LongPathsEnabled -ne 1) {
} }
Set-Progress -Percentage 2 -Message "Validating parameters..."
###PARAMETER VALIDATION ###PARAMETER VALIDATION
#Validate drivers folder #Validate drivers folder
@@ -3930,6 +3937,7 @@ WriteLog 'Creating dirty.txt file'
New-Item -Path .\ -Name "dirty.txt" -ItemType "file" | Out-Null New-Item -Path .\ -Name "dirty.txt" -ItemType "file" | Out-Null
#Get drivers first since user could be prompted for additional info #Get drivers first since user could be prompted for additional info
Set-Progress -Percentage 3 -Message "Processing drivers..."
if ($driversJsonPath -and (Test-Path $driversJsonPath) -and ($InstallDrivers -or $CopyDrivers)) { if ($driversJsonPath -and (Test-Path $driversJsonPath) -and ($InstallDrivers -or $CopyDrivers)) {
WriteLog "Processing drivers from JSON file: $driversJsonPath" WriteLog "Processing drivers from JSON file: $driversJsonPath"
Import-Module "$PSScriptRoot\FFUUI.Core\FFUUI.Core.psm1" Import-Module "$PSScriptRoot\FFUUI.Core\FFUUI.Core.psm1"
@@ -4118,8 +4126,8 @@ elseif (($Make -and $Model) -and ($InstallDrivers -or $CopyDrivers)) {
} }
} }
#Get Windows ADK #Get Windows ADK
Set-Progress -Percentage 5 -Message "Validating ADK installation..."
try { try {
$adkPath = Get-ADK $adkPath = Get-ADK
#Need to use the Deployment and Imaging tools environment to use dism from the Sept 2023 ADK to optimize FFU #Need to use the Deployment and Imaging tools environment to use dism from the Sept 2023 ADK to optimize FFU
@@ -4133,6 +4141,7 @@ catch {
#Create apps ISO for Office and/or 3rd party apps #Create apps ISO for Office and/or 3rd party apps
if ($InstallApps) { if ($InstallApps) {
Set-Progress -Percentage 6 -Message "Downloading and preparing applications..."
if (Test-Path -Path $AppsISO) { if (Test-Path -Path $AppsISO) {
WriteLog "Apps ISO exists at: $AppsISO" WriteLog "Apps ISO exists at: $AppsISO"
WriteLog "Will use existing ISO" WriteLog "Will use existing ISO"
@@ -4517,6 +4526,7 @@ if ($InstallApps) {
} }
#Create Apps ISO #Create Apps ISO
Set-Progress -Percentage 10 -Message "Creating Apps ISO..."
WriteLog "Creating $AppsISO file" WriteLog "Creating $AppsISO file"
New-AppsISO New-AppsISO
WriteLog "$AppsISO created successfully" WriteLog "$AppsISO created successfully"
@@ -4531,7 +4541,7 @@ if ($InstallApps) {
#Create VHDX #Create VHDX
try { try {
Set-Progress -Percentage 11 -Message "Downloading Windows Updates for VHDX..."
#Update latest Cumulative Update if both $UpdateLatestCU is $true and $UpdatePreviewCU is $false #Update latest Cumulative Update if both $UpdateLatestCU is $true and $UpdatePreviewCU is $false
#Changed to use MU Catalog instead of using Get-LatestWindowsKB #Changed to use MU Catalog instead of using Get-LatestWindowsKB
#The Windows release info page is updated later than the MU Catalog #The Windows release info page is updated later than the MU Catalog
@@ -4789,6 +4799,7 @@ try {
if ($AllowVHDXCaching) { if ($AllowVHDXCaching) {
WriteLog 'AllowVHDXCaching is true, checking for cached VHDX file' WriteLog 'AllowVHDXCaching is true, checking for cached VHDX file'
if (Test-Path -Path $VHDXCacheFolder) { if (Test-Path -Path $VHDXCacheFolder) {
Set-Progress -Percentage 40 -Message "Windows Update download complete."
WriteLog "Found $VHDXCacheFolder" WriteLog "Found $VHDXCacheFolder"
$vhdxJsons = @(Get-ChildItem -File -Path $VHDXCacheFolder -Filter '*_config.json' | Sort-Object -Property CreationTime -Descending) $vhdxJsons = @(Get-ChildItem -File -Path $VHDXCacheFolder -Filter '*_config.json' | Sort-Object -Property CreationTime -Descending)
WriteLog "Found $($vhdxJsons.Count) cached VHDX files" WriteLog "Found $($vhdxJsons.Count) cached VHDX files"
@@ -4857,6 +4868,7 @@ try {
} }
if (-Not $cachedVHDXFileFound) { if (-Not $cachedVHDXFileFound) {
Set-Progress -Percentage 15 -Message "Creating VHDX and applying base Windows image..."
if ($ISOPath) { if ($ISOPath) {
$wimPath = Get-WimFromISO $wimPath = Get-WimFromISO
} }
@@ -4874,6 +4886,7 @@ try {
New-MSRPartition -VhdxDisk $vhdxDisk New-MSRPartition -VhdxDisk $vhdxDisk
Set-Progress -Percentage 16 -Message "Applying base Windows image to VHDX..."
$osPartition = New-OSPartition -VhdxDisk $vhdxDisk -OSPartitionSize $OSPartitionSize -WimPath $WimPath -WimIndex $index $osPartition = New-OSPartition -VhdxDisk $vhdxDisk -OSPartitionSize $OSPartitionSize -WimPath $WimPath -WimIndex $index
$osPartitionDriveLetter = $osPartition[1].DriveLetter $osPartitionDriveLetter = $osPartition[1].DriveLetter
$WindowsPartition = $osPartitionDriveLetter + ':\' $WindowsPartition = $osPartitionDriveLetter + ':\'
@@ -4888,6 +4901,7 @@ try {
#Add Windows packages #Add Windows packages
if ($UpdateLatestCU -or $UpdateLatestNet -or $UpdatePreviewCU ) { if ($UpdateLatestCU -or $UpdateLatestNet -or $UpdatePreviewCU ) {
try { try {
Set-Progress -Percentage 25 -Message "Applying Windows Updates to VHDX..."
WriteLog "Adding KBs to $WindowsPartition" WriteLog "Adding KBs to $WindowsPartition"
WriteLog 'This can take 10+ minutes depending on how old the media is and the size of the KB. Please be patient' WriteLog 'This can take 10+ minutes depending on how old the media is and the size of the KB. Please be patient'
# If WindowsRelease is 2016, we need to add the SSU first # If WindowsRelease is 2016, we need to add the SSU first
@@ -5017,6 +5031,7 @@ try {
WriteLog 'Copy completed' WriteLog 'Copy completed'
} }
Set-Progress -Percentage 40 -Message "Finalizing VHDX..."
if ($AllowVHDXCaching -and !$cachedVHDXFileFound) { if ($AllowVHDXCaching -and !$cachedVHDXFileFound) {
WriteLog 'Caching VHDX file' WriteLog 'Caching VHDX file'
@@ -5082,6 +5097,7 @@ catch {
#If installing apps (Office or 3rd party), we need to build a VM and capture that FFU, if not, just cut the FFU from the VHDX file #If installing apps (Office or 3rd party), we need to build a VM and capture that FFU, if not, just cut the FFU from the VHDX file
if ($InstallApps) { if ($InstallApps) {
Set-Progress -Percentage 41 -Message "Starting VM for app installation..."
#Create VM and attach VHDX #Create VM and attach VHDX
try { try {
WriteLog 'Creating new FFU VM' WriteLog 'Creating new FFU VM'
@@ -5109,6 +5125,7 @@ if ($InstallApps) {
If ($CreateCaptureMedia) { If ($CreateCaptureMedia) {
#Create Capture Media #Create Capture Media
try { try {
Set-Progress -Percentage 45 -Message "Creating WinPE capture media..."
#This should happen while the FFUVM is building #This should happen while the FFUVM is building
New-PEMedia -Capture $true New-PEMedia -Capture $true
} }
@@ -5131,17 +5148,20 @@ try {
} }
#Check if VM is done provisioning #Check if VM is done provisioning
If ($InstallApps) { If ($InstallApps) {
Set-Progress -Percentage 50 -Message "Installing applications in VM; please wait for VM to shut down..."
do { do {
$FFUVM = Get-VM -Name $FFUVM.Name $FFUVM = Get-VM -Name $FFUVM.Name
Start-Sleep -Seconds 10 Start-Sleep -Seconds 10
WriteLog 'Waiting for VM to shutdown' WriteLog 'Waiting for VM to shutdown'
} while ($FFUVM.State -ne 'Off') } while ($FFUVM.State -ne 'Off')
WriteLog 'VM Shutdown' WriteLog 'VM Shutdown'
Set-Progress -Percentage 65 -Message "Optimizing VHDX before capture..."
Optimize-FFUCaptureDrive -VhdxPath $VHDXPath Optimize-FFUCaptureDrive -VhdxPath $VHDXPath
#Capture FFU file #Capture FFU file
New-FFU $FFUVM.Name New-FFU $FFUVM.Name
} }
else { else {
Set-Progress -Percentage 81 -Message "Starting FFU capture from VHDX..."
#Shorten Windows SKU for use in FFU file name to remove spaces and long names #Shorten Windows SKU for use in FFU file name to remove spaces and long names
WriteLog "Shortening Windows SKU: $WindowsSKU for FFU file name" WriteLog "Shortening Windows SKU: $WindowsSKU for FFU file name"
$shortenedWindowsSKU = Get-ShortenedWindowsSKU -WindowsSKU $WindowsSKU $shortenedWindowsSKU = Get-ShortenedWindowsSKU -WindowsSKU $WindowsSKU
@@ -5213,6 +5233,7 @@ catch {
#Create Deployment Media #Create Deployment Media
If ($CreateDeploymentMedia) { If ($CreateDeploymentMedia) {
Set-Progress -Percentage 91 -Message "Creating deployment media..."
try { try {
New-PEMedia -Deploy $true New-PEMedia -Deploy $true
} }
@@ -5224,6 +5245,7 @@ If ($CreateDeploymentMedia) {
} }
} }
If ($BuildUSBDrive) { If ($BuildUSBDrive) {
Set-Progress -Percentage 95 -Message "Building USB drive..."
try { try {
If (Test-Path -Path $DeployISO) { If (Test-Path -Path $DeployISO) {
New-DeploymentUSB -CopyFFU New-DeploymentUSB -CopyFFU
@@ -5250,6 +5272,7 @@ If ($RemoveFFU) {
} }
} }
Set-Progress -Percentage 99 -Message "Finalizing and cleaning up..."
If ($CleanupCaptureISO) { If ($CleanupCaptureISO) {
try { try {
If (Test-Path -Path $CaptureISO) { If (Test-Path -Path $CaptureISO) {
@@ -5329,6 +5352,7 @@ Remove-Item -Path .\dirty.txt -Force | out-null
if ($VerbosePreference -ne 'Continue') { if ($VerbosePreference -ne 'Continue') {
Write-Host 'Script complete' Write-Host 'Script complete'
} }
Set-Progress -Percentage 100 -Message "Build process complete."
# Record the end time # Record the end time
$endTime = Get-Date $endTime = Get-Date
Write-Host "FFU build process completed at" $endTime Write-Host "FFU build process completed at" $endTime
+41 -2
View File
@@ -199,9 +199,19 @@ $script:uiState.Controls.btnRun.Add_Click({
# Read from log stream # Read from log stream
if ($null -ne $script:uiState.Data.logStreamReader) { if ($null -ne $script:uiState.Data.logStreamReader) {
while ($null -ne ($line = $script:uiState.Data.logStreamReader.ReadLine())) { while ($null -ne ($line = $script:uiState.Data.logStreamReader.ReadLine())) {
# Add the full line to the log view first to maintain consistency
$script:uiState.Data.logData.Add($line) $script:uiState.Data.logData.Add($line)
# Auto-scroll to the new item
$script:uiState.Controls.lstLogOutput.ScrollIntoView($line) $script:uiState.Controls.lstLogOutput.ScrollIntoView($line)
# Now, check if it's a progress line and update the UI accordingly
if ($line -match '\[PROGRESS\] (\d{1,3}) \| (.*)') {
$percentage = [double]$matches[1]
$message = $matches[2]
# Update progress bar and status text
$script:uiState.Controls.pbOverallProgress.Value = $percentage
$script:uiState.Controls.txtStatus.Text = $message
}
} }
} }
@@ -225,7 +235,17 @@ $script:uiState.Controls.btnRun.Add_Click({
# Final read of the log stream # Final read of the log stream
if ($null -ne $script:uiState.Data.logStreamReader) { if ($null -ne $script:uiState.Data.logStreamReader) {
while ($null -ne ($line = $script:uiState.Data.logStreamReader.ReadLine())) { while ($null -ne ($line = $script:uiState.Data.logStreamReader.ReadLine())) {
# Add the full line to the log view first
$script:uiState.Data.logData.Add($line) $script:uiState.Data.logData.Add($line)
# Now, check if it's a progress line and update the UI accordingly
if ($line -match '\[PROGRESS\] (\d{1,3}) \| (.*)') {
$percentage = [double]$matches[1]
$message = $matches[2]
$script:uiState.Controls.pbOverallProgress.Value = $percentage
$script:uiState.Controls.txtStatus.Text = $message
}
} }
$script:uiState.Data.logStreamReader.Close() $script:uiState.Data.logStreamReader.Close()
$script:uiState.Data.logStreamReader.Dispose() $script:uiState.Data.logStreamReader.Dispose()
@@ -234,18 +254,37 @@ $script:uiState.Controls.btnRun.Add_Click({
$finalStatusText = "FFU build completed successfully." $finalStatusText = "FFU build completed successfully."
if ($currentJob.State -eq 'Failed') { if ($currentJob.State -eq 'Failed') {
# Try to get a detailed error message.
$reason = $null
if ($currentJob.JobStateInfo.Reason) {
$reason = $currentJob.JobStateInfo.Reason.Message $reason = $currentJob.JobStateInfo.Reason.Message
}
# If the primary reason is empty, check the child job's error stream for more details.
if ([string]::IsNullOrWhiteSpace($reason) -and $currentJob.ChildJobs.Count -gt 0) {
$errorMessages = $currentJob.ChildJobs[0].Error | ForEach-Object { $_.ToString() }
if ($errorMessages) {
$reason = $errorMessages -join [System.Environment]::NewLine
}
}
# Fallback if no specific reason is found
if ([string]::IsNullOrWhiteSpace($reason)) {
$reason = "An unknown error occurred. The job failed without a specific reason."
}
$finalStatusText = "FFU build failed. Check FFUDevelopment.log for details." $finalStatusText = "FFU build failed. Check FFUDevelopment.log for details."
WriteLog "BuildFFUVM.ps1 job failed. Reason: $reason" WriteLog "BuildFFUVM.ps1 job failed. Reason: $reason"
[System.Windows.MessageBox]::Show("The build process failed. Please check the log file for details.`n`nError: $reason", "Build Error", "OK", "Error") | Out-Null [System.Windows.MessageBox]::Show("The build process failed. Please check the log file for details.`n`nError: $reason", "Build Error", "OK", "Error") | Out-Null
$script:uiState.Controls.pbOverallProgress.Visibility = 'Collapsed'
} }
else { else {
WriteLog "BuildFFUVM.ps1 job completed successfully." WriteLog "BuildFFUVM.ps1 job completed successfully."
$script:uiState.Controls.pbOverallProgress.Value = 100
} }
# Update UI elements # Update UI elements
$script:uiState.Controls.txtStatus.Text = $finalStatusText $script:uiState.Controls.txtStatus.Text = $finalStatusText
$script:uiState.Controls.pbOverallProgress.Visibility = 'Collapsed'
$script:uiState.Controls.btnRun.IsEnabled = $true $script:uiState.Controls.btnRun.IsEnabled = $true
# Clean up the job object # Clean up the job object
Binary file not shown.
@@ -246,4 +246,15 @@ function Start-BitsTransferWithRetry {
throw $lastError throw $lastError
} }
function Set-Progress {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[int]$Percentage,
[Parameter(Mandatory)]
[string]$Message
)
WriteLog "[PROGRESS] $Percentage | $Message"
}
Export-ModuleMember -Function * Export-ModuleMember -Function *
@@ -716,13 +716,13 @@ if ($dismExitCode -ne 0) {
$errorMessage = "Failed to apply FFU. LastExitCode = $dismExitCode." $errorMessage = "Failed to apply FFU. LastExitCode = $dismExitCode."
if ($dismExitCode -eq 1393) { if ($dismExitCode -eq 1393) {
WriteLog "Failed to apply FFU - LastExitCode = $dismExitCode" WriteLog "Failed to apply FFU - LastExitCode = $dismExitCode"
WriteLog "This is likely due to a mismatched LogicalSectorByteSize" WriteLog "This is likely due to a mismatched LogicalSectorSizeBytes"
WriteLog "BytesPerSector value from Win32_Diskdrive is $BytesPerSector" WriteLog "BytesPerSector value from Win32_Diskdrive is $BytesPerSector"
if ($BytesPerSector -eq 4096) { if ($BytesPerSector -eq 4096) {
WriteLog "The FFU build process by default uses a 512 LogicalSectorByteSize. Rebuild the FFU by adding -LogicalSectorByteSize 4096 to the command line" WriteLog "The FFU build process by default uses a 512 LogicalSectorSizeBytes. Rebuild the FFU by adding -LogicalSectorSizeBytes 4096 to the command line"
} }
elseif ($BytesPerSector -eq 512) { elseif ($BytesPerSector -eq 512) {
WriteLog "This FFU was likely built with a LogicalSectorByteSize of 4096. Rebuild the FFU by adding -LogicalSectorByteSize 512 to the command line" WriteLog "This FFU was likely built with a LogicalSectorSizeBytes of 4096. Rebuild the FFU by adding -LogicalSectorSizeBytes 512 to the command line"
} }
$errorMessage += " This is likely due to a mismatched logical sector size. Check logs for details." $errorMessage += " This is likely due to a mismatched logical sector size. Check logs for details."
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 834 B