From c0599cba1f9038963b4943336190895fa237e772 Mon Sep 17 00:00:00 2001 From: Jeffrey <9938813+Raphire@users.noreply.github.com> Date: Sun, 9 Aug 2026 01:24:27 +0200 Subject: [PATCH] feat: add timeout for WinGet uninstall to prevent hanging (#731) --- Scripts/AppRemoval/Remove-SelectedApps.ps1 | 32 +++++++++++++++++----- Tests/Remove-SelectedApps.Tests.ps1 | 27 +++++++++++++++++- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/Scripts/AppRemoval/Remove-SelectedApps.ps1 b/Scripts/AppRemoval/Remove-SelectedApps.ps1 index a33319c..cfa7b27 100644 --- a/Scripts/AppRemoval/Remove-SelectedApps.ps1 +++ b/Scripts/AppRemoval/Remove-SelectedApps.ps1 @@ -96,24 +96,42 @@ function Remove-SelectedApps { Uninstalls an app via WinGet and/or schedules its removal. .DESCRIPTION - Runs winget uninstall for a single app. If the User or Sysprep - parameter was passed, also schedules removal for future logins. + Runs winget uninstall for a single app, with a bounded execution time. + If the User or Sysprep parameter was passed, also schedules removal for + future logins. .PARAMETER app The WinGet package ID to uninstall (e.g. 'Microsoft.BingNews'). + + .PARAMETER TimeoutSeconds + Maximum time to allow the foreground WinGet uninstall to run. Defaults + to 120 seconds. #> function Remove-WinGetApp { - param([string]$app) + param( + [string]$app, + [int]$TimeoutSeconds = 120 + ) if (-not $script:WingetInstalled) { Write-Host "ERROR: WinGet is either not installed or is outdated, $app could not be removed" -ForegroundColor Red return } - Invoke-NonBlocking -ScriptBlock { - param($appId) - winget uninstall --accept-source-agreements --disable-interactivity --id $appId - } -ArgumentList $app + try { + Invoke-NonBlocking -ScriptBlock { + param($appId) + winget uninstall --accept-source-agreements --disable-interactivity --id $appId + } -ArgumentList $app -TimeoutSeconds $TimeoutSeconds + } + catch { + if ($_.Exception.Message -like 'Operation timed out after *') { + Write-Host "WinGet uninstall for $app did not complete within $TimeoutSeconds seconds: $_" -ForegroundColor Red + } + else { + Write-Host "WinGet uninstall for $app failed: $_" -ForegroundColor Red + } + } if ($script:Params.ContainsKey("User")) { Write-Host "Adding scheduled task to uninstall $app for user $(Get-UserName)..." diff --git a/Tests/Remove-SelectedApps.Tests.ps1 b/Tests/Remove-SelectedApps.Tests.ps1 index 512d3b2..ff891c4 100644 --- a/Tests/Remove-SelectedApps.Tests.ps1 +++ b/Tests/Remove-SelectedApps.Tests.ps1 @@ -2,7 +2,7 @@ BeforeAll { function Get-TargetUserForAppRemoval { 'AllUsers' } function Get-WingetInstalledApps { param($TimeOut, [switch]$NonBlocking) @() } function Test-AppInWingetList { param($appId, $InstalledList) $false } - function Invoke-NonBlocking { param($ScriptBlock, $ArgumentList) } + function Invoke-NonBlocking { param($ScriptBlock, $ArgumentList, $TimeoutSeconds) } function Get-UserName { 'Alice' } function Invoke-ForceRemoveEdge {} function Show-MessageBox { 'No' } @@ -109,6 +109,31 @@ Describe 'Remove-WinGetApp' { Should -Invoke Invoke-NonBlocking -Times 1 -Exactly -ParameterFilter { $ArgumentList -eq 'One.App' } Should -Invoke Set-RunOnceWingetTask -Times $Scheduled -Exactly -ParameterFilter { $appId -eq 'One.App' } } + + It 'limits a foreground winget uninstall to two minutes' { + Remove-WinGetApp -app 'One.App' + Should -Invoke Invoke-NonBlocking -Times 1 -Exactly -ParameterFilter { + $ArgumentList -eq 'One.App' -and $TimeoutSeconds -eq 120 + } + } + + It 'passes a specified foreground winget uninstall timeout' { + Remove-WinGetApp -app 'One.App' -TimeoutSeconds 30 + Should -Invoke Invoke-NonBlocking -Times 1 -Exactly -ParameterFilter { + $ArgumentList -eq 'One.App' -and $TimeoutSeconds -eq 30 + } + } + + It 'reports a timed-out winget uninstall and continues' { + $script:Params = @{ User = 'Alice' } + Mock Invoke-NonBlocking { throw 'Operation timed out after 120 seconds' } + + { Remove-WinGetApp -app 'One.App' } | Should -Not -Throw + Should -Invoke Set-RunOnceWingetTask -Times 1 -Exactly + Should -Invoke Write-Host -Times 1 -Exactly -ParameterFilter { + $Object -like '*did not complete within 120 seconds*' -and $ForegroundColor -eq 'Red' + } + } } Describe 'Remove-AppxApp' {