mirror of
https://github.com/Raphire/Win11Debloat.git
synced 2026-08-23 08:02:07 +00:00
Enhance output documentation and error handling
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Forcefully uninstalls Microsoft Edge and removes its leftover shortcuts and autostart entries.
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean. $true when Edge is uninstalled and cleanup succeeds; otherwise $false.
|
||||
#>
|
||||
function Invoke-ForceRemoveEdge {
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
@@ -13,7 +16,8 @@ function Invoke-ForceRemoveEdge {
|
||||
|
||||
$regView = [Microsoft.Win32.RegistryView]::Registry32
|
||||
$hklm = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $regView)
|
||||
$hklm.CreateSubKey('SOFTWARE\Microsoft\EdgeUpdateDev').SetValue('AllowUninstall', '')
|
||||
$edgeUpdateKey = $hklm.CreateSubKey('SOFTWARE\Microsoft\EdgeUpdateDev')
|
||||
$edgeUpdateKey.SetValue('AllowUninstall', '')
|
||||
|
||||
# Create stub (This somehow allows uninstalling Edge)
|
||||
$edgeStub = "$env:SystemRoot\SystemApps\Microsoft.MicrosoftEdge_8wekyb3d8bbwe"
|
||||
@@ -95,11 +99,19 @@ function Invoke-ForceRemoveEdge {
|
||||
return $false
|
||||
}
|
||||
finally {
|
||||
if ($edgeUpdateKey) { $edgeUpdateKey.Dispose() }
|
||||
if ($uninstallRegKey) { $uninstallRegKey.Dispose() }
|
||||
if ($hklm) { $hklm.Dispose() }
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Removes an Edge autostart registry value when it exists.
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean. $true when the value is absent or removed; $false when inspection or removal fails.
|
||||
#>
|
||||
function Remove-EdgeAutostartValue {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
|
||||
.EXAMPLE
|
||||
Remove-SelectedApps -appsList (Generate-AppsList)
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean. $true when all removals can be confirmed; otherwise $false.
|
||||
#>
|
||||
function Remove-SelectedApps {
|
||||
param (
|
||||
@@ -53,10 +56,11 @@ function Remove-SelectedApps {
|
||||
Write-Host "Removing $app"
|
||||
|
||||
if ((Get-AppRemovalMethod $app) -eq 'WinGet') {
|
||||
# WinGet exit codes are not a reliable removal outcome. The single
|
||||
# post-removal inventory check below determines final success.
|
||||
$null = Remove-WinGetApp -app $app
|
||||
$removalSucceeded = Remove-WinGetApp -app $app
|
||||
$wingetRemovedApps += $app
|
||||
if (($script:Params.ContainsKey('User') -or $script:Params.ContainsKey('Sysprep')) -and -not $removalSucceeded) {
|
||||
$wingetRemovalFailures[$app] = $true
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (-not (Remove-AppxApp -app $app -targetUser $targetUser)) {
|
||||
@@ -116,8 +120,12 @@ function Remove-SelectedApps {
|
||||
|
||||
.DESCRIPTION
|
||||
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.
|
||||
WinGet's own exit code/success reporting is unreliable and is only logged
|
||||
for diagnostics; it never causes this function to report failure. Callers
|
||||
verify removal with a post-removal inventory check instead. This function
|
||||
only reports failure when the winget invocation itself throws a terminating
|
||||
error (e.g. it times out or cannot be started). 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').
|
||||
@@ -125,6 +133,10 @@ function Remove-SelectedApps {
|
||||
.PARAMETER TimeoutSeconds
|
||||
Maximum time to allow the foreground WinGet uninstall to run. Defaults
|
||||
to 120 seconds.
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean. $true unless the winget invocation threw a terminating error
|
||||
or any required RunOnce scheduling failed; otherwise $false.
|
||||
#>
|
||||
function Remove-WinGetApp {
|
||||
param(
|
||||
@@ -137,27 +149,23 @@ function Remove-WinGetApp {
|
||||
return $false
|
||||
}
|
||||
|
||||
$uninstallSucceeded = $true
|
||||
$uninstallCommandSucceeded = $true
|
||||
$exitCode = $null
|
||||
try {
|
||||
$uninstallResult = Invoke-NonBlocking -ScriptBlock {
|
||||
param($appId)
|
||||
$output = @(& winget uninstall --accept-source-agreements --disable-interactivity --id $appId 2>&1)
|
||||
$exitCode = $LASTEXITCODE
|
||||
return [PSCustomObject]@{
|
||||
Success = ($exitCode -eq 0)
|
||||
ExitCode = $exitCode
|
||||
ExitCode = $LASTEXITCODE
|
||||
Output = $output
|
||||
}
|
||||
} -ArgumentList $app -TimeoutSeconds $TimeoutSeconds
|
||||
$uninstallSucceeded = [bool]($uninstallResult -and $uninstallResult.Success)
|
||||
Write-WinGetUninstallOutput -Output $(if ($uninstallResult) { $uninstallResult.Output } else { $null })
|
||||
if (-not $uninstallSucceeded) {
|
||||
$exitCode = if ($uninstallResult) { $uninstallResult.ExitCode } else { 'unknown' }
|
||||
Write-Verbose "WinGet uninstall for $app returned exit code $exitCode. The post-removal inventory check will determine whether the app is still installed."
|
||||
}
|
||||
$exitCode = if ($uninstallResult) { $uninstallResult.ExitCode } else { 'unknown' }
|
||||
Write-Verbose "WinGet uninstall for $app returned exit code $exitCode."
|
||||
}
|
||||
catch {
|
||||
$uninstallSucceeded = $false
|
||||
$uninstallCommandSucceeded = $false
|
||||
if ($_.Exception.Message -like 'Operation timed out after *') {
|
||||
Write-Verbose "WinGet uninstall for $app did not complete within $TimeoutSeconds seconds: $_"
|
||||
}
|
||||
@@ -176,9 +184,16 @@ function Remove-WinGetApp {
|
||||
$scheduleSucceeded = Set-RunOnceWingetTask -appId $app
|
||||
}
|
||||
|
||||
return ($uninstallSucceeded -and $scheduleSucceeded)
|
||||
return ($uninstallCommandSucceeded -and $scheduleSucceeded)
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Writes captured WinGet uninstall output to the verbose stream.
|
||||
|
||||
.OUTPUTS
|
||||
None.
|
||||
#>
|
||||
function Write-WinGetUninstallOutput {
|
||||
param(
|
||||
[object[]]$Output
|
||||
@@ -308,6 +323,9 @@ function Get-AppRemovalMethod {
|
||||
following all winget uninstall attempts. In GUI mode, displays a
|
||||
warning message box; in CLI mode, prompts via Read-Host. On
|
||||
confirmation, performs a force-remove of the Edge package.
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean. $true when Edge is forcefully removed; otherwise $false.
|
||||
#>
|
||||
function Request-EdgeForceRemove {
|
||||
if ($script:GuiWindow) {
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
# Import & execute regfile
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Imports and executes a registry file.
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean. $true when the registry file is applied or previewed successfully; otherwise $false.
|
||||
#>
|
||||
function Import-RegistryFile {
|
||||
param (
|
||||
$message,
|
||||
|
||||
@@ -198,11 +198,11 @@ function Invoke-FeatureUndo {
|
||||
return $false
|
||||
}
|
||||
'EnableWindowsSandbox' {
|
||||
Write-Host "> $($feature.ApplyUndoText)..."
|
||||
Write-Host "> $undoText..."
|
||||
return (Disable-WindowsFeature 'Containers-DisposableClientVM')
|
||||
}
|
||||
'EnableWindowsSubsystemForLinux' {
|
||||
Write-Host "> $($feature.ApplyUndoText)..."
|
||||
Write-Host "> $undoText..."
|
||||
if (-not (Disable-WindowsFeature 'Microsoft-Windows-Subsystem-Linux')) { return $false }
|
||||
return (Disable-WindowsFeature 'VirtualMachinePlatform')
|
||||
}
|
||||
@@ -271,7 +271,10 @@ function Invoke-ApplyFeatures {
|
||||
& $script:ApplyProgressCallback $step $TotalSteps $displayName
|
||||
}
|
||||
|
||||
if (-not (Invoke-FeatureApply -FeatureId $featureId)) {
|
||||
# Compare app-removal failure counts so a feature that only fails due to
|
||||
# app removal isn't also double-reported as a feature failure.
|
||||
$appRemovalFailuresBefore = $script:AppRemovalFailures
|
||||
if ((-not (Invoke-FeatureApply -FeatureId $featureId)) -and ($script:AppRemovalFailures -eq $appRemovalFailuresBefore)) {
|
||||
$script:FeatureFailures++
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Creates a system restore point.
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean. $true when a restore point is created; otherwise $false.
|
||||
#>
|
||||
function Invoke-SystemRestorePoint {
|
||||
$failed = $false
|
||||
$isSilent = ($script:Params -and $script:Params.ContainsKey('Silent')) -or $script:Silent
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
|
||||
.EXAMPLE
|
||||
Replace-StartMenuForAllUsers -startMenuTemplate "C:\CustomLayout.bin"
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean. $true when all resolved profiles are updated or the change is previewed; otherwise $false.
|
||||
#>
|
||||
function Replace-StartMenuForAllUsers {
|
||||
param (
|
||||
@@ -99,6 +102,9 @@ function Replace-StartMenuForAllUsers {
|
||||
|
||||
.EXAMPLE
|
||||
Replace-StartMenu -startMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -startMenuTemplate "C:\CustomLayout.bin"
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean. $true when the template is valid and copied, or the change is previewed; otherwise $false.
|
||||
#>
|
||||
function Replace-StartMenu {
|
||||
param (
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
|
||||
.EXAMPLE
|
||||
DisableStoreSearchSuggestionsForAllUsers
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean. $true when a profile is processed and all ACL changes succeed; otherwise $false.
|
||||
#>
|
||||
function Set-StoreSearchSuggestionsDisabledForAllUsers {
|
||||
$success = $true
|
||||
@@ -60,6 +63,9 @@ function Set-StoreSearchSuggestionsDisabledForAllUsers {
|
||||
|
||||
.EXAMPLE
|
||||
DisableStoreSearchSuggestions -StoreAppsDatabase "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean. $true when the database ACL is restricted or previewed; otherwise $false.
|
||||
#>
|
||||
function Set-StoreSearchSuggestionsDisabled {
|
||||
param (
|
||||
@@ -115,6 +121,9 @@ function Set-StoreSearchSuggestionsDisabled {
|
||||
|
||||
.EXAMPLE
|
||||
EnableStoreSearchSuggestionsForAllUsers
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean. $true when a profile is processed and all ACL changes succeed; otherwise $false.
|
||||
#>
|
||||
function Set-StoreSearchSuggestionsEnabledForAllUsers {
|
||||
$success = $true
|
||||
@@ -164,6 +173,9 @@ function Set-StoreSearchSuggestionsEnabledForAllUsers {
|
||||
|
||||
.EXAMPLE
|
||||
EnableStoreSearchSuggestions -StoreAppsDatabase "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean. $true when the deny ACL is removed, the database is absent, or the change is previewed; otherwise $false.
|
||||
#>
|
||||
function Set-StoreSearchSuggestionsEnabled {
|
||||
param (
|
||||
|
||||
@@ -34,6 +34,9 @@ function Get-TelemetryScheduledTasks {
|
||||
|
||||
.EXAMPLE
|
||||
Disable-TelemetryScheduledTasks
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean. $true when every task is disabled, absent, already disabled, or previewed; otherwise $false.
|
||||
#>
|
||||
function Disable-TelemetryScheduledTasks {
|
||||
Write-Host "> Disabling telemetry scheduled tasks..."
|
||||
@@ -97,6 +100,9 @@ function Disable-TelemetryScheduledTasks {
|
||||
|
||||
.EXAMPLE
|
||||
Enable-TelemetryScheduledTasks
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean. $true when every task is enabled, absent, already enabled, or previewed; otherwise $false.
|
||||
#>
|
||||
function Enable-TelemetryScheduledTasks {
|
||||
Write-Host "> Enabling telemetry scheduled tasks..."
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
# Enables a Windows optional feature and pipes its output to the console
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Enables a Windows optional feature and pipes its output to the console.
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean. $true when enabling succeeds or is previewed; otherwise $false.
|
||||
#>
|
||||
function Enable-WindowsFeature {
|
||||
param (
|
||||
[string]$FeatureName
|
||||
@@ -44,7 +50,13 @@ function Enable-WindowsFeature {
|
||||
return $true
|
||||
}
|
||||
|
||||
# Disables a Windows optional feature and pipes its output to the console
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Disables a Windows optional feature and pipes its output to the console.
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean. $true when disabling succeeds or is previewed; otherwise $false.
|
||||
#>
|
||||
function Disable-WindowsFeature {
|
||||
param (
|
||||
[string]$FeatureName
|
||||
|
||||
@@ -234,8 +234,8 @@ function Get-AppRemovalScopeTarget {
|
||||
"AppRemovalScopeAllUsers" { return 'AllUsers' }
|
||||
"AppRemovalScopeCurrentUser" { return 'CurrentUser' }
|
||||
default {
|
||||
Write-Warning "Unrecognized app-removal scope item '$($selectedItem.Name)'. Defaulting to AllUsers."
|
||||
return 'AllUsers'
|
||||
Write-Warning "Unrecognized app-removal scope item '$($selectedItem.Name)'. Skipping app removal."
|
||||
return $null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,8 +114,7 @@ function Show-ApplyModal {
|
||||
try {
|
||||
Invoke-AllChanges
|
||||
|
||||
$featureFailureCount = [int]$script:FeatureFailures
|
||||
$failureCount = $featureFailureCount
|
||||
$failureCount = [int]$script:FeatureFailures + [int]$script:AppRemovalFailures
|
||||
$appRemovalVerificationUnavailable = [bool]$script:AppRemovalVerificationUnavailable
|
||||
|
||||
# Restart explorer if requested
|
||||
@@ -156,7 +155,7 @@ function Show-ApplyModal {
|
||||
}
|
||||
else {
|
||||
$script:ApplyCompletionTitleEl.Text = "Changes Applied with Errors"
|
||||
$script:ApplyCompletionMessageEl.Text = "$featureFailureCount change(s) failed. See console for details."
|
||||
$script:ApplyCompletionMessageEl.Text = "$failureCount change(s) failed. See console for details."
|
||||
}
|
||||
} else {
|
||||
Write-Host "All changes have been applied successfully!"
|
||||
|
||||
@@ -670,13 +670,15 @@ function Show-MainWindow {
|
||||
if ($selectedApps.Count -gt 0) {
|
||||
if (-not (Confirm-UnsafeAppRemoval -SelectedApps $selectedApps -Owner $window)) { return }
|
||||
|
||||
$scopeTarget = Get-AppRemovalScopeTarget -AppRemovalScopeCombo $appRemovalScopeCombo -OtherUsernameTextBox $otherUsernameTextBox
|
||||
if ([string]::IsNullOrWhiteSpace($scopeTarget)) {
|
||||
Write-Warning 'App removal was cancelled because the selected removal scope is invalid.'
|
||||
return
|
||||
}
|
||||
|
||||
Add-Parameter 'RemoveApps'
|
||||
Add-Parameter 'Apps' ($selectedApps -join ',')
|
||||
|
||||
$scopeTarget = Get-AppRemovalScopeTarget -AppRemovalScopeCombo $appRemovalScopeCombo -OtherUsernameTextBox $otherUsernameTextBox
|
||||
if (-not [string]::IsNullOrWhiteSpace($scopeTarget)) {
|
||||
Add-Parameter 'AppRemovalTarget' $scopeTarget
|
||||
}
|
||||
Add-Parameter 'AppRemovalTarget' $scopeTarget
|
||||
}
|
||||
|
||||
# Apply dynamic tweaks
|
||||
|
||||
@@ -191,6 +191,13 @@ function Invoke-RegistryOperation {
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Applies all parsed operations from a registry file.
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean. $true when all operations complete, including WhatIf; otherwise $false.
|
||||
#>
|
||||
function Invoke-RegistryOperationsFromRegFile {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
|
||||
@@ -25,6 +25,33 @@ function Test-ConfigConsistency {
|
||||
return 'The configuration file contains no importable data.'
|
||||
}
|
||||
|
||||
if ($null -ne $Config.Apps) {
|
||||
if ($Config.Apps -isnot [string] -and $Config.Apps -isnot [System.Collections.IEnumerable]) {
|
||||
return 'Configuration Apps entries must be strings.'
|
||||
}
|
||||
foreach ($app in @($Config.Apps)) {
|
||||
if ($app -isnot [string]) {
|
||||
return 'Configuration Apps entries must be strings.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($categoryName in @('Tweaks', 'Deployment')) {
|
||||
$category = $Config.$categoryName
|
||||
if ($null -eq $category) { continue }
|
||||
|
||||
if ($category -is [string] -or $category -isnot [System.Collections.IEnumerable]) {
|
||||
return "Configuration $categoryName entries must contain Name and Value properties."
|
||||
}
|
||||
foreach ($setting in @($category)) {
|
||||
$hasName = if ($setting -is [System.Collections.IDictionary]) { $setting.Contains('Name') } else { $null -ne $setting.PSObject.Properties['Name'] }
|
||||
$hasValue = if ($setting -is [System.Collections.IDictionary]) { $setting.Contains('Value') } else { $null -ne $setting.PSObject.Properties['Value'] }
|
||||
if (-not $setting -or -not $hasName -or -not $hasValue -or $setting.Name -isnot [string] -or [string]::IsNullOrWhiteSpace($setting.Name)) {
|
||||
return "Configuration $categoryName entries must contain Name and Value properties."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$lookup = @{}
|
||||
foreach ($setting in @($Config.Deployment)) {
|
||||
if ($setting -and $setting.Name) {
|
||||
@@ -35,16 +62,30 @@ function Test-ConfigConsistency {
|
||||
$hasScope = $lookup.ContainsKey('AppRemovalScopeIndex')
|
||||
$hasUser = $lookup.ContainsKey('UserSelectionIndex')
|
||||
|
||||
$scopeIndex = $null
|
||||
if ($hasScope) {
|
||||
if (-not [int]::TryParse("$($lookup['AppRemovalScopeIndex'])", [ref]$scopeIndex) -or $scopeIndex -notin @(0, 1, 2)) {
|
||||
return 'AppRemovalScopeIndex must be a supported numeric value (0, 1, or 2).'
|
||||
}
|
||||
}
|
||||
|
||||
$userIndex = $null
|
||||
if ($hasUser) {
|
||||
if (-not [int]::TryParse("$($lookup['UserSelectionIndex'])", [ref]$userIndex) -or $userIndex -notin @(0, 1, 2)) {
|
||||
return 'UserSelectionIndex must be a supported numeric value (0, 1, or 2).'
|
||||
}
|
||||
}
|
||||
|
||||
# "Current user only" (index 1) is only valid together with "Current User" (index 0)
|
||||
if ($hasScope -and [int]$lookup['AppRemovalScopeIndex'] -eq 1) {
|
||||
if (-not $hasUser -or [int]$lookup['UserSelectionIndex'] -ne 0) {
|
||||
if ($hasScope -and $scopeIndex -eq 1) {
|
||||
if (-not $hasUser -or $userIndex -ne 0) {
|
||||
return "App removal scope 'Current user only' (AppRemovalScopeIndex 1) requires the deployment target 'Current User' (UserSelectionIndex 0)."
|
||||
}
|
||||
}
|
||||
|
||||
# "Target user only" (index 2) is only valid together with "Other User" (index 1)
|
||||
if ($hasScope -and [int]$lookup['AppRemovalScopeIndex'] -eq 2) {
|
||||
if (-not $hasUser -or [int]$lookup['UserSelectionIndex'] -ne 1) {
|
||||
if ($hasScope -and $scopeIndex -eq 2) {
|
||||
if (-not $hasUser -or $userIndex -ne 1) {
|
||||
return "App removal scope 'Target user only' (AppRemovalScopeIndex 2) requires the deployment target 'Other User' (UserSelectionIndex 1)."
|
||||
}
|
||||
if (-not $lookup.ContainsKey('OtherUsername') -or [string]::IsNullOrWhiteSpace("$($lookup['OtherUsername'])")) {
|
||||
|
||||
Reference in New Issue
Block a user