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.
|
# Set the module's verbose preference to match the script's - allows logging verbose output to console.
|
||||||
$moduleInfo = Get-Module -Name 'FFU.Common'
|
$moduleInfo = Get-Module -Name 'FFU.Common'
|
||||||
if ($moduleInfo) {
|
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
|
# 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();
|
allDriverModels = [System.Collections.Generic.List[PSCustomObject]]::new();
|
||||||
appsScriptVariablesDataList = [System.Collections.Generic.List[PSCustomObject]]::new();
|
appsScriptVariablesDataList = [System.Collections.Generic.List[PSCustomObject]]::new();
|
||||||
versionData = $null;
|
versionData = $null;
|
||||||
vmSwitchMap = @{}
|
vmSwitchMap = @{};
|
||||||
|
logData = $null;
|
||||||
|
logStreamReader = $null;
|
||||||
|
pollTimer = $null
|
||||||
};
|
};
|
||||||
Flags = @{
|
Flags = @{
|
||||||
installAppsForcedByUpdates = $false;
|
installAppsForcedByUpdates = $false;
|
||||||
@@ -36,8 +39,8 @@ $script:uiState = [PSCustomObject]@{
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Remove any existing modules to avoid conflicts
|
# Remove any existing modules to avoid conflicts
|
||||||
if (Get-Module -Name 'FFU.Common.Core' -ErrorAction SilentlyContinue) {
|
if (Get-Module -Name 'FFU.Common' -ErrorAction SilentlyContinue) {
|
||||||
Remove-Module -Name 'FFU.Common.Core' -Force
|
Remove-Module -Name 'FFU.Common' -Force
|
||||||
}
|
}
|
||||||
if (Get-Module -Name 'FFUUI.Core' -ErrorAction SilentlyContinue) {
|
if (Get-Module -Name 'FFUUI.Core' -ErrorAction SilentlyContinue) {
|
||||||
Remove-Module -Name 'FFUUI.Core' -Force
|
Remove-Module -Name 'FFUUI.Core' -Force
|
||||||
@@ -114,6 +117,14 @@ $script:uiState.Controls.btnRun.Add_Click({
|
|||||||
# Disable button to prevent multiple clicks
|
# Disable button to prevent multiple clicks
|
||||||
$btnRun.IsEnabled = $false
|
$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
|
$progressBar = $script:uiState.Controls.pbOverallProgress
|
||||||
$txtStatus = $script:uiState.Controls.txtStatus
|
$txtStatus = $script:uiState.Controls.txtStatus
|
||||||
$progressBar.Visibility = 'Visible'
|
$progressBar.Visibility = 'Visible'
|
||||||
@@ -151,25 +162,63 @@ $script:uiState.Controls.btnRun.Add_Click({
|
|||||||
# Start the job and store it in the shared state object
|
# Start the job and store it in the shared state object
|
||||||
$script:uiState.Data.currentBuildJob = Start-Job -ScriptBlock $scriptBlock -ArgumentList @($buildParams, $PSScriptRoot)
|
$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
|
# Create a timer to poll the job status from the UI thread
|
||||||
$timer = New-Object System.Windows.Threading.DispatcherTimer
|
$script:uiState.Data.pollTimer = New-Object System.Windows.Threading.DispatcherTimer
|
||||||
$timer.Interval = [TimeSpan]::FromSeconds(1)
|
$script:uiState.Data.pollTimer.Interval = [TimeSpan]::FromSeconds(1)
|
||||||
|
|
||||||
# Add the Tick event handler
|
# 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
|
# This scriptblock runs on the UI thread, so it can safely access script-scoped variables
|
||||||
$currentJob = $script:uiState.Data.currentBuildJob
|
$currentJob = $script:uiState.Data.currentBuildJob
|
||||||
|
|
||||||
# If job is somehow null, stop the timer
|
# Read from log stream
|
||||||
if ($null -eq $currentJob) {
|
if ($null -ne $script:uiState.Data.logStreamReader) {
|
||||||
$timer.Stop()
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if the job has reached a terminal state
|
# Check if the job has reached a terminal state
|
||||||
if ($currentJob.State -in 'Completed', 'Failed', 'Stopped') {
|
if ($currentJob.State -in 'Completed', 'Failed', 'Stopped') {
|
||||||
# Stop the timer, we're done polling
|
# 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."
|
$finalStatusText = "FFU build completed successfully."
|
||||||
if ($currentJob.State -eq 'Failed') {
|
if ($currentJob.State -eq 'Failed') {
|
||||||
@@ -197,7 +246,7 @@ $script:uiState.Controls.btnRun.Add_Click({
|
|||||||
})
|
})
|
||||||
|
|
||||||
# Start the timer
|
# Start the timer
|
||||||
$timer.Start()
|
$script:uiState.Data.pollTimer.Start()
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
# This catch block handles errors during the setup of the job (e.g., Get-UIConfig fails)
|
# 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
|
WriteLog $errorMessage
|
||||||
[System.Windows.MessageBox]::Show($errorMessage, "Error", "OK", "Error")
|
[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
|
# Re-enable UI elements
|
||||||
$script:uiState.Controls.txtStatus.Text = "FFU build failed to start."
|
$script:uiState.Controls.txtStatus.Text = "FFU build failed to start."
|
||||||
$script:uiState.Controls.pbOverallProgress.Visibility = 'Collapsed'
|
$script:uiState.Controls.pbOverallProgress.Visibility = 'Collapsed'
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- TabControl with multiple tabs -->
|
<!-- 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 -->
|
<!-- TAB: Home -->
|
||||||
<TabItem Header="Home" Padding="20">
|
<TabItem Header="Home" Padding="20">
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
@@ -794,6 +794,13 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</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>
|
</TabControl>
|
||||||
|
|
||||||
<!-- Progress Bar -->
|
<!-- Progress Bar -->
|
||||||
|
|||||||
@@ -756,5 +756,27 @@ function Register-EventHandlers {
|
|||||||
$localState = $window.Tag
|
$localState = $window.Tag
|
||||||
Invoke-SaveConfiguration -State $localState
|
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 *
|
Export-ModuleMember -Function *
|
||||||
@@ -150,6 +150,14 @@ function Initialize-UIControls {
|
|||||||
$State.Controls.btnLoadConfig = $window.FindName('btnLoadConfig')
|
$State.Controls.btnLoadConfig = $window.FindName('btnLoadConfig')
|
||||||
$State.Controls.btnBuildConfig = $window.FindName('btnBuildConfig')
|
$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 {
|
function Initialize-VMSwitchData {
|
||||||
|
|||||||
Reference in New Issue
Block a user