mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-08-11 02:21:16 +10:00
Fix UI helper errors during headless -Preset and -Config runs (#4941)
* Make UI helpers no-op when no window exists The -Preset and -Config paths run Invoke-WinUtilAutoRun before the XAML form is created and before PresentationCore is loaded, so every call into Invoke-WPFUIThread failed with InvokeMethodOnNull and every call into Set-WinUtilTweaksProgressIndicator failed to resolve [Windows.Visibility]. The errors were non-terminating, so tweaks still applied, but each run filled the log with noise. Loading presentationframework earlier does not help: Visibility lives in PresentationCore, which only loads once a WPF object is instantiated, and the XAML-named progress controls do not exist in these paths either. Guarding the two helpers covers Invoke-WPFtweaksbutton, Invoke-WPFundoall and Invoke-WPFFeatureInstall, which the existing per-call-site $hasUI convention never reached. * Suppress stray console output from automation runs Invoke-WPFRunspace returns an IAsyncResult that no caller uses, and Set-WinUtilRegistry was the only New-PSDrive call site that did not suppress its output. A GUI click handler discards both, but the -Preset and -Config paths call the workflows directly, so an async handle dump and a PSDrive table landed in the user's log. The handle itself is kept because pester/runspace.Tests.ps1 asserts that Invoke-WPFRunspace returns a single IAsyncResult, so the suppression goes at the four call sites reachable from Invoke-WinUtilAutoRun. * Add project learning for window-free UI helpers
This commit is contained in:
@@ -163,6 +163,7 @@ When the user corrects an agent approach, add or tighten one concrete rule here
|
|||||||
- Import Pester 5.8.0 before running tests so `Invoke-Pester -Output Detailed -CI` does not resolve to Windows' inbox Pester 3.4.0.
|
- Import Pester 5.8.0 before running tests so `Invoke-Pester -Output Detailed -CI` does not resolve to Windows' inbox Pester 3.4.0.
|
||||||
- Keep package install/uninstall process launches simple unless explicitly requested; do not add a separate stdout/stderr process logging helper for winget or Chocolatey.
|
- Keep package install/uninstall process launches simple unless explicitly requested; do not add a separate stdout/stderr process logging helper for winget or Chocolatey.
|
||||||
- When the active log file is owned by `Start-Transcript`, do not call `Add-Content` against that file; write to host output so the transcript captures the line in the same log file without recording a terminating-error diagnostic.
|
- When the active log file is owned by `Start-Transcript`, do not call `Add-Content` against that file; write to host output so the transcript captures the line in the same log file without recording a terminating-error diagnostic.
|
||||||
|
- Keep UI helpers such as `Invoke-WPFUIThread` and `Set-WinUtilTweaksProgressIndicator` safe to call without a window; the `-Preset` and `-Config` paths run the workflows before the form is created and before PresentationCore is loaded.
|
||||||
- Log install/uninstall package names and package-manager IDs before queuing background runspace work; do not rely on runspace host output for the package identity.
|
- Log install/uninstall package names and package-manager IDs before queuing background runspace work; do not rely on runspace host output for the package identity.
|
||||||
- For Win11 Creator, start each new ISO modification in a fresh `WinUtil_Win11ISO_*` temp directory; existing-work detection is only for resuming/exporting already modified media.
|
- For Win11 Creator, start each new ISO modification in a fresh `WinUtil_Win11ISO_*` temp directory; existing-work detection is only for resuming/exporting already modified media.
|
||||||
- For Win11 Creator driver injection, keep offline WIM servicing to one mount, one `/Add-Driver`, and one commit; do not export editions or run unrelated WIM cleanup, and reject damaged metadata before ISO export.
|
- For Win11 Creator driver injection, keep offline WIM servicing to one mount, one `/Add-Driver`, and one commit; do not export editions or run unrelated WIM cleanup, and reject damaged metadata before ISO export.
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function Set-WinUtilRegistry {
|
|||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if(!(Test-Path 'HKU:\')) {New-PSDrive -PSProvider Registry -Name HKU -Root HKEY_USERS}
|
if(!(Test-Path 'HKU:\')) {New-PSDrive -PSProvider Registry -Name HKU -Root HKEY_USERS | Out-Null}
|
||||||
|
|
||||||
If (!(Test-Path $Path)) {
|
If (!(Test-Path $Path)) {
|
||||||
Write-Host "$Path was not found. Creating..."
|
Write-Host "$Path was not found. Creating..."
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ function Set-WinUtilTweaksProgressIndicator {
|
|||||||
[int]$Percent
|
[int]$Percent
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if ($null -eq $sync.form -or $null -eq $sync.form.Dispatcher) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
$indicatorVisible = if ($Visible) { [Windows.Visibility]::Visible } else { [Windows.Visibility]::Collapsed }
|
$indicatorVisible = if ($Visible) { [Windows.Visibility]::Visible } else { [Windows.Visibility]::Collapsed }
|
||||||
$indicatorLabel = $Label
|
$indicatorLabel = $Label
|
||||||
$hasLabel = $PSBoundParameters.ContainsKey('Label')
|
$hasLabel = $PSBoundParameters.ContainsKey('Label')
|
||||||
|
|||||||
@@ -96,5 +96,5 @@ function Invoke-WPFAppxRemoval {
|
|||||||
$sync.ProcessRunning = $false
|
$sync.ProcessRunning = $false
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
} | Out-Null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,5 +36,5 @@ function Invoke-WPFFeatureInstall {
|
|||||||
Write-Host "--- Features are Installed ---"
|
Write-Host "--- Features are Installed ---"
|
||||||
Write-Host "--- A Reboot may be required ---"
|
Write-Host "--- A Reboot may be required ---"
|
||||||
Write-Host "==================================="
|
Write-Host "==================================="
|
||||||
}
|
} | Out-Null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,5 +110,5 @@ function Invoke-WPFInstall {
|
|||||||
}
|
}
|
||||||
$sync.ProcessRunning = $False
|
$sync.ProcessRunning = $False
|
||||||
}
|
}
|
||||||
}
|
} | Out-Null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
function Invoke-WPFUIThread ($ScriptBlock) {
|
function Invoke-WPFUIThread ($ScriptBlock) {
|
||||||
|
if ($null -eq $sync.form -or $null -eq $sync.form.Dispatcher) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
$sync.form.Dispatcher.Invoke([action]$ScriptBlock)
|
$sync.form.Dispatcher.Invoke([action]$ScriptBlock)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,5 +88,5 @@ function Invoke-WPFtweaksbutton {
|
|||||||
Write-Host "-- Tweaks are Finished ---"
|
Write-Host "-- Tweaks are Finished ---"
|
||||||
Write-Host "================================="
|
Write-Host "================================="
|
||||||
Write-WinUtilLog -Component "Tweaks" -Message "Tweaks workflow completed."
|
Write-WinUtilLog -Component "Tweaks" -Message "Tweaks workflow completed."
|
||||||
}
|
} | Out-Null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
#===========================================================================
|
||||||
|
# Tests - UI Helpers During Headless (-Preset / -Config) Runs
|
||||||
|
#===========================================================================
|
||||||
|
|
||||||
|
BeforeAll {
|
||||||
|
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||||
|
|
||||||
|
if (-not ("Windows.Visibility" -as [type])) {
|
||||||
|
Add-Type @"
|
||||||
|
namespace Windows
|
||||||
|
{
|
||||||
|
public enum Visibility
|
||||||
|
{
|
||||||
|
Visible,
|
||||||
|
Collapsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFUIThread.ps1")
|
||||||
|
. (Join-Path $script:repoRoot "functions\private\Set-WinUtilTweaksProgressIndicator.ps1")
|
||||||
|
|
||||||
|
function script:New-WinUtilFakeForm {
|
||||||
|
$dispatcher = New-Object psobject
|
||||||
|
$dispatcher | Add-Member -MemberType NoteProperty -Name InvokeCount -Value 0
|
||||||
|
$dispatcher | Add-Member -MemberType ScriptMethod -Name Invoke -Value {
|
||||||
|
param($Action)
|
||||||
|
|
||||||
|
$null = $Action
|
||||||
|
$this.InvokeCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = New-Object psobject
|
||||||
|
$form | Add-Member -MemberType NoteProperty -Name Dispatcher -Value $dispatcher
|
||||||
|
return $form
|
||||||
|
}
|
||||||
|
|
||||||
|
function script:New-WinUtilFakeIndicatorControlSet {
|
||||||
|
@{
|
||||||
|
Bar = [pscustomobject]@{ Visibility = [Windows.Visibility]::Collapsed }
|
||||||
|
Label = [pscustomobject]@{ Text = "" }
|
||||||
|
Value = [pscustomobject]@{ Value = 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Describe "Invoke-WPFUIThread without a window" {
|
||||||
|
AfterEach {
|
||||||
|
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
It "does nothing when the automation paths run before the form is created" {
|
||||||
|
$script:sync = [Hashtable]::Synchronized(@{})
|
||||||
|
$script:blockRan = $false
|
||||||
|
|
||||||
|
{ Invoke-WPFUIThread -ScriptBlock { $script:blockRan = $true } } | Should -Not -Throw
|
||||||
|
$script:blockRan | Should -BeFalse
|
||||||
|
}
|
||||||
|
|
||||||
|
It "does nothing when the form exists but has no dispatcher" {
|
||||||
|
$script:sync = [Hashtable]::Synchronized(@{ Form = [pscustomobject]@{ Dispatcher = $null } })
|
||||||
|
$script:blockRan = $false
|
||||||
|
|
||||||
|
{ Invoke-WPFUIThread -ScriptBlock { $script:blockRan = $true } } | Should -Not -Throw
|
||||||
|
$script:blockRan | Should -BeFalse
|
||||||
|
}
|
||||||
|
|
||||||
|
It "still marshals onto the dispatcher when a window exists" {
|
||||||
|
$form = New-WinUtilFakeForm
|
||||||
|
$script:sync = [Hashtable]::Synchronized(@{ Form = $form })
|
||||||
|
|
||||||
|
Invoke-WPFUIThread -ScriptBlock { }
|
||||||
|
|
||||||
|
$form.Dispatcher.InvokeCount | Should -Be 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Describe "Set-WinUtilTweaksProgressIndicator without a window" {
|
||||||
|
AfterEach {
|
||||||
|
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
It "returns before resolving WPF types when the form is missing" {
|
||||||
|
$script:sync = [Hashtable]::Synchronized(@{})
|
||||||
|
|
||||||
|
{ Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Creating restore point" -Percent 0 } | Should -Not -Throw
|
||||||
|
}
|
||||||
|
|
||||||
|
It "still updates the indicator controls when a window exists" {
|
||||||
|
$controls = New-WinUtilFakeIndicatorControlSet
|
||||||
|
$script:sync = [Hashtable]::Synchronized(@{
|
||||||
|
Form = New-WinUtilFakeForm
|
||||||
|
WPFTweaksProgressBar = $controls.Bar
|
||||||
|
WPFTweaksProgressLabel = $controls.Label
|
||||||
|
WPFTweaksProgressValue = $controls.Value
|
||||||
|
})
|
||||||
|
|
||||||
|
Mock Invoke-WPFUIThread { & $ScriptBlock }
|
||||||
|
|
||||||
|
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Applying WPFTweaksTelemetry (1/17)" -Percent 42
|
||||||
|
|
||||||
|
$controls.Bar.Visibility | Should -Be ([Windows.Visibility]::Visible)
|
||||||
|
$controls.Label.Text | Should -Be "Applying WPFTweaksTelemetry (1/17)"
|
||||||
|
$controls.Value.Value | Should -Be 42
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user