From 7043af47c368ef6b52a8edcfb03eb9dc31ce12fc Mon Sep 17 00:00:00 2001 From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com> Date: Thu, 10 Jul 2025 19:56:59 -0700 Subject: [PATCH] Feat: Add live log monitoring tab to UI Introduces a new "Monitor" tab in the `BuildFFUVM` UI to display live log output from the build process. When a build is started, the UI now automatically switches to the Monitor tab. It tails the main log file in real-time and displays the content in a list view, which auto-scrolls as new entries appear. This provides immediate visual feedback on the build progress and any errors without needing to manually open the log file. Additionally, this change adds a Ctrl+C keyboard shortcut to copy selected log lines from the monitor view to the clipboard. --- FFUDevelopment/BuildFFUVM.ps1 | 2 +- FFUDevelopment/BuildFFUVM_UI.ps1 | 78 ++++++++++++++++--- FFUDevelopment/BuildFFUVM_UI.xaml | 9 ++- .../FFUUI.Core/FFUUI.Core.Handlers.psm1 | 22 ++++++ .../FFUUI.Core/FFUUI.Core.Initialize.psm1 | 8 ++ 5 files changed, 106 insertions(+), 13 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index 45d0e39..4eb54ab 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -421,7 +421,7 @@ Import-Module "$PSScriptRoot\FFU.Common" -Force # Set the module's verbose preference to match the script's - allows logging verbose output to console. $moduleInfo = Get-Module -Name 'FFU.Common' if ($moduleInfo) { - & $moduleInfo { $script:VerbosePreference = $args[0] } $VerbosePreference + & $moduleInfo { $script:VerbosePreference = $args[0] } $VerbosePreference | Out-Null } # If a config file is specified and it exists, load it diff --git a/FFUDevelopment/BuildFFUVM_UI.ps1 b/FFUDevelopment/BuildFFUVM_UI.ps1 index 876b702..76f6410 100644 --- a/FFUDevelopment/BuildFFUVM_UI.ps1 +++ b/FFUDevelopment/BuildFFUVM_UI.ps1 @@ -22,7 +22,10 @@ $script:uiState = [PSCustomObject]@{ allDriverModels = [System.Collections.Generic.List[PSCustomObject]]::new(); appsScriptVariablesDataList = [System.Collections.Generic.List[PSCustomObject]]::new(); versionData = $null; - vmSwitchMap = @{} + vmSwitchMap = @{}; + logData = $null; + logStreamReader = $null; + pollTimer = $null }; Flags = @{ installAppsForcedByUpdates = $false; @@ -36,8 +39,8 @@ $script:uiState = [PSCustomObject]@{ } # Remove any existing modules to avoid conflicts -if (Get-Module -Name 'FFU.Common.Core' -ErrorAction SilentlyContinue) { - Remove-Module -Name 'FFU.Common.Core' -Force +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 @@ -114,6 +117,14 @@ $script:uiState.Controls.btnRun.Add_Click({ # 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 + if ($null -ne $script:uiState.Data.logData) { + $script:uiState.Data.logData.Clear() + } + $progressBar = $script:uiState.Controls.pbOverallProgress $txtStatus = $script:uiState.Controls.txtStatus $progressBar.Visibility = 'Visible' @@ -151,25 +162,63 @@ $script:uiState.Controls.btnRun.Add_Click({ # Start the job and store it in the shared state object $script:uiState.Data.currentBuildJob = Start-Job -ScriptBlock $scriptBlock -ArgumentList @($buildParams, $PSScriptRoot) + # Open a stream reader to the main log file + $mainLogPath = "$($config.FFUDevelopmentPath)\FFUDevelopment.log" + # Wait a moment for the file to be created by the new process + Start-Sleep -Seconds 1 + 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. Monitor tab will not update." + } + # Create a timer to poll the job status from the UI thread - $timer = New-Object System.Windows.Threading.DispatcherTimer - $timer.Interval = [TimeSpan]::FromSeconds(1) + $script:uiState.Data.pollTimer = New-Object System.Windows.Threading.DispatcherTimer + $script:uiState.Data.pollTimer.Interval = [TimeSpan]::FromSeconds(1) # Add the Tick event handler - $timer.Add_Tick({ + $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 - # If job is somehow null, stop the timer - if ($null -eq $currentJob) { - $timer.Stop() + # Read from log stream + if ($null -ne $script:uiState.Data.logStreamReader) { + while ($null -ne ($line = $script:uiState.Data.logStreamReader.ReadLine())) { + $script:uiState.Data.logData.Add($line) + # Auto-scroll to the new item + $script:uiState.Controls.lstLogOutput.ScrollIntoView($line) + } + } + + # 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 - $timer.Stop() + if ($null -ne $sender) { + $sender.Stop() + } + $script:uiState.Data.pollTimer = $null + + # Final read of the log stream + if ($null -ne $script:uiState.Data.logStreamReader) { + while ($null -ne ($line = $script:uiState.Data.logStreamReader.ReadLine())) { + $script:uiState.Data.logData.Add($line) + } + $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') { @@ -197,7 +246,7 @@ $script:uiState.Controls.btnRun.Add_Click({ }) # Start the timer - $timer.Start() + $script:uiState.Data.pollTimer.Start() } catch { # This catch block handles errors during the setup of the job (e.g., Get-UIConfig fails) @@ -205,6 +254,13 @@ $script:uiState.Controls.btnRun.Add_Click({ 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' diff --git a/FFUDevelopment/BuildFFUVM_UI.xaml b/FFUDevelopment/BuildFFUVM_UI.xaml index d33fba3..20791ec 100644 --- a/FFUDevelopment/BuildFFUVM_UI.xaml +++ b/FFUDevelopment/BuildFFUVM_UI.xaml @@ -72,7 +72,7 @@ - + @@ -794,6 +794,13 @@ + + + + + + + diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1 index bc7c0aa..86d9d6e 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1 @@ -756,5 +756,27 @@ function Register-EventHandlers { $localState = $window.Tag Invoke-SaveConfiguration -State $localState }) + + # Monitor Tab Event Handlers + $State.Controls.lstLogOutput.Add_KeyDown({ + param($eventSource, $keyEventArgs) + # Check for Ctrl+C + if ($keyEventArgs.Key -eq 'C' -and ($keyEventArgs.KeyboardDevice.Modifiers -band [System.Windows.Input.ModifierKeys]::Control)) { + $listBox = $eventSource + if ($listBox.SelectedItems.Count -gt 0) { + $selectedLines = $listBox.SelectedItems | ForEach-Object { $_.ToString() } + $clipboardText = $selectedLines -join [System.Environment]::NewLine + + try { + [System.Windows.Clipboard]::SetText($clipboardText) + WriteLog "Copied $($listBox.SelectedItems.Count) log lines to clipboard." + } + catch { + WriteLog "Error copying to clipboard: $($_.Exception.Message)" + } + } + $keyEventArgs.Handled = $true + } + }) } Export-ModuleMember -Function * \ No newline at end of file diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1 index e9b1d37..4514b3f 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1 @@ -150,6 +150,14 @@ function Initialize-UIControls { $State.Controls.btnLoadConfig = $window.FindName('btnLoadConfig') $State.Controls.btnBuildConfig = $window.FindName('btnBuildConfig') + # Monitor Tab + $State.Controls.MainTabControl = $window.FindName('MainTabControl') + $State.Controls.MonitorTab = $window.FindName('MonitorTab') + $State.Controls.lstLogOutput = $window.FindName('lstLogOutput') + + # Initialize and bind the log data collection + $State.Data.logData = New-Object System.Collections.ObjectModel.ObservableCollection[string] + $State.Controls.lstLogOutput.ItemsSource = $State.Data.logData } function Initialize-VMSwitchData {