mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 02:09:35 -06:00
1b80d008ef
Adds cleanup logic to the window's `Closed` event handler to properly terminate any active background build job. This prevents the build process from becoming an orphaned job if the user closes the UI while a build is in progress. The change ensures the job, its polling timer, and log streams are all stopped and disposed of correctly.
429 lines
20 KiB
PowerShell
429 lines
20 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Launches the FFU Development UI, a WPF application for configuring and running the FFU build process.
|
|
.DESCRIPTION
|
|
The BuildFFUVM_UI.ps1 script is the main entry point for the FFU Development user interface. It initializes and displays a WPF-based graphical interface defined in BuildFFUVM_UI.xaml.
|
|
|
|
The script is responsible for:
|
|
- Initializing a global state object to manage UI controls, data, and application flags.
|
|
- Importing the required FFU.Common and FFUUI.Core modules which contain the business logic.
|
|
- Ensuring system prerequisites, such as PowerShell 7 and Long Path Support, are met.
|
|
- Loading the XAML window, initializing UI controls with default values, and registering all event handlers.
|
|
- Launching the core build script (BuildFFUVM.ps1) in a background job when the user initiates a build.
|
|
- Providing real-time feedback by monitoring the build log file and updating the UI's progress bar and log viewer.
|
|
- Handling cleanup operations, such as reverting system settings, when the application is closed.
|
|
|
|
This script acts as the primary host for the UI, connecting the user interface with the underlying build and logic modules.
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
[System.STAThread()]
|
|
param()
|
|
|
|
# Check PowerShell Version
|
|
if ($PSVersionTable.PSVersion.Major -lt 7) {
|
|
Write-Error "PowerShell 7 or later is required to run this script."
|
|
exit 1
|
|
}
|
|
|
|
# Creating custom state object to hold UI state and data
|
|
$FFUDevelopmentPath = $PSScriptRoot
|
|
|
|
$script:uiState = [PSCustomObject]@{
|
|
FFUDevelopmentPath = $FFUDevelopmentPath;
|
|
Window = $null;
|
|
Controls = @{
|
|
featureCheckBoxes = @{};
|
|
UpdateInstallAppsBasedOnUpdates = $null
|
|
};
|
|
Data = @{
|
|
allDriverModels = [System.Collections.Generic.List[PSCustomObject]]::new();
|
|
appsScriptVariablesDataList = [System.Collections.Generic.List[PSCustomObject]]::new();
|
|
versionData = $null;
|
|
vmSwitchMap = @{};
|
|
logData = $null;
|
|
logStreamReader = $null;
|
|
pollTimer = $null
|
|
};
|
|
Flags = @{
|
|
installAppsForcedByUpdates = $false;
|
|
prevInstallAppsStateBeforeUpdates = $null;
|
|
installAppsCheckedByOffice = $false;
|
|
lastSortProperty = $null;
|
|
lastSortAscending = $true
|
|
};
|
|
Defaults = @{};
|
|
LogFilePath = "$FFUDevelopmentPath\FFUDevelopment_UI.log"
|
|
}
|
|
|
|
# Remove any existing modules to avoid conflicts
|
|
if (Get-Module -Name 'FFU.Common' -ErrorAction SilentlyContinue) {
|
|
Remove-Module -Name 'FFU.Common' -Force
|
|
}
|
|
if (Get-Module -Name 'FFUUI.Core' -ErrorAction SilentlyContinue) {
|
|
Remove-Module -Name 'FFUUI.Core' -Force
|
|
}
|
|
# Import Modules
|
|
Import-Module "$PSScriptRoot\FFU.Common" -Force
|
|
Import-Module "$PSScriptRoot\FFUUI.Core" -Force
|
|
|
|
# Set the log path
|
|
Set-CommonCoreLogPath -Path $script:uiState.LogFilePath
|
|
|
|
# Setting long path support - this prevents issues where some applications have deep directory structures
|
|
# and driver extraction fails due to long paths.
|
|
$script:uiState.Flags.originalLongPathsValue = $null # Store original value
|
|
try {
|
|
$script:uiState.Flags.originalLongPathsValue = Get-ItemPropertyValue -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -ErrorAction SilentlyContinue
|
|
}
|
|
catch {
|
|
# Key or value might not exist, which is fine.
|
|
WriteLog "Could not read initial LongPathsEnabled value (may not exist)."
|
|
}
|
|
|
|
# Enable long paths if not already enabled
|
|
if ($script:uiState.Flags.originalLongPathsValue -ne 1) {
|
|
try {
|
|
WriteLog 'LongPathsEnabled is not set to 1. Setting it to 1 for the duration of this script.'
|
|
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1 -Force
|
|
WriteLog 'LongPathsEnabled set to 1.'
|
|
}
|
|
catch {
|
|
WriteLog "Error setting LongPathsEnabled registry key: $($_.Exception.Message). Long path issues might persist."
|
|
}
|
|
}
|
|
else {
|
|
WriteLog "LongPathsEnabled is already set to 1."
|
|
}
|
|
|
|
if (Test-Path -Path $script:uiState.LogFilePath) {
|
|
Remove-item -Path $script:uiState.LogFilePath -Force
|
|
}
|
|
|
|
Add-Type -AssemblyName WindowsBase
|
|
Add-Type -AssemblyName PresentationCore, PresentationFramework
|
|
Add-Type -AssemblyName System.Windows.Forms
|
|
|
|
# Load XAML
|
|
$xamlPath = Join-Path $PSScriptRoot "BuildFFUVM_UI.xaml"
|
|
if (-not (Test-Path $xamlPath)) {
|
|
Write-Error "XAML file not found: $xamlPath"
|
|
return
|
|
}
|
|
$xamlString = Get-Content $xamlPath -Raw
|
|
$reader = New-Object System.IO.StringReader($xamlString)
|
|
$xmlReader = [System.Xml.XmlReader]::Create($reader)
|
|
$window = [Windows.Markup.XamlReader]::Load($xmlReader)
|
|
|
|
$window.Add_Loaded({
|
|
# Pass the state object to all initialization functions
|
|
$script:uiState.Window = $window
|
|
$window.Tag = $script:uiState
|
|
Initialize-UIControls -State $script:uiState
|
|
Initialize-UIDefaults -State $script:uiState
|
|
Initialize-DynamicUIElements -State $script:uiState
|
|
Register-EventHandlers -State $script:uiState
|
|
})
|
|
|
|
|
|
# Button: Build FFU
|
|
$script:uiState.Controls.btnRun = $window.FindName('btnRun')
|
|
$script:uiState.Controls.btnRun.Add_Click({
|
|
# Get a local reference to the button for convenience in this handler
|
|
$btnRun = $script:uiState.Controls.btnRun
|
|
try {
|
|
# Disable button to prevent multiple clicks
|
|
$btnRun.IsEnabled = $false
|
|
|
|
# Switch to Monitor Tab
|
|
$script:uiState.Controls.MainTabControl.SelectedItem = $script:uiState.Controls.MonitorTab
|
|
|
|
# Clear previous log data and reset autoscroll
|
|
if ($null -ne $script:uiState.Data.logData) {
|
|
$script:uiState.Data.logData.Clear()
|
|
$script:uiState.Flags.autoScrollLog = $true
|
|
}
|
|
|
|
$progressBar = $script:uiState.Controls.pbOverallProgress
|
|
$txtStatus = $script:uiState.Controls.txtStatus
|
|
$progressBar.Visibility = 'Visible'
|
|
$txtStatus.Text = "Starting FFU build..."
|
|
|
|
# Gather config on the UI thread before starting the job
|
|
$config = Get-UIConfig -State $script:uiState
|
|
$configFilePath = Join-Path $config.FFUDevelopmentPath "\config\FFUConfig.json"
|
|
$config | ConvertTo-Json -Depth 10 | Set-Content -Path $configFilePath -Encoding UTF8
|
|
|
|
if ($config.InstallOffice -and $config.OfficeConfigXMLFile) {
|
|
Copy-Item -Path $config.OfficeConfigXMLFile -Destination $config.OfficePath -Force
|
|
WriteLog "Office Configuration XML file copied successfully."
|
|
}
|
|
|
|
$txtStatus.Text = "Executing BuildFFUVM.ps1 in the background..."
|
|
WriteLog "Executing BuildFFUVM.ps1 in the background..."
|
|
|
|
# Prepare parameters for splatting
|
|
$buildParams = @{
|
|
ConfigFile = $configFilePath
|
|
}
|
|
if ($config.Verbose) {
|
|
$buildParams['Verbose'] = $true
|
|
}
|
|
|
|
# Define the script block to run in the background job
|
|
$scriptBlock = {
|
|
param($buildParams, $PSScriptRoot)
|
|
|
|
# This script runs in a new process. BuildFFUVM.ps1 is expected to handle its own module imports.
|
|
& "$PSScriptRoot\BuildFFUVM.ps1" @buildParams
|
|
}
|
|
|
|
# Delete the old log file before starting the build job to ensure we don't read stale content.
|
|
$mainLogPath = Join-Path $config.FFUDevelopmentPath "FFUDevelopment.log"
|
|
if (Test-Path $mainLogPath) {
|
|
WriteLog "Removing old FFUDevelopment.log file."
|
|
Remove-Item -Path $mainLogPath -Force
|
|
}
|
|
|
|
# Start the job and store it in the shared state object
|
|
$script:uiState.Data.currentBuildJob = Start-Job -ScriptBlock $scriptBlock -ArgumentList @($buildParams, $PSScriptRoot)
|
|
|
|
# Wait for the new log file to be created by the background job.
|
|
$logWaitTimeout = 15 # seconds
|
|
$watch = [System.Diagnostics.Stopwatch]::StartNew()
|
|
while (-not (Test-Path $mainLogPath) -and $watch.Elapsed.TotalSeconds -lt $logWaitTimeout) {
|
|
Start-Sleep -Milliseconds 250
|
|
}
|
|
$watch.Stop()
|
|
|
|
# Open a stream reader to the main log file
|
|
if (Test-Path $mainLogPath) {
|
|
$fileStream = [System.IO.File]::Open($mainLogPath, 'Open', 'Read', 'ReadWrite')
|
|
$script:uiState.Data.logStreamReader = [System.IO.StreamReader]::new($fileStream)
|
|
}
|
|
else {
|
|
WriteLog "Warning: Main log file not found at $mainLogPath after waiting. Monitor tab will not update."
|
|
}
|
|
|
|
# Create a timer to poll the job status from the UI thread
|
|
$script:uiState.Data.pollTimer = New-Object System.Windows.Threading.DispatcherTimer
|
|
$script:uiState.Data.pollTimer.Interval = [TimeSpan]::FromSeconds(1)
|
|
|
|
# Add the Tick event handler
|
|
$script:uiState.Data.pollTimer.Add_Tick({
|
|
param($sender, $e)
|
|
# This scriptblock runs on the UI thread, so it can safely access script-scoped variables
|
|
$currentJob = $script:uiState.Data.currentBuildJob
|
|
|
|
# Read from log stream
|
|
if ($null -ne $script:uiState.Data.logStreamReader) {
|
|
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)
|
|
if ($script:uiState.Flags.autoScrollLog) {
|
|
$script:uiState.Controls.lstLogOutput.ScrollIntoView($line)
|
|
$script:uiState.Controls.lstLogOutput.SelectedIndex = $script:uiState.Controls.lstLogOutput.Items.Count - 1
|
|
}
|
|
|
|
# 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
|
|
}
|
|
}
|
|
}
|
|
|
|
# If job is somehow null or the timer has been nulled out, stop the timer
|
|
if ($null -eq $currentJob -or $null -eq $script:uiState.Data.pollTimer) {
|
|
if ($null -ne $sender) {
|
|
$sender.Stop()
|
|
}
|
|
$script:uiState.Data.pollTimer = $null
|
|
return
|
|
}
|
|
|
|
# Check if the job has reached a terminal state
|
|
if ($currentJob.State -in 'Completed', 'Failed', 'Stopped') {
|
|
# Stop the timer, we're done polling
|
|
if ($null -ne $sender) {
|
|
$sender.Stop()
|
|
}
|
|
$script:uiState.Data.pollTimer = $null
|
|
|
|
# Final read of the log stream
|
|
if ($null -ne $script:uiState.Data.logStreamReader) {
|
|
$lastLine = $null
|
|
while ($null -ne ($line = $script:uiState.Data.logStreamReader.ReadLine())) {
|
|
# Add the full line to the log view first
|
|
$script:uiState.Data.logData.Add($line)
|
|
$lastLine = $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
|
|
}
|
|
}
|
|
|
|
# After the final read, scroll to the last line if autoscroll is enabled
|
|
if ($script:uiState.Flags.autoScrollLog -and $null -ne $lastLine) {
|
|
$script:uiState.Controls.lstLogOutput.ScrollIntoView($lastLine)
|
|
$script:uiState.Controls.lstLogOutput.SelectedIndex = $script:uiState.Controls.lstLogOutput.Items.Count - 1
|
|
}
|
|
|
|
$script:uiState.Data.logStreamReader.Close()
|
|
$script:uiState.Data.logStreamReader.Dispose()
|
|
$script:uiState.Data.logStreamReader = $null
|
|
}
|
|
|
|
$finalStatusText = "FFU build completed successfully."
|
|
if ($currentJob.State -eq 'Failed') {
|
|
# Try to get a detailed error message.
|
|
$reason = $null
|
|
if ($currentJob.JobStateInfo.Reason) {
|
|
$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."
|
|
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
|
|
$script:uiState.Controls.pbOverallProgress.Visibility = 'Collapsed'
|
|
}
|
|
else {
|
|
WriteLog "BuildFFUVM.ps1 job completed successfully."
|
|
$script:uiState.Controls.pbOverallProgress.Value = 100
|
|
}
|
|
|
|
# Update UI elements
|
|
$script:uiState.Controls.txtStatus.Text = $finalStatusText
|
|
$script:uiState.Controls.btnRun.IsEnabled = $true
|
|
|
|
# Clean up the job object
|
|
$currentJob | Receive-Job -ErrorAction SilentlyContinue | Out-Null
|
|
Remove-Job -Job $currentJob -Force
|
|
|
|
# Clear the job from the state
|
|
$script:uiState.Data.currentBuildJob = $null
|
|
}
|
|
})
|
|
|
|
# Start the timer
|
|
$script:uiState.Data.pollTimer.Start()
|
|
}
|
|
catch {
|
|
# This catch block handles errors during the setup of the job (e.g., Get-UIConfig fails)
|
|
$errorMessage = "An error occurred before starting the build job: $_"
|
|
WriteLog $errorMessage
|
|
[System.Windows.MessageBox]::Show($errorMessage, "Error", "OK", "Error")
|
|
|
|
# Clean up stream reader if it was opened
|
|
if ($null -ne $script:uiState.Data.logStreamReader) {
|
|
$script:uiState.Data.logStreamReader.Close()
|
|
$script:uiState.Data.logStreamReader.Dispose()
|
|
$script:uiState.Data.logStreamReader = $null
|
|
}
|
|
|
|
# Re-enable UI elements
|
|
$script:uiState.Controls.txtStatus.Text = "FFU build failed to start."
|
|
$script:uiState.Controls.pbOverallProgress.Visibility = 'Collapsed'
|
|
if ($null -ne $script:uiState.Controls.btnRun) {
|
|
$script:uiState.Controls.btnRun.IsEnabled = $true
|
|
}
|
|
}
|
|
})
|
|
|
|
# Add handler for Remove button clicks
|
|
$window.Add_SourceInitialized({
|
|
$listView = $window.FindName('lstApplications')
|
|
$listView.AddHandler(
|
|
[System.Windows.Controls.Button]::ClickEvent,
|
|
[System.Windows.RoutedEventHandler] {
|
|
param($buttonSender, $clickEventArgs)
|
|
if ($clickEventArgs.OriginalSource -is [System.Windows.Controls.Button] -and $clickEventArgs.OriginalSource.Content -eq "Remove") {
|
|
Remove-Application -priority $clickEventArgs.OriginalSource.Tag -State $script:uiState
|
|
}
|
|
}
|
|
)
|
|
})
|
|
|
|
# Register cleanup to reclaim memory and revert LongPathsEnabled setting when the UI window closes
|
|
$window.Add_Closed({
|
|
# Stop any running build job if the window is closed
|
|
#DEBUG
|
|
Writelog "DEBUG CurrentBuildJob: $($script:uiState.Data.currentBuildJob)"
|
|
#END DEBUG
|
|
if ($null -ne $script:uiState.Data.currentBuildJob) {
|
|
WriteLog "UI closing, stopping background build job."
|
|
|
|
# Stop the timer
|
|
if ($null -ne $script:uiState.Data.pollTimer) {
|
|
$script:uiState.Data.pollTimer.Stop()
|
|
$script:uiState.Data.pollTimer = $null
|
|
}
|
|
|
|
# Close the log stream
|
|
if ($null -ne $script:uiState.Data.logStreamReader) {
|
|
$script:uiState.Data.logStreamReader.Close()
|
|
$script:uiState.Data.logStreamReader.Dispose()
|
|
$script:uiState.Data.logStreamReader = $null
|
|
}
|
|
|
|
# Stop and remove the job
|
|
$jobToStop = $script:uiState.Data.currentBuildJob
|
|
$script:uiState.Data.currentBuildJob = $null # Clear it from state first
|
|
|
|
try {
|
|
Stop-Job -Job $jobToStop
|
|
Remove-Job -Job $jobToStop
|
|
WriteLog "Background job stopped and removed."
|
|
}
|
|
catch {
|
|
WriteLog "Error stopping or removing background job: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
# Revert LongPathsEnabled registry setting if it was changed by this script
|
|
if ($script:uiState.Flags.originalLongPathsValue -ne 1) {
|
|
# Only revert if we changed it from something other than 1
|
|
try {
|
|
$currentValue = Get-ItemPropertyValue -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -ErrorAction SilentlyContinue
|
|
if ($currentValue -eq 1) {
|
|
# Double-check it's still 1 before reverting
|
|
$revertValue = if ($null -eq $script:uiState.Flags.originalLongPathsValue) { 0 } else { $script:uiState.Flags.originalLongPathsValue } # Revert to original or 0 if it didn't exist
|
|
WriteLog "Reverting LongPathsEnabled registry key back to original value ($revertValue)."
|
|
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value $revertValue -Force
|
|
WriteLog "LongPathsEnabled reverted."
|
|
}
|
|
}
|
|
catch {
|
|
WriteLog "Error reverting LongPathsEnabled registry key: $($_.Exception.Message)."
|
|
}
|
|
}
|
|
|
|
# # Garbage collection
|
|
# [System.GC]::Collect()
|
|
# [System.GC]::WaitForPendingFinalizers()
|
|
})
|
|
|
|
[void]$window.ShowDialog()
|