mirror of
https://github.com/Raphire/Win11Debloat.git
synced 2026-08-22 07:36:25 +00:00
Enhance app removal verification and error reporting (#735)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
<#
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -41,6 +41,6 @@ function Import-AppsFromFile {
|
||||
}
|
||||
catch {
|
||||
Write-Error "Unable to read apps list from file: $appsFilePath"
|
||||
Wait-ForKeyPress
|
||||
Wait-ForKeyPress -ExitCode 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -37,16 +37,33 @@ Describe 'Get-WingetInstalledApps' {
|
||||
Should -Invoke Invoke-NonBlocking -Times 1 -Exactly
|
||||
}
|
||||
|
||||
It 'returns an empty collection when winget output has no table separator' {
|
||||
It 'returns null when winget output has no table separator' {
|
||||
$script:WingetTestJob = New-WingetTestJob
|
||||
Mock Start-Job { $script:WingetTestJob }
|
||||
Mock Wait-Job { $script:WingetTestJob }
|
||||
Mock Receive-Job { @('Name Id', 'No parseable table') }
|
||||
|
||||
@(Get-WingetInstalledApps) | Should -BeNullOrEmpty
|
||||
Get-WingetInstalledApps | Should -BeNullOrEmpty
|
||||
Should -Invoke Remove-Job -Times 1 -Exactly -ParameterFilter { -not $Force }
|
||||
}
|
||||
|
||||
It 'returns an empty collection for a valid table with no data rows' {
|
||||
$script:WingetTestJob = New-WingetTestJob
|
||||
Mock Start-Job { $script:WingetTestJob }
|
||||
Mock Wait-Job { $script:WingetTestJob }
|
||||
Mock Receive-Job {
|
||||
@(
|
||||
'Name Id Version'
|
||||
'-----------------------------------------------------------------------'
|
||||
)
|
||||
}
|
||||
|
||||
$result = @(Get-WingetInstalledApps)
|
||||
|
||||
$result | Should -HaveCount 1
|
||||
@($result[0]).Count | Should -Be 0
|
||||
}
|
||||
|
||||
It 'parses valid rows and skips malformed rows' {
|
||||
$script:WingetTestJob = New-WingetTestJob
|
||||
Mock Start-Job { $script:WingetTestJob }
|
||||
@@ -61,7 +78,7 @@ Describe 'Get-WingetInstalledApps' {
|
||||
)
|
||||
}
|
||||
|
||||
$result = @(Get-WingetInstalledApps)
|
||||
$result = @(Get-WingetInstalledApps)[0]
|
||||
|
||||
$result | Should -HaveCount 2
|
||||
$result.Id | Should -Be @('Contoso.App', 'Fabrikam.Tools')
|
||||
@@ -80,7 +97,7 @@ Describe 'Get-WingetInstalledApps' {
|
||||
)
|
||||
}
|
||||
|
||||
$result = @(Get-WingetInstalledApps)
|
||||
$result = @(Get-WingetInstalledApps)[0]
|
||||
|
||||
$result | Should -HaveCount 2
|
||||
$result[0].Name | Should -Be 'Contoso hulpmiddel voor gegevens'
|
||||
|
||||
@@ -325,6 +325,7 @@ Describe 'Invoke-AllChanges' {
|
||||
Mock Invoke-ApplyFeatures {}
|
||||
Mock Invoke-UndoFeatures {}
|
||||
Mock Write-Host {}
|
||||
Mock Write-Warning {}
|
||||
}
|
||||
|
||||
It 'backs up registry work before applying and undoing selected features' {
|
||||
@@ -407,6 +408,28 @@ Describe 'Invoke-AllChanges' {
|
||||
|
||||
Invoke-AllChanges
|
||||
|
||||
Should -Invoke Write-Host -Times 1 -Exactly -ParameterFilter { $Object -match '2 registry import change' }
|
||||
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match '2 registry import change' }
|
||||
}
|
||||
|
||||
It 'reports app removal failures after all requested work completes' {
|
||||
$script:Params = @{ CustomApply = $true }
|
||||
$script:UndoParams = @{}
|
||||
Mock Invoke-ApplyFeatures { $script:AppRemovalFailures = 2 }
|
||||
|
||||
Invoke-AllChanges
|
||||
|
||||
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match '2 app removal\(s\) failed' }
|
||||
}
|
||||
|
||||
It 'warns when app removals could not be verified' {
|
||||
$script:Params = @{ CustomApply = $true }
|
||||
$script:UndoParams = @{}
|
||||
Mock Invoke-ApplyFeatures { $script:AppRemovalVerificationUnavailable = $true }
|
||||
Mock Write-Warning {}
|
||||
|
||||
Invoke-AllChanges
|
||||
|
||||
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -eq 'Unable to verify if all apps were uninstalled successfully.' }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
BeforeAll {
|
||||
function Get-TargetUserForAppRemoval { 'AllUsers' }
|
||||
function Get-WingetInstalledApps { param($TimeOut, [switch]$NonBlocking) @() }
|
||||
function Get-WingetInstalledApps { param($TimeOut, [switch]$NonBlocking) return ,@() }
|
||||
function Test-AppInWingetList { param($appId, $InstalledList) $false }
|
||||
function Invoke-NonBlocking { param($ScriptBlock, $ArgumentList, $TimeoutSeconds) }
|
||||
function Get-UserName { 'Alice' }
|
||||
@@ -8,6 +8,7 @@ BeforeAll {
|
||||
function Show-MessageBox { 'No' }
|
||||
function Invoke-WithTargetUserHive { param($TargetUserName, $ScriptBlock, $ArgumentObject) }
|
||||
function Invoke-RegistryOperation { param($Operation, $RegFilePath) }
|
||||
function Resolve-UserProfileContext { param($UserName) $null }
|
||||
|
||||
. (Join-Path $PSScriptRoot '..\Scripts\AppRemoval\Remove-SelectedApps.ps1')
|
||||
}
|
||||
@@ -18,12 +19,14 @@ Describe 'Remove-SelectedApps' {
|
||||
$script:CancelRequested = $false
|
||||
$script:ApplySubStepCallback = $null
|
||||
$script:WingetInstalled = $true
|
||||
$script:AppRemovalFailures = 0
|
||||
$script:AppRemovalVerificationUnavailable = $false
|
||||
Mock Get-TargetUserForAppRemoval { 'AllUsers' }
|
||||
Mock Get-AppRemovalMethod { 'Appx' }
|
||||
Mock Remove-WinGetApp {}
|
||||
Mock Remove-AppxApp {}
|
||||
Mock Test-AppStillInstalled { $false }
|
||||
Mock Get-WingetInstalledApps { @() }
|
||||
Mock Remove-WinGetApp { $true }
|
||||
Mock Remove-AppxApp { $true }
|
||||
Mock Test-AppInWingetList { $false }
|
||||
Mock Get-WingetInstalledApps { return ,@() }
|
||||
Mock Request-EdgeForceRemove {}
|
||||
Mock Write-Host {}
|
||||
}
|
||||
@@ -43,6 +46,13 @@ Describe 'Remove-SelectedApps' {
|
||||
Should -Invoke Remove-AppxApp -Times 1 -Exactly -ParameterFilter { $app -eq 'Appx.App' -and $targetUser -eq 'AllUsers' }
|
||||
}
|
||||
|
||||
It 'verifies WinGet removals against the fetched Winget list' {
|
||||
Mock Get-AppRemovalMethod { 'WinGet' }
|
||||
Mock Get-WingetInstalledApps { return ,@([PSCustomObject]@{ Id = 'Other.App' }) }
|
||||
Remove-SelectedApps -appsList @('Winget.App')
|
||||
Should -Invoke Test-AppInWingetList -Times 1 -Exactly -ParameterFilter { $appId -eq 'Winget.App' -and $InstalledList[0].Id -eq 'Other.App' }
|
||||
}
|
||||
|
||||
It 'stops before the first removal when cancellation is requested' {
|
||||
$script:CancelRequested = $true
|
||||
Remove-SelectedApps -appsList @('One.App')
|
||||
@@ -50,9 +60,45 @@ Describe 'Remove-SelectedApps' {
|
||||
Should -Invoke Remove-AppxApp -Times 0 -Exactly
|
||||
}
|
||||
|
||||
It 'counts failed Appx removals and reports them' {
|
||||
Mock Remove-AppxApp { $false }
|
||||
|
||||
Remove-SelectedApps -appsList @('One.App')
|
||||
|
||||
$script:AppRemovalFailures | Should -Be 1
|
||||
}
|
||||
|
||||
It 'counts a failed WinGet removal' {
|
||||
Mock Get-AppRemovalMethod { 'WinGet' }
|
||||
Mock Remove-WinGetApp { $false }
|
||||
|
||||
Remove-SelectedApps -appsList @('One.App')
|
||||
|
||||
$script:AppRemovalFailures | Should -Be 1
|
||||
}
|
||||
|
||||
It 'counts a WinGet removal that remains installed after a successful command' {
|
||||
Mock Get-AppRemovalMethod { 'WinGet' }
|
||||
Mock Test-AppInWingetList { $true }
|
||||
|
||||
Remove-SelectedApps -appsList @('One.App')
|
||||
|
||||
$script:AppRemovalFailures | Should -Be 1
|
||||
}
|
||||
|
||||
It 'records an unavailable WinGet inventory as an unverified removal' {
|
||||
Mock Get-AppRemovalMethod { 'WinGet' }
|
||||
Mock Get-WingetInstalledApps { $null }
|
||||
|
||||
Remove-SelectedApps -appsList @('One.App')
|
||||
|
||||
$script:AppRemovalVerificationUnavailable | Should -BeTrue
|
||||
Should -Invoke Test-AppInWingetList -Times 0 -Exactly
|
||||
}
|
||||
|
||||
It 'prompts for forced Edge removal at most once after failed winget removals' {
|
||||
Mock Get-AppRemovalMethod { 'WinGet' }
|
||||
Mock Test-AppStillInstalled { $true }
|
||||
Mock Test-AppInWingetList { $true }
|
||||
Remove-SelectedApps -appsList @('Microsoft.Edge', 'XPFFTQ037JWMHS')
|
||||
Should -Invoke Request-EdgeForceRemove -Times 1 -Exactly
|
||||
}
|
||||
@@ -86,15 +132,16 @@ Describe 'Remove-WinGetApp' {
|
||||
BeforeEach {
|
||||
$script:Params = @{}
|
||||
$script:WingetInstalled = $true
|
||||
Mock Invoke-NonBlocking {}
|
||||
Mock Set-RunOnceWingetTask {}
|
||||
Mock Invoke-NonBlocking { $true }
|
||||
Mock Set-RunOnceWingetTask { $true }
|
||||
Mock Get-UserName { 'Alice' }
|
||||
Mock Write-Host {}
|
||||
Mock Write-Error {}
|
||||
}
|
||||
|
||||
It 'reports unavailable winget without invoking or scheduling removal' {
|
||||
$script:WingetInstalled = $false
|
||||
Remove-WinGetApp -app 'One.App'
|
||||
Remove-WinGetApp -app 'One.App' | Should -BeFalse
|
||||
Should -Invoke Invoke-NonBlocking -Times 0 -Exactly
|
||||
Should -Invoke Set-RunOnceWingetTask -Times 0 -Exactly
|
||||
}
|
||||
@@ -130,55 +177,52 @@ Describe 'Remove-WinGetApp' {
|
||||
|
||||
{ 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'
|
||||
Should -Invoke Write-Error -Times 1 -Exactly -ParameterFilter {
|
||||
$Message -like '*did not complete within 120 seconds*'
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Describe 'Remove-AppxApp' {
|
||||
BeforeEach { Mock Invoke-NonBlocking {} }
|
||||
BeforeEach { Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $true } } }
|
||||
|
||||
It 'passes the wildcard and target user data for <Target>' -ForEach @(
|
||||
@{ Target = 'AllUsers'; ExpectedArguments = 1 }
|
||||
@{ Target = 'CurrentUser'; ExpectedArguments = 1 }
|
||||
@{ Target = 'Alice'; ExpectedArguments = 2 }
|
||||
@{ Target = 'AllUsers' }
|
||||
@{ Target = 'CurrentUser' }
|
||||
@{ Target = 'Alice' }
|
||||
) {
|
||||
Remove-AppxApp -app 'One.App' -targetUser $Target
|
||||
Should -Invoke Invoke-NonBlocking -Times 1 -Exactly -ParameterFilter {
|
||||
@($ArgumentList).Count -eq $ExpectedArguments -and @($ArgumentList)[0] -eq '*One.App*' -and
|
||||
($ExpectedArguments -eq 1 -or @($ArgumentList)[1] -eq 'Alice')
|
||||
@($ArgumentList).Count -eq 2 -and @($ArgumentList)[0] -eq '*One.App*' -and @($ArgumentList)[1] -eq $Target
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Test-AppStillInstalled' {
|
||||
BeforeEach {
|
||||
$script:WingetInstalled = $true
|
||||
Mock Get-AppxPackage { $null }
|
||||
Mock Test-AppInWingetList { $false }
|
||||
Mock Get-WingetInstalledApps { @() }
|
||||
Mock Write-Warning {}
|
||||
It 'returns false when package discovery reports a non-terminating error' {
|
||||
Mock Invoke-NonBlocking { param($ScriptBlock, $ArgumentList) & $ScriptBlock @ArgumentList }
|
||||
Mock Get-AppxPackage { Write-Error 'access denied' }
|
||||
|
||||
Remove-AppxApp -app 'One.App' -targetUser 'CurrentUser' | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'prefers all-user Appx detection and avoids winget lookup' {
|
||||
Mock Get-AppxPackage { [PSCustomObject]@{ Name = 'One.App' } }
|
||||
Test-AppStillInstalled -appId 'One.App' | Should -BeTrue
|
||||
Should -Invoke Get-AppxPackage -Times 1 -Exactly -ParameterFilter { $AllUsers }
|
||||
Should -Invoke Get-WingetInstalledApps -Times 0 -Exactly
|
||||
It 'returns false when package removal reports a non-terminating error' {
|
||||
Mock Invoke-NonBlocking { param($ScriptBlock, $ArgumentList) & $ScriptBlock @ArgumentList }
|
||||
Mock Get-AppxPackage { [PSCustomObject]@{ PackageFullName = 'One.App_1.0' } }
|
||||
Mock Remove-AppxPackage { Write-Error 'access denied' }
|
||||
|
||||
Remove-AppxApp -app 'One.App' -targetUser 'CurrentUser' | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'uses a supplied winget list without launching a live query' {
|
||||
Mock Test-AppInWingetList { $true }
|
||||
Test-AppStillInstalled -appId 'One.App' -InstalledList @([PSCustomObject]@{ Id = 'One.App' }) | Should -BeTrue
|
||||
Should -Invoke Get-WingetInstalledApps -Times 0 -Exactly
|
||||
It 'returns false and reports a terminating Appx failure' {
|
||||
Mock Invoke-NonBlocking { throw 'access denied' }
|
||||
Mock Write-Error {}
|
||||
|
||||
Remove-AppxApp -app 'One.App' -targetUser 'CurrentUser' | Should -BeFalse
|
||||
Should -Invoke Write-Error -Times 1 -Exactly -ParameterFilter {
|
||||
$Message -like '*Unable to remove One.App via Appx*access denied*'
|
||||
}
|
||||
}
|
||||
|
||||
It 'warns when a non-Appx app cannot be verified without winget' {
|
||||
$script:WingetInstalled = $false
|
||||
Test-AppStillInstalled -appId 'One.App' | Should -BeFalse
|
||||
Should -Invoke Write-Warning -Times 1 -Exactly
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Set-RunOnceWingetTask' {
|
||||
|
||||
@@ -8,4 +8,14 @@ Describe 'Wait-ForKeyPress' {
|
||||
|
||||
$LASTEXITCODE | Should -Be 0
|
||||
}
|
||||
|
||||
It 'uses the requested exit code' {
|
||||
$scriptPath = Join-Path $PSScriptRoot '..\Scripts\CLI\Wait-ForKeyPress.ps1'
|
||||
$command = "function Stop-Transcript {}; `$global:Silent = `$true; . '$scriptPath'; Wait-ForKeyPress -ExitCode 1"
|
||||
$encodedCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($command))
|
||||
|
||||
& powershell.exe -NoProfile -EncodedCommand $encodedCommand
|
||||
|
||||
$LASTEXITCODE | Should -Be 1
|
||||
}
|
||||
}
|
||||
|
||||
+22
-12
@@ -155,9 +155,18 @@ if (-not $isAdmin) {
|
||||
}
|
||||
}
|
||||
|
||||
Start-Process powershell -ArgumentList $elevatedArgs -Verb RunAs
|
||||
try {
|
||||
Start-Process powershell -ArgumentList $elevatedArgs -Verb RunAs -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
Write-Error "Failed to start Win11Debloat as Administrator: $_"
|
||||
Exit 1
|
||||
}
|
||||
|
||||
Exit 0
|
||||
}
|
||||
exit
|
||||
|
||||
Exit 1
|
||||
}
|
||||
|
||||
# Define script-level variables & paths
|
||||
@@ -194,13 +203,16 @@ $script:GuiWindow = $null
|
||||
$script:CancelRequested = $false
|
||||
$script:ApplyProgressCallback = $null
|
||||
$script:ApplySubStepCallback = $null
|
||||
$script:RegistryImportFailures = 0
|
||||
$script:AppRemovalFailures = 0
|
||||
$script:AppRemovalVerificationUnavailable = $false
|
||||
|
||||
# Check if current powershell environment is limited by security policies
|
||||
# Check if current PowerShell environment is limited by security policies
|
||||
if ($ExecutionContext.SessionState.LanguageMode -ne "FullLanguage") {
|
||||
Write-Error "Win11Debloat is unable to run on your system, powershell execution is restricted by security policies"
|
||||
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
|
||||
Exit 1
|
||||
}
|
||||
|
||||
Clear-Host
|
||||
@@ -286,10 +298,9 @@ catch { }
|
||||
# Check if script has all required files
|
||||
if (-not ((Test-Path $script:DefaultSettingsFilePath) -and (Test-Path $script:AppsListFilePath) -and (Test-Path $script:RegfilesPath) -and (Test-Path $script:AssetsPath) -and (Test-Path $script:AppSelectionSchema) -and (Test-Path $script:ApplyChangesWindowSchema) -and (Test-Path $script:SharedStylesSchema) -and (Test-Path $script:BubbleHintSchema) -and (Test-Path $script:RestoreBackupWindowSchema) -and (Test-Path $script:FeaturesFilePath))) {
|
||||
Write-Error "Win11Debloat is unable to find required files, please ensure all script files are present"
|
||||
Write-Output ""
|
||||
Write-Output "Press any key to exit..."
|
||||
$null = [System.Console]::ReadKey()
|
||||
Exit
|
||||
Exit 1
|
||||
}
|
||||
|
||||
# Load feature info from file
|
||||
@@ -306,10 +317,9 @@ try {
|
||||
}
|
||||
catch {
|
||||
Write-Error "Failed to load feature info from Features.json file"
|
||||
Write-Output ""
|
||||
Write-Output "Press any key to exit..."
|
||||
$null = [System.Console]::ReadKey()
|
||||
Exit
|
||||
Exit 1
|
||||
}
|
||||
|
||||
# Check if WinGet is installed & if it is, check if the version is at least v1.4
|
||||
@@ -482,7 +492,7 @@ if ($script:Params.ContainsKey("Sysprep")) {
|
||||
# Exit script if run in Sysprep mode on Windows 10
|
||||
if ($WinVersion -lt 22000) {
|
||||
Write-Error "Win11Debloat Sysprep mode is not supported on Windows 10"
|
||||
Wait-ForKeyPress
|
||||
Wait-ForKeyPress -ExitCode 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,7 +525,7 @@ if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSa
|
||||
if (-not (Test-Path $script:SavedSettingsFilePath)) {
|
||||
Write-CliHeader 'Custom Mode'
|
||||
Write-Error "Unable to find LastUsedSettings.json file, no changes were made"
|
||||
Wait-ForKeyPress
|
||||
Wait-ForKeyPress -ExitCode 1
|
||||
}
|
||||
|
||||
Show-CliLastUsedSettings
|
||||
@@ -526,7 +536,7 @@ if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSa
|
||||
}
|
||||
catch {
|
||||
Write-Error "$_"
|
||||
Wait-ForKeyPress
|
||||
Wait-ForKeyPress -ExitCode 1
|
||||
}
|
||||
|
||||
if (-not $Silent) {
|
||||
|
||||
Reference in New Issue
Block a user