mirror of
https://github.com/Raphire/Win11Debloat.git
synced 2026-08-23 16:12:08 +00:00
Enhance output documentation and error handling
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Forcefully uninstalls Microsoft Edge and removes its leftover shortcuts and autostart entries.
|
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 {
|
function Invoke-ForceRemoveEdge {
|
||||||
if ($script:Params.ContainsKey("WhatIf")) {
|
if ($script:Params.ContainsKey("WhatIf")) {
|
||||||
@@ -13,7 +16,8 @@ function Invoke-ForceRemoveEdge {
|
|||||||
|
|
||||||
$regView = [Microsoft.Win32.RegistryView]::Registry32
|
$regView = [Microsoft.Win32.RegistryView]::Registry32
|
||||||
$hklm = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $regView)
|
$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)
|
# Create stub (This somehow allows uninstalling Edge)
|
||||||
$edgeStub = "$env:SystemRoot\SystemApps\Microsoft.MicrosoftEdge_8wekyb3d8bbwe"
|
$edgeStub = "$env:SystemRoot\SystemApps\Microsoft.MicrosoftEdge_8wekyb3d8bbwe"
|
||||||
@@ -95,11 +99,19 @@ function Invoke-ForceRemoveEdge {
|
|||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
if ($edgeUpdateKey) { $edgeUpdateKey.Dispose() }
|
||||||
if ($uninstallRegKey) { $uninstallRegKey.Dispose() }
|
if ($uninstallRegKey) { $uninstallRegKey.Dispose() }
|
||||||
if ($hklm) { $hklm.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 {
|
function Remove-EdgeAutostartValue {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
|
|||||||
@@ -18,6 +18,9 @@
|
|||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
Remove-SelectedApps -appsList (Generate-AppsList)
|
Remove-SelectedApps -appsList (Generate-AppsList)
|
||||||
|
|
||||||
|
.OUTPUTS
|
||||||
|
System.Boolean. $true when all removals can be confirmed; otherwise $false.
|
||||||
#>
|
#>
|
||||||
function Remove-SelectedApps {
|
function Remove-SelectedApps {
|
||||||
param (
|
param (
|
||||||
@@ -53,10 +56,11 @@ function Remove-SelectedApps {
|
|||||||
Write-Host "Removing $app"
|
Write-Host "Removing $app"
|
||||||
|
|
||||||
if ((Get-AppRemovalMethod $app) -eq 'WinGet') {
|
if ((Get-AppRemovalMethod $app) -eq 'WinGet') {
|
||||||
# WinGet exit codes are not a reliable removal outcome. The single
|
$removalSucceeded = Remove-WinGetApp -app $app
|
||||||
# post-removal inventory check below determines final success.
|
|
||||||
$null = Remove-WinGetApp -app $app
|
|
||||||
$wingetRemovedApps += $app
|
$wingetRemovedApps += $app
|
||||||
|
if (($script:Params.ContainsKey('User') -or $script:Params.ContainsKey('Sysprep')) -and -not $removalSucceeded) {
|
||||||
|
$wingetRemovalFailures[$app] = $true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (-not (Remove-AppxApp -app $app -targetUser $targetUser)) {
|
if (-not (Remove-AppxApp -app $app -targetUser $targetUser)) {
|
||||||
@@ -116,8 +120,12 @@ function Remove-SelectedApps {
|
|||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Runs winget uninstall for a single app, with a bounded execution time.
|
Runs winget uninstall for a single app, with a bounded execution time.
|
||||||
If the User or Sysprep parameter was passed, also schedules removal for
|
WinGet's own exit code/success reporting is unreliable and is only logged
|
||||||
future logins.
|
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
|
.PARAMETER app
|
||||||
The WinGet package ID to uninstall (e.g. 'Microsoft.BingNews').
|
The WinGet package ID to uninstall (e.g. 'Microsoft.BingNews').
|
||||||
@@ -125,6 +133,10 @@ function Remove-SelectedApps {
|
|||||||
.PARAMETER TimeoutSeconds
|
.PARAMETER TimeoutSeconds
|
||||||
Maximum time to allow the foreground WinGet uninstall to run. Defaults
|
Maximum time to allow the foreground WinGet uninstall to run. Defaults
|
||||||
to 120 seconds.
|
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 {
|
function Remove-WinGetApp {
|
||||||
param(
|
param(
|
||||||
@@ -137,27 +149,23 @@ function Remove-WinGetApp {
|
|||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
|
|
||||||
$uninstallSucceeded = $true
|
$uninstallCommandSucceeded = $true
|
||||||
|
$exitCode = $null
|
||||||
try {
|
try {
|
||||||
$uninstallResult = Invoke-NonBlocking -ScriptBlock {
|
$uninstallResult = Invoke-NonBlocking -ScriptBlock {
|
||||||
param($appId)
|
param($appId)
|
||||||
$output = @(& winget uninstall --accept-source-agreements --disable-interactivity --id $appId 2>&1)
|
$output = @(& winget uninstall --accept-source-agreements --disable-interactivity --id $appId 2>&1)
|
||||||
$exitCode = $LASTEXITCODE
|
|
||||||
return [PSCustomObject]@{
|
return [PSCustomObject]@{
|
||||||
Success = ($exitCode -eq 0)
|
ExitCode = $LASTEXITCODE
|
||||||
ExitCode = $exitCode
|
|
||||||
Output = $output
|
Output = $output
|
||||||
}
|
}
|
||||||
} -ArgumentList $app -TimeoutSeconds $TimeoutSeconds
|
} -ArgumentList $app -TimeoutSeconds $TimeoutSeconds
|
||||||
$uninstallSucceeded = [bool]($uninstallResult -and $uninstallResult.Success)
|
|
||||||
Write-WinGetUninstallOutput -Output $(if ($uninstallResult) { $uninstallResult.Output } else { $null })
|
Write-WinGetUninstallOutput -Output $(if ($uninstallResult) { $uninstallResult.Output } else { $null })
|
||||||
if (-not $uninstallSucceeded) {
|
|
||||||
$exitCode = if ($uninstallResult) { $uninstallResult.ExitCode } else { 'unknown' }
|
$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."
|
Write-Verbose "WinGet uninstall for $app returned exit code $exitCode."
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
$uninstallSucceeded = $false
|
$uninstallCommandSucceeded = $false
|
||||||
if ($_.Exception.Message -like 'Operation timed out after *') {
|
if ($_.Exception.Message -like 'Operation timed out after *') {
|
||||||
Write-Verbose "WinGet uninstall for $app did not complete within $TimeoutSeconds seconds: $_"
|
Write-Verbose "WinGet uninstall for $app did not complete within $TimeoutSeconds seconds: $_"
|
||||||
}
|
}
|
||||||
@@ -176,9 +184,16 @@ function Remove-WinGetApp {
|
|||||||
$scheduleSucceeded = Set-RunOnceWingetTask -appId $app
|
$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 {
|
function Write-WinGetUninstallOutput {
|
||||||
param(
|
param(
|
||||||
[object[]]$Output
|
[object[]]$Output
|
||||||
@@ -308,6 +323,9 @@ function Get-AppRemovalMethod {
|
|||||||
following all winget uninstall attempts. In GUI mode, displays a
|
following all winget uninstall attempts. In GUI mode, displays a
|
||||||
warning message box; in CLI mode, prompts via Read-Host. On
|
warning message box; in CLI mode, prompts via Read-Host. On
|
||||||
confirmation, performs a force-remove of the Edge package.
|
confirmation, performs a force-remove of the Edge package.
|
||||||
|
|
||||||
|
.OUTPUTS
|
||||||
|
System.Boolean. $true when Edge is forcefully removed; otherwise $false.
|
||||||
#>
|
#>
|
||||||
function Request-EdgeForceRemove {
|
function Request-EdgeForceRemove {
|
||||||
if ($script:GuiWindow) {
|
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 {
|
function Import-RegistryFile {
|
||||||
param (
|
param (
|
||||||
$message,
|
$message,
|
||||||
|
|||||||
@@ -198,11 +198,11 @@ function Invoke-FeatureUndo {
|
|||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
'EnableWindowsSandbox' {
|
'EnableWindowsSandbox' {
|
||||||
Write-Host "> $($feature.ApplyUndoText)..."
|
Write-Host "> $undoText..."
|
||||||
return (Disable-WindowsFeature 'Containers-DisposableClientVM')
|
return (Disable-WindowsFeature 'Containers-DisposableClientVM')
|
||||||
}
|
}
|
||||||
'EnableWindowsSubsystemForLinux' {
|
'EnableWindowsSubsystemForLinux' {
|
||||||
Write-Host "> $($feature.ApplyUndoText)..."
|
Write-Host "> $undoText..."
|
||||||
if (-not (Disable-WindowsFeature 'Microsoft-Windows-Subsystem-Linux')) { return $false }
|
if (-not (Disable-WindowsFeature 'Microsoft-Windows-Subsystem-Linux')) { return $false }
|
||||||
return (Disable-WindowsFeature 'VirtualMachinePlatform')
|
return (Disable-WindowsFeature 'VirtualMachinePlatform')
|
||||||
}
|
}
|
||||||
@@ -271,7 +271,10 @@ function Invoke-ApplyFeatures {
|
|||||||
& $script:ApplyProgressCallback $step $TotalSteps $displayName
|
& $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++
|
$script:FeatureFailures++
|
||||||
}
|
}
|
||||||
Write-Host ""
|
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 {
|
function Invoke-SystemRestorePoint {
|
||||||
$failed = $false
|
$failed = $false
|
||||||
$isSilent = ($script:Params -and $script:Params.ContainsKey('Silent')) -or $script:Silent
|
$isSilent = ($script:Params -and $script:Params.ContainsKey('Silent')) -or $script:Silent
|
||||||
|
|||||||
@@ -18,6 +18,9 @@
|
|||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
Replace-StartMenuForAllUsers -startMenuTemplate "C:\CustomLayout.bin"
|
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 {
|
function Replace-StartMenuForAllUsers {
|
||||||
param (
|
param (
|
||||||
@@ -99,6 +102,9 @@ function Replace-StartMenuForAllUsers {
|
|||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
Replace-StartMenu -startMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -startMenuTemplate "C:\CustomLayout.bin"
|
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 {
|
function Replace-StartMenu {
|
||||||
param (
|
param (
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
DisableStoreSearchSuggestionsForAllUsers
|
DisableStoreSearchSuggestionsForAllUsers
|
||||||
|
|
||||||
|
.OUTPUTS
|
||||||
|
System.Boolean. $true when a profile is processed and all ACL changes succeed; otherwise $false.
|
||||||
#>
|
#>
|
||||||
function Set-StoreSearchSuggestionsDisabledForAllUsers {
|
function Set-StoreSearchSuggestionsDisabledForAllUsers {
|
||||||
$success = $true
|
$success = $true
|
||||||
@@ -60,6 +63,9 @@ function Set-StoreSearchSuggestionsDisabledForAllUsers {
|
|||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
DisableStoreSearchSuggestions -StoreAppsDatabase "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
|
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 {
|
function Set-StoreSearchSuggestionsDisabled {
|
||||||
param (
|
param (
|
||||||
@@ -115,6 +121,9 @@ function Set-StoreSearchSuggestionsDisabled {
|
|||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
EnableStoreSearchSuggestionsForAllUsers
|
EnableStoreSearchSuggestionsForAllUsers
|
||||||
|
|
||||||
|
.OUTPUTS
|
||||||
|
System.Boolean. $true when a profile is processed and all ACL changes succeed; otherwise $false.
|
||||||
#>
|
#>
|
||||||
function Set-StoreSearchSuggestionsEnabledForAllUsers {
|
function Set-StoreSearchSuggestionsEnabledForAllUsers {
|
||||||
$success = $true
|
$success = $true
|
||||||
@@ -164,6 +173,9 @@ function Set-StoreSearchSuggestionsEnabledForAllUsers {
|
|||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
EnableStoreSearchSuggestions -StoreAppsDatabase "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
|
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 {
|
function Set-StoreSearchSuggestionsEnabled {
|
||||||
param (
|
param (
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ function Get-TelemetryScheduledTasks {
|
|||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
Disable-TelemetryScheduledTasks
|
Disable-TelemetryScheduledTasks
|
||||||
|
|
||||||
|
.OUTPUTS
|
||||||
|
System.Boolean. $true when every task is disabled, absent, already disabled, or previewed; otherwise $false.
|
||||||
#>
|
#>
|
||||||
function Disable-TelemetryScheduledTasks {
|
function Disable-TelemetryScheduledTasks {
|
||||||
Write-Host "> Disabling telemetry scheduled tasks..."
|
Write-Host "> Disabling telemetry scheduled tasks..."
|
||||||
@@ -97,6 +100,9 @@ function Disable-TelemetryScheduledTasks {
|
|||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
Enable-TelemetryScheduledTasks
|
Enable-TelemetryScheduledTasks
|
||||||
|
|
||||||
|
.OUTPUTS
|
||||||
|
System.Boolean. $true when every task is enabled, absent, already enabled, or previewed; otherwise $false.
|
||||||
#>
|
#>
|
||||||
function Enable-TelemetryScheduledTasks {
|
function Enable-TelemetryScheduledTasks {
|
||||||
Write-Host "> Enabling telemetry scheduled tasks..."
|
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 {
|
function Enable-WindowsFeature {
|
||||||
param (
|
param (
|
||||||
[string]$FeatureName
|
[string]$FeatureName
|
||||||
@@ -44,7 +50,13 @@ function Enable-WindowsFeature {
|
|||||||
return $true
|
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 {
|
function Disable-WindowsFeature {
|
||||||
param (
|
param (
|
||||||
[string]$FeatureName
|
[string]$FeatureName
|
||||||
|
|||||||
@@ -234,8 +234,8 @@ function Get-AppRemovalScopeTarget {
|
|||||||
"AppRemovalScopeAllUsers" { return 'AllUsers' }
|
"AppRemovalScopeAllUsers" { return 'AllUsers' }
|
||||||
"AppRemovalScopeCurrentUser" { return 'CurrentUser' }
|
"AppRemovalScopeCurrentUser" { return 'CurrentUser' }
|
||||||
default {
|
default {
|
||||||
Write-Warning "Unrecognized app-removal scope item '$($selectedItem.Name)'. Defaulting to AllUsers."
|
Write-Warning "Unrecognized app-removal scope item '$($selectedItem.Name)'. Skipping app removal."
|
||||||
return 'AllUsers'
|
return $null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,8 +114,7 @@ function Show-ApplyModal {
|
|||||||
try {
|
try {
|
||||||
Invoke-AllChanges
|
Invoke-AllChanges
|
||||||
|
|
||||||
$featureFailureCount = [int]$script:FeatureFailures
|
$failureCount = [int]$script:FeatureFailures + [int]$script:AppRemovalFailures
|
||||||
$failureCount = $featureFailureCount
|
|
||||||
$appRemovalVerificationUnavailable = [bool]$script:AppRemovalVerificationUnavailable
|
$appRemovalVerificationUnavailable = [bool]$script:AppRemovalVerificationUnavailable
|
||||||
|
|
||||||
# Restart explorer if requested
|
# Restart explorer if requested
|
||||||
@@ -156,7 +155,7 @@ function Show-ApplyModal {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$script:ApplyCompletionTitleEl.Text = "Changes Applied with Errors"
|
$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 {
|
} else {
|
||||||
Write-Host "All changes have been applied successfully!"
|
Write-Host "All changes have been applied successfully!"
|
||||||
|
|||||||
@@ -670,14 +670,16 @@ function Show-MainWindow {
|
|||||||
if ($selectedApps.Count -gt 0) {
|
if ($selectedApps.Count -gt 0) {
|
||||||
if (-not (Confirm-UnsafeAppRemoval -SelectedApps $selectedApps -Owner $window)) { return }
|
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 'RemoveApps'
|
||||||
Add-Parameter 'Apps' ($selectedApps -join ',')
|
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
|
# Apply dynamic tweaks
|
||||||
foreach ($tweakAction in @(Get-PendingTweakActions -Window $window -ShowAppliedTweaksMode:$showAppliedTweaksMode)) {
|
foreach ($tweakAction in @(Get-PendingTweakActions -Window $window -ShowAppliedTweaksMode:$showAppliedTweaksMode)) {
|
||||||
|
|||||||
@@ -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 {
|
function Invoke-RegistryOperationsFromRegFile {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
|
|||||||
@@ -25,6 +25,33 @@ function Test-ConfigConsistency {
|
|||||||
return 'The configuration file contains no importable data.'
|
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 = @{}
|
$lookup = @{}
|
||||||
foreach ($setting in @($Config.Deployment)) {
|
foreach ($setting in @($Config.Deployment)) {
|
||||||
if ($setting -and $setting.Name) {
|
if ($setting -and $setting.Name) {
|
||||||
@@ -35,16 +62,30 @@ function Test-ConfigConsistency {
|
|||||||
$hasScope = $lookup.ContainsKey('AppRemovalScopeIndex')
|
$hasScope = $lookup.ContainsKey('AppRemovalScopeIndex')
|
||||||
$hasUser = $lookup.ContainsKey('UserSelectionIndex')
|
$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)
|
# "Current user only" (index 1) is only valid together with "Current User" (index 0)
|
||||||
if ($hasScope -and [int]$lookup['AppRemovalScopeIndex'] -eq 1) {
|
if ($hasScope -and $scopeIndex -eq 1) {
|
||||||
if (-not $hasUser -or [int]$lookup['UserSelectionIndex'] -ne 0) {
|
if (-not $hasUser -or $userIndex -ne 0) {
|
||||||
return "App removal scope 'Current user only' (AppRemovalScopeIndex 1) requires the deployment target 'Current User' (UserSelectionIndex 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)
|
# "Target user only" (index 2) is only valid together with "Other User" (index 1)
|
||||||
if ($hasScope -and [int]$lookup['AppRemovalScopeIndex'] -eq 2) {
|
if ($hasScope -and $scopeIndex -eq 2) {
|
||||||
if (-not $hasUser -or [int]$lookup['UserSelectionIndex'] -ne 1) {
|
if (-not $hasUser -or $userIndex -ne 1) {
|
||||||
return "App removal scope 'Target user only' (AppRemovalScopeIndex 2) requires the deployment target 'Other User' (UserSelectionIndex 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'])")) {
|
if (-not $lookup.ContainsKey('OtherUsername') -or [string]::IsNullOrWhiteSpace("$($lookup['OtherUsername'])")) {
|
||||||
|
|||||||
@@ -61,6 +61,30 @@ Describe 'Test-ConfigConsistency' {
|
|||||||
Test-ConfigConsistency -Config $config | Should -Match 'no importable data'
|
Test-ConfigConsistency -Config $config | Should -Match 'no importable data'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
It 'reports an error for invalid app entries' {
|
||||||
|
$config = [PSCustomObject]@{ Version = '1.0'; Apps = 42 }
|
||||||
|
|
||||||
|
Test-ConfigConsistency -Config $config | Should -Match 'Apps entries must be strings'
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'reports an error for nonnumeric deployment indexes' {
|
||||||
|
$config = [PSCustomObject]@{
|
||||||
|
Version = '1.0'
|
||||||
|
Deployment = @(@{ Name = 'AppRemovalScopeIndex'; Value = 'all' })
|
||||||
|
}
|
||||||
|
|
||||||
|
Test-ConfigConsistency -Config $config | Should -Match 'AppRemovalScopeIndex must be a supported numeric value'
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'reports an error for out-of-range deployment indexes' {
|
||||||
|
$config = [PSCustomObject]@{
|
||||||
|
Version = '1.0'
|
||||||
|
Deployment = @(@{ Name = 'UserSelectionIndex'; Value = 3 })
|
||||||
|
}
|
||||||
|
|
||||||
|
Test-ConfigConsistency -Config $config | Should -Match 'UserSelectionIndex must be a supported numeric value'
|
||||||
|
}
|
||||||
|
|
||||||
It 'returns null for a consistent all-users scope' {
|
It 'returns null for a consistent all-users scope' {
|
||||||
$config = [PSCustomObject]@{
|
$config = [PSCustomObject]@{
|
||||||
Version = '1.0'
|
Version = '1.0'
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ Describe 'Get-AppRemovalScopeTarget' {
|
|||||||
Get-AppRemovalScopeTarget -AppRemovalScopeCombo $combo -OtherUsernameTextBox $usernameBox | Should -BeNullOrEmpty
|
Get-AppRemovalScopeTarget -AppRemovalScopeCombo $combo -OtherUsernameTextBox $usernameBox | Should -BeNullOrEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
It 'defaults to AllUsers for an unrecognized ComboBoxItem Name' {
|
It 'returns null for an unrecognized ComboBoxItem Name' {
|
||||||
$combo = New-Object System.Windows.Controls.ComboBox
|
$combo = New-Object System.Windows.Controls.ComboBox
|
||||||
$item = New-Object System.Windows.Controls.ComboBoxItem
|
$item = New-Object System.Windows.Controls.ComboBoxItem
|
||||||
$item.Name = 'SomeUnrelatedControl'
|
$item.Name = 'SomeUnrelatedControl'
|
||||||
@@ -219,7 +219,7 @@ Describe 'Get-AppRemovalScopeTarget' {
|
|||||||
$combo.SelectedItem = $item
|
$combo.SelectedItem = $item
|
||||||
$usernameBox = New-Object System.Windows.Controls.TextBox
|
$usernameBox = New-Object System.Windows.Controls.TextBox
|
||||||
|
|
||||||
Get-AppRemovalScopeTarget -AppRemovalScopeCombo $combo -OtherUsernameTextBox $usernameBox | Should -Be 'AllUsers'
|
Get-AppRemovalScopeTarget -AppRemovalScopeCombo $combo -OtherUsernameTextBox $usernameBox | Should -BeNullOrEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
It 'returns an empty string for the target-user scope when the username is blank' {
|
It 'returns an empty string for the target-user scope when the username is blank' {
|
||||||
|
|||||||
@@ -91,6 +91,17 @@ Describe 'Remove-SelectedApps' {
|
|||||||
$script:AppRemovalFailures | Should -Be 0
|
$script:AppRemovalFailures | Should -Be 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
It 'counts a failed WinGet scheduling result for a target user' {
|
||||||
|
$script:Params = @{ User = 'Alice' }
|
||||||
|
Mock Get-AppRemovalMethod { 'WinGet' }
|
||||||
|
Mock Remove-WinGetApp { $false }
|
||||||
|
Mock Test-AppInWingetList { $false }
|
||||||
|
|
||||||
|
Remove-SelectedApps -appsList @('One.App') | Should -BeFalse
|
||||||
|
|
||||||
|
$script:AppRemovalFailures | Should -Be 1
|
||||||
|
}
|
||||||
|
|
||||||
It 'counts a WinGet removal that remains installed after a successful command' {
|
It 'counts a WinGet removal that remains installed after a successful command' {
|
||||||
Mock Get-AppRemovalMethod { 'WinGet' }
|
Mock Get-AppRemovalMethod { 'WinGet' }
|
||||||
Mock Test-AppInWingetList { $true }
|
Mock Test-AppInWingetList { $true }
|
||||||
@@ -197,41 +208,28 @@ Describe 'Remove-WinGetApp' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
It 'returns false when winget exits unsuccessfully' {
|
It 'returns true and logs WinGet output regardless of exit code' -ForEach @(
|
||||||
Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $false; ExitCode = 1; Output = @('failure') } }
|
@{ ExitCode = 0; Output = 'Successfully uninstalled One.App' }
|
||||||
Mock Write-Verbose {}
|
@{ ExitCode = 1; Output = 'Package was not found' }
|
||||||
|
@{ ExitCode = -1978335212; Output = 'No installed package found matching input criteria.' }
|
||||||
Remove-WinGetApp -app 'One.App' | Should -BeFalse
|
) {
|
||||||
|
$script:expectedExitCode = $ExitCode
|
||||||
Should -Invoke Write-Verbose -Times 1 -Exactly -ParameterFilter { $Message -match 'exit code 1' }
|
$script:expectedOutput = $Output
|
||||||
}
|
Mock Invoke-NonBlocking { [PSCustomObject]@{ ExitCode = $script:expectedExitCode; Output = @($script:expectedOutput) } }
|
||||||
|
|
||||||
It 'returns false for a non-zero WinGet exit code without writing an error record' {
|
|
||||||
Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $false; ExitCode = -1978335212; Output = @('No installed package found matching input criteria.') } }
|
|
||||||
Mock Write-Verbose {}
|
|
||||||
|
|
||||||
Remove-WinGetApp -app 'One.App' | Should -BeFalse
|
|
||||||
|
|
||||||
Should -Invoke Write-Verbose -Times 1 -Exactly -ParameterFilter { $Message -match 'post-removal inventory check' }
|
|
||||||
Should -Invoke Write-Verbose -Times 1 -Exactly -ParameterFilter { $Message -match 'No installed package found' }
|
|
||||||
}
|
|
||||||
|
|
||||||
It 'writes captured winget output to the verbose stream before returning success' {
|
|
||||||
Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $true; ExitCode = 0; Output = @('Successfully uninstalled One.App') } }
|
|
||||||
Mock Write-Verbose {}
|
Mock Write-Verbose {}
|
||||||
|
|
||||||
Remove-WinGetApp -app 'One.App' | Should -BeTrue
|
Remove-WinGetApp -app 'One.App' | Should -BeTrue
|
||||||
|
|
||||||
Should -Invoke Write-Verbose -Times 1 -Exactly -ParameterFilter { $Message -eq 'Successfully uninstalled One.App' }
|
Should -Invoke Write-Verbose -Times 1 -Exactly -ParameterFilter { $Message -eq $script:expectedOutput }
|
||||||
|
Should -Invoke Write-Verbose -Times 1 -Exactly -ParameterFilter { $Message -eq "WinGet uninstall for One.App returned exit code $script:expectedExitCode." }
|
||||||
}
|
}
|
||||||
|
|
||||||
It 'writes captured winget diagnostics to the verbose stream when winget fails' {
|
It 'returns the RunOnce scheduling result for a target user' {
|
||||||
Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $false; ExitCode = 1; Output = @('Package was not found') } }
|
$script:Params = @{ User = 'Alice' }
|
||||||
Mock Write-Verbose {}
|
Mock Invoke-NonBlocking { [PSCustomObject]@{ ExitCode = 1; Output = @() } }
|
||||||
|
Mock Set-RunOnceWingetTask { $false }
|
||||||
|
|
||||||
Remove-WinGetApp -app 'One.App' | Should -BeFalse
|
Remove-WinGetApp -app 'One.App' | Should -BeFalse
|
||||||
|
|
||||||
Should -Invoke Write-Verbose -Times 1 -Exactly -ParameterFilter { $Message -eq 'Package was not found' }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -250,6 +248,15 @@ Describe 'Remove-EdgeAutostartValue' {
|
|||||||
Should -Invoke Remove-ItemProperty -Times 0 -Exactly
|
Should -Invoke Remove-ItemProperty -Times 0 -Exactly
|
||||||
}
|
}
|
||||||
|
|
||||||
|
It 'treats a missing registry key as already cleaned up' {
|
||||||
|
Mock Get-ItemProperty { throw [System.Management.Automation.ItemNotFoundException]::new('not found') }
|
||||||
|
Mock Remove-ItemProperty {}
|
||||||
|
|
||||||
|
Remove-EdgeAutostartValue -Path 'HKCU:\Software\Example' -Name 'Microsoft Edge Update' | Should -BeTrue
|
||||||
|
|
||||||
|
Should -Invoke Remove-ItemProperty -Times 0 -Exactly
|
||||||
|
}
|
||||||
|
|
||||||
It 'removes an existing value' {
|
It 'removes an existing value' {
|
||||||
Mock Get-ItemProperty { [PSCustomObject]@{ 'Microsoft Edge Update' = 'enabled' } }
|
Mock Get-ItemProperty { [PSCustomObject]@{ 'Microsoft Edge Update' = 'enabled' } }
|
||||||
Mock Remove-ItemProperty {}
|
Mock Remove-ItemProperty {}
|
||||||
|
|||||||
Reference in New Issue
Block a user