Compare commits

..
41 changed files with 1416 additions and 643 deletions
+6 -68
View File
@@ -63,33 +63,6 @@
} }
], ],
"UiGroups": [ "UiGroups": [
{
"GroupId": "DesktopSpotlight",
"Label": "Windows spotlight on the desktop",
"ToolTip": "This setting controls the Windows spotlight desktop background option. Keep the rotating wallpaper with or without the 'Learn about this picture' shortcut, or disable Windows spotlight entirely.",
"Category": "Appearance",
"Priority": 4,
"Values": [
{
"Label": "Show 'Learn about this picture' shortcut (Default)",
"FeatureIds": [
"EnableDesktopSpotlight"
]
},
{
"Label": "Hide 'Learn about this picture' shortcut",
"FeatureIds": [
"HideDesktopSpotlightIcon"
]
},
{
"Label": "Disable Windows spotlight desktop background",
"FeatureIds": [
"DisableDesktopSpotlight"
]
}
]
},
{ {
"GroupId": "SearchIcon", "GroupId": "SearchIcon",
"Label": "Taskbar search style", "Label": "Taskbar search style",
@@ -419,7 +392,6 @@
"Label": "Disable telemetry, tracking & targeted ads", "Label": "Disable telemetry, tracking & targeted ads",
"ToolTip": "This setting disables telemetry, diagnostic data collection, activity history, app-launch tracking, targeted ads and more. It limits the data that is sent to Microsoft about your device and usage. If you are a Windows Insider, updates may be blocked until optional diagnostic data collection is turned back on.", "ToolTip": "This setting disables telemetry, diagnostic data collection, activity history, app-launch tracking, targeted ads and more. It limits the data that is sent to Microsoft about your device and usage. If you are a Windows Insider, updates may be blocked until optional diagnostic data collection is turned back on.",
"Category": "Privacy & Suggested Content", "Category": "Privacy & Suggested Content",
"Priority": 1,
"RegistryKey": "Disable_Telemetry.reg", "RegistryKey": "Disable_Telemetry.reg",
"ApplyText": "Disabling telemetry and diagnostic data collection", "ApplyText": "Disabling telemetry and diagnostic data collection",
"UndoLabel": "Enable telemetry, tracking & targeted ads", "UndoLabel": "Enable telemetry, tracking & targeted ads",
@@ -433,7 +405,6 @@
"Label": "Disable tips, tricks & suggested content throughout Windows", "Label": "Disable tips, tricks & suggested content throughout Windows",
"ToolTip": "This setting removes many annoying distractions from Windows. This includes things like notifications, reminders and sync provider ads. It also prevents automated installation of suggested apps.", "ToolTip": "This setting removes many annoying distractions from Windows. This includes things like notifications, reminders and sync provider ads. It also prevents automated installation of suggested apps.",
"Category": "Privacy & Suggested Content", "Category": "Privacy & Suggested Content",
"Priority": 2,
"RegistryKey": "Disable_Windows_Suggestions.reg", "RegistryKey": "Disable_Windows_Suggestions.reg",
"ApplyText": "Disabling tips, tricks, suggestions and ads throughout Windows", "ApplyText": "Disabling tips, tricks, suggestions and ads throughout Windows",
"UndoLabel": "Enable tips, tricks & suggested content throughout Windows", "UndoLabel": "Enable tips, tricks & suggested content throughout Windows",
@@ -447,7 +418,6 @@
"Label": "Disable Windows Notifications (From apps and other senders)", "Label": "Disable Windows Notifications (From apps and other senders)",
"ToolTip": "Disables native Windows notifications from apps and other senders, this includes notifications from apps like Discord, WhatsApp, Teams, and Slack.", "ToolTip": "Disables native Windows notifications from apps and other senders, this includes notifications from apps like Discord, WhatsApp, Teams, and Slack.",
"Category": "Privacy & Suggested Content", "Category": "Privacy & Suggested Content",
"Priority": 3,
"RegistryKey": "Disable_Notifications.reg", "RegistryKey": "Disable_Notifications.reg",
"ApplyText": "Disabling Windows notifications and reminders", "ApplyText": "Disabling Windows notifications and reminders",
"UndoLabel": "Enable Windows notifications and reminders", "UndoLabel": "Enable Windows notifications and reminders",
@@ -461,7 +431,6 @@
"Label": "Disable Windows location services & app location access", "Label": "Disable Windows location services & app location access",
"ToolTip": "This will turn off Windows Location Services and deny apps access to your location. This feature uses policies, which will lock down certain settings.", "ToolTip": "This will turn off Windows Location Services and deny apps access to your location. This feature uses policies, which will lock down certain settings.",
"Category": "Privacy & Suggested Content", "Category": "Privacy & Suggested Content",
"Priority": 4,
"RegistryKey": "Disable_Location_Services.reg", "RegistryKey": "Disable_Location_Services.reg",
"ApplyText": "Disabling Windows location services and app location access", "ApplyText": "Disabling Windows location services and app location access",
"UndoLabel": "Enable Windows location services & app location access", "UndoLabel": "Enable Windows location services & app location access",
@@ -475,7 +444,6 @@
"Label": "Disable Find My Device location tracking", "Label": "Disable Find My Device location tracking",
"ToolTip": "This will turn off the 'Find My Device' feature, which periodically sends your device's location to Microsoft. This feature uses policies, which will lock down certain settings.", "ToolTip": "This will turn off the 'Find My Device' feature, which periodically sends your device's location to Microsoft. This feature uses policies, which will lock down certain settings.",
"Category": "Privacy & Suggested Content", "Category": "Privacy & Suggested Content",
"Priority": 5,
"RegistryKey": "Disable_Find_My_Device.reg", "RegistryKey": "Disable_Find_My_Device.reg",
"ApplyText": "Disabling Find My Device location tracking", "ApplyText": "Disabling Find My Device location tracking",
"UndoLabel": "Enable Find My Device location tracking", "UndoLabel": "Enable Find My Device location tracking",
@@ -489,7 +457,6 @@
"Label": "Disable tips & tricks on the lock screen", "Label": "Disable tips & tricks on the lock screen",
"ToolTip": "This will turn off the lockscreen spotlight option and disable the tips, tricks and fun facts that appear on the lock screen.", "ToolTip": "This will turn off the lockscreen spotlight option and disable the tips, tricks and fun facts that appear on the lock screen.",
"Category": "Privacy & Suggested Content", "Category": "Privacy & Suggested Content",
"Priority": 6,
"RegistryKey": "Disable_Lockscreen_Tips.reg", "RegistryKey": "Disable_Lockscreen_Tips.reg",
"ApplyText": "Disabling tips & tricks on the lock screen", "ApplyText": "Disabling tips & tricks on the lock screen",
"UndoLabel": "Enable tips & tricks on the lock screen", "UndoLabel": "Enable tips & tricks on the lock screen",
@@ -498,45 +465,19 @@
"MinVersion": null, "MinVersion": null,
"MaxVersion": null "MaxVersion": null
}, },
{
"FeatureId": "EnableDesktopSpotlight",
"Label": "Enable Windows spotlight desktop background and shortcut",
"ToolTip": "Restores the Windows spotlight desktop background and the 'Learn about this picture' shortcut.",
"Category": "Appearance",
"RegistryKey": "Enable_Desktop_Spotlight.reg",
"ApplyText": "Restoring the Windows spotlight desktop background and shortcut",
"UndoLabel": null,
"ApplyUndoText": null,
"RegistryUndoKey": null,
"MinVersion": null,
"MaxVersion": null
},
{ {
"FeatureId": "DisableDesktopSpotlight", "FeatureId": "DisableDesktopSpotlight",
"Label": "Disable Windows spotlight desktop background", "Label": "Disable Windows Spotlight for desktop",
"ToolTip": "Turns off Windows spotlight for the desktop background, which displays rotating images and may show tips and fun facts. This feature uses policies, which will lock down certain settings.", "ToolTip": "This will turn off the 'Windows Spotlight' feature for the desktop background, which shows different background images and occasionally tips and fun facts on the desktop. This feature uses policies, which will lock down certain settings.",
"Category": "Appearance", "Category": "Privacy & Suggested Content",
"RegistryKey": "Disable_Desktop_Spotlight.reg", "RegistryKey": "Disable_Desktop_Spotlight.reg",
"ApplyText": "Disabling the Windows spotlight desktop background", "ApplyText": "Disabling the 'Windows Spotlight' desktop background option",
"UndoLabel": "Enable Windows spotlight desktop background", "UndoLabel": "Enable Windows Spotlight for desktop",
"ApplyUndoText": "Restoring the Windows spotlight desktop background and shortcut", "ApplyUndoText": "Enabling the 'Windows Spotlight' desktop background option",
"RegistryUndoKey": "Enable_Desktop_Spotlight.reg", "RegistryUndoKey": "Enable_Desktop_Spotlight.reg",
"MinVersion": null, "MinVersion": null,
"MaxVersion": null "MaxVersion": null
}, },
{
"FeatureId": "HideDesktopSpotlightIcon",
"Label": "Hide the 'Learn about this picture' desktop shortcut",
"ToolTip": "Hides the 'Learn about this picture' desktop shortcut while keeping Windows spotlight wallpaper rotation enabled. It does not disable Windows spotlight.",
"Category": "Appearance",
"RegistryKey": "Hide_Desktop_Spotlight_Icon.reg",
"ApplyText": "Hiding the Windows spotlight 'Learn about this picture' desktop shortcut",
"UndoLabel": "Show the 'Learn about this picture' desktop shortcut",
"ApplyUndoText": "Showing the Windows spotlight 'Learn about this picture' desktop shortcut",
"RegistryUndoKey": "Show_Desktop_Spotlight_Icon.reg",
"MinVersion": null,
"MaxVersion": null
},
{ {
"FeatureId": "DisableEdgeAds", "FeatureId": "DisableEdgeAds",
"Label": "Disable ads, suggestions and newsfeed in Edge", "Label": "Disable ads, suggestions and newsfeed in Edge",
@@ -816,7 +757,6 @@
"UndoLabel": "Disable dark theme for system and apps", "UndoLabel": "Disable dark theme for system and apps",
"ApplyUndoText": "Disabling dark mode for system and apps", "ApplyUndoText": "Disabling dark mode for system and apps",
"RegistryUndoKey": "Enable_Light_Mode.reg", "RegistryUndoKey": "Enable_Light_Mode.reg",
"Priority": 1,
"MinVersion": null, "MinVersion": null,
"MaxVersion": null "MaxVersion": null
}, },
@@ -1386,7 +1326,6 @@
"UndoLabel": "Enable transparency effects", "UndoLabel": "Enable transparency effects",
"ApplyUndoText": "Enabling transparency effects", "ApplyUndoText": "Enabling transparency effects",
"RegistryUndoKey": "Enable_Transparency.reg", "RegistryUndoKey": "Enable_Transparency.reg",
"Priority": 2,
"MinVersion": null, "MinVersion": null,
"MaxVersion": null "MaxVersion": null
}, },
@@ -1400,7 +1339,6 @@
"UndoLabel": "Enable animations and visual effects", "UndoLabel": "Enable animations and visual effects",
"ApplyUndoText": "Enabling animations and visual effects", "ApplyUndoText": "Enabling animations and visual effects",
"RegistryUndoKey": "Enable_Animations.reg", "RegistryUndoKey": "Enable_Animations.reg",
"Priority": 3,
"RequiresReboot": true, "RequiresReboot": true,
"MinVersion": null, "MinVersion": null,
"MaxVersion": null "MaxVersion": null
-1
View File
@@ -118,7 +118,6 @@ Below is an overview of the key features and functionality offered by Win11Deblo
- Enable dark mode for system and apps. - Enable dark mode for system and apps.
- Disable transparency, animations and visual effects. - Disable transparency, animations and visual effects.
- Hide the 'Learn about this picture' shortcut for desktop spotlight, or disable the Windows spotlight background option entirely.
#### Start Menu & Search #### Start Menu & Search
Binary file not shown.
-7
View File
@@ -1,7 +0,0 @@
Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Policies\Microsoft\Windows\CloudContent]
"DisableSpotlightCollectionOnDesktop"=-
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel]
"{2cc5ca98-6485-489a-920e-b3e88a6ccce3}"=-
-7
View File
@@ -1,7 +0,0 @@
Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Policies\Microsoft\Windows\CloudContent]
"DisableSpotlightCollectionOnDesktop"=-
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel]
"{2cc5ca98-6485-489a-920e-b3e88a6ccce3}"=dword:00000001
Binary file not shown.
@@ -1,7 +0,0 @@
Windows Registry Editor Version 5.00
[hkey_users\default\Software\Policies\Microsoft\Windows\CloudContent]
"DisableSpotlightCollectionOnDesktop"=-
[hkey_users\default\Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel]
"{2cc5ca98-6485-489a-920e-b3e88a6ccce3}"=-
@@ -1,7 +0,0 @@
Windows Registry Editor Version 5.00
[hkey_users\default\Software\Policies\Microsoft\Windows\CloudContent]
"DisableSpotlightCollectionOnDesktop"=-
[hkey_users\default\Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel]
"{2cc5ca98-6485-489a-920e-b3e88a6ccce3}"=dword:00000001
@@ -1,7 +0,0 @@
Windows Registry Editor Version 5.00
[hkey_users\default\Software\Policies\Microsoft\Windows\CloudContent]
"DisableSpotlightCollectionOnDesktop"=-
[hkey_users\default\Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel]
"{2cc5ca98-6485-489a-920e-b3e88a6ccce3}"=-
@@ -1,4 +0,0 @@
Windows Registry Editor Version 5.00
[hkey_users\default\Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel]
"{2cc5ca98-6485-489a-920e-b3e88a6ccce3}"=-
Binary file not shown.
@@ -1,4 +0,0 @@
Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel]
"{2cc5ca98-6485-489a-920e-b3e88a6ccce3}"=-
+104 -23
View File
@@ -1,36 +1,50 @@
<# <#
.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")) {
Write-Host "[WhatIf] Forcefully uninstall Microsoft Edge" -ForegroundColor Cyan Write-Host "[WhatIf] Forcefully uninstall Microsoft Edge" -ForegroundColor Cyan
Write-Host "" return $true
return
} }
Write-Host "> Forcefully uninstalling Microsoft Edge..." try {
Write-Host "> Forcefully uninstalling Microsoft Edge..."
$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"
New-Item $edgeStub -ItemType Directory | Out-Null New-Item $edgeStub -ItemType Directory -Force -ErrorAction Stop | Out-Null
New-Item "$edgeStub\MicrosoftEdge.exe" | Out-Null New-Item "$edgeStub\MicrosoftEdge.exe" -ItemType File -Force -ErrorAction Stop | Out-Null
# Remove edge # Remove edge
$uninstallRegKey = $hklm.OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft Edge') $uninstallRegKey = $hklm.OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft Edge')
if ($null -ne $uninstallRegKey) { if ($null -eq $uninstallRegKey) {
Write-Host "Unable to forcefully uninstall Microsoft Edge, uninstaller could not be found" -ForegroundColor Red
return $false
}
Write-Host "Running uninstaller..." Write-Host "Running uninstaller..."
$uninstallString = $uninstallRegKey.GetValue('UninstallString') + ' --force-uninstall' $uninstallString = $uninstallRegKey.GetValue('UninstallString') + ' --force-uninstall'
Invoke-NonBlocking -ScriptBlock { $exitCode = Invoke-NonBlocking -ScriptBlock {
param($cmd) param($cmd)
Start-Process cmd.exe "/c $cmd" -WindowStyle Hidden -Wait $process = Start-Process cmd.exe "/c $cmd" -WindowStyle Hidden -Wait -PassThru
return $process.ExitCode
} -ArgumentList $uninstallString } -ArgumentList $uninstallString
if ($exitCode -ne 0) {
Write-Warning "Microsoft Edge uninstaller failed with exit code $exitCode."
return $false
}
Write-Host "Removing leftover files..." Write-Host "Removing leftover files..."
$cleanupSucceeded = $true
$edgePaths = @( $edgePaths = @(
"$env:ProgramData\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk", "$env:ProgramData\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk",
@@ -44,22 +58,89 @@ function Invoke-ForceRemoveEdge {
foreach ($path in $edgePaths) { foreach ($path in $edgePaths) {
if (Test-Path -Path $path) { if (Test-Path -Path $path) {
Remove-Item -Path $path -Force -Recurse -ErrorAction SilentlyContinue try {
Write-Host " Removed $path" -ForegroundColor DarkGray Remove-Item -Path $path -Force -Recurse -ErrorAction Stop
Write-Host " Removed $path" -ForegroundColor DarkGray
}
catch {
Write-Warning "Failed to remove Edge leftover '$path': $($_.Exception.Message)"
$cleanupSucceeded = $false
}
} }
} }
Write-Host "Cleaning up registry..." Write-Host "Cleaning up registry..."
$registryCleanupSucceeded = $true
# Remove MS Edge from autostart # Remove MS Edge from autostart. Missing values are already-clean state,
reg delete "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run" /v "MicrosoftEdgeAutoLaunch_A9F6DCE4ABADF4F51CF45CD7129E3C6C" /f *>$null # while failures to inspect or remove an existing value are reported.
reg delete "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run" /v "Microsoft Edge Update" /f *>$null $autostartValues = @(
reg delete "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run" /v "MicrosoftEdgeAutoLaunch_A9F6DCE4ABADF4F51CF45CD7129E3C6C" /f *>$null @{ Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'; Name = 'MicrosoftEdgeAutoLaunch_A9F6DCE4ABADF4F51CF45CD7129E3C6C' },
reg delete "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run" /v "Microsoft Edge Update" /f *>$null @{ Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'; Name = 'Microsoft Edge Update' },
@{ Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run'; Name = 'MicrosoftEdgeAutoLaunch_A9F6DCE4ABADF4F51CF45CD7129E3C6C' },
@{ Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run'; Name = 'Microsoft Edge Update' }
)
foreach ($autostartValue in $autostartValues) {
if (-not (Remove-EdgeAutostartValue -Path $autostartValue.Path -Name $autostartValue.Name)) {
$registryCleanupSucceeded = $false
}
}
if (-not $cleanupSucceeded -or -not $registryCleanupSucceeded) {
Write-Warning "Microsoft Edge was uninstalled, but some leftover files or autostart entries could not be removed."
return $false
}
Write-Host "Microsoft Edge was uninstalled" Write-Host "Microsoft Edge was uninstalled"
return $true
} }
else { catch {
Write-Host "Unable to forcefully uninstall Microsoft Edge, uninstaller could not be found" -ForegroundColor Red Write-Warning "Failed to forcefully uninstall Microsoft Edge: $($_.Exception.Message)"
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)]
[string]$Path,
[Parameter(Mandatory)]
[string]$Name
)
try {
$properties = Get-ItemProperty -Path $Path -ErrorAction Stop
}
catch [System.Management.Automation.ItemNotFoundException] {
return $true
}
catch {
Write-Warning "Failed to inspect Edge autostart entry '$Path\$Name': $($_.Exception.Message)"
return $false
}
if (-not $properties.PSObject.Properties[$Name]) {
return $true
}
try {
Remove-ItemProperty -Path $Path -Name $Name -ErrorAction Stop
return $true
}
catch {
Write-Warning "Failed to remove Edge autostart entry '$Path\$Name': $($_.Exception.Message)"
return $false
} }
} }
+73 -22
View File
@@ -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 (
@@ -29,10 +32,10 @@ function Remove-SelectedApps {
Write-Host "[WhatIf] Remove App Package: $app" -ForegroundColor Cyan Write-Host "[WhatIf] Remove App Package: $app" -ForegroundColor Cyan
} }
Write-Host "" return $true
return
} }
$failuresBefore = $script:AppRemovalFailures
$targetUser = Get-TargetUserForAppRemoval $targetUser = Get-TargetUserForAppRemoval
$appCount = @($appsList).Count $appCount = @($appsList).Count
$appIndex = 0 $appIndex = 0
@@ -42,7 +45,7 @@ function Remove-SelectedApps {
$wingetRemovalFailures = @{} $wingetRemovalFailures = @{}
Foreach ($app in $appsList) { Foreach ($app in $appsList) {
if ($script:CancelRequested) { return } if ($script:CancelRequested) { return $false }
$appIndex++ $appIndex++
@@ -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') {
if (-not (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 $wingetRemovalFailures[$app] = $true
} }
$wingetRemovedApps += $app
} }
else { else {
if (-not (Remove-AppxApp -app $app -targetUser $targetUser)) { if (-not (Remove-AppxApp -app $app -targetUser $targetUser)) {
@@ -66,17 +70,20 @@ function Remove-SelectedApps {
} }
if ($script:CancelRequested) { if ($script:CancelRequested) {
Write-Host "" return $false
return
} }
# Check whether any winget-removed apps are still present, and report errors for each one. # Check whether any winget-removed apps are still present, and report errors for each one.
if ($wingetRemovedApps.Count -gt 0) { if ($wingetRemovedApps.Count -gt 0) {
$postRemovalList = if ($script:WingetInstalled) { Get-WingetInstalledApps -TimeOut 10 -NonBlocking } else { $null } $postRemovalList = if ($script:WingetInstalled) { Get-WingetInstalledApps -TimeOut 10 -NonBlocking } else { $null }
$edgeForceRemoveRequested = $false $edgeForceRemoveRequested = $false
$edgeForceRemoveSucceeded = $false
if ($null -eq $postRemovalList) { if ($null -eq $postRemovalList) {
$script:AppRemovalVerificationUnavailable = $true $script:AppRemovalVerificationUnavailable = $true
foreach ($app in $wingetRemovedApps) {
$wingetRemovalFailures[$app] = $true
}
} }
else { else {
foreach ($app in $wingetRemovedApps) { foreach ($app in $wingetRemovedApps) {
@@ -87,8 +94,11 @@ function Remove-SelectedApps {
if ($edgeIds -contains $app) { if ($edgeIds -contains $app) {
Write-Host "Unable to uninstall Microsoft Edge via WinGet" -ForegroundColor Red Write-Host "Unable to uninstall Microsoft Edge via WinGet" -ForegroundColor Red
if (-not $edgeForceRemoveRequested) { if (-not $edgeForceRemoveRequested) {
Request-EdgeForceRemove
$edgeForceRemoveRequested = $true $edgeForceRemoveRequested = $true
$edgeForceRemoveSucceeded = Request-EdgeForceRemove
}
if ($edgeForceRemoveSucceeded) {
continue
} }
} }
else { else {
@@ -101,7 +111,7 @@ function Remove-SelectedApps {
$script:AppRemovalFailures += $wingetRemovalFailures.Count $script:AppRemovalFailures += $wingetRemovalFailures.Count
Write-Host "" return ($script:AppRemovalFailures -eq $failuresBefore)
} }
<# <#
@@ -110,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').
@@ -119,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(
@@ -131,22 +149,28 @@ function Remove-WinGetApp {
return $false return $false
} }
$uninstallSucceeded = $true $uninstallCommandSucceeded = $true
$exitCode = $null
try { try {
$uninstallSucceeded = Invoke-NonBlocking -ScriptBlock { $uninstallResult = Invoke-NonBlocking -ScriptBlock {
param($appId) param($appId)
$null = & winget uninstall --accept-source-agreements --disable-interactivity --id $appId 2>&1 $output = @(& winget uninstall --accept-source-agreements --disable-interactivity --id $appId 2>&1)
return $true return [PSCustomObject]@{
ExitCode = $LASTEXITCODE
Output = $output
}
} -ArgumentList $app -TimeoutSeconds $TimeoutSeconds } -ArgumentList $app -TimeoutSeconds $TimeoutSeconds
$uninstallSucceeded = [bool]$uninstallSucceeded Write-WinGetUninstallOutput -Output $(if ($uninstallResult) { $uninstallResult.Output } else { $null })
$exitCode = if ($uninstallResult) { $uninstallResult.ExitCode } else { 'unknown' }
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-Error "WinGet uninstall for $app did not complete within $TimeoutSeconds seconds: $_" Write-Verbose "WinGet uninstall for $app did not complete within $TimeoutSeconds seconds: $_"
} }
else { else {
Write-Error "WinGet uninstall for $app failed: $_" Write-Verbose "WinGet uninstall for $app failed: $_"
} }
} }
@@ -160,7 +184,29 @@ 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 {
param(
[object[]]$Output
)
foreach ($line in @($Output)) {
if ($null -eq $line) { continue }
$lineText = if ($line -is [System.Management.Automation.ErrorRecord]) { $line.Exception.Message } else { $line.ToString() }
if ([string]::IsNullOrWhiteSpace($lineText)) { continue }
Write-Verbose $lineText
}
} }
<# <#
@@ -277,19 +323,24 @@ 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) {
$result = Show-MessageBox -Message 'Unable to uninstall Microsoft Edge via WinGet. Would you like to forcefully uninstall it? NOT RECOMMENDED!' -Title 'Force Uninstall Microsoft Edge?' -Button 'YesNo' -Icon 'Warning' $result = Show-MessageBox -Message 'Unable to uninstall Microsoft Edge via WinGet. Would you like to forcefully uninstall it? NOT RECOMMENDED!' -Title 'Force Uninstall Microsoft Edge?' -Button 'YesNo' -Icon 'Warning'
if ($result -eq 'Yes') { if ($result -eq 'Yes') {
Write-Host "" Write-Host ""
Invoke-ForceRemoveEdge return (Invoke-ForceRemoveEdge)
} }
} }
elseif ($(Read-Host -Prompt "Would you like to forcefully uninstall Microsoft Edge? NOT RECOMMENDED! (y/n)") -eq 'y') { elseif ($(Read-Host -Prompt "Would you like to forcefully uninstall Microsoft Edge? NOT RECOMMENDED! (y/n)") -eq 'y') {
Write-Host "" Write-Host ""
Invoke-ForceRemoveEdge return (Invoke-ForceRemoveEdge)
} }
return $false
} }
<# <#
+102 -96
View File
@@ -1,114 +1,120 @@
# 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,
$path $path
) )
Write-Host $message
$usesOfflineHive = $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User")
$regFilePath = Get-RegistryFilePathForFeature -RegistryKey $path
if (-not (Test-Path $regFilePath)) {
$errorMessage = "Unable to find registry file: $path ($regFilePath)"
$script:RegistryImportFailures++
Write-Host "Error: $errorMessage" -ForegroundColor Red
Write-Host ""
throw $errorMessage
}
$importScript = {
param($targetRegFilePath, $hiveContext)
if ($script:Params.ContainsKey("WhatIf")) {
Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath
Write-Host ""
return
}
# When the target user's hive is already loaded under their SID, the .reg file's
# HKEY_USERS\Default paths won't match. Use the PowerShell registry writer instead,
# which remaps Default → SID via Split-RegistryPath.
$usePowerShellFallbackOnly = $hiveContext -and [bool]$hiveContext.WasAlreadyLoaded
if ($usePowerShellFallbackOnly) {
Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath
Write-Host "The operation completed successfully via PowerShell registry writer."
Write-Host ""
return
}
$regResult = Invoke-NonBlocking -ScriptBlock {
param($targetRegFilePath)
$result = @{
Output = @()
ExitCode = 0
Error = $null
}
try {
$global:LASTEXITCODE = 0
$output = reg import $targetRegFilePath 2>&1
$importExitCode = $LASTEXITCODE
if ($output) {
$result.Output = @($output)
}
$result.ExitCode = $importExitCode
if ($importExitCode -ne 0) {
throw "Registry import failed with exit code $importExitCode for '$targetRegFilePath'"
}
}
catch {
$result.Error = $_.Exception.Message
$result.ExitCode = if ($LASTEXITCODE -ne 0) { $LASTEXITCODE } else { 1 }
}
return $result
} -ArgumentList $targetRegFilePath
$regOutput = @($regResult.Output)
$hasSuccess = ($regResult.ExitCode -eq 0) -and -not $regResult.Error
if ($regOutput) {
foreach ($line in $regOutput) {
$lineText = if ($line -is [System.Management.Automation.ErrorRecord]) { $line.Exception.Message } else { $line.ToString() }
if ($lineText -and $lineText.Length -gt 0) {
if ($hasSuccess) {
Write-Host $lineText
}
else {
Write-Host $lineText -ForegroundColor Red
}
}
}
}
if (-not $hasSuccess) {
$details = if ($regResult.Error) { $regResult.Error } else { "Exit code: $($regResult.ExitCode)" }
Write-Warning "reg import failed for '$path'. Falling back to PowerShell registry writer. Details: $details"
Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath
Write-Host "The operation completed successfully via PowerShell registry writer."
}
Write-Host ""
}
try { try {
Write-Host $message
$usesOfflineHive = $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User")
$regFilePath = Get-RegistryFilePathForFeature -RegistryKey $path
if (-not (Test-Path $regFilePath)) {
$errorMessage = "Unable to find registry file: $path ($regFilePath)"
Write-Host "Error: $errorMessage" -ForegroundColor Red
return $false
}
$importScript = {
param($targetRegFilePath, $hiveContext)
if ($script:Params.ContainsKey("WhatIf")) {
return (Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath)
}
# When the target user's hive is already loaded under their SID, the .reg file's
# HKEY_USERS\Default paths won't match. Use the PowerShell registry writer instead,
# which remaps Default → SID via Split-RegistryPath.
$usePowerShellFallbackOnly = $hiveContext -and [bool]$hiveContext.WasAlreadyLoaded
if ($usePowerShellFallbackOnly) {
$fallbackSucceeded = Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath
if ($fallbackSucceeded) {
Write-Host "The operation completed successfully via PowerShell registry writer."
}
return $fallbackSucceeded
}
$regResult = Invoke-NonBlocking -ScriptBlock {
param($targetRegFilePath)
$result = @{
Output = @()
ExitCode = 0
Error = $null
}
try {
$global:LASTEXITCODE = 0
$output = reg import $targetRegFilePath 2>&1
$importExitCode = $LASTEXITCODE
if ($output) {
$result.Output = @($output)
}
$result.ExitCode = $importExitCode
if ($importExitCode -ne 0) {
throw "Registry import failed with exit code $importExitCode for '$targetRegFilePath'"
}
}
catch {
$result.Error = $_.Exception.Message
$result.ExitCode = if ($LASTEXITCODE -ne 0) { $LASTEXITCODE } else { 1 }
}
return $result
} -ArgumentList $targetRegFilePath
$regOutput = @($regResult.Output)
$hasSuccess = ($regResult.ExitCode -eq 0) -and -not $regResult.Error
if ($regOutput) {
foreach ($line in $regOutput) {
$lineText = if ($line -is [System.Management.Automation.ErrorRecord]) { $line.Exception.Message } else { $line.ToString() }
if ($lineText -and $lineText.Length -gt 0) {
if ($hasSuccess) {
Write-Host $lineText
}
else {
Write-Host $lineText -ForegroundColor Red
}
}
}
}
if (-not $hasSuccess) {
$details = if ($regResult.Error) { $regResult.Error } else { "Exit code: $($regResult.ExitCode)" }
Write-Warning "reg import failed for '$path'. Falling back to PowerShell registry writer. Details: $details"
$fallbackSucceeded = Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath
if ($fallbackSucceeded) {
Write-Host "The operation completed successfully via PowerShell registry writer."
}
return $fallbackSucceeded
}
return $true
}
if ($usesOfflineHive) { if ($usesOfflineHive) {
# Sysprep targets Default user, User targets the specified user. Logged-in users already have their hive mounted under HKU\<SID>. # Sysprep targets Default user, User targets the specified user. Logged-in users already have their hive mounted under HKU\<SID>.
$targetUserName = if ($script:Params.ContainsKey("Sysprep")) { "Default" } else { $script:Params.Item("User") } $targetUserName = if ($script:Params.ContainsKey("Sysprep")) { "Default" } else { $script:Params.Item("User") }
Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock $importScript -ArgumentObject $regFilePath -PassHiveContext $succeeded = Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock $importScript -ArgumentObject $regFilePath -PassHiveContext
} }
else { else {
& $importScript $regFilePath $null $succeeded = & $importScript $regFilePath $null
} }
return [bool]$succeeded
} }
catch { catch {
$script:RegistryImportFailures++
Write-Host $_.Exception.Message -ForegroundColor Red Write-Host $_.Exception.Message -ForegroundColor Red
Write-Host "" return $false
} }
} }
+125 -96
View File
@@ -7,7 +7,9 @@
- Registry-backed: imports the .reg file via Import-RegistryFile, then runs - Registry-backed: imports the .reg file via Import-RegistryFile, then runs
any post-import side effects (e.g., removing companion app packages). any post-import side effects (e.g., removing companion app packages).
- Custom logic: app removal, Windows optional features, start menu - Custom logic: app removal, Windows optional features, start menu
replacement, and other special-case features. replacement, and other special-case features. Returns $true when the
feature completes successfully; otherwise writes a warning and returns
$false.
#> #>
function Invoke-FeatureApply { function Invoke-FeatureApply {
param( param(
@@ -15,30 +17,32 @@ function Invoke-FeatureApply {
[string]$FeatureId [string]$FeatureId
) )
# Resolve feature metadata from Features.json try {
$feature = $script:Features[$FeatureId] # Resolve feature metadata from Features.json
$applyText = $feature.ApplyText $feature = $script:Features[$FeatureId]
$applyText = $feature.ApplyText
# ---- Registry-backed features: import .reg file, then handle side effects ---- # ---- Registry-backed features: import .reg file, then handle additional tasks ----
if ($feature.RegistryKey) { if ($feature.RegistryKey) {
Import-RegistryFile "> $applyText..." $feature.RegistryKey if (-not (Import-RegistryFile "> $applyText..." $feature.RegistryKey)) {
return $false
}
# Post-import side effects for specific features
switch ($FeatureId) { switch ($FeatureId) {
'DisableBing' { 'DisableBing' {
# Also remove the app package for Bing search # Also remove the app package for Bing search
Remove-SelectedApps @('Microsoft.BingSearch') return (Remove-SelectedApps @('Microsoft.BingSearch'))
} }
'DisableCopilot' { 'DisableCopilot' {
# Also remove the app packages for Copilot # Also remove the app packages for Copilot
Remove-SelectedApps @('Microsoft.Copilot', 'XP9CXNGPPJ97XX') return (Remove-SelectedApps @('Microsoft.Copilot', 'XP9CXNGPPJ97XX'))
} }
'DisableTelemetry' { 'DisableTelemetry' {
# Also disable telemetry scheduled tasks # Also disable telemetry scheduled tasks
Disable-TelemetryScheduledTasks return (Disable-TelemetryScheduledTasks)
} }
} }
return return $true
} }
# ---- Custom features (no registry backing, or special handling required) ---- # ---- Custom features (no registry backing, or special handling required) ----
@@ -49,30 +53,25 @@ function Invoke-FeatureApply {
if ($appsList.Count -eq 0) { if ($appsList.Count -eq 0) {
Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow
Write-Host "" return $true
return
} }
Write-Host "$($appsList.Count) apps selected for removal" Write-Host "$($appsList.Count) apps selected for removal"
Remove-SelectedApps $appsList return (Remove-SelectedApps $appsList)
return
} }
'RemoveGamingApps' { 'RemoveGamingApps' {
$appsList = @('Microsoft.GamingApp', 'Microsoft.XboxGameOverlay', 'Microsoft.XboxGamingOverlay') $appsList = @('Microsoft.GamingApp', 'Microsoft.XboxGameOverlay', 'Microsoft.XboxGamingOverlay')
Write-Host "> $applyText..." Write-Host "> $applyText..."
Remove-SelectedApps $appsList return (Remove-SelectedApps $appsList)
return
} }
'RemoveHPApps' { 'RemoveHPApps' {
$appsList = @('AD2F1837.HPAIExperienceCenter', 'AD2F1837.HPJumpStarts', 'AD2F1837.HPPCHardwareDiagnosticsWindows', 'AD2F1837.HPPowerManager', 'AD2F1837.HPPrivacySettings', 'AD2F1837.HPSupportAssistant', 'AD2F1837.HPSureShieldAI', 'AD2F1837.HPSystemInformation', 'AD2F1837.HPQuickDrop', 'AD2F1837.HPWorkWell', 'AD2F1837.myHP', 'AD2F1837.HPDesktopSupportUtilities', 'AD2F1837.HPQuickTouch', 'AD2F1837.HPEasyClean', 'AD2F1837.HPConnectedMusic', 'AD2F1837.HPFileViewer', 'AD2F1837.HPRegistration', 'AD2F1837.HPWelcome', 'AD2F1837.HPConnectedPhotopoweredbySnapfish', 'AD2F1837.HPPrinterControl') $appsList = @('AD2F1837.HPAIExperienceCenter', 'AD2F1837.HPJumpStarts', 'AD2F1837.HPPCHardwareDiagnosticsWindows', 'AD2F1837.HPPowerManager', 'AD2F1837.HPPrivacySettings', 'AD2F1837.HPSupportAssistant', 'AD2F1837.HPSureShieldAI', 'AD2F1837.HPSystemInformation', 'AD2F1837.HPQuickDrop', 'AD2F1837.HPWorkWell', 'AD2F1837.myHP', 'AD2F1837.HPDesktopSupportUtilities', 'AD2F1837.HPQuickTouch', 'AD2F1837.HPEasyClean', 'AD2F1837.HPConnectedMusic', 'AD2F1837.HPFileViewer', 'AD2F1837.HPRegistration', 'AD2F1837.HPWelcome', 'AD2F1837.HPConnectedPhotopoweredbySnapfish', 'AD2F1837.HPPrinterControl')
Write-Host "> $applyText..." Write-Host "> $applyText..."
Remove-SelectedApps $appsList return (Remove-SelectedApps $appsList)
return
} }
'ForceRemoveEdge' { 'ForceRemoveEdge' {
Write-Host "> $applyText..." Write-Host "> $applyText..."
Invoke-ForceRemoveEdge return (Invoke-ForceRemoveEdge)
return
} }
'DisableWidgets' { 'DisableWidgets' {
Write-Host "> $applyText..." Write-Host "> $applyText..."
@@ -81,76 +80,75 @@ function Invoke-FeatureApply {
Get-Process *Widget* -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue Get-Process *Widget* -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
} }
Remove-SelectedApps @('Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime') return (Remove-SelectedApps @('Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime'))
return
} }
'EnableWindowsSandbox' { 'EnableWindowsSandbox' {
Write-Host "> $applyText..." Write-Host "> $applyText..."
Enable-WindowsFeature "Containers-DisposableClientVM" return (Enable-WindowsFeature "Containers-DisposableClientVM")
Write-Host ""
return
} }
'EnableWindowsSubsystemForLinux' { 'EnableWindowsSubsystemForLinux' {
Write-Host "> $applyText..." Write-Host "> $applyText..."
Enable-WindowsFeature "VirtualMachinePlatform" if (-not (Enable-WindowsFeature "VirtualMachinePlatform")) { return $false }
Enable-WindowsFeature "Microsoft-Windows-Subsystem-Linux" return (Enable-WindowsFeature "Microsoft-Windows-Subsystem-Linux")
Write-Host ""
return
} }
'ClearStart' { 'ClearStart' {
Write-Host "> $applyText for user $(Get-UserName)..." Write-Host "> $applyText for user $(Get-UserName)..."
$startMenuBinFile = Get-StartMenuBinPathForUser -UserName (Get-UserName) $startMenuBinFile = Get-StartMenuBinPathForUser -UserName (Get-UserName)
if (-not [string]::IsNullOrWhiteSpace($startMenuBinFile)) { if (-not [string]::IsNullOrWhiteSpace($startMenuBinFile)) {
Replace-StartMenu -startMenuBinFile $startMenuBinFile return (Replace-StartMenu -startMenuBinFile $startMenuBinFile)
} }
Write-Host "" Write-Warning "Unable to apply '$applyText': the Start menu path for user $(Get-UserName) could not be resolved."
return return $false
} }
'ReplaceStart' { 'ReplaceStart' {
Write-Host "> $applyText for user $(Get-UserName)..." Write-Host "> $applyText for user $(Get-UserName)..."
$startMenuBinFile = Get-StartMenuBinPathForUser -UserName (Get-UserName) $startMenuBinFile = Get-StartMenuBinPathForUser -UserName (Get-UserName)
if (-not [string]::IsNullOrWhiteSpace($startMenuBinFile)) { if (-not [string]::IsNullOrWhiteSpace($startMenuBinFile)) {
Replace-StartMenu -startMenuBinFile $startMenuBinFile -startMenuTemplate $script:Params.Item("ReplaceStart") return (Replace-StartMenu -startMenuBinFile $startMenuBinFile -startMenuTemplate $script:Params.Item("ReplaceStart"))
} }
Write-Host "" Write-Warning "Unable to apply '$applyText': the Start menu path for user $(Get-UserName) could not be resolved."
return return $false
} }
'ClearStartAllUsers' { 'ClearStartAllUsers' {
Replace-StartMenuForAllUsers return (Replace-StartMenuForAllUsers)
return
} }
'ReplaceStartAllUsers' { 'ReplaceStartAllUsers' {
Replace-StartMenuForAllUsers -startMenuTemplate $script:Params.Item("ReplaceStartAllUsers") return (Replace-StartMenuForAllUsers -startMenuTemplate $script:Params.Item("ReplaceStartAllUsers"))
return
} }
'DisableStoreSearchSuggestions' { 'DisableStoreSearchSuggestions' {
if ($script:Params.ContainsKey("Sysprep")) { if ($script:Params.ContainsKey("Sysprep")) {
Write-Host "> Disabling Microsoft Store search suggestions in the start menu for all users..." Write-Host "> Disabling Microsoft Store search suggestions in the start menu for all users..."
Set-StoreSearchSuggestionsDisabledForAllUsers return (Set-StoreSearchSuggestionsDisabledForAllUsers)
Write-Host ""
return
} }
Write-Host "> Disabling Microsoft Store search suggestions for user $(Get-UserName)..." Write-Host "> Disabling Microsoft Store search suggestions for user $(Get-UserName)..."
$storeDb = Get-StoreAppsDatabasePathForUser -UserName (Get-UserName) $storeDb = Get-StoreAppsDatabasePathForUser -UserName (Get-UserName)
if ($storeDb) { if ($storeDb) {
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase $storeDb return (Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase $storeDb)
} }
Write-Host "" Write-Warning "Unable to disable Microsoft Store search suggestions because the Store database for user $(Get-UserName) could not be resolved."
return return $false
} }
} }
}
catch {
Write-Warning "Failed to apply '$applyText': $($_.Exception.Message)"
return $false
}
Write-Warning "Unknown feature '$FeatureId' could not be applied."
return $false
} }
<# <#
.SYNOPSIS .SYNOPSIS
Undoes a single feature that has no RegistryUndoKey. Undoes a single feature.
.DESCRIPTION .DESCRIPTION
Handles undo for features that require custom logic rather than a simple Handles registry-backed undo imports and custom undo logic. Returns
.reg file import. Features with a RegistryUndoKey are handled directly $true when the requested undo succeeds; otherwise writes a warning and
via Import-RegistryFile in Invoke-UndoFeatures. returns $false.
#> #>
function Invoke-FeatureUndo { function Invoke-FeatureUndo {
param( param(
@@ -159,43 +157,65 @@ function Invoke-FeatureUndo {
) )
$feature = if ($script:Features.ContainsKey($FeatureId)) { $script:Features[$FeatureId] } else { $null } $feature = if ($script:Features.ContainsKey($FeatureId)) { $script:Features[$FeatureId] } else { $null }
if (-not $feature) {
Write-Warning "Unknown feature '$FeatureId' could not be undone."
return $false
}
switch ($FeatureId) { $undoText = if ($feature.ApplyUndoText) { $feature.ApplyUndoText } elseif ($feature.UndoLabel) { $feature.UndoLabel } else { $FeatureId }
'DisableStoreSearchSuggestions' {
if ($script:Params.ContainsKey('Sysprep')) { try {
Write-Host "> Re-enabling Microsoft Store search suggestions in the start menu for all users..." # ---- Registry-backed features: import undo data, then handle additional tasks ----
Set-StoreSearchSuggestionsEnabledForAllUsers if ($feature.RegistryUndoKey) {
Write-Host "" if (-not (Import-RegistryFile "> $undoText" (Resolve-UndoRegFilePath $feature.RegistryUndoKey))) {
return return $false
} }
Write-Host "> Re-enabling Microsoft Store search suggestions for user $(Get-UserName)..." switch ($FeatureId) {
$storeDb = Get-StoreAppsDatabasePathForUser -UserName (Get-UserName) 'DisableTelemetry' {
if ($storeDb) { # Also re-enable telemetry scheduled tasks.
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase $storeDb return (Enable-TelemetryScheduledTasks)
}
} }
Write-Host ""
return return $true
} }
'EnableWindowsSandbox' {
Write-Host "> $($feature.ApplyUndoText)..." # ---- Custom undo features (no registry backing) ----
Disable-WindowsFeature 'Containers-DisposableClientVM' switch ($FeatureId) {
Write-Host "" 'DisableStoreSearchSuggestions' {
return if ($script:Params.ContainsKey('Sysprep')) {
} Write-Host "> Re-enabling Microsoft Store search suggestions in the start menu for all users..."
'EnableWindowsSubsystemForLinux' { return (Set-StoreSearchSuggestionsEnabledForAllUsers)
Write-Host "> $($feature.ApplyUndoText)..." }
Disable-WindowsFeature 'Microsoft-Windows-Subsystem-Linux'
Disable-WindowsFeature 'VirtualMachinePlatform' Write-Host "> Re-enabling Microsoft Store search suggestions for user $(Get-UserName)..."
Write-Host "" $storeDb = Get-StoreAppsDatabasePathForUser -UserName (Get-UserName)
return if ($storeDb) {
} return (Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase $storeDb)
'DisableTelemetry' { }
# Also re-enable telemetry scheduled tasks Write-Warning "Unable to re-enable Microsoft Store search suggestions because the Store database for user $(Get-UserName) could not be resolved."
Enable-TelemetryScheduledTasks return $false
return }
'EnableWindowsSandbox' {
Write-Host "> $undoText..."
return (Disable-WindowsFeature 'Containers-DisposableClientVM')
}
'EnableWindowsSubsystemForLinux' {
Write-Host "> $undoText..."
if (-not (Disable-WindowsFeature 'Microsoft-Windows-Subsystem-Linux')) { return $false }
return (Disable-WindowsFeature 'VirtualMachinePlatform')
}
} }
} }
catch {
Write-Warning "Failed to undo '$undoText': $($_.Exception.Message)"
return $false
}
Write-Warning "Feature '$FeatureId' does not support undo."
return $false
} }
@@ -251,7 +271,13 @@ function Invoke-ApplyFeatures {
& $script:ApplyProgressCallback $step $TotalSteps $displayName & $script:ApplyProgressCallback $step $TotalSteps $displayName
} }
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 ""
$step++ $step++
} }
} }
@@ -262,9 +288,8 @@ function Invoke-ApplyFeatures {
Undoes a list of features, reporting progress for each. Undoes a list of features, reporting progress for each.
.DESCRIPTION .DESCRIPTION
Iterates through the provided feature IDs. Features with a RegistryUndoKey Iterates through the provided feature IDs and delegates each to
are handled by importing the undo .reg file; all others delegate to Invoke-FeatureUndo, which handles registry-backed and custom undo logic.
Invoke-FeatureUndo for custom undo logic.
This is called by Invoke-AllChanges during the undo phase. This is called by Invoke-AllChanges during the undo phase.
#> #>
function Invoke-UndoFeatures { function Invoke-UndoFeatures {
@@ -291,11 +316,10 @@ function Invoke-UndoFeatures {
& $script:ApplyProgressCallback $step $TotalSteps $undoText & $script:ApplyProgressCallback $step $TotalSteps $undoText
} }
if ($f -and $f.RegistryUndoKey) { if (-not (Invoke-FeatureUndo -FeatureId $featureId)) {
Import-RegistryFile "> $undoText" (Resolve-UndoRegFilePath $f.RegistryUndoKey) $script:FeatureFailures++
} }
Write-Host ""
Invoke-FeatureUndo -FeatureId $featureId
$step++ $step++
} }
} }
@@ -324,8 +348,8 @@ function Invoke-AllChanges {
throw "Win11Debloat is running as the SYSTEM account. Use the '-User' or '-Sysprep' parameter to target a specific user." throw "Win11Debloat is running as the SYSTEM account. Use the '-User' or '-Sysprep' parameter to target a specific user."
} }
$script:RegistryImportFailures = 0
$script:AppRemovalFailures = 0 $script:AppRemovalFailures = 0
$script:FeatureFailures = 0
$script:AppRemovalVerificationUnavailable = $false $script:AppRemovalVerificationUnavailable = $false
# ---- Gather work items ---- # ---- Gather work items ----
@@ -405,7 +429,11 @@ function Invoke-AllChanges {
} }
else { else {
Write-Host "> Creating a system restore point..." Write-Host "> Creating a system restore point..."
Invoke-SystemRestorePoint $restorePointSucceeded = Invoke-SystemRestorePoint
if (-not $restorePointSucceeded) {
if ($script:CancelRequested) { return }
$script:FeatureFailures++
}
Write-Host "" Write-Host ""
} }
} }
@@ -429,19 +457,20 @@ function Invoke-AllChanges {
} }
# ================================================================ # ================================================================
# Final: Report registry import and app removal failures # Final: Report failures
# ================================================================ # ================================================================
if ($script:RegistryImportFailures -gt 0) {
Write-Host ""
Write-Warning "$($script:RegistryImportFailures) registry import change(s) failed. See output above for details."
}
if ($script:AppRemovalFailures -gt 0) { if ($script:AppRemovalFailures -gt 0) {
Write-Host "" Write-Host ""
Write-Warning "$($script:AppRemovalFailures) app removal(s) failed. See output above for details." Write-Warning "$($script:AppRemovalFailures) app removal(s) failed. See output above for details."
} }
if ($script:FeatureFailures -gt 0) {
Write-Host ""
Write-Warning "$($script:FeatureFailures) feature change(s) failed. See output above for details."
}
if ($script:AppRemovalVerificationUnavailable) { if ($script:AppRemovalVerificationUnavailable) {
Write-Host ""
Write-Warning "Unable to verify if all apps were uninstalled successfully." Write-Warning "Unable to verify if all apps were uninstalled successfully."
} }
+26 -9
View File
@@ -1,10 +1,25 @@
function Invoke-SystemRestorePoint { <#
$SysRestore = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRestore" -Name "RPSessionInterval" .SYNOPSIS
$failed = $false Creates a system restore point.
if ($SysRestore.RPSessionInterval -eq 0) { .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
try {
$SysRestore = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRestore" -Name "RPSessionInterval" -ErrorAction Stop
}
catch {
Write-Host "Error: Unable to determine whether System Restore is enabled: $($_.Exception.Message)" -ForegroundColor Red
$failed = $true
}
if (-not $failed -and $SysRestore.RPSessionInterval -eq 0) {
# In GUI mode, skip the prompt and just try to enable it # In GUI mode, skip the prompt and just try to enable it
if ($script:GuiWindow -or $Silent -or $( Read-Host -Prompt "System restore is disabled, would you like to enable it and create a restore point? (y/n)") -eq 'y') { if ($script:GuiWindow -or $isSilent -or $( Read-Host -Prompt "System restore is disabled, would you like to enable it and create a restore point? (y/n)") -eq 'y') {
try { try {
$enableResult = Invoke-NonBlocking -TimeoutSeconds 90 -ScriptBlock { $enableResult = Invoke-NonBlocking -TimeoutSeconds 90 -ScriptBlock {
try { try {
@@ -26,7 +41,6 @@ function Invoke-SystemRestorePoint {
} }
} }
else { else {
Write-Host ""
$failed = $true $failed = $true
} }
} }
@@ -79,17 +93,20 @@ function Invoke-SystemRestorePoint {
if ($result -ne "Yes") { if ($result -ne "Yes") {
$script:CancelRequested = $true $script:CancelRequested = $true
return return $false
} }
} }
elseif (-not $Silent) { elseif (-not $isSilent) {
Write-Host "Failed to create a system restore point. Do you want to continue without a restore point? (y/n)" -ForegroundColor Yellow Write-Host "Failed to create a system restore point. Do you want to continue without a restore point? (y/n)" -ForegroundColor Yellow
if ($( Read-Host ) -ne 'y') { if ($( Read-Host ) -ne 'y') {
$script:CancelRequested = $true $script:CancelRequested = $true
return return $false
} }
} }
Write-Host "Warning: Continuing without restore point" -ForegroundColor Yellow Write-Host "Warning: Continuing without restore point" -ForegroundColor Yellow
return $false
} }
return $true
} }
+48 -23
View File
@@ -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 (
@@ -29,8 +32,7 @@ function Replace-StartMenuForAllUsers {
# Check if template bin file exists # Check if template bin file exists
if (-not (Test-Path $startMenuTemplate)) { if (-not (Test-Path $startMenuTemplate)) {
Write-Host "Error: Unable to clear start menu, start2.bin file missing from script folder" -ForegroundColor Red Write-Host "Error: Unable to clear start menu, start2.bin file missing from script folder" -ForegroundColor Red
Write-Host "" return $false
return
} }
# Get path to start menu file for all users # Get path to start menu file for all users
@@ -38,8 +40,11 @@ function Replace-StartMenuForAllUsers {
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue $usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
# Go through all users and replace the start menu file # Go through all users and replace the start menu file
$success = $true
ForEach ($startMenuPath in $usersStartMenuPaths) { ForEach ($startMenuPath in $usersStartMenuPaths) {
Replace-StartMenu -startMenuBinFile "$($startMenuPath.Fullname)\start2.bin" -startMenuTemplate $startMenuTemplate if (-not (Replace-StartMenu -startMenuBinFile "$($startMenuPath.Fullname)\start2.bin" -startMenuTemplate $startMenuTemplate)) {
$success = $false
}
} }
# Also replace the start menu file for the default user profile # Also replace the start menu file for the default user profile
@@ -47,19 +52,29 @@ function Replace-StartMenuForAllUsers {
if ($script:Params.ContainsKey("WhatIf")) { if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Replace Start Menu for Default user profile with template $startMenuTemplate" -ForegroundColor Cyan Write-Host "[WhatIf] Replace Start Menu for Default user profile with template $startMenuTemplate" -ForegroundColor Cyan
return return $true
} }
# Create folder if it doesn't exist # Create folder if it doesn't exist
if (-not (Test-Path $defaultStartMenuPath)) { if (-not (Test-Path $defaultStartMenuPath)) {
new-item $defaultStartMenuPath -ItemType Directory -Force | Out-Null try {
Write-Host "Created LocalState folder for default user profile" New-Item $defaultStartMenuPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
Write-Host "Created LocalState folder for default user profile"
}
catch {
Write-Warning "Failed to create the Default profile Start Menu directory: $($_.Exception.Message)"
return $false
}
} }
# Copy template to default profile # Copy template to default profile
Replace-StartMenu -startMenuBinFile "$($defaultStartMenuPath)\start2.bin" -startMenuTemplate $startMenuTemplate if (-not (Replace-StartMenu -startMenuBinFile "$($defaultStartMenuPath)\start2.bin" -startMenuTemplate $startMenuTemplate)) {
Write-Host "Replaced start menu for the default user profile" $success = $false
Write-Host "" }
else {
Write-Host "Replaced start menu for the default user profile"
}
return $success
} }
@@ -87,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 (
@@ -98,19 +116,19 @@ function Replace-StartMenu {
# Check if template bin file exists # Check if template bin file exists
if (-not (Test-Path $startMenuTemplate)) { if (-not (Test-Path $startMenuTemplate)) {
Write-Host "Error: Unable to replace start menu, template file not found" -ForegroundColor Red Write-Host "Error: Unable to replace start menu, template file not found" -ForegroundColor Red
return return $false
} }
if ([IO.Path]::GetExtension($startMenuTemplate) -ne ".bin") { if ([IO.Path]::GetExtension($startMenuTemplate) -ne ".bin") {
Write-Host "Error: Unable to replace start menu, template file is not a valid .bin file" -ForegroundColor Red Write-Host "Error: Unable to replace start menu, template file is not a valid .bin file" -ForegroundColor Red
return return $false
} }
$userName = Get-StartMenuUserNameFromPath -StartMenuBinFile $startMenuBinFile $userName = Get-StartMenuUserNameFromPath -StartMenuBinFile $startMenuBinFile
if ($script:Params.ContainsKey("WhatIf")) { if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Replace Start Menu for user $userName with template $startMenuTemplate" -ForegroundColor Cyan Write-Host "[WhatIf] Replace Start Menu for user $userName with template $startMenuTemplate" -ForegroundColor Cyan
return return $true
} }
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss' $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
@@ -118,20 +136,27 @@ function Replace-StartMenu {
$startMenuDir = Split-Path $startMenuBinFile -Parent $startMenuDir = Split-Path $startMenuBinFile -Parent
$backupBinFile = Join-Path $startMenuDir $backupFileName $backupBinFile = Join-Path $startMenuDir $backupFileName
if (Test-Path $startMenuBinFile) { try {
# Backup current start menu file if (Test-Path $startMenuBinFile) {
Copy-Item -Path $startMenuBinFile -Destination $backupBinFile -Force # Backup current start menu file
Write-Verbose "Start menu backup for user $userName saved to $backupFileName" Copy-Item -Path $startMenuBinFile -Destination $backupBinFile -Force -ErrorAction Stop
} Write-Verbose "Start menu backup for user $userName saved to $backupFileName"
else { }
Write-Host "Unable to find original start2.bin file for user $userName, no backup was created for this user" -ForegroundColor Yellow else {
New-Item -ItemType File -Path $startMenuBinFile -Force Write-Host "Unable to find original start2.bin file for user $userName, no backup was created for this user" -ForegroundColor Yellow
} New-Item -ItemType File -Path $startMenuBinFile -Force -ErrorAction Stop | Out-Null
}
# Copy template file # Copy template file
Copy-Item -Path $startMenuTemplate -Destination $startMenuBinFile -Force Copy-Item -Path $startMenuTemplate -Destination $startMenuBinFile -Force -ErrorAction Stop
}
catch {
Write-Warning "Failed to replace Start Menu for user ${userName}: $($_.Exception.Message)"
return $false
}
Write-Host "Replaced start menu for user $userName" Write-Host "Replaced start menu for user $userName"
return $true
} }
<# <#
+78 -25
View File
@@ -10,22 +10,41 @@
.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
$processedProfiles = 0
# Get path to Store app database for all users # Get path to Store app database for all users
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages" $userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages"
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue $usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
# Go through all users and disable start search suggestions # Go through all users and disable start search suggestions
foreach ($storeDbPath in $usersStoreDbPaths) { foreach ($storeDbPath in $usersStoreDbPaths) {
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db") $processedProfiles++
if (-not (Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"))) {
$success = $false
}
} }
# Also disable start search suggestions for the default user profile # Also disable start search suggestions for the default user profile
$defaultStoreDbPath = Get-StoreAppsDatabasePathForUser -UserName "Default" $defaultStoreDbPath = Get-StoreAppsDatabasePathForUser -UserName "Default"
if ($defaultStoreDbPath) { if ($defaultStoreDbPath) {
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase $defaultStoreDbPath $processedProfiles++
if (-not (Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase $defaultStoreDbPath)) {
$success = $false
}
} }
if ($processedProfiles -eq 0) {
Write-Warning 'Unable to disable Microsoft Store search suggestions because no target user profiles could be resolved.'
return $false
}
return $success
} }
@@ -44,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 (
@@ -56,24 +78,22 @@ function Set-StoreSearchSuggestionsDisabled {
if ($script:Params.ContainsKey("WhatIf")) { if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Disable Microsoft Store search suggestions for user $userName by restricting access to ${StoreAppsDatabase}" -ForegroundColor Cyan Write-Host "[WhatIf] Disable Microsoft Store search suggestions for user $userName by restricting access to ${StoreAppsDatabase}" -ForegroundColor Cyan
return return $true
}
# This file doesn't exist in EEA (No Store app suggestions).
if (-not (Test-Path -Path $StoreAppsDatabase))
{
Write-Host "Unable to find Store app database for user $userName, creating it now to prevent Windows from creating it later..." -ForegroundColor Yellow
$storeDbDir = Split-Path -Path $StoreAppsDatabase -Parent
if (-not (Test-Path -Path $storeDbDir)) {
New-Item -Path $storeDbDir -ItemType Directory -Force | Out-Null
}
New-Item -Path $StoreAppsDatabase -ItemType File -Force | Out-Null
} }
try { try {
# This file doesn't exist in EEA (No Store app suggestions).
if (-not (Test-Path -Path $StoreAppsDatabase)) {
Write-Host "Unable to find Store app database for user $userName, creating it now to prevent Windows from creating it later..." -ForegroundColor Yellow
$storeDbDir = Split-Path -Path $StoreAppsDatabase -Parent
if (-not (Test-Path -Path $storeDbDir)) {
New-Item -Path $storeDbDir -ItemType Directory -Force -ErrorAction Stop | Out-Null
}
New-Item -Path $StoreAppsDatabase -ItemType File -Force -ErrorAction Stop | Out-Null
}
$AccountSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0') # 'EVERYONE' group $AccountSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0') # 'EVERYONE' group
$Acl = Get-Acl -Path $StoreAppsDatabase -ErrorAction Stop $Acl = Get-Acl -Path $StoreAppsDatabase -ErrorAction Stop
$Ace = [System.Security.AccessControl.FileSystemAccessRule]::new($AccountSid, 'FullControl', 'Deny') $Ace = [System.Security.AccessControl.FileSystemAccessRule]::new($AccountSid, 'FullControl', 'Deny')
@@ -82,10 +102,11 @@ function Set-StoreSearchSuggestionsDisabled {
} }
catch { catch {
Write-Warning "Failed to restrict ACL for store database '$StoreAppsDatabase': $($_.Exception.Message)" Write-Warning "Failed to restrict ACL for store database '$StoreAppsDatabase': $($_.Exception.Message)"
return return $false
} }
Write-Host "Disabled Microsoft Store search suggestions for user $userName" Write-Host "Disabled Microsoft Store search suggestions for user $userName"
return $true
} }
<# <#
@@ -100,22 +121,41 @@ 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
$processedProfiles = 0
# Get path to Store app database for all users # Get path to Store app database for all users
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages" $userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages"
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue $usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
# Go through all users and re-enable start search suggestions # Go through all users and re-enable start search suggestions
foreach ($storeDbPath in $usersStoreDbPaths) { foreach ($storeDbPath in $usersStoreDbPaths) {
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db") $processedProfiles++
if (-not (Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"))) {
$success = $false
}
} }
# Also re-enable for the default user profile # Also re-enable for the default user profile
$defaultStoreDbPath = Get-StoreAppsDatabasePathForUser -UserName "Default" $defaultStoreDbPath = Get-StoreAppsDatabasePathForUser -UserName "Default"
if ($defaultStoreDbPath) { if ($defaultStoreDbPath) {
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase $defaultStoreDbPath $processedProfiles++
if (-not (Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase $defaultStoreDbPath)) {
$success = $false
}
} }
if ($processedProfiles -eq 0) {
Write-Warning 'Unable to re-enable Microsoft Store search suggestions because no target user profiles could be resolved.'
return $false
}
return $success
} }
<# <#
@@ -133,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 (
@@ -145,23 +188,31 @@ function Set-StoreSearchSuggestionsEnabled {
if ($script:Params.ContainsKey("WhatIf")) { if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Re-enable Microsoft Store search suggestions for user $userName by restoring access to ${StoreAppsDatabase}" -ForegroundColor Cyan Write-Host "[WhatIf] Re-enable Microsoft Store search suggestions for user $userName by restoring access to ${StoreAppsDatabase}" -ForegroundColor Cyan
return return $true
} }
if (-not (Test-Path -Path $StoreAppsDatabase)) { if (-not (Test-Path -Path $StoreAppsDatabase)) {
Write-Host "Store app database not found for user $userName, nothing to undo" Write-Host "Store app database not found for user $userName, nothing to undo"
return return $true
} }
# Ensure we can modify/delete the file even if restrictive ACLs were set. # Ensure we can modify/delete the file even if restrictive ACLs were set.
$global:LASTEXITCODE = 0 $global:LASTEXITCODE = 0
takeown /F "$StoreAppsDatabase" /A | Out-Null takeown /F "$StoreAppsDatabase" /A | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Warning "Failed to take ownership of store database '$StoreAppsDatabase' while undoing Microsoft Store search suggestions. Exit code: $LASTEXITCODE"
return $false
}
icacls "$StoreAppsDatabase" /grant *S-1-5-32-544:F /C | Out-Null icacls "$StoreAppsDatabase" /grant *S-1-5-32-544:F /C | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Warning "Failed to grant Administrators access to store database '$StoreAppsDatabase' while undoing Microsoft Store search suggestions. Exit code: $LASTEXITCODE"
return $false
}
$everyoneSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0') # 'EVERYONE' group $everyoneSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0') # 'EVERYONE' group
try { try {
$acl = Get-Acl -Path $StoreAppsDatabase $acl = Get-Acl -Path $StoreAppsDatabase -ErrorAction Stop
$denyRules = @( $denyRules = @(
$acl.Access | Where-Object { $acl.Access | Where-Object {
if ($_.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Deny) { return $false } if ($_.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Deny) { return $false }
@@ -179,7 +230,7 @@ function Set-StoreSearchSuggestionsEnabled {
$null = $acl.RemoveAccessRuleSpecific($denyRule) $null = $acl.RemoveAccessRuleSpecific($denyRule)
} }
Set-Acl -Path $StoreAppsDatabase -AclObject $acl | Out-Null Set-Acl -Path $StoreAppsDatabase -AclObject $acl -ErrorAction Stop | Out-Null
} }
catch { catch {
Write-Warning "Failed to normalize ACL for store database '$StoreAppsDatabase': $($_.Exception.Message)" Write-Warning "Failed to normalize ACL for store database '$StoreAppsDatabase': $($_.Exception.Message)"
@@ -188,9 +239,11 @@ function Set-StoreSearchSuggestionsEnabled {
try { try {
Remove-Item -Path $StoreAppsDatabase -Force -ErrorAction Stop Remove-Item -Path $StoreAppsDatabase -Force -ErrorAction Stop
Write-Host "Re-enabled Microsoft Store search suggestions for user $userName" Write-Host "Re-enabled Microsoft Store search suggestions for user $userName"
return $true
} }
catch { catch {
throw "Failed to remove '$StoreAppsDatabase' while undoing Microsoft Store search suggestions for user $userName. $($_.Exception.Message)" Write-Warning "Failed to remove '$StoreAppsDatabase' while undoing Microsoft Store search suggestions for user $userName. $($_.Exception.Message)"
return $false
} }
} }
+54 -14
View File
@@ -34,23 +34,36 @@ 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..."
$tasks = Get-TelemetryScheduledTasks $tasks = Get-TelemetryScheduledTasks
$success = $true
foreach ($task in $tasks) { foreach ($task in $tasks) {
if ($script:CancelRequested) { return } if ($script:CancelRequested) { return $false }
if ($script:Params.ContainsKey("WhatIf")) { if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Disable Scheduled Task: $($task.Path)$($task.Name)" -ForegroundColor Cyan Write-Host "[WhatIf] Disable Scheduled Task: $($task.Path)$($task.Name)" -ForegroundColor Cyan
continue continue
} }
$result = Invoke-NonBlocking -ScriptBlock { try {
$result = Invoke-NonBlocking -ScriptBlock {
param($path, $name) param($path, $name)
Import-Module ScheduledTasks -ErrorAction SilentlyContinue try {
$taskObj = Get-ScheduledTask -TaskPath $path -TaskName $name -ErrorAction SilentlyContinue Import-Module ScheduledTasks -ErrorAction Stop
$taskObj = Get-ScheduledTask -TaskPath $path -TaskName $name -ErrorAction Stop
}
catch {
if ($_.Exception -isnot [System.Management.Automation.CommandNotFoundException] -and $_.CategoryInfo.Category -eq [System.Management.Automation.ErrorCategory]::ObjectNotFound) {
return @{ Success = $true; Status = 'NotFound' }
}
return @{ Success = $false; Status = 'Error'; Error = $_.Exception.Message }
}
if (-not $taskObj) { if (-not $taskObj) {
return @{ Success = $true; Status = 'NotFound' } return @{ Success = $true; Status = 'NotFound' }
} }
@@ -64,17 +77,24 @@ function Disable-TelemetryScheduledTasks {
} }
} }
return @{ Success = $true; Status = 'AlreadyDisabled' } return @{ Success = $true; Status = 'AlreadyDisabled' }
} -ArgumentList @($task.Path, $task.Name) } -ArgumentList @($task.Path, $task.Name)
}
catch {
Write-Warning "Failed to disable Scheduled Task: $($task.Path)$($task.Name) - $($_.Exception.Message)"
$success = $false
continue
}
switch ($result.Status) { switch ($result.Status) {
'Disabled' { Write-Host "Disabled Scheduled Task: $($task.Path)$($task.Name)" } 'Disabled' { Write-Host "Disabled Scheduled Task: $($task.Path)$($task.Name)" }
'AlreadyDisabled' { Write-Host "Scheduled Task $($task.Path)$($task.Name) is already disabled" -ForegroundColor DarkGray } 'AlreadyDisabled' { Write-Host "Scheduled Task $($task.Path)$($task.Name) is already disabled" -ForegroundColor DarkGray }
'NotFound' { Write-Host "Scheduled Task $($task.Path)$($task.Name) not found" -ForegroundColor DarkGray } 'NotFound' { Write-Host "Scheduled Task $($task.Path)$($task.Name) not found" -ForegroundColor DarkGray }
'Error' { Write-Host "Failed to disable Scheduled Task: $($task.Path)$($task.Name) - $($result.Error)" -ForegroundColor Yellow } 'Error' { Write-Host "Failed to disable Scheduled Task: $($task.Path)$($task.Name) - $($result.Error)" -ForegroundColor Yellow; $success = $false }
default { Write-Warning "Unable to determine the result of disabling Scheduled Task: $($task.Path)$($task.Name)."; $success = $false }
} }
} }
Write-Host "" return $success
} }
<# <#
@@ -88,23 +108,36 @@ 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..."
$tasks = Get-TelemetryScheduledTasks $tasks = Get-TelemetryScheduledTasks
$success = $true
foreach ($task in $tasks) { foreach ($task in $tasks) {
if ($script:CancelRequested) { return } if ($script:CancelRequested) { return $false }
if ($script:Params.ContainsKey("WhatIf")) { if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Enable Scheduled Task: $($task.Path)$($task.Name)" -ForegroundColor Cyan Write-Host "[WhatIf] Enable Scheduled Task: $($task.Path)$($task.Name)" -ForegroundColor Cyan
continue continue
} }
$result = Invoke-NonBlocking -ScriptBlock { try {
$result = Invoke-NonBlocking -ScriptBlock {
param($path, $name) param($path, $name)
Import-Module ScheduledTasks -ErrorAction SilentlyContinue try {
$taskObj = Get-ScheduledTask -TaskPath $path -TaskName $name -ErrorAction SilentlyContinue Import-Module ScheduledTasks -ErrorAction Stop
$taskObj = Get-ScheduledTask -TaskPath $path -TaskName $name -ErrorAction Stop
}
catch {
if ($_.Exception -isnot [System.Management.Automation.CommandNotFoundException] -and $_.CategoryInfo.Category -eq [System.Management.Automation.ErrorCategory]::ObjectNotFound) {
return @{ Success = $true; Status = 'NotFound' }
}
return @{ Success = $false; Status = 'Error'; Error = $_.Exception.Message }
}
if (-not $taskObj) { if (-not $taskObj) {
return @{ Success = $true; Status = 'NotFound' } return @{ Success = $true; Status = 'NotFound' }
} }
@@ -118,15 +151,22 @@ function Enable-TelemetryScheduledTasks {
} }
} }
return @{ Success = $true; Status = 'AlreadyEnabled' } return @{ Success = $true; Status = 'AlreadyEnabled' }
} -ArgumentList @($task.Path, $task.Name) } -ArgumentList @($task.Path, $task.Name)
}
catch {
Write-Warning "Failed to enable Scheduled Task: $($task.Path)$($task.Name) - $($_.Exception.Message)"
$success = $false
continue
}
switch ($result.Status) { switch ($result.Status) {
'Enabled' { Write-Host "Enabled Scheduled Task: $($task.Path)$($task.Name)" } 'Enabled' { Write-Host "Enabled Scheduled Task: $($task.Path)$($task.Name)" }
'AlreadyEnabled' { Write-Host "Scheduled Task $($task.Path)$($task.Name) is already enabled." -ForegroundColor DarkGray } 'AlreadyEnabled' { Write-Host "Scheduled Task $($task.Path)$($task.Name) is already enabled." -ForegroundColor DarkGray }
'NotFound' { Write-Host "Scheduled Task $($task.Path)$($task.Name) not found." -ForegroundColor DarkGray } 'NotFound' { Write-Host "Scheduled Task $($task.Path)$($task.Name) not found." -ForegroundColor DarkGray }
'Error' { Write-Host "Failed to enable Scheduled Task: $($task.Path)$($task.Name) - $($result.Error)" -ForegroundColor Yellow } 'Error' { Write-Host "Failed to enable Scheduled Task: $($task.Path)$($task.Name) - $($result.Error)" -ForegroundColor Yellow; $success = $false }
default { Write-Warning "Unable to determine the result of enabling Scheduled Task: $($task.Path)$($task.Name)."; $success = $false }
} }
} }
Write-Host "" return $success
} }
+80 -22
View File
@@ -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
@@ -6,22 +12,51 @@ function Enable-WindowsFeature {
if ($script:Params.ContainsKey("WhatIf")) { if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Enable Windows feature: $FeatureName" -ForegroundColor Cyan Write-Host "[WhatIf] Enable Windows feature: $FeatureName" -ForegroundColor Cyan
Write-Host "" return $true
return
} }
$result = Invoke-NonBlocking -ScriptBlock { try {
param($name) $result = Invoke-NonBlocking -ScriptBlock {
Enable-WindowsOptionalFeature -Online -FeatureName $name -All -NoRestart param($name)
} -ArgumentList $FeatureName try {
$output = Enable-WindowsOptionalFeature -Online -FeatureName $name -All -NoRestart -ErrorAction Stop
$dismResult = @($result) | Where-Object { $_ -is [Microsoft.Dism.Commands.ImageObject] } return [PSCustomObject]@{
if ($dismResult) { Success = $true
Write-Host ($dismResult | Out-String).Trim() Output = if ($output) { ($output | Out-String).Trim() } else { $null }
Error = $null
}
}
catch {
return [PSCustomObject]@{
Success = $false
Output = $null
Error = $_.Exception.Message
}
}
} -ArgumentList $FeatureName
} }
catch {
Write-Warning "Failed to enable Windows feature '$FeatureName': $($_.Exception.Message)"
return $false
}
if (-not $result -or -not $result.Success) {
$details = if ($result -and $result.Error) { ": $($result.Error)" } else { '' }
Write-Warning "Failed to enable Windows feature '$FeatureName'$details"
return $false
}
if ($result.Output) { Write-Host $result.Output }
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
@@ -29,19 +64,42 @@ function Disable-WindowsFeature {
if ($script:Params.ContainsKey("WhatIf")) { if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Disable Windows feature: $FeatureName" -ForegroundColor Cyan Write-Host "[WhatIf] Disable Windows feature: $FeatureName" -ForegroundColor Cyan
Write-Host "" return $true
return
} }
$result = Invoke-NonBlocking -ScriptBlock { try {
param($name) $result = Invoke-NonBlocking -ScriptBlock {
Disable-WindowsOptionalFeature -Online -FeatureName $name -NoRestart param($name)
} -ArgumentList $FeatureName try {
$output = Disable-WindowsOptionalFeature -Online -FeatureName $name -NoRestart -ErrorAction Stop
$dismResult = @($result) | Where-Object { $_ -is [Microsoft.Dism.Commands.ImageObject] } return [PSCustomObject]@{
if ($dismResult) { Success = $true
Write-Host ($dismResult | Out-String).Trim() Output = if ($output) { ($output | Out-String).Trim() } else { $null }
Error = $null
}
}
catch {
return [PSCustomObject]@{
Success = $false
Output = $null
Error = $_.Exception.Message
}
}
} -ArgumentList $FeatureName
} }
catch {
Write-Warning "Failed to disable Windows feature '$FeatureName': $($_.Exception.Message)"
return $false
}
if (-not $result -or -not $result.Success) {
$details = if ($result -and $result.Error) { ": $($result.Error)" } else { '' }
Write-Warning "Failed to disable Windows feature '$FeatureName'$details"
return $false
}
if ($result.Output) { Write-Host $result.Output }
return $true
} }
function Test-WindowsOptionalFeatureEnabled { function Test-WindowsOptionalFeatureEnabled {
+4 -2
View File
@@ -233,9 +233,11 @@ function Get-AppRemovalScopeTarget {
switch ($selectedItem.Name) { switch ($selectedItem.Name) {
"AppRemovalScopeAllUsers" { return 'AllUsers' } "AppRemovalScopeAllUsers" { return 'AllUsers' }
"AppRemovalScopeCurrentUser" { return 'CurrentUser' } "AppRemovalScopeCurrentUser" { return 'CurrentUser' }
default {
Write-Warning "Unrecognized app-removal scope item '$($selectedItem.Name)'. Skipping app removal."
return $null
}
} }
return $null
} }
function Invoke-AppPreset { function Invoke-AppPreset {
+3 -3
View File
@@ -369,7 +369,7 @@ function New-DynamicTweakControls {
try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {} try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {}
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock } if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
} }
$script:UiControlMappings[$comboName] = @{ Type = 'feature'; FeatureId = $soleFeature.FeatureId; Label = $soleFeature.Label; Category = $categoryName; CategoryId = $categoryId } $script:UiControlMappings[$comboName] = @{ Type = 'feature'; FeatureId = $soleFeature.FeatureId; Label = $soleFeature.Label; CategoryId = $categoryId }
} }
continue continue
} }
@@ -389,7 +389,7 @@ function New-DynamicTweakControls {
try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {} try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {}
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock } if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
} }
$script:UiControlMappings[$comboName] = @{ Type = 'group'; Values = $filteredValues; Label = $group.Label; Category = $categoryName; CategoryId = $categoryId } $script:UiControlMappings[$comboName] = @{ Type = 'group'; Values = $filteredValues; Label = $group.Label; CategoryId = $categoryId }
} }
elseif ($item.Type -eq 'feature') { elseif ($item.Type -eq 'feature') {
$feature = $item.Data $feature = $item.Data
@@ -416,7 +416,7 @@ function New-DynamicTweakControls {
try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {} try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {}
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock } if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
} }
$script:UiControlMappings[$comboName] = @{ Type = 'feature'; FeatureId = $feature.FeatureId; Label = $feature.Label; Category = $categoryName; CategoryId = $categoryId } $script:UiControlMappings[$comboName] = @{ Type = 'feature'; FeatureId = $feature.FeatureId; Label = $feature.Label; CategoryId = $categoryId }
} }
} }
} }
+2 -8
View File
@@ -114,9 +114,7 @@ function Show-ApplyModal {
try { try {
Invoke-AllChanges Invoke-AllChanges
$registryImportFailureCount = [int]$script:RegistryImportFailures $failureCount = [int]$script:FeatureFailures + [int]$script:AppRemovalFailures
$appRemovalFailureCount = [int]$script:AppRemovalFailures
$failureCount = $registryImportFailureCount + $appRemovalFailureCount
$appRemovalVerificationUnavailable = [bool]$script:AppRemovalVerificationUnavailable $appRemovalVerificationUnavailable = [bool]$script:AppRemovalVerificationUnavailable
# Restart explorer if requested # Restart explorer if requested
@@ -157,11 +155,7 @@ function Show-ApplyModal {
} }
else { else {
$script:ApplyCompletionTitleEl.Text = "Changes Applied with Errors" $script:ApplyCompletionTitleEl.Text = "Changes Applied with Errors"
$failureMessages = @() $script:ApplyCompletionMessageEl.Text = "$failureCount change(s) failed. See console for details."
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 { } else {
Write-Host "All changes have been applied successfully!" Write-Host "All changes have been applied successfully!"
@@ -506,20 +506,15 @@ function Import-Configuration {
return return
} }
if (-not $config.Version) { $consistencyError = Test-ConfigConsistency -Config $config
Write-Error "Invalid configuration file format: '$($openDialog.FileName)'" if ($consistencyError) {
Show-MessageBox -Message "Invalid configuration file format." -Title 'Invalid Config' -Button 'OK' -Icon 'Error' | Out-Null Write-Error "Invalid configuration file '$($openDialog.FileName)': $consistencyError"
Show-MessageBox -Message "Invalid configuration file: $consistencyError" -Title 'Invalid Config' -Button 'OK' -Icon 'Error' | Out-Null
return return
} }
$availableCategories = Get-AvailableImportExportCategories -Config $config $availableCategories = Get-AvailableImportExportCategories -Config $config
if ($availableCategories.Count -eq 0) {
Write-Warning "Configuration file '$($openDialog.FileName)' contains no importable data."
Show-MessageBox -Message "The selected file contains no importable data." -Title 'Invalid Config' -Button 'OK' -Icon 'Error' | Out-Null
return
}
Write-Host "Available categories in config: $($availableCategories -join ', ')" Write-Host "Available categories in config: $($availableCategories -join ', ')"
$appCount = @($config.Apps | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) }).Count $appCount = @($config.Apps | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) }).Count
+7 -5
View File
@@ -670,13 +670,15 @@ 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 ',')
Add-Parameter 'AppRemovalTarget' $scopeTarget
$scopeTarget = Get-AppRemovalScopeTarget -AppRemovalScopeCombo $appRemovalScopeCombo -OtherUsernameTextBox $otherUsernameTextBox
if ($scopeTarget) {
Add-Parameter 'AppRemovalTarget' $scopeTarget
}
} }
# Apply dynamic tweaks # Apply dynamic tweaks
+1 -4
View File
@@ -39,8 +39,6 @@ param (
[switch]$DisableBing, [switch]$DisableBing,
[switch]$DisableStoreSearchSuggestions, [switch]$DisableStoreSearchSuggestions,
[switch]$DisableDesktopSpotlight, [switch]$DisableDesktopSpotlight,
[switch]$HideDesktopSpotlightIcon,
[switch]$EnableDesktopSpotlight,
[switch]$DisableLockscreenTips, [switch]$DisableLockscreenTips,
[switch]$DisableSuggestions, [switch]$DisableSuggestions,
[switch]$DisableLocationServices, [switch]$DisableLocationServices,
@@ -136,8 +134,7 @@ try {
Invoke-RestMethod $sourceUri -OutFile $tempArchivePath Invoke-RestMethod $sourceUri -OutFile $tempArchivePath
} }
catch { catch {
Write-Host "Unable to fetch required files from GitHub. Please check your internet connection and try again." -ForegroundColor Red Write-Host "Error: Unable to fetch required files from GitHub. Please check your internet connection and try again." -ForegroundColor Red
Write-Error -ErrorRecord $_
Write-Output "" Write-Output ""
Write-Output "Press enter to exit..." Write-Output "Press enter to exit..."
Read-Host | Out-Null Read-Host | Out-Null
+11 -1
View File
@@ -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)]
@@ -203,7 +210,7 @@ function Invoke-RegistryOperationsFromRegFile {
if ($script:Params.ContainsKey("WhatIf")) { if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Apply $totalOperations registry changes from '$RegFilePath'" -ForegroundColor Cyan Write-Host "[WhatIf] Apply $totalOperations registry changes from '$RegFilePath'" -ForegroundColor Cyan
return return $true
} }
foreach ($operation in $operations) { foreach ($operation in $operations) {
@@ -222,5 +229,8 @@ function Invoke-RegistryOperationsFromRegFile {
if ($accessDeniedCount -gt 0) { if ($accessDeniedCount -gt 0) {
Write-Warning "Registry fallback import completed with $accessDeniedCount access-restricted operation(s) skipped in '$RegFilePath'." Write-Warning "Registry fallback import completed with $accessDeniedCount access-restricted operation(s) skipped in '$RegFilePath'."
return $false
} }
return $true
} }
@@ -31,6 +31,11 @@ function Import-ConfigToParams {
throw "Failed to read config file: $resolvedConfigPath" throw "Failed to read config file: $resolvedConfigPath"
} }
$consistencyError = Test-ConfigConsistency -Config $configJson
if ($consistencyError) {
throw "Invalid config file '$resolvedConfigPath': $consistencyError"
}
$importedItems = 0 $importedItems = 0
if ($configJson.Apps) { if ($configJson.Apps) {
@@ -0,0 +1,97 @@
<#
.SYNOPSIS
Validates that a configuration file is structurally consistent before it is applied.
.DESCRIPTION
Returns $null when the configuration is valid, otherwise a string describing the
first problem found. Used by both the CLI and GUI import paths to reject invalid
configs before any settings are applied.
.OUTPUTS
System.String. $null when valid, otherwise an error message.
#>
function Test-ConfigConsistency {
param($Config)
if (-not $Config) {
return 'Configuration is empty or could not be read.'
}
if (-not $Config.Version) {
return 'Configuration is missing a Version field.'
}
if (-not $Config.Apps -and -not $Config.Tweaks -and -not $Config.Deployment) {
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) {
$lookup[$setting.Name] = $setting.Value
}
}
$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 $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 $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'])")) {
return "App removal scope 'Target user only' (AppRemovalScopeIndex 2) requires an 'OtherUsername' value."
}
}
return $null
}
-92
View File
@@ -1,92 +0,0 @@
BeforeAll {
$script:RepoRoot = Join-Path $PSScriptRoot '..'
$script:FeaturesPath = Join-Path $script:RepoRoot 'Config\Features.json'
$script:Win11DebloatPath = Join-Path $script:RepoRoot 'Win11Debloat.ps1'
$script:GetScriptPath = Join-Path $script:RepoRoot 'Scripts\Get.ps1'
$script:RegfilesPath = Join-Path $script:RepoRoot 'Regfiles'
$script:Features = @((Get-Content -LiteralPath $script:FeaturesPath -Raw | ConvertFrom-Json).Features)
$script:LauncherOnlyParameters = @('Dev', 'Verbose', 'WhatIf')
function Get-ScriptParameterNames {
param(
[Parameter(Mandatory)]
[string]$Path
)
$tokens = $null
$parseErrors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$parseErrors)
$parseErrors | Should -BeNullOrEmpty
@($ast.ParamBlock.Parameters | ForEach-Object { $_.Name.VariablePath.UserPath })
}
}
Describe 'FeatureId parameter contracts' {
It 'declares every FeatureId as a parameter on Win11Debloat.ps1' {
$parameterNames = Get-ScriptParameterNames -Path $script:Win11DebloatPath
$missing = @($script:Features.FeatureId | Where-Object { $parameterNames -notcontains $_ })
$missing | Should -HaveCount 0 -Because ($missing -join ', ')
}
It 'declares every FeatureId as a parameter on Scripts/Get.ps1' {
$parameterNames = Get-ScriptParameterNames -Path $script:GetScriptPath
$missing = @($script:Features.FeatureId | Where-Object { $parameterNames -notcontains $_ })
$missing | Should -HaveCount 0 -Because ($missing -join ', ')
}
It 'keeps Scripts/Get.ps1 aligned with Win11Debloat.ps1 plus launcher-only switches' {
$win11DebloatParameters = Get-ScriptParameterNames -Path $script:Win11DebloatPath
$getParameters = Get-ScriptParameterNames -Path $script:GetScriptPath
$missingFromGet = @($win11DebloatParameters | Where-Object { $getParameters -notcontains $_ })
$missingLauncherOnly = @(
$script:LauncherOnlyParameters |
Where-Object { $getParameters -notcontains $_ }
)
$unexpectedOnGet = @(
$getParameters |
Where-Object { $win11DebloatParameters -notcontains $_ -and $script:LauncherOnlyParameters -notcontains $_ }
)
$missingFromGet | Should -HaveCount 0 -Because ($missingFromGet -join ', ')
$missingLauncherOnly | Should -HaveCount 0 -Because ($missingLauncherOnly -join ', ')
$unexpectedOnGet | Should -HaveCount 0 -Because ($unexpectedOnGet -join ', ')
}
It 'keeps every RegistryKey file in Regfiles and Regfiles/Sysprep' {
$missing = @(
foreach ($feature in $script:Features) {
if ([string]::IsNullOrWhiteSpace($feature.RegistryKey)) { continue }
$applyPath = Join-Path $script:RegfilesPath $feature.RegistryKey
$sysprepPath = Join-Path (Join-Path $script:RegfilesPath 'Sysprep') $feature.RegistryKey
if (-not (Test-Path -LiteralPath $applyPath -PathType Leaf)) {
"$($feature.FeatureId) missing Regfiles\$($feature.RegistryKey)"
}
if (-not (Test-Path -LiteralPath $sysprepPath -PathType Leaf)) {
"$($feature.FeatureId) missing Regfiles\Sysprep\$($feature.RegistryKey)"
}
}
)
$missing | Should -HaveCount 0 -Because ($missing -join '; ')
}
It 'keeps every RegistryUndoKey file in Regfiles/Undo or Regfiles' {
$missing = @(
foreach ($feature in $script:Features) {
if ([string]::IsNullOrWhiteSpace($feature.RegistryUndoKey)) { continue }
$undoPath = Join-Path (Join-Path $script:RegfilesPath 'Undo') $feature.RegistryUndoKey
$rootPath = Join-Path $script:RegfilesPath $feature.RegistryUndoKey
if (-not ((Test-Path -LiteralPath $undoPath -PathType Leaf) -or (Test-Path -LiteralPath $rootPath -PathType Leaf))) {
"$($feature.FeatureId) missing undo file $($feature.RegistryUndoKey)"
}
}
)
$missing | Should -HaveCount 0 -Because ($missing -join '; ')
}
}
+121
View File
@@ -2,6 +2,7 @@ BeforeAll {
. (Join-Path $PSScriptRoot '..\Scripts\FileIO\Import-JsonFile.ps1') . (Join-Path $PSScriptRoot '..\Scripts\FileIO\Import-JsonFile.ps1')
. (Join-Path $PSScriptRoot '..\Scripts\Helpers\Add-Parameter.ps1') . (Join-Path $PSScriptRoot '..\Scripts\Helpers\Add-Parameter.ps1')
. (Join-Path $PSScriptRoot '..\Scripts\Helpers\Import-ConfigToParams.ps1') . (Join-Path $PSScriptRoot '..\Scripts\Helpers\Import-ConfigToParams.ps1')
. (Join-Path $PSScriptRoot '..\Scripts\Helpers\Test-ConfigConsistency.ps1')
$script:ConfigFixturePath = Join-Path $PSScriptRoot 'TestData\JsonFileLoading\ExportedConfig.WithSettings.json' $script:ConfigFixturePath = Join-Path $PSScriptRoot 'TestData\JsonFileLoading\ExportedConfig.WithSettings.json'
$script:SkipRegistryBackupFixturePath = Join-Path $PSScriptRoot 'TestData\JsonFileLoading\ExportedConfig.SkipRegistryBackup.json' $script:SkipRegistryBackupFixturePath = Join-Path $PSScriptRoot 'TestData\JsonFileLoading\ExportedConfig.SkipRegistryBackup.json'
} }
@@ -44,3 +45,123 @@ Describe 'Import-ConfigToParams' {
$script:Params['SkipRegistryBackup'] | Should -BeTrue $script:Params['SkipRegistryBackup'] | Should -BeTrue
} }
} }
Describe 'Test-ConfigConsistency' {
It 'reports an error for an empty config' {
Test-ConfigConsistency -Config $null | Should -Match 'empty or could not be read'
}
It 'reports an error for a config missing a Version' {
$config = [PSCustomObject]@{ Tweaks = @( @{ Name = 'DisableTelemetry'; Value = $true } ) }
Test-ConfigConsistency -Config $config | Should -Match 'missing a Version'
}
It 'reports an error for a config with no importable data' {
$config = [PSCustomObject]@{ Version = '1.0' }
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 malformed tweak entries' {
$config = [PSCustomObject]@{ Version = '1.0'; Tweaks = @(@{ Value = $true }) }
Test-ConfigConsistency -Config $config | Should -Match 'Tweaks entries must contain Name and Value properties'
}
It 'reports an error for deployment entries missing a required property' {
$config = [PSCustomObject]@{ Version = '1.0'; Deployment = @(@{ Name = 'CreateRestorePoint' }) }
Test-ConfigConsistency -Config $config | Should -Match 'Deployment entries must contain Name and Value properties'
}
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' {
$config = [PSCustomObject]@{
Version = '1.0'
Deployment = @(
@{ Name = 'UserSelectionIndex'; Value = 0 }
@{ Name = 'AppRemovalScopeIndex'; Value = 0 }
)
}
Test-ConfigConsistency -Config $config | Should -BeNullOrEmpty
}
It 'returns null for target-user scope combined with Other User and a username' {
$config = [PSCustomObject]@{
Version = '1.0'
Deployment = @(
@{ Name = 'UserSelectionIndex'; Value = 1 }
@{ Name = 'OtherUsername'; Value = 'jdoe' }
@{ Name = 'AppRemovalScopeIndex'; Value = 2 }
)
}
Test-ConfigConsistency -Config $config | Should -BeNullOrEmpty
}
It 'returns null for current-user-only scope combined with Current User' {
$config = [PSCustomObject]@{
Version = '1.0'
Deployment = @(
@{ Name = 'UserSelectionIndex'; Value = 0 }
@{ Name = 'AppRemovalScopeIndex'; Value = 1 }
)
}
Test-ConfigConsistency -Config $config | Should -BeNullOrEmpty
}
It 'reports an error for current-user-only scope without Current User selected' {
$config = [PSCustomObject]@{
Version = '1.0'
Deployment = @(
@{ Name = 'UserSelectionIndex'; Value = 1 }
@{ Name = 'AppRemovalScopeIndex'; Value = 1 }
)
}
Test-ConfigConsistency -Config $config | Should -Match "requires the deployment target 'Current User'"
}
It 'reports an error for target-user scope without Other User selected' {
$config = [PSCustomObject]@{
Version = '1.0'
Deployment = @(
@{ Name = 'UserSelectionIndex'; Value = 0 }
@{ Name = 'AppRemovalScopeIndex'; Value = 2 }
)
}
Test-ConfigConsistency -Config $config | Should -Match "requires the deployment target 'Other User'"
}
It 'reports an error for target-user scope with a blank username' {
$config = [PSCustomObject]@{
Version = '1.0'
Deployment = @(
@{ Name = 'UserSelectionIndex'; Value = 1 }
@{ Name = 'OtherUsername'; Value = ' ' }
@{ Name = 'AppRemovalScopeIndex'; Value = 2 }
)
}
Test-ConfigConsistency -Config $config | Should -Match "requires an 'OtherUsername' value"
}
}
+22 -11
View File
@@ -9,27 +9,33 @@ BeforeAll {
Describe 'Import-RegistryFile' { Describe 'Import-RegistryFile' {
BeforeEach { BeforeEach {
$script:Params = @{} $script:Params = @{}
$script:RegistryImportFailures = 0
$script:regPath = Join-Path $TestDrive 'feature.reg' $script:regPath = Join-Path $TestDrive 'feature.reg'
'' | Set-Content -LiteralPath $script:regPath '' | Set-Content -LiteralPath $script:regPath
Mock Get-RegistryFilePathForFeature { $script:regPath } Mock Get-RegistryFilePathForFeature { $script:regPath }
Mock Invoke-RegistryOperationsFromRegFile {} Mock Invoke-RegistryOperationsFromRegFile { $true }
Mock Invoke-WithTargetUserHive {} Mock Invoke-WithTargetUserHive {}
Mock Invoke-NonBlocking { [PSCustomObject]@{ Output = @(); ExitCode = 0; Error = $null } } Mock Invoke-NonBlocking { [PSCustomObject]@{ Output = @(); ExitCode = 0; Error = $null } }
Mock Write-Host {} Mock Write-Host {}
Mock Write-Warning {} Mock Write-Warning {}
} }
It 'throws and increments the failure count when the registry file is missing' { It 'returns false when the registry file is missing' {
Mock Get-RegistryFilePathForFeature { Join-Path $TestDrive 'missing.reg' } Mock Get-RegistryFilePathForFeature { Join-Path $TestDrive 'missing.reg' }
{ Import-RegistryFile -message 'Apply' -path 'missing.reg' } | Should -Throw 'Unable to find registry file:*'
$script:RegistryImportFailures | Should -Be 1 Import-RegistryFile -message 'Apply' -path 'missing.reg' | Should -BeFalse
Should -Invoke Invoke-NonBlocking -Times 0 -Exactly
}
It 'returns false when registry file resolution throws' {
Mock Get-RegistryFilePathForFeature { throw 'path resolution failed' }
Import-RegistryFile -message 'Apply' -path 'feature.reg' | Should -BeFalse
Should -Invoke Invoke-NonBlocking -Times 0 -Exactly Should -Invoke Invoke-NonBlocking -Times 0 -Exactly
} }
It 'uses the PowerShell writer only in WhatIf mode' { It 'uses the PowerShell writer only in WhatIf mode' {
$script:Params = @{ WhatIf = $true } $script:Params = @{ WhatIf = $true }
Import-RegistryFile -message 'Apply' -path 'feature.reg' Import-RegistryFile -message 'Apply' -path 'feature.reg' | Should -BeTrue
Should -Invoke Invoke-RegistryOperationsFromRegFile -Times 1 -Exactly -ParameterFilter { $RegFilePath -eq $script:regPath } Should -Invoke Invoke-RegistryOperationsFromRegFile -Times 1 -Exactly -ParameterFilter { $RegFilePath -eq $script:regPath }
Should -Invoke Invoke-NonBlocking -Times 0 -Exactly Should -Invoke Invoke-NonBlocking -Times 0 -Exactly
} }
@@ -41,7 +47,7 @@ Describe 'Import-RegistryFile' {
& $ScriptBlock $ArgumentObject ([PSCustomObject]@{ WasAlreadyLoaded = $true }) & $ScriptBlock $ArgumentObject ([PSCustomObject]@{ WasAlreadyLoaded = $true })
} }
Import-RegistryFile -message 'Apply' -path 'feature.reg' Import-RegistryFile -message 'Apply' -path 'feature.reg' | Should -BeTrue
Should -Invoke Invoke-WithTargetUserHive -Times 1 -Exactly -ParameterFilter { $TargetUserName -eq 'Alice' -and $PassHiveContext } Should -Invoke Invoke-WithTargetUserHive -Times 1 -Exactly -ParameterFilter { $TargetUserName -eq 'Alice' -and $PassHiveContext }
Should -Invoke Invoke-RegistryOperationsFromRegFile -Times 1 -Exactly Should -Invoke Invoke-RegistryOperationsFromRegFile -Times 1 -Exactly
@@ -51,17 +57,22 @@ Describe 'Import-RegistryFile' {
It 'falls back to the PowerShell writer when reg import fails' { It 'falls back to the PowerShell writer when reg import fails' {
Mock Invoke-NonBlocking { [PSCustomObject]@{ Output = @('denied'); ExitCode = 5; Error = 'access denied' } } Mock Invoke-NonBlocking { [PSCustomObject]@{ Output = @('denied'); ExitCode = 5; Error = 'access denied' } }
Import-RegistryFile -message 'Apply' -path 'feature.reg' Import-RegistryFile -message 'Apply' -path 'feature.reg' | Should -BeTrue
Should -Invoke Invoke-RegistryOperationsFromRegFile -Times 1 -Exactly Should -Invoke Invoke-RegistryOperationsFromRegFile -Times 1 -Exactly
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -like "reg import failed*" } Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -like "reg import failed*" }
$script:RegistryImportFailures | Should -Be 0 }
It 'returns false when the fallback cannot apply every registry operation' {
Mock Invoke-NonBlocking { [PSCustomObject]@{ Output = @('denied'); ExitCode = 5; Error = 'access denied' } }
Mock Invoke-RegistryOperationsFromRegFile { $false }
Import-RegistryFile -message 'Apply' -path 'feature.reg' | Should -BeFalse
} }
It 'does not invoke the fallback after a successful reg import' { It 'does not invoke the fallback after a successful reg import' {
Import-RegistryFile -message 'Apply' -path 'feature.reg' Import-RegistryFile -message 'Apply' -path 'feature.reg' | Should -BeTrue
Should -Invoke Invoke-NonBlocking -Times 1 -Exactly Should -Invoke Invoke-NonBlocking -Times 1 -Exactly
Should -Invoke Invoke-RegistryOperationsFromRegFile -Times 0 -Exactly Should -Invoke Invoke-RegistryOperationsFromRegFile -Times 0 -Exactly
$script:RegistryImportFailures | Should -Be 0
} }
} }
+106 -37
View File
@@ -1,24 +1,24 @@
BeforeAll { BeforeAll {
function Import-RegistryFile { param($Message, $path) } function Import-RegistryFile { param($Message, $path) }
function Remove-SelectedApps { param($Apps) } function Remove-SelectedApps { param($Apps) $true }
function Invoke-ForceRemoveEdge {} function Invoke-ForceRemoveEdge { $true }
function Disable-TelemetryScheduledTasks {} function Disable-TelemetryScheduledTasks { $true }
function Enable-TelemetryScheduledTasks {} function Enable-TelemetryScheduledTasks { $true }
function Generate-AppsList { @() } function Generate-AppsList { @() }
function Get-FriendlyTargetUserName { 'current user' } function Get-FriendlyTargetUserName { 'current user' }
function Set-StoreSearchSuggestionsEnabledForAllUsers {} function Set-StoreSearchSuggestionsEnabledForAllUsers { $true }
function Set-StoreSearchSuggestionsEnabled { param($StoreAppsDatabase) } function Set-StoreSearchSuggestionsEnabled { param($StoreAppsDatabase) $true }
function Get-StoreAppsDatabasePathForUser { param($UserName) 'store.db' } function Get-StoreAppsDatabasePathForUser { param($UserName) 'store.db' }
function Get-UserName { 'Alice' } function Get-UserName { 'Alice' }
function Disable-WindowsFeature { param($FeatureName) } function Disable-WindowsFeature { param($FeatureName) $true }
function New-RegistrySettingsBackup { param($ActionableKeys, $ExtraFeatures) } function New-RegistrySettingsBackup { param($ActionableKeys, $ExtraFeatures) }
function Invoke-SystemRestorePoint {} function Invoke-SystemRestorePoint {}
function Enable-WindowsFeature { param($FeatureName) } function Enable-WindowsFeature { param($FeatureName) $true }
function Get-StartMenuBinPathForUser { param($UserName) 'start.bin' } function Get-StartMenuBinPathForUser { param($UserName) 'start.bin' }
function Replace-StartMenu { param($startMenuBinFile, $startMenuTemplate) } function Replace-StartMenu { param($startMenuBinFile, $startMenuTemplate) $true }
function Replace-StartMenuForAllUsers { param($startMenuTemplate) } function Replace-StartMenuForAllUsers { param($startMenuTemplate) $true }
function Set-StoreSearchSuggestionsDisabledForAllUsers {} function Set-StoreSearchSuggestionsDisabledForAllUsers { $true }
function Set-StoreSearchSuggestionsDisabled { param($StoreAppsDatabase) } function Set-StoreSearchSuggestionsDisabled { param($StoreAppsDatabase) $true }
. (Join-Path $PSScriptRoot '..\Scripts\Features\Invoke-Changes.ps1') . (Join-Path $PSScriptRoot '..\Scripts\Features\Invoke-Changes.ps1')
} }
@@ -62,19 +62,19 @@ Describe 'Invoke-FeatureApply' {
ReplaceStartAllUsers = [PSCustomObject]@{ ApplyText = 'Replace Start all users'; RegistryKey = '' } ReplaceStartAllUsers = [PSCustomObject]@{ ApplyText = 'Replace Start all users'; RegistryKey = '' }
DisableStoreSearchSuggestions = [PSCustomObject]@{ ApplyText = 'Disable Store suggestions'; RegistryKey = '' } DisableStoreSearchSuggestions = [PSCustomObject]@{ ApplyText = 'Disable Store suggestions'; RegistryKey = '' }
} }
Mock Import-RegistryFile {} Mock Import-RegistryFile { $true }
Mock Remove-SelectedApps {} Mock Remove-SelectedApps { $true }
Mock Invoke-ForceRemoveEdge {} Mock Invoke-ForceRemoveEdge { $true }
Mock Disable-TelemetryScheduledTasks {} Mock Disable-TelemetryScheduledTasks { $true }
Mock Generate-AppsList { @() } Mock Generate-AppsList { @() }
Mock Get-FriendlyTargetUserName { 'current user' } Mock Get-FriendlyTargetUserName { 'current user' }
Mock Enable-WindowsFeature {} Mock Enable-WindowsFeature { $true }
Mock Get-StartMenuBinPathForUser { 'start.bin' } Mock Get-StartMenuBinPathForUser { 'start.bin' }
Mock Get-UserName { 'Alice' } Mock Get-UserName { 'Alice' }
Mock Replace-StartMenu {} Mock Replace-StartMenu { $true }
Mock Replace-StartMenuForAllUsers {} Mock Replace-StartMenuForAllUsers { $true }
Mock Set-StoreSearchSuggestionsDisabledForAllUsers {} Mock Set-StoreSearchSuggestionsDisabledForAllUsers { $true }
Mock Set-StoreSearchSuggestionsDisabled {} Mock Set-StoreSearchSuggestionsDisabled { $true }
Mock Get-StoreAppsDatabasePathForUser { 'store.db' } Mock Get-StoreAppsDatabasePathForUser { 'store.db' }
Mock Get-Process { @() } Mock Get-Process { @() }
Mock Stop-Process { param($InputObject) } Mock Stop-Process { param($InputObject) }
@@ -95,6 +95,14 @@ Describe 'Invoke-FeatureApply' {
Should -Invoke Disable-TelemetryScheduledTasks -Times 1 -Exactly Should -Invoke Disable-TelemetryScheduledTasks -Times 1 -Exactly
} }
It 'returns false without side effects when a registry import fails' {
Mock Import-RegistryFile { $false }
Invoke-FeatureApply -FeatureId 'DisableTelemetry' | Should -BeFalse
Should -Invoke Disable-TelemetryScheduledTasks -Times 0 -Exactly
}
It 'does not call app removal when the generated selection is empty' { It 'does not call app removal when the generated selection is empty' {
Invoke-FeatureApply -FeatureId 'RemoveApps' Invoke-FeatureApply -FeatureId 'RemoveApps'
@@ -127,6 +135,23 @@ Describe 'Invoke-FeatureApply' {
Should -Invoke Remove-SelectedApps -Times 0 -Exactly Should -Invoke Remove-SelectedApps -Times 0 -Exactly
} }
It 'returns false when applying a feature throws' {
Mock Invoke-ForceRemoveEdge { throw 'access denied' }
Mock Write-Warning {}
Invoke-FeatureApply -FeatureId 'ForceRemoveEdge' | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match "Failed to apply 'Force remove Edge'.*access denied" }
}
It 'returns false for an unknown feature' {
Mock Write-Warning {}
Invoke-FeatureApply -FeatureId 'Unknown' | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match "Unknown feature 'Unknown'.*could not be applied" }
}
It 'uses the expected static app list for <FeatureId>' -ForEach @( It 'uses the expected static app list for <FeatureId>' -ForEach @(
@{ FeatureId = 'RemoveGamingApps'; MinimumCount = 3; ExpectedApp = 'Microsoft.GamingApp' } @{ FeatureId = 'RemoveGamingApps'; MinimumCount = 3; ExpectedApp = 'Microsoft.GamingApp' }
@{ FeatureId = 'RemoveHPApps'; MinimumCount = 10; ExpectedApp = 'AD2F1837.myHP' } @{ FeatureId = 'RemoveHPApps'; MinimumCount = 10; ExpectedApp = 'AD2F1837.myHP' }
@@ -215,7 +240,7 @@ Describe 'Invoke-ApplyFeatures' {
} }
$script:progressCalls = New-Object System.Collections.Generic.List[object] $script:progressCalls = New-Object System.Collections.Generic.List[object]
$script:ApplyProgressCallback = { param($Step, $Total, $Text) $script:progressCalls.Add(@($Step, $Total, $Text)) } $script:ApplyProgressCallback = { param($Step, $Total, $Text) $script:progressCalls.Add(@($Step, $Total, $Text)) }
Mock Invoke-FeatureApply {} Mock Invoke-FeatureApply { $true }
} }
It 'reports progress and applies each feature in order' { It 'reports progress and applies each feature in order' {
@@ -235,6 +260,19 @@ Describe 'Invoke-ApplyFeatures' {
Should -Invoke Invoke-FeatureApply -Times 0 -Exactly Should -Invoke Invoke-FeatureApply -Times 0 -Exactly
$script:progressCalls | Should -HaveCount 0 $script:progressCalls | Should -HaveCount 0
} }
It 'counts a failed feature application and continues with later features' {
$script:FeatureFailures = 0
Mock Invoke-FeatureApply {
param($FeatureId)
return ($FeatureId -ne 'One')
}
Invoke-ApplyFeatures -FeatureIds @('One', 'Two') -StartStep 1 -TotalSteps 2
$script:FeatureFailures | Should -Be 1
Should -Invoke Invoke-FeatureApply -Times 2 -Exactly
}
} }
Describe 'Invoke-UndoFeatures' { Describe 'Invoke-UndoFeatures' {
@@ -246,14 +284,13 @@ Describe 'Invoke-UndoFeatures' {
CustomUndo = [PSCustomObject]@{ UndoLabel = 'Undo custom'; ApplyUndoText = ''; RegistryUndoKey = '' } CustomUndo = [PSCustomObject]@{ UndoLabel = 'Undo custom'; ApplyUndoText = ''; RegistryUndoKey = '' }
} }
Mock Resolve-UndoRegFilePath { param($FileName) "Undo\$FileName" } Mock Resolve-UndoRegFilePath { param($FileName) "Undo\$FileName" }
Mock Import-RegistryFile {} Mock Import-RegistryFile { $true }
Mock Invoke-FeatureUndo {} Mock Invoke-FeatureUndo { $true }
} }
It 'imports registry undo data and still invokes custom undo side effects' { It 'delegates registry-backed undo work to the feature undo handler' {
Invoke-UndoFeatures -FeatureIds @('RegistryUndo') -StartStep 1 -TotalSteps 1 Invoke-UndoFeatures -FeatureIds @('RegistryUndo') -StartStep 1 -TotalSteps 1
Should -Invoke Import-RegistryFile -Times 1 -Exactly -ParameterFilter { $path -eq 'Undo\undo.reg' }
Should -Invoke Invoke-FeatureUndo -Times 1 -Exactly -ParameterFilter { $FeatureId -eq 'RegistryUndo' } Should -Invoke Invoke-FeatureUndo -Times 1 -Exactly -ParameterFilter { $FeatureId -eq 'RegistryUndo' }
} }
@@ -264,6 +301,16 @@ Describe 'Invoke-UndoFeatures' {
Should -Invoke Invoke-FeatureUndo -Times 2 -Exactly Should -Invoke Invoke-FeatureUndo -Times 2 -Exactly
} }
It 'counts one failure when a feature undo fails' {
$script:FeatureFailures = 0
Mock Invoke-FeatureUndo { $false }
Invoke-UndoFeatures -FeatureIds @('RegistryUndo') -StartStep 1 -TotalSteps 1
$script:FeatureFailures | Should -Be 1
Should -Invoke Invoke-FeatureUndo -Times 1 -Exactly
}
It 'stops before undoing when cancellation is requested' { It 'stops before undoing when cancellation is requested' {
$script:CancelRequested = $true $script:CancelRequested = $true
@@ -283,12 +330,14 @@ Describe 'Invoke-FeatureUndo' {
DisableTelemetry = [PSCustomObject]@{} DisableTelemetry = [PSCustomObject]@{}
DisableStoreSearchSuggestions = [PSCustomObject]@{} DisableStoreSearchSuggestions = [PSCustomObject]@{}
} }
Mock Set-StoreSearchSuggestionsEnabledForAllUsers {} Mock Set-StoreSearchSuggestionsEnabledForAllUsers { $true }
Mock Set-StoreSearchSuggestionsEnabled {} Mock Set-StoreSearchSuggestionsEnabled { $true }
Mock Get-StoreAppsDatabasePathForUser { 'store.db' } Mock Get-StoreAppsDatabasePathForUser { 'store.db' }
Mock Get-UserName { 'Alice' } Mock Get-UserName { 'Alice' }
Mock Disable-WindowsFeature {} Mock Disable-WindowsFeature { $true }
Mock Enable-TelemetryScheduledTasks {} Mock Enable-TelemetryScheduledTasks { $true }
Mock Import-RegistryFile { $true }
Mock Resolve-UndoRegFilePath { param($FileName) "Undo\$FileName" }
Mock Write-Host {} Mock Write-Host {}
} }
@@ -304,16 +353,35 @@ Describe 'Invoke-FeatureUndo' {
It 'disables both WSL optional features in dependency-safe order' { It 'disables both WSL optional features in dependency-safe order' {
$script:disabledFeatures = [System.Collections.Generic.List[string]]::new() $script:disabledFeatures = [System.Collections.Generic.List[string]]::new()
Mock Disable-WindowsFeature { param($FeatureName) $script:disabledFeatures.Add($FeatureName) } Mock Disable-WindowsFeature { param($FeatureName) $script:disabledFeatures.Add($FeatureName); $true }
Invoke-FeatureUndo -FeatureId 'EnableWindowsSubsystemForLinux' Invoke-FeatureUndo -FeatureId 'EnableWindowsSubsystemForLinux'
$script:disabledFeatures | Should -Be @('Microsoft-Windows-Subsystem-Linux', 'VirtualMachinePlatform') $script:disabledFeatures | Should -Be @('Microsoft-Windows-Subsystem-Linux', 'VirtualMachinePlatform')
} }
It 'disables Sandbox and re-enables telemetry tasks' { It 'disables Sandbox and re-enables telemetry tasks' {
$script:Features.DisableTelemetry = [PSCustomObject]@{ ApplyUndoText = 'Enable telemetry'; RegistryUndoKey = 'enable-telemetry.reg' }
Invoke-FeatureUndo -FeatureId 'EnableWindowsSandbox' Invoke-FeatureUndo -FeatureId 'EnableWindowsSandbox'
Invoke-FeatureUndo -FeatureId 'DisableTelemetry' Invoke-FeatureUndo -FeatureId 'DisableTelemetry'
Should -Invoke Disable-WindowsFeature -Times 1 -Exactly -ParameterFilter { $FeatureName -eq 'Containers-DisposableClientVM' } Should -Invoke Disable-WindowsFeature -Times 1 -Exactly -ParameterFilter { $FeatureName -eq 'Containers-DisposableClientVM' }
Should -Invoke Enable-TelemetryScheduledTasks -Times 1 -Exactly Should -Invoke Enable-TelemetryScheduledTasks -Times 1 -Exactly
Should -Invoke Import-RegistryFile -Times 1 -Exactly -ParameterFilter { $path -eq 'Undo\enable-telemetry.reg' }
}
It 'returns false without side effects when a registry undo import fails' {
$script:Features.DisableTelemetry = [PSCustomObject]@{ ApplyUndoText = 'Enable telemetry'; RegistryUndoKey = 'enable-telemetry.reg' }
Mock Import-RegistryFile { $false }
Invoke-FeatureUndo -FeatureId 'DisableTelemetry' | Should -BeFalse
Should -Invoke Enable-TelemetryScheduledTasks -Times 0 -Exactly
}
It 'warns and returns false for an unknown feature' {
Mock Write-Warning {}
Invoke-FeatureUndo -FeatureId 'Unknown' | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match "Unknown feature 'Unknown'.*could not be undone" }
} }
} }
@@ -332,7 +400,7 @@ Describe 'Invoke-AllChanges' {
Mock Test-RunningAsSystem { $false } Mock Test-RunningAsSystem { $false }
Mock Resolve-UndoRegFilePath { param($FileName) "Undo\$FileName" } Mock Resolve-UndoRegFilePath { param($FileName) "Undo\$FileName" }
Mock New-RegistrySettingsBackup {} Mock New-RegistrySettingsBackup {}
Mock Invoke-SystemRestorePoint {} Mock Invoke-SystemRestorePoint { $true }
Mock Invoke-ApplyFeatures {} Mock Invoke-ApplyFeatures {}
Mock Invoke-UndoFeatures {} Mock Invoke-UndoFeatures {}
Mock Write-Host {} Mock Write-Host {}
@@ -406,20 +474,21 @@ Describe 'Invoke-AllChanges' {
$script:Params = @{ CreateRestorePoint = $true; CustomApply = $true } $script:Params = @{ CreateRestorePoint = $true; CustomApply = $true }
$script:UndoParams = @{} $script:UndoParams = @{}
$script:order = [System.Collections.Generic.List[string]]::new() $script:order = [System.Collections.Generic.List[string]]::new()
Mock Invoke-SystemRestorePoint { $script:order.Add('restore-point') } Mock Invoke-SystemRestorePoint { $script:order.Add('restore-point'); $true }
Mock Invoke-ApplyFeatures { $script:order.Add('apply') } Mock Invoke-ApplyFeatures { $script:order.Add('apply') }
Invoke-AllChanges Invoke-AllChanges
$script:order | Should -Be @('restore-point', 'apply') $script:order | Should -Be @('restore-point', 'apply')
} }
It 'reports registry import failures after all requested work completes' { It 'counts a restore point failure as a feature failure when the user chooses to continue' {
$script:Params = @{ CustomApply = $true } $script:Params = @{ CreateRestorePoint = $true; CustomApply = $true }
$script:UndoParams = @{} $script:UndoParams = @{}
Mock Invoke-ApplyFeatures { $script:RegistryImportFailures = 2 } Mock Invoke-SystemRestorePoint { $false }
Invoke-AllChanges Invoke-AllChanges
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match '2 registry import change' } $script:FeatureFailures | Should -Be 1
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match '1 feature change\(s\) failed\.' }
} }
It 'reports app removal failures after all requested work completes' { It 'reports app removal failures after all requested work completes' {
+20
View File
@@ -27,6 +27,26 @@ Describe 'Invoke-SystemRestorePoint' {
$script:CancelRequested | Should -BeFalse $script:CancelRequested | Should -BeFalse
} }
It 'returns false through the continuation flow when the System Restore state cannot be read' {
Mock Get-ItemProperty { throw 'registry access denied' }
Mock Read-Host { 'y' }
Invoke-SystemRestorePoint | Should -BeFalse
$script:CancelRequested | Should -BeFalse
Should -Invoke Invoke-NonBlocking -Times 0 -Exactly
}
It 'returns false without prompting when the System Restore state cannot be read in silent mode' {
$script:Silent = $true
Mock Get-ItemProperty { throw 'registry access denied' }
Invoke-SystemRestorePoint | Should -BeFalse
$script:CancelRequested | Should -BeFalse
Should -Invoke Read-Host -Times 0 -Exactly
}
It 'is loaded by the main entry point' { It 'is loaded by the main entry point' {
$entryPoint = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\Win11Debloat.ps1') -Raw $entryPoint = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\Win11Debloat.ps1') -Raw
$expectedImport = [regex]::Escape('Scripts/Features/Invoke-SystemRestorePoint.ps1') $expectedImport = [regex]::Escape('Scripts/Features/Invoke-SystemRestorePoint.ps1')
+2 -2
View File
@@ -117,7 +117,7 @@ Describe 'Get-PendingTweakActions' {
$checkBox = New-Object System.Windows.Controls.CheckBox $checkBox = New-Object System.Windows.Controls.CheckBox
$checkBox.Visibility = 'Visible' $checkBox.Visibility = 'Visible'
$window.RegisterName('DisableTelemetryCheckBox', $checkBox) $window.RegisterName('DisableTelemetryCheckBox', $checkBox)
$script:UiControlMappings = @{ DisableTelemetryCheckBox = [PSCustomObject]@{ Category = 'Privacy & Suggested Content'; CategoryId = 'PrivacySuggestedContent'; Type = 'feature'; FeatureId = 'DisableTelemetry' } } $script:UiControlMappings = @{ DisableTelemetryCheckBox = [PSCustomObject]@{ CategoryId = 'PrivacySuggestedContent'; Type = 'feature'; FeatureId = 'DisableTelemetry' } }
$map = Get-CategoryTweakPresetMap -Window $window -CategoryId 'PrivacySuggestedContent' $map = Get-CategoryTweakPresetMap -Window $window -CategoryId 'PrivacySuggestedContent'
@@ -131,7 +131,7 @@ Describe 'Get-PendingTweakActions' {
$checkBox.Visibility = 'Visible' $checkBox.Visibility = 'Visible'
$window.RegisterName('LegacyCheckBox', $checkBox) $window.RegisterName('LegacyCheckBox', $checkBox)
# New-DynamicTweakControls falls back to Name when a category has no CategoryId of its own. # New-DynamicTweakControls falls back to Name when a category has no CategoryId of its own.
$script:UiControlMappings = @{ LegacyCheckBox = [PSCustomObject]@{ Category = 'Legacy Category'; CategoryId = 'Legacy Category'; Type = 'feature'; FeatureId = 'SomeFeature' } } $script:UiControlMappings = @{ LegacyCheckBox = [PSCustomObject]@{ CategoryId = 'Legacy Category'; Type = 'feature'; FeatureId = 'SomeFeature' } }
$map = Get-CategoryTweakPresetMap -Window $window -CategoryId 'Legacy Category' $map = Get-CategoryTweakPresetMap -Window $window -CategoryId 'Legacy Category'
+102 -2
View File
@@ -11,6 +11,7 @@ BeforeAll {
function Resolve-UserProfileContext { param($UserName) $null } function Resolve-UserProfileContext { param($UserName) $null }
. (Join-Path $PSScriptRoot '..\Scripts\AppRemoval\Remove-SelectedApps.ps1') . (Join-Path $PSScriptRoot '..\Scripts\AppRemoval\Remove-SelectedApps.ps1')
. (Join-Path $PSScriptRoot '..\Scripts\AppRemoval\Invoke-ForceRemoveEdge.ps1')
} }
Describe 'Remove-SelectedApps' { Describe 'Remove-SelectedApps' {
@@ -71,9 +72,33 @@ Describe 'Remove-SelectedApps' {
It 'counts a failed WinGet removal' { It 'counts a failed WinGet removal' {
Mock Get-AppRemovalMethod { 'WinGet' } Mock Get-AppRemovalMethod { 'WinGet' }
Mock Remove-WinGetApp { $false } Mock Remove-WinGetApp { $false }
Mock Test-AppInWingetList { $true }
Mock Write-Host {}
Remove-SelectedApps -appsList @('One.App') Remove-SelectedApps -appsList @('One.App')
$script:AppRemovalFailures | Should -Be 1
Should -Invoke Write-Host -Times 1 -Exactly -ParameterFilter { $Object -eq 'Unable to uninstall One.App via WinGet' -and $ForegroundColor -eq 'Red' }
}
It 'does not count a non-zero WinGet command when the app is absent after verification' {
Mock Get-AppRemovalMethod { 'WinGet' }
Mock Remove-WinGetApp { $false }
Mock Test-AppInWingetList { $false }
Remove-SelectedApps -appsList @('One.App') | Should -BeTrue
$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 $script:AppRemovalFailures | Should -Be 1
} }
@@ -132,7 +157,7 @@ Describe 'Remove-WinGetApp' {
BeforeEach { BeforeEach {
$script:Params = @{} $script:Params = @{}
$script:WingetInstalled = $true $script:WingetInstalled = $true
Mock Invoke-NonBlocking { $true } Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $true; ExitCode = 0; Output = @() } }
Mock Set-RunOnceWingetTask { $true } Mock Set-RunOnceWingetTask { $true }
Mock Get-UserName { 'Alice' } Mock Get-UserName { 'Alice' }
Mock Write-Host {} Mock Write-Host {}
@@ -174,14 +199,89 @@ Describe 'Remove-WinGetApp' {
It 'reports a timed-out winget uninstall and continues' { It 'reports a timed-out winget uninstall and continues' {
$script:Params = @{ User = 'Alice' } $script:Params = @{ User = 'Alice' }
Mock Invoke-NonBlocking { throw 'Operation timed out after 120 seconds' } Mock Invoke-NonBlocking { throw 'Operation timed out after 120 seconds' }
Mock Write-Verbose {}
{ Remove-WinGetApp -app 'One.App' } | Should -Not -Throw { Remove-WinGetApp -app 'One.App' } | Should -Not -Throw
Should -Invoke Set-RunOnceWingetTask -Times 1 -Exactly Should -Invoke Set-RunOnceWingetTask -Times 1 -Exactly
Should -Invoke Write-Error -Times 1 -Exactly -ParameterFilter { Should -Invoke Write-Verbose -Times 1 -Exactly -ParameterFilter {
$Message -like '*did not complete within 120 seconds*' $Message -like '*did not complete within 120 seconds*'
} }
} }
It 'returns true and logs WinGet output regardless of exit code' -ForEach @(
@{ ExitCode = 0; Output = 'Successfully uninstalled One.App' }
@{ ExitCode = 1; Output = 'Package was not found' }
@{ ExitCode = -1978335212; Output = 'No installed package found matching input criteria.' }
) {
$script:expectedExitCode = $ExitCode
$script:expectedOutput = $Output
Mock Invoke-NonBlocking { [PSCustomObject]@{ ExitCode = $script:expectedExitCode; Output = @($script:expectedOutput) } }
Mock Write-Verbose {}
Remove-WinGetApp -app 'One.App' | Should -BeTrue
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 'returns the RunOnce scheduling result for a target user' {
$script:Params = @{ User = 'Alice' }
Mock Invoke-NonBlocking { [PSCustomObject]@{ ExitCode = 1; Output = @() } }
Mock Set-RunOnceWingetTask { $false }
Remove-WinGetApp -app 'One.App' | Should -BeFalse
}
}
Describe 'Remove-EdgeAutostartValue' {
BeforeEach {
Mock Write-Warning {}
}
It 'treats a missing value as already cleaned up' {
Mock Get-ItemProperty { [PSCustomObject]@{} }
Mock Remove-ItemProperty {}
Remove-EdgeAutostartValue -Path 'HKCU:\Software\Example' -Name 'Microsoft Edge Update' | Should -BeTrue
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' {
Mock Get-ItemProperty { [PSCustomObject]@{ 'Microsoft Edge Update' = 'enabled' } }
Mock Remove-ItemProperty {}
Remove-EdgeAutostartValue -Path 'HKCU:\Software\Example' -Name 'Microsoft Edge Update' | Should -BeTrue
Should -Invoke Remove-ItemProperty -Times 1 -Exactly -ParameterFilter { $Path -eq 'HKCU:\Software\Example' -and $Name -eq 'Microsoft Edge Update' }
}
It 'returns false when an existing value cannot be removed' {
Mock Get-ItemProperty { [PSCustomObject]@{ 'Microsoft Edge Update' = 'enabled' } }
Mock Remove-ItemProperty { throw 'access denied' }
Remove-EdgeAutostartValue -Path 'HKCU:\Software\Example' -Name 'Microsoft Edge Update' | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match 'access denied' }
}
It 'returns false when the registry key cannot be inspected' {
Mock Get-ItemProperty { throw 'access denied' }
Remove-EdgeAutostartValue -Path 'HKCU:\Software\Example' -Name 'Microsoft Edge Update' | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match 'access denied' }
}
} }
Describe 'Remove-AppxApp' { Describe 'Remove-AppxApp' {
+32 -16
View File
@@ -37,20 +37,20 @@ Describe 'Store-search suggestion all-user operations' {
) )
} }
Mock Get-StoreAppsDatabasePathForUser { 'C:\Users\Default\AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db' } Mock Get-StoreAppsDatabasePathForUser { 'C:\Users\Default\AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db' }
Mock Set-StoreSearchSuggestionsDisabled {} Mock Set-StoreSearchSuggestionsDisabled { $true }
Mock Set-StoreSearchSuggestionsEnabled {} Mock Set-StoreSearchSuggestionsEnabled { $true }
Mock Write-Warning {} Mock Write-Warning {}
} }
It 'disables suggestions for every discovered and Default profile' { It 'disables suggestions for every discovered and Default profile' {
Set-StoreSearchSuggestionsDisabledForAllUsers Set-StoreSearchSuggestionsDisabledForAllUsers | Should -BeTrue
Should -Invoke Set-StoreSearchSuggestionsDisabled -Times 3 -Exactly Should -Invoke Set-StoreSearchSuggestionsDisabled -Times 3 -Exactly
Should -Invoke Set-StoreSearchSuggestionsDisabled -Times 1 -Exactly -ParameterFilter { $StoreAppsDatabase -match 'Users\\Default\\' } Should -Invoke Set-StoreSearchSuggestionsDisabled -Times 1 -Exactly -ParameterFilter { $StoreAppsDatabase -match 'Users\\Default\\' }
} }
It 'enables suggestions for every discovered and Default profile' { It 'enables suggestions for every discovered and Default profile' {
Set-StoreSearchSuggestionsEnabledForAllUsers Set-StoreSearchSuggestionsEnabledForAllUsers | Should -BeTrue
Should -Invoke Set-StoreSearchSuggestionsEnabled -Times 3 -Exactly Should -Invoke Set-StoreSearchSuggestionsEnabled -Times 3 -Exactly
Should -Invoke Set-StoreSearchSuggestionsEnabled -Times 1 -Exactly -ParameterFilter { $StoreAppsDatabase -match 'Users\\Default\\' } Should -Invoke Set-StoreSearchSuggestionsEnabled -Times 1 -Exactly -ParameterFilter { $StoreAppsDatabase -match 'Users\\Default\\' }
@@ -72,6 +72,22 @@ Describe 'Store-search suggestion all-user operations' {
Should -Invoke Set-StoreSearchSuggestionsEnabled -Times 2 -Exactly Should -Invoke Set-StoreSearchSuggestionsEnabled -Times 2 -Exactly
} }
It 'returns false when any profile cannot be updated' {
Mock Set-StoreSearchSuggestionsDisabled { $false } -ParameterFilter { $StoreAppsDatabase -match 'Users\\Bob\\' }
Set-StoreSearchSuggestionsDisabledForAllUsers | Should -BeFalse
Should -Invoke Set-StoreSearchSuggestionsDisabled -Times 3 -Exactly
}
It 'returns false when no target profile can be resolved' {
Mock Get-ChildItem { @() }
Mock Get-StoreAppsDatabasePathForUser { $null }
Mock Write-Warning {}
Set-StoreSearchSuggestionsDisabledForAllUsers | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match 'no target user profiles' }
}
} }
Describe 'Set-StoreSearchSuggestionsDisabled' { Describe 'Set-StoreSearchSuggestionsDisabled' {
@@ -84,7 +100,7 @@ Describe 'Set-StoreSearchSuggestionsDisabled' {
} }
It 'does not touch the filesystem in WhatIf mode' { It 'does not touch the filesystem in WhatIf mode' {
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' | Should -BeTrue
Should -Invoke Test-Path -Times 0 -Exactly Should -Invoke Test-Path -Times 0 -Exactly
Should -Invoke Get-Acl -Times 0 -Exactly Should -Invoke Get-Acl -Times 0 -Exactly
@@ -99,7 +115,7 @@ Describe 'Set-StoreSearchSuggestionsDisabled' {
Mock Get-Acl { $acl } Mock Get-Acl { $acl }
Mock Set-Acl {} Mock Set-Acl {}
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' | Should -BeTrue
Should -Invoke New-Item -Times 2 -Exactly Should -Invoke New-Item -Times 2 -Exactly
Should -Invoke New-Item -Times 1 -Exactly -ParameterFilter { $ItemType -eq 'Directory' } Should -Invoke New-Item -Times 1 -Exactly -ParameterFilter { $ItemType -eq 'Directory' }
@@ -118,7 +134,7 @@ Describe 'Set-StoreSearchSuggestionsDisabled' {
Mock Get-Acl { $acl } Mock Get-Acl { $acl }
Mock Set-Acl {} Mock Set-Acl {}
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' | Should -BeTrue
Should -Invoke New-Item -Times 0 -Exactly Should -Invoke New-Item -Times 0 -Exactly
Should -Invoke Get-Acl -Times 1 -Exactly Should -Invoke Get-Acl -Times 1 -Exactly
@@ -133,7 +149,7 @@ Describe 'Set-StoreSearchSuggestionsDisabled' {
Mock Set-Acl { throw 'ACL must not be written after a read failure.' } Mock Set-Acl { throw 'ACL must not be written after a read failure.' }
Mock Write-Warning {} Mock Write-Warning {}
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly Should -Invoke Write-Warning -Times 1 -Exactly
Should -Invoke Write-Host -Times 0 -Exactly -ParameterFilter { $Object -like 'Disabled Microsoft Store search suggestions*' } Should -Invoke Write-Host -Times 0 -Exactly -ParameterFilter { $Object -like 'Disabled Microsoft Store search suggestions*' }
@@ -165,7 +181,7 @@ Describe 'Set-StoreSearchSuggestionsEnabled' {
} }
It 'does nothing when the Store database does not exist' { It 'does nothing when the Store database does not exist' {
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' | Should -BeTrue
Should -Invoke Get-Acl -Times 0 -Exactly Should -Invoke Get-Acl -Times 0 -Exactly
Should -Invoke Remove-Item -Times 0 -Exactly Should -Invoke Remove-Item -Times 0 -Exactly
@@ -177,7 +193,7 @@ Describe 'Set-StoreSearchSuggestionsEnabled' {
Mock takeown { throw 'WhatIf should not take ownership.' } Mock takeown { throw 'WhatIf should not take ownership.' }
Mock icacls { throw 'WhatIf should not change ACLs.' } Mock icacls { throw 'WhatIf should not change ACLs.' }
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' | Should -BeTrue
Should -Invoke Test-Path -Times 0 -Exactly Should -Invoke Test-Path -Times 0 -Exactly
Should -Invoke takeown -Times 0 -Exactly Should -Invoke takeown -Times 0 -Exactly
@@ -193,7 +209,7 @@ Describe 'Set-StoreSearchSuggestionsEnabled' {
Mock Set-Acl {} Mock Set-Acl {}
Mock Remove-Item {} Mock Remove-Item {}
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' | Should -BeTrue
Should -Invoke takeown -Times 1 -Exactly Should -Invoke takeown -Times 1 -Exactly
Should -Invoke icacls -Times 1 -Exactly Should -Invoke icacls -Times 1 -Exactly
@@ -211,14 +227,14 @@ Describe 'Set-StoreSearchSuggestionsEnabled' {
Mock Remove-Item {} Mock Remove-Item {}
Mock Write-Warning {} Mock Write-Warning {}
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' | Should -BeTrue
Should -Invoke Write-Warning -Times 1 -Exactly Should -Invoke Write-Warning -Times 1 -Exactly
Should -Invoke Set-Acl -Times 0 -Exactly Should -Invoke Set-Acl -Times 0 -Exactly
Should -Invoke Remove-Item -Times 1 -Exactly Should -Invoke Remove-Item -Times 1 -Exactly
} }
It 'throws a contextual error when the database cannot be removed' { It 'returns false when the database cannot be removed' {
$acl = New-TestStoreDatabaseAcl $acl = New-TestStoreDatabaseAcl
Mock Test-Path { $true } Mock Test-Path { $true }
Mock takeown {} Mock takeown {}
@@ -226,9 +242,9 @@ Describe 'Set-StoreSearchSuggestionsEnabled' {
Mock Get-Acl { $acl } Mock Get-Acl { $acl }
Mock Set-Acl {} Mock Set-Acl {}
Mock Remove-Item { throw 'database is locked' } Mock Remove-Item { throw 'database is locked' }
Mock Write-Warning {}
{ Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' | Should -BeFalse
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match 'Failed to remove.*database is locked' }
} | Should -Throw '*Failed to remove*database is locked*'
} }
} }
+131
View File
@@ -95,6 +95,69 @@ Describe 'Disable-TelemetryScheduledTasks' {
$result.Error | Should -Match 'access denied' $result.Error | Should -Match 'access denied'
} }
It 'returns an error when scheduled-task lookup fails' {
Mock Invoke-NonBlocking {
param($ScriptBlock, $ArgumentList)
$script:taskBlock = $ScriptBlock
$script:taskArguments = $ArgumentList
}
Mock Import-Module {}
Mock Get-ScheduledTask { throw 'scheduler unavailable' }
Disable-TelemetryScheduledTasks
$result = & $script:taskBlock @script:taskArguments
$result.Status | Should -Be 'Error'
$result.Error | Should -Match 'scheduler unavailable'
}
It 'returns an error when the scheduled-task module cannot load' {
Mock Invoke-NonBlocking {
param($ScriptBlock, $ArgumentList)
$script:taskBlock = $ScriptBlock
$script:taskArguments = $ArgumentList
}
Mock Import-Module { throw 'module unavailable' }
Disable-TelemetryScheduledTasks
$result = & $script:taskBlock @script:taskArguments
$result.Status | Should -Be 'Error'
$result.Error | Should -Match 'module unavailable'
}
It 'returns an error when the scheduled-task command is unavailable' {
Mock Invoke-NonBlocking {
param($ScriptBlock, $ArgumentList)
$script:taskBlock = $ScriptBlock
$script:taskArguments = $ArgumentList
}
Mock Import-Module {}
Mock Get-ScheduledTask {
throw [System.Management.Automation.ErrorRecord]::new(
[System.Management.Automation.CommandNotFoundException]::new('Get-ScheduledTask unavailable'),
'CommandNotFoundException',
[System.Management.Automation.ErrorCategory]::ObjectNotFound,
$null)
}
Disable-TelemetryScheduledTasks
(& $script:taskBlock @script:taskArguments).Status | Should -Be 'Error'
}
It 'treats an absent scheduled task as not found' {
Mock Invoke-NonBlocking {
param($ScriptBlock, $ArgumentList)
$script:taskBlock = $ScriptBlock
$script:taskArguments = $ArgumentList
}
Mock Import-Module {}
Mock Get-ScheduledTask { $null }
Disable-TelemetryScheduledTasks
(& $script:taskBlock @script:taskArguments).Status | Should -Be 'NotFound'
}
It 'reports <Status> task results' -ForEach @( It 'reports <Status> task results' -ForEach @(
@{ Status = 'Disabled'; Expected = 'Disabled Scheduled Task' } @{ Status = 'Disabled'; Expected = 'Disabled Scheduled Task' }
@{ Status = 'AlreadyDisabled'; Expected = 'already disabled' } @{ Status = 'AlreadyDisabled'; Expected = 'already disabled' }
@@ -108,6 +171,15 @@ Describe 'Disable-TelemetryScheduledTasks' {
Should -Invoke Write-Host -Times 1 -Exactly -ParameterFilter { $Object -like "*$Expected*" } Should -Invoke Write-Host -Times 1 -Exactly -ParameterFilter { $Object -like "*$Expected*" }
} }
It 'returns false for an unknown scheduler result' {
Mock Get-TelemetryScheduledTasks { @(@{ Path = '\Microsoft\Windows\Test\'; Name = 'Telemetry' }) }
Mock Invoke-NonBlocking { $null }
Mock Write-Warning {}
Disable-TelemetryScheduledTasks | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly
}
} }
Describe 'Enable-TelemetryScheduledTasks' { Describe 'Enable-TelemetryScheduledTasks' {
@@ -166,6 +238,56 @@ Describe 'Enable-TelemetryScheduledTasks' {
Should -Invoke Enable-ScheduledTask -Times 1 -Exactly -ParameterFilter { $TaskPath -eq '\Microsoft\Windows\Test\' -and $TaskName -eq 'Second' } Should -Invoke Enable-ScheduledTask -Times 1 -Exactly -ParameterFilter { $TaskPath -eq '\Microsoft\Windows\Test\' -and $TaskName -eq 'Second' }
} }
It 'returns an error when scheduled-task lookup fails' {
Mock Invoke-NonBlocking {
param($ScriptBlock, $ArgumentList)
$script:taskBlock = $ScriptBlock
$script:taskArguments = $ArgumentList
}
Mock Import-Module {}
Mock Get-ScheduledTask { throw 'scheduler unavailable' }
Enable-TelemetryScheduledTasks
$result = & $script:taskBlock @script:taskArguments
$result.Status | Should -Be 'Error'
$result.Error | Should -Match 'scheduler unavailable'
}
It 'returns an error when the scheduled-task module cannot load' {
Mock Invoke-NonBlocking {
param($ScriptBlock, $ArgumentList)
$script:taskBlock = $ScriptBlock
$script:taskArguments = $ArgumentList
}
Mock Import-Module { throw 'module unavailable' }
Enable-TelemetryScheduledTasks
$result = & $script:taskBlock @script:taskArguments
$result.Status | Should -Be 'Error'
$result.Error | Should -Match 'module unavailable'
}
It 'returns an error when the scheduled-task command is unavailable' {
Mock Invoke-NonBlocking {
param($ScriptBlock, $ArgumentList)
$script:taskBlock = $ScriptBlock
$script:taskArguments = $ArgumentList
}
Mock Import-Module {}
Mock Get-ScheduledTask {
throw [System.Management.Automation.ErrorRecord]::new(
[System.Management.Automation.CommandNotFoundException]::new('Get-ScheduledTask unavailable'),
'CommandNotFoundException',
[System.Management.Automation.ErrorCategory]::ObjectNotFound,
$null)
}
Enable-TelemetryScheduledTasks
(& $script:taskBlock @script:taskArguments).Status | Should -Be 'Error'
}
It 'reports <Status> task results' -ForEach @( It 'reports <Status> task results' -ForEach @(
@{ Status = 'Enabled'; Expected = 'Enabled Scheduled Task' } @{ Status = 'Enabled'; Expected = 'Enabled Scheduled Task' }
@{ Status = 'AlreadyEnabled'; Expected = 'already enabled' } @{ Status = 'AlreadyEnabled'; Expected = 'already enabled' }
@@ -179,4 +301,13 @@ Describe 'Enable-TelemetryScheduledTasks' {
Should -Invoke Write-Host -Times 1 -Exactly -ParameterFilter { $Object -like "*$Expected*" } Should -Invoke Write-Host -Times 1 -Exactly -ParameterFilter { $Object -like "*$Expected*" }
} }
It 'returns false when the scheduler throws' {
Mock Get-TelemetryScheduledTasks { @(@{ Path = '\Microsoft\Windows\Test\'; Name = 'Telemetry' }) }
Mock Invoke-NonBlocking { throw 'scheduler unavailable' }
Mock Write-Warning {}
Enable-TelemetryScheduledTasks | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match 'scheduler unavailable' }
}
} }
+50 -8
View File
@@ -23,12 +23,12 @@ BeforeAll {
Describe 'Enable-WindowsFeature' { Describe 'Enable-WindowsFeature' {
BeforeEach { BeforeEach {
$script:Params = @{} $script:Params = @{}
Mock Invoke-NonBlocking { @() } Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $true; Output = $null; Error = $null } }
Mock Write-Host {} Mock Write-Host {}
} }
It 'schedules the requested feature with the non-blocking runner' { It 'schedules the requested feature with the non-blocking runner' {
Enable-WindowsFeature -FeatureName 'Feature.One' Enable-WindowsFeature -FeatureName 'Feature.One' | Should -BeTrue
Should -Invoke Invoke-NonBlocking -Times 1 -Exactly -ParameterFilter { $ArgumentList -eq 'Feature.One' } Should -Invoke Invoke-NonBlocking -Times 1 -Exactly -ParameterFilter { $ArgumentList -eq 'Feature.One' }
} }
@@ -40,7 +40,7 @@ Describe 'Enable-WindowsFeature' {
$script:optionalFeatureBlock = $ScriptBlock $script:optionalFeatureBlock = $ScriptBlock
$script:optionalFeatureArguments = $ArgumentList $script:optionalFeatureArguments = $ArgumentList
} }
Enable-WindowsFeature -FeatureName 'Feature.One' Enable-WindowsFeature -FeatureName 'Feature.One' | Should -BeFalse
& $script:optionalFeatureBlock $script:optionalFeatureArguments & $script:optionalFeatureBlock $script:optionalFeatureArguments
$global:OptionalFeatureCalls | Should -HaveCount 1 $global:OptionalFeatureCalls | Should -HaveCount 1
@@ -51,6 +51,27 @@ Describe 'Enable-WindowsFeature' {
$global:OptionalFeatureCalls[0].NoRestart | Should -BeTrue $global:OptionalFeatureCalls[0].NoRestart | Should -BeTrue
} }
It 'writes optional-feature output while returning true' {
Mock Invoke-NonBlocking {
param($ScriptBlock, $ArgumentList)
& $ScriptBlock $ArgumentList
}
Mock Enable-WindowsOptionalFeature { [PSCustomObject]@{ State = 'Enabled'; RestartNeeded = $false } }
Enable-WindowsFeature -FeatureName 'Feature.One' | Should -BeTrue
Should -Invoke Write-Host -Times 1 -Exactly -ParameterFilter { $Object -match 'Enabled' }
}
It 'reports the worker error and returns false' {
Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $false; Output = $null; Error = 'feature servicing failed' } }
Mock Write-Warning {}
Enable-WindowsFeature -FeatureName 'Feature.One' | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match 'feature servicing failed' }
}
It 'does not schedule changes in WhatIf mode' { It 'does not schedule changes in WhatIf mode' {
$script:Params = @{ WhatIf = $true } $script:Params = @{ WhatIf = $true }
@@ -69,19 +90,19 @@ Describe 'Enable-WindowsFeature' {
Enable-WindowsFeature -FeatureName 'Feature.One' Enable-WindowsFeature -FeatureName 'Feature.One'
{ & $script:optionalFeatureBlock $script:optionalFeatureArguments } | Should -Throw 'feature servicing failed' (& $script:optionalFeatureBlock $script:optionalFeatureArguments).Success | Should -BeFalse
} }
} }
Describe 'Disable-WindowsFeature' { Describe 'Disable-WindowsFeature' {
BeforeEach { BeforeEach {
$script:Params = @{} $script:Params = @{}
Mock Invoke-NonBlocking { @() } Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $true; Output = $null; Error = $null } }
Mock Write-Host {} Mock Write-Host {}
} }
It 'schedules the requested feature with the non-blocking runner' { It 'schedules the requested feature with the non-blocking runner' {
Disable-WindowsFeature -FeatureName 'Feature.One' Disable-WindowsFeature -FeatureName 'Feature.One' | Should -BeTrue
Should -Invoke Invoke-NonBlocking -Times 1 -Exactly -ParameterFilter { $ArgumentList -eq 'Feature.One' } Should -Invoke Invoke-NonBlocking -Times 1 -Exactly -ParameterFilter { $ArgumentList -eq 'Feature.One' }
} }
@@ -93,7 +114,7 @@ Describe 'Disable-WindowsFeature' {
$script:optionalFeatureBlock = $ScriptBlock $script:optionalFeatureBlock = $ScriptBlock
$script:optionalFeatureArguments = $ArgumentList $script:optionalFeatureArguments = $ArgumentList
} }
Disable-WindowsFeature -FeatureName 'Feature.One' Disable-WindowsFeature -FeatureName 'Feature.One' | Should -BeFalse
& $script:optionalFeatureBlock $script:optionalFeatureArguments & $script:optionalFeatureBlock $script:optionalFeatureArguments
$global:OptionalFeatureCalls | Should -HaveCount 1 $global:OptionalFeatureCalls | Should -HaveCount 1
@@ -104,6 +125,27 @@ Describe 'Disable-WindowsFeature' {
$global:OptionalFeatureCalls[0].NoRestart | Should -BeTrue $global:OptionalFeatureCalls[0].NoRestart | Should -BeTrue
} }
It 'writes optional-feature output while returning true' {
Mock Invoke-NonBlocking {
param($ScriptBlock, $ArgumentList)
& $ScriptBlock $ArgumentList
}
Mock Disable-WindowsOptionalFeature { [PSCustomObject]@{ State = 'Disabled'; RestartNeeded = $false } }
Disable-WindowsFeature -FeatureName 'Feature.One' | Should -BeTrue
Should -Invoke Write-Host -Times 1 -Exactly -ParameterFilter { $Object -match 'Disabled' }
}
It 'reports the worker error and returns false' {
Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $false; Output = $null; Error = 'feature servicing failed' } }
Mock Write-Warning {}
Disable-WindowsFeature -FeatureName 'Feature.One' | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match 'feature servicing failed' }
}
It 'does not schedule changes in WhatIf mode' { It 'does not schedule changes in WhatIf mode' {
$script:Params = @{ WhatIf = $true } $script:Params = @{ WhatIf = $true }
@@ -122,7 +164,7 @@ Describe 'Disable-WindowsFeature' {
Disable-WindowsFeature -FeatureName 'Feature.One' Disable-WindowsFeature -FeatureName 'Feature.One'
{ & $script:optionalFeatureBlock $script:optionalFeatureArguments } | Should -Throw 'feature servicing failed' (& $script:optionalFeatureBlock $script:optionalFeatureArguments).Success | Should -BeFalse
} }
} }
+1 -2
View File
@@ -38,8 +38,6 @@ param (
[switch]$DisableStoreSearchSuggestions, [switch]$DisableStoreSearchSuggestions,
[switch]$DisableSearchHighlights, [switch]$DisableSearchHighlights,
[switch]$DisableDesktopSpotlight, [switch]$DisableDesktopSpotlight,
[switch]$HideDesktopSpotlightIcon,
[switch]$EnableDesktopSpotlight,
[switch]$DisableLockscreenTips, [switch]$DisableLockscreenTips,
[switch]$DisableSuggestions, [switch]$DisableSuggestions,
[switch]$DisableLocationServices, [switch]$DisableLocationServices,
@@ -429,6 +427,7 @@ if (-not $script:WingetInstalled -and -not $Silent) {
. "$PSScriptRoot/Scripts/Helpers/Get-FriendlyTargetUserName.ps1" . "$PSScriptRoot/Scripts/Helpers/Get-FriendlyTargetUserName.ps1"
. "$PSScriptRoot/Scripts/Helpers/Get-RebootFeatureLabels.ps1" . "$PSScriptRoot/Scripts/Helpers/Get-RebootFeatureLabels.ps1"
. "$PSScriptRoot/Scripts/Helpers/Import-ConfigToParams.ps1" . "$PSScriptRoot/Scripts/Helpers/Import-ConfigToParams.ps1"
. "$PSScriptRoot/Scripts/Helpers/Test-ConfigConsistency.ps1"
. "$PSScriptRoot/Scripts/Helpers/Get-TargetUserForAppRemoval.ps1" . "$PSScriptRoot/Scripts/Helpers/Get-TargetUserForAppRemoval.ps1"
. "$PSScriptRoot/Scripts/Helpers/Get-RegFileOperations.ps1" . "$PSScriptRoot/Scripts/Helpers/Get-RegFileOperations.ps1"
. "$PSScriptRoot/Scripts/Helpers/Test-TargetUserName.ps1" . "$PSScriptRoot/Scripts/Helpers/Test-TargetUserName.ps1"