mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 02:09:35 -06:00
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- TabControl with multiple tabs -->
|
||||
<TabControl TabStripPlacement="Left" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" FontSize="14" Padding="10" Grid.Row="0">
|
||||
<TabControl x:Name="MainTabControl" TabStripPlacement="Left" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" FontSize="14" Padding="10" Grid.Row="0">
|
||||
<!-- TAB: Home -->
|
||||
<TabItem Header="Home" Padding="20">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
@@ -794,6 +794,13 @@
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
||||
<!-- TAB: Monitor -->
|
||||
<TabItem Header="Monitor" x:Name="MonitorTab" Padding="20">
|
||||
<Grid Margin="10">
|
||||
<ListBox x:Name="lstLogOutput" SelectionMode="Extended" VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Recycling" ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.HorizontalScrollBarVisibility="Auto" HorizontalContentAlignment="Stretch"/>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
|
||||
@@ -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 *
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user