Enhance app removal verification and error reporting (#735)

This commit is contained in:
Jeffrey
2026-08-12 15:56:29 +02:00
committed by GitHub
parent 9505a5a374
commit d1338cd027
16 changed files with 328 additions and 179 deletions
@@ -66,7 +66,14 @@ function Get-WingetInstalledApps {
}
}
if ($dataStart -lt 0 -or $dataStart -ge $lines.Count) { return @() }
# A missing table separator means the output is malformed or empty
if ($dataStart -lt 0) {
return $null
}
if ($dataStart -ge $lines.Count) {
return ,@()
}
$apps = [System.Collections.Generic.List[object]]::new()
@@ -94,7 +101,7 @@ function Get-WingetInstalledApps {
}
}
return @($apps)
return ,@($apps)
}
Remove-Job -Job $job -Force -ErrorAction SilentlyContinue
+82 -92
View File
@@ -39,6 +39,7 @@ function Remove-SelectedApps {
$edgeIds = @('Microsoft.Edge', 'XPFFTQ037JWMHS')
$wingetRemovedApps = @()
$wingetRemovalFailures = @{}
Foreach ($app in $appsList) {
if ($script:CancelRequested) { return }
@@ -52,11 +53,15 @@ function Remove-SelectedApps {
Write-Host "Removing $app"
if ((Get-AppRemovalMethod $app) -eq 'WinGet') {
Remove-WinGetApp -app $app
if (-not (Remove-WinGetApp -app $app)) {
$wingetRemovalFailures[$app] = $true
}
$wingetRemovedApps += $app
}
else {
Remove-AppxApp -app $app -targetUser $targetUser
if (-not (Remove-AppxApp -app $app -targetUser $targetUser)) {
$script:AppRemovalFailures++
}
}
}
@@ -70,24 +75,32 @@ function Remove-SelectedApps {
$postRemovalList = if ($script:WingetInstalled) { Get-WingetInstalledApps -TimeOut 10 -NonBlocking } else { $null }
$edgeForceRemoveRequested = $false
foreach ($app in $wingetRemovedApps) {
if (-not (Test-AppStillInstalled -appId $app -InstalledList $postRemovalList)) {
continue
}
if ($edgeIds -contains $app) {
Write-Host "Unable to uninstall Microsoft Edge via WinGet" -ForegroundColor Red
if (-not $edgeForceRemoveRequested) {
Request-EdgeForceRemove
$edgeForceRemoveRequested = $true
if ($null -eq $postRemovalList) {
$script:AppRemovalVerificationUnavailable = $true
}
else {
foreach ($app in $wingetRemovedApps) {
if (-not (Test-AppInWingetList -appId $app -InstalledList $postRemovalList)) {
continue
}
}
else {
Write-Host "Unable to uninstall $app via WinGet" -ForegroundColor Red
if ($edgeIds -contains $app) {
Write-Host "Unable to uninstall Microsoft Edge via WinGet" -ForegroundColor Red
if (-not $edgeForceRemoveRequested) {
Request-EdgeForceRemove
$edgeForceRemoveRequested = $true
}
}
else {
Write-Host "Unable to uninstall $app via WinGet" -ForegroundColor Red
}
$wingetRemovalFailures[$app] = $true
}
}
}
$script:AppRemovalFailures += $wingetRemovalFailures.Count
Write-Host ""
}
@@ -114,33 +127,40 @@ function Remove-WinGetApp {
)
if (-not $script:WingetInstalled) {
Write-Host "ERROR: WinGet is either not installed or is outdated, $app could not be removed" -ForegroundColor Red
return
Write-Error "WinGet is either not installed or is outdated; $app could not be removed"
return $false
}
$uninstallSucceeded = $true
try {
Invoke-NonBlocking -ScriptBlock {
$uninstallSucceeded = Invoke-NonBlocking -ScriptBlock {
param($appId)
winget uninstall --accept-source-agreements --disable-interactivity --id $appId
$null = & winget uninstall --accept-source-agreements --disable-interactivity --id $appId 2>&1
return $true
} -ArgumentList $app -TimeoutSeconds $TimeoutSeconds
$uninstallSucceeded = [bool]$uninstallSucceeded
}
catch {
$uninstallSucceeded = $false
if ($_.Exception.Message -like 'Operation timed out after *') {
Write-Host "WinGet uninstall for $app did not complete within $TimeoutSeconds seconds: $_" -ForegroundColor Red
Write-Error "WinGet uninstall for $app did not complete within $TimeoutSeconds seconds: $_"
}
else {
Write-Host "WinGet uninstall for $app failed: $_" -ForegroundColor Red
Write-Error "WinGet uninstall for $app failed: $_"
}
}
$scheduleSucceeded = $true
if ($script:Params.ContainsKey("User")) {
Write-Host "Adding scheduled task to uninstall $app for user $(Get-UserName)..."
Set-RunOnceWingetTask -appId $app
$scheduleSucceeded = Set-RunOnceWingetTask -appId $app
}
elseif ($script:Params.ContainsKey("Sysprep")) {
Write-Host "Adding scheduled task to uninstall $app for new users..."
Set-RunOnceWingetTask -appId $app
$scheduleSucceeded = Set-RunOnceWingetTask -appId $app
}
return ($uninstallSucceeded -and $scheduleSucceeded)
}
<#
@@ -159,80 +179,48 @@ function Remove-AppxApp {
$appPattern = '*' + $app + '*'
try {
switch ($targetUser) {
"AllUsers" {
Invoke-NonBlocking -ScriptBlock {
param($pattern)
Get-AppxPackage -Name $pattern -AllUsers | Remove-AppxPackage -AllUsers -ErrorAction Continue
Get-AppxProvisionedPackage -Online | Where-Object { $_.PackageName -like $pattern } | ForEach-Object { Remove-ProvisionedAppxPackage -Online -AllUsers -PackageName $_.PackageName }
} -ArgumentList $appPattern
}
"CurrentUser" {
Invoke-NonBlocking -ScriptBlock {
param($pattern)
Get-AppxPackage -Name $pattern | Remove-AppxPackage -ErrorAction Continue
} -ArgumentList $appPattern
}
default {
Invoke-NonBlocking -ScriptBlock {
param($pattern, $user)
$userAccount = New-Object System.Security.Principal.NTAccount($user)
$removalResult = Invoke-NonBlocking -ScriptBlock {
param($pattern, $target)
$removalErrors = @()
$getPackageParams = @{ Name = $pattern; ErrorAction = 'Continue'; ErrorVariable = '+removalErrors' }
$removePackageParams = @{ ErrorAction = 'Continue'; ErrorVariable = '+removalErrors' }
switch ($target) {
'AllUsers' {
$getPackageParams.AllUsers = $true
$removePackageParams.AllUsers = $true
}
'CurrentUser' { }
default {
$userAccount = New-Object System.Security.Principal.NTAccount($target)
$userSid = $userAccount.Translate([System.Security.Principal.SecurityIdentifier]).Value
Get-AppxPackage -Name $pattern -User $userSid | Remove-AppxPackage -User $userSid -ErrorAction Continue
} -ArgumentList @($appPattern, $targetUser)
$getPackageParams.User = $userSid
$removePackageParams.User = $userSid
}
}
}
foreach ($package in @(Get-AppxPackage @getPackageParams)) {
$removePackageParams.Package = $package.PackageFullName
$null = Remove-AppxPackage @removePackageParams
}
if ($target -eq 'AllUsers') {
$provisionedPackages = @(Get-AppxProvisionedPackage -Online -ErrorAction Continue -ErrorVariable +removalErrors | Where-Object { $_.PackageName -like $pattern })
foreach ($package in $provisionedPackages) {
$null = Remove-ProvisionedAppxPackage -Online -AllUsers -PackageName $package.PackageName -ErrorAction Continue -ErrorVariable +removalErrors
}
}
return [PSCustomObject]@{ Success = ($removalErrors.Count -eq 0) }
} -ArgumentList @($appPattern, $targetUser)
}
catch {
Write-Verbose "Something went wrong while trying to remove $($app): $_"
}
}
<#
.SYNOPSIS
Checks whether an app package is still installed after a removal attempt.
.DESCRIPTION
Checks Get-AppxPackage across all users first (fast, no process launch),
then falls back to a pre-fetched or live winget list for non-Appx packages.
Uses Test-AppInWingetList which provides exact-match-first with substring
fallback against the parsed winget objects.
Returns $true if the app is still present, $false otherwise.
.PARAMETER appId
The package identifier to check (e.g. 'Microsoft.BingNews').
.PARAMETER InstalledList
Optional pre-fetched array of winget objects from Get-WingetInstalledApps.
When provided, used directly; otherwise a live winget call is made.
#>
function Test-AppStillInstalled {
param(
[string]$appId,
[object[]]$InstalledList
)
# Check Get-AppxPackage for all users first (fast, covers all Store apps).
if (Get-AppxPackage -Name "$appId" -AllUsers -ErrorAction SilentlyContinue) {
return $true
Write-Error "Unable to remove $app via Appx: $_"
return $false
}
# Use the pre-fetched list if provided; otherwise fall back to a live winget call.
if ($InstalledList) {
return (Test-AppInWingetList -appId $appId -InstalledList $InstalledList)
}
if ($script:WingetInstalled) {
$liveList = Get-WingetInstalledApps -TimeOut 10 -NonBlocking
if (Test-AppInWingetList -appId $appId -InstalledList $liveList) {
return $true
}
}
else {
Write-Warning "Unable to verify whether '$appId' is still installed (WinGet is unavailable)"
}
return $false
return [bool]($removalResult -and $removalResult.Success)
}
<#
@@ -352,8 +340,10 @@ function Set-RunOnceWingetTask {
param($op)
Invoke-RegistryOperation -Operation $op -RegFilePath '<dynamic>'
} -ArgumentObject $operation
return $true
}
catch {
Write-Host "Failed to schedule uninstall task for $($appId): $_" -ForegroundColor Red
Write-Error "Failed to schedule uninstall task for $($appId): $_"
return $false
}
}
+2 -2
View File
@@ -39,7 +39,7 @@ function Show-CliDefaultModeOptions {
}
catch {
Write-Error "Failed to load settings from DefaultSettings.json file: $_"
Wait-ForKeyPress
Wait-ForKeyPress -ExitCode 1
}
Save-Settings
@@ -51,4 +51,4 @@ function Show-CliDefaultModeOptions {
Write-PendingChanges
Write-CliHeader 'Default Mode'
}
}
+2 -2
View File
@@ -7,7 +7,7 @@ function Show-CliLastUsedSettings {
}
catch {
Write-Error "Failed to load settings from LastUsedSettings.json file: $_"
Wait-ForKeyPress
Wait-ForKeyPress -ExitCode 1
}
if ($Silent) {
@@ -17,4 +17,4 @@ function Show-CliLastUsedSettings {
Write-PendingChanges
Write-CliHeader 'Custom Mode'
}
}
+12 -1
View File
@@ -1,4 +1,15 @@
<#
.SYNOPSIS
Waits for user acknowledgement, then exits the script.
.PARAMETER ExitCode
Process exit code to return after acknowledgement. Defaults to 0.
#>
function Wait-ForKeyPress {
param(
[int]$ExitCode = 0
)
# Suppress prompt if Silent parameter was passed
if (-not $Silent) {
Write-Output ""
@@ -7,5 +18,5 @@ function Wait-ForKeyPress {
}
Stop-Transcript
Exit
Exit $ExitCode
}
+14 -2
View File
@@ -320,6 +320,8 @@ function Invoke-AllChanges {
}
$script:RegistryImportFailures = 0
$script:AppRemovalFailures = 0
$script:AppRemovalVerificationUnavailable = $false
# ---- Gather work items ----
$applyIds = @()
@@ -422,12 +424,22 @@ function Invoke-AllChanges {
}
# ================================================================
# Final: Report registry import failures
# Final: Report registry import and app removal failures
# ================================================================
if ($script:RegistryImportFailures -gt 0) {
Write-Host ""
Write-Host "$($script:RegistryImportFailures) registry import change(s) failed. See output above for details." -ForegroundColor Yellow
Write-Warning "$($script:RegistryImportFailures) registry import change(s) failed. See output above for details."
}
if ($script:AppRemovalFailures -gt 0) {
Write-Host ""
Write-Warning "$($script:AppRemovalFailures) app removal(s) failed. See output above for details."
}
if ($script:AppRemovalVerificationUnavailable) {
Write-Warning "Unable to verify if all apps were uninstalled successfully."
}
}
<#
+1 -1
View File
@@ -23,7 +23,7 @@ function Invoke-RestartExplorer {
Write-Host "Warning: '$displayLabel' requires a reboot to take full effect" -ForegroundColor Yellow
}
# Only restart if the powershell process matches the OS architecture.
# Only restart if the PowerShell process matches the OS architecture.
# Restarting explorer from a 32bit PowerShell window will fail on a 64bit OS
if ([Environment]::Is64BitProcess -eq [Environment]::Is64BitOperatingSystem) {
Write-Host "Restarting the Windows Explorer process... (This may cause your screen to flicker)"
+1 -1
View File
@@ -41,6 +41,6 @@ function Import-AppsFromFile {
}
catch {
Write-Error "Unable to read apps list from file: $appsFilePath"
Wait-ForKeyPress
Wait-ForKeyPress -ExitCode 1
}
}
+24 -8
View File
@@ -115,6 +115,9 @@ function Show-ApplyModal {
Invoke-AllChanges
$registryImportFailureCount = [int]$script:RegistryImportFailures
$appRemovalFailureCount = [int]$script:AppRemovalFailures
$failureCount = $registryImportFailureCount + $appRemovalFailureCount
$appRemovalVerificationUnavailable = [bool]$script:AppRemovalVerificationUnavailable
# Restart explorer if requested
if ($InvokeRestartExplorer -and -not $script:CancelRequested) {
@@ -128,11 +131,6 @@ function Show-ApplyModal {
}
Write-Host ""
if ($script:CancelRequested) {
Write-Host "Script execution was cancelled by the user. Some changes may not have been applied."
} elseif ($registryImportFailureCount -eq 0) {
Write-Host "All changes have been applied successfully!"
}
# Show completion state
$script:ApplyProgressBarEl.Value = 100
@@ -140,16 +138,34 @@ function Show-ApplyModal {
$script:ApplyCompletionPanel.Visibility = 'Visible'
if ($script:CancelRequested) {
Write-Warning "Script execution was cancelled by the user. Any remaining changes were not applied."
$script:ApplyCompletionIconEl.Text = [char]0xE7BA
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e8912d"))
$script:ApplyCompletionTitleEl.Text = "Cancelled"
$script:ApplyCompletionMessageEl.Text = "Script execution was cancelled by the user."
} elseif ($registryImportFailureCount -gt 0) {
} elseif ($failureCount -gt 0 -or $appRemovalVerificationUnavailable) {
if ($failureCount -gt 0) {
Write-Host "Script completed with $failureCount error(s)."
}
$script:ApplyCompletionIconEl.Text = [char]0xE7BA
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e8912d"))
$script:ApplyCompletionTitleEl.Text = "Changes Applied with Errors"
$script:ApplyCompletionMessageEl.Text = "$registryImportFailureCount registry change(s) failed. See console for details."
if ($failureCount -eq 0 -and $appRemovalVerificationUnavailable) {
$script:ApplyCompletionTitleEl.Text = "Changes Applied"
$script:ApplyCompletionMessageEl.Text = "All changes were applied without errors, but Win11Debloat could not confirm that all selected apps were successfully uninstalled."
}
else {
$script:ApplyCompletionTitleEl.Text = "Changes Applied with Errors"
$failureMessages = @()
if ($registryImportFailureCount -gt 0) { $failureMessages += "$registryImportFailureCount registry change(s) failed" }
if ($appRemovalFailureCount -gt 0) { $failureMessages += "$appRemovalFailureCount app removal(s) failed" }
if ($appRemovalVerificationUnavailable) { $failureMessages += "Unable to verify if all apps were uninstalled successfully" }
$script:ApplyCompletionMessageEl.Text = "$($failureMessages -join '; '). See console for details."
}
} else {
Write-Host "All changes have been applied successfully!"
$script:ApplyCompletionTitleEl.Text = "Changes Applied"
# Show completion message with reboot instructions if any applied features require reboot
+19 -10
View File
@@ -105,13 +105,12 @@ param (
[switch]$HideDriveLetters
)
# Show error if current powershell environment does not have LanguageMode set to FullLanguage
# Check if current PowerShell environment is limited by security policies
if ($ExecutionContext.SessionState.LanguageMode -ne "FullLanguage") {
Write-Host "Error: Win11Debloat is unable to run on your system. PowerShell execution is restricted by security policies" -ForegroundColor Red
Write-Output ""
Write-Output "Press enter to exit..."
Read-Host | Out-Null
Exit
Write-Error "Win11Debloat is unable to run on your system, PowerShell execution is restricted by security policies"
Write-Output "Press any key to exit..."
$null = [System.Console]::ReadKey()
Exit 1
}
Clear-Host
@@ -139,7 +138,7 @@ catch {
Write-Output ""
Write-Output "Press enter to exit..."
Read-Host | Out-Null
Exit
Exit 1
}
# Remove old script folder if it exists, but keep configs, logs and backups
@@ -207,7 +206,7 @@ $arguments = $($PSBoundParameters.GetEnumerator() | Where-Object { $_.Key -ne 'D
Write-Output ""
Write-Output "> Launching Win11Debloat..."
# Minimize the powershell window when no parameters are provided
# Minimize the PowerShell window when no parameters are provided
if ($arguments.Count -eq 0) {
$windowStyle = "Minimized"
}
@@ -215,7 +214,7 @@ else {
$windowStyle = "Normal"
}
# Remove Powershell 7 modules from path to prevent module loading issues in the script
# Remove PowerShell 7 modules from path to prevent module loading issues in the script
if ($PSVersionTable.PSVersion.Major -ge 7) {
$NewPSModulePath = $env:PSModulePath -split ';' | Where-Object -FilterScript { $_ -like '*WindowsPowerShell*' }
$env:PSModulePath = $NewPSModulePath -join ';'
@@ -223,11 +222,20 @@ if ($PSVersionTable.PSVersion.Major -ge 7) {
# Run Win11Debloat script with the provided arguments
$debloatScriptPath = Join-Path $tempWorkPath 'Win11Debloat.ps1'
$debloatProcess = Start-Process powershell.exe -WindowStyle $windowStyle -PassThru -ArgumentList "-executionpolicy bypass -File `"$debloatScriptPath`" $arguments" -Verb RunAs
$exitCode = 0
$debloatProcess = $null
try {
$debloatProcess = Start-Process powershell.exe -WindowStyle $windowStyle -PassThru -ArgumentList "-executionpolicy bypass -File `"$debloatScriptPath`" $arguments" -Verb RunAs -ErrorAction Stop
}
catch {
$exitCode = 1
Write-Error "Failed to start Win11Debloat: $_"
}
# Wait for the process to finish before continuing
if ($null -ne $debloatProcess) {
$debloatProcess.WaitForExit()
$exitCode = $debloatProcess.ExitCode
}
# Remove all remaining script files, except for configs, logs and backups
@@ -240,3 +248,4 @@ if (Test-Path $tempWorkPath) {
}
Write-Output ""
Exit $exitCode
+2 -2
View File
@@ -46,9 +46,9 @@ function Get-UserDirectory {
}
catch {
Write-Error "Something went wrong when trying to find the user directory path for user $userName. Please ensure the user exists on this system"
Wait-ForKeyPress
Wait-ForKeyPress -ExitCode 1
}
Write-Error "Unable to find user directory path for user $userName"
Wait-ForKeyPress
Wait-ForKeyPress -ExitCode 1
}