Compare commits

..
41 changed files with 1416 additions and 643 deletions
+6 -68
View File
@@ -63,33 +63,6 @@
}
],
"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",
"Label": "Taskbar search style",
@@ -419,7 +392,6 @@
"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.",
"Category": "Privacy & Suggested Content",
"Priority": 1,
"RegistryKey": "Disable_Telemetry.reg",
"ApplyText": "Disabling telemetry and diagnostic data collection",
"UndoLabel": "Enable telemetry, tracking & targeted ads",
@@ -433,7 +405,6 @@
"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.",
"Category": "Privacy & Suggested Content",
"Priority": 2,
"RegistryKey": "Disable_Windows_Suggestions.reg",
"ApplyText": "Disabling tips, tricks, suggestions and ads throughout Windows",
"UndoLabel": "Enable tips, tricks & suggested content throughout Windows",
@@ -447,7 +418,6 @@
"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.",
"Category": "Privacy & Suggested Content",
"Priority": 3,
"RegistryKey": "Disable_Notifications.reg",
"ApplyText": "Disabling Windows notifications and reminders",
"UndoLabel": "Enable Windows notifications and reminders",
@@ -461,7 +431,6 @@
"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.",
"Category": "Privacy & Suggested Content",
"Priority": 4,
"RegistryKey": "Disable_Location_Services.reg",
"ApplyText": "Disabling Windows location services and app location access",
"UndoLabel": "Enable Windows location services & app location access",
@@ -475,7 +444,6 @@
"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.",
"Category": "Privacy & Suggested Content",
"Priority": 5,
"RegistryKey": "Disable_Find_My_Device.reg",
"ApplyText": "Disabling Find My Device location tracking",
"UndoLabel": "Enable Find My Device location tracking",
@@ -489,7 +457,6 @@
"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.",
"Category": "Privacy & Suggested Content",
"Priority": 6,
"RegistryKey": "Disable_Lockscreen_Tips.reg",
"ApplyText": "Disabling tips & tricks on the lock screen",
"UndoLabel": "Enable tips & tricks on the lock screen",
@@ -498,45 +465,19 @@
"MinVersion": 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",
"Label": "Disable Windows spotlight desktop background",
"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.",
"Category": "Appearance",
"Label": "Disable Windows Spotlight for desktop",
"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": "Privacy & Suggested Content",
"RegistryKey": "Disable_Desktop_Spotlight.reg",
"ApplyText": "Disabling the Windows spotlight desktop background",
"UndoLabel": "Enable Windows spotlight desktop background",
"ApplyUndoText": "Restoring the Windows spotlight desktop background and shortcut",
"ApplyText": "Disabling the 'Windows Spotlight' desktop background option",
"UndoLabel": "Enable Windows Spotlight for desktop",
"ApplyUndoText": "Enabling the 'Windows Spotlight' desktop background option",
"RegistryUndoKey": "Enable_Desktop_Spotlight.reg",
"MinVersion": 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",
"Label": "Disable ads, suggestions and newsfeed in Edge",
@@ -816,7 +757,6 @@
"UndoLabel": "Disable dark theme for system and apps",
"ApplyUndoText": "Disabling dark mode for system and apps",
"RegistryUndoKey": "Enable_Light_Mode.reg",
"Priority": 1,
"MinVersion": null,
"MaxVersion": null
},
@@ -1386,7 +1326,6 @@
"UndoLabel": "Enable transparency effects",
"ApplyUndoText": "Enabling transparency effects",
"RegistryUndoKey": "Enable_Transparency.reg",
"Priority": 2,
"MinVersion": null,
"MaxVersion": null
},
@@ -1400,7 +1339,6 @@
"UndoLabel": "Enable animations and visual effects",
"ApplyUndoText": "Enabling animations and visual effects",
"RegistryUndoKey": "Enable_Animations.reg",
"Priority": 3,
"RequiresReboot": true,
"MinVersion": 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.
- 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
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
Forcefully uninstalls Microsoft Edge and removes its leftover shortcuts and autostart entries.
.OUTPUTS
System.Boolean. $true when Edge is uninstalled and cleanup succeeds; otherwise $false.
#>
function Invoke-ForceRemoveEdge {
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Forcefully uninstall Microsoft Edge" -ForegroundColor Cyan
Write-Host ""
return
return $true
}
Write-Host "> Forcefully uninstalling Microsoft Edge..."
try {
Write-Host "> Forcefully uninstalling Microsoft Edge..."
$regView = [Microsoft.Win32.RegistryView]::Registry32
$hklm = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $regView)
$hklm.CreateSubKey('SOFTWARE\Microsoft\EdgeUpdateDev').SetValue('AllowUninstall', '')
$regView = [Microsoft.Win32.RegistryView]::Registry32
$hklm = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $regView)
$edgeUpdateKey = $hklm.CreateSubKey('SOFTWARE\Microsoft\EdgeUpdateDev')
$edgeUpdateKey.SetValue('AllowUninstall', '')
# Create stub (This somehow allows uninstalling Edge)
$edgeStub = "$env:SystemRoot\SystemApps\Microsoft.MicrosoftEdge_8wekyb3d8bbwe"
New-Item $edgeStub -ItemType Directory | Out-Null
New-Item "$edgeStub\MicrosoftEdge.exe" | Out-Null
# Create stub (This somehow allows uninstalling Edge)
$edgeStub = "$env:SystemRoot\SystemApps\Microsoft.MicrosoftEdge_8wekyb3d8bbwe"
New-Item $edgeStub -ItemType Directory -Force -ErrorAction Stop | Out-Null
New-Item "$edgeStub\MicrosoftEdge.exe" -ItemType File -Force -ErrorAction Stop | Out-Null
# Remove edge
$uninstallRegKey = $hklm.OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft Edge')
if ($null -ne $uninstallRegKey) {
$uninstallRegKey = $hklm.OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft Edge')
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..."
$uninstallString = $uninstallRegKey.GetValue('UninstallString') + ' --force-uninstall'
Invoke-NonBlocking -ScriptBlock {
$exitCode = Invoke-NonBlocking -ScriptBlock {
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
if ($exitCode -ne 0) {
Write-Warning "Microsoft Edge uninstaller failed with exit code $exitCode."
return $false
}
Write-Host "Removing leftover files..."
$cleanupSucceeded = $true
$edgePaths = @(
"$env:ProgramData\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk",
@@ -44,22 +58,89 @@ function Invoke-ForceRemoveEdge {
foreach ($path in $edgePaths) {
if (Test-Path -Path $path) {
Remove-Item -Path $path -Force -Recurse -ErrorAction SilentlyContinue
Write-Host " Removed $path" -ForegroundColor DarkGray
try {
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..."
$registryCleanupSucceeded = $true
# Remove MS Edge from autostart
reg delete "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run" /v "MicrosoftEdgeAutoLaunch_A9F6DCE4ABADF4F51CF45CD7129E3C6C" /f *>$null
reg delete "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run" /v "Microsoft Edge Update" /f *>$null
reg delete "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run" /v "MicrosoftEdgeAutoLaunch_A9F6DCE4ABADF4F51CF45CD7129E3C6C" /f *>$null
reg delete "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run" /v "Microsoft Edge Update" /f *>$null
# Remove MS Edge from autostart. Missing values are already-clean state,
# while failures to inspect or remove an existing value are reported.
$autostartValues = @(
@{ Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'; Name = 'MicrosoftEdgeAutoLaunch_A9F6DCE4ABADF4F51CF45CD7129E3C6C' },
@{ 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"
return $true
}
else {
Write-Host "Unable to forcefully uninstall Microsoft Edge, uninstaller could not be found" -ForegroundColor Red
catch {
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
Remove-SelectedApps -appsList (Generate-AppsList)
.OUTPUTS
System.Boolean. $true when all removals can be confirmed; otherwise $false.
#>
function Remove-SelectedApps {
param (
@@ -29,10 +32,10 @@ function Remove-SelectedApps {
Write-Host "[WhatIf] Remove App Package: $app" -ForegroundColor Cyan
}
Write-Host ""
return
return $true
}
$failuresBefore = $script:AppRemovalFailures
$targetUser = Get-TargetUserForAppRemoval
$appCount = @($appsList).Count
$appIndex = 0
@@ -42,7 +45,7 @@ function Remove-SelectedApps {
$wingetRemovalFailures = @{}
Foreach ($app in $appsList) {
if ($script:CancelRequested) { return }
if ($script:CancelRequested) { return $false }
$appIndex++
@@ -53,10 +56,11 @@ function Remove-SelectedApps {
Write-Host "Removing $app"
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
}
$wingetRemovedApps += $app
}
else {
if (-not (Remove-AppxApp -app $app -targetUser $targetUser)) {
@@ -66,17 +70,20 @@ function Remove-SelectedApps {
}
if ($script:CancelRequested) {
Write-Host ""
return
return $false
}
# Check whether any winget-removed apps are still present, and report errors for each one.
if ($wingetRemovedApps.Count -gt 0) {
$postRemovalList = if ($script:WingetInstalled) { Get-WingetInstalledApps -TimeOut 10 -NonBlocking } else { $null }
$edgeForceRemoveRequested = $false
$edgeForceRemoveSucceeded = $false
if ($null -eq $postRemovalList) {
$script:AppRemovalVerificationUnavailable = $true
foreach ($app in $wingetRemovedApps) {
$wingetRemovalFailures[$app] = $true
}
}
else {
foreach ($app in $wingetRemovedApps) {
@@ -87,8 +94,11 @@ function Remove-SelectedApps {
if ($edgeIds -contains $app) {
Write-Host "Unable to uninstall Microsoft Edge via WinGet" -ForegroundColor Red
if (-not $edgeForceRemoveRequested) {
Request-EdgeForceRemove
$edgeForceRemoveRequested = $true
$edgeForceRemoveSucceeded = Request-EdgeForceRemove
}
if ($edgeForceRemoveSucceeded) {
continue
}
}
else {
@@ -101,7 +111,7 @@ function Remove-SelectedApps {
$script:AppRemovalFailures += $wingetRemovalFailures.Count
Write-Host ""
return ($script:AppRemovalFailures -eq $failuresBefore)
}
<#
@@ -110,8 +120,12 @@ function Remove-SelectedApps {
.DESCRIPTION
Runs winget uninstall for a single app, with a bounded execution time.
If the User or Sysprep parameter was passed, also schedules removal for
future logins.
WinGet's own exit code/success reporting is unreliable and is only logged
for diagnostics; it never causes this function to report failure. Callers
verify removal with a post-removal inventory check instead. This function
only reports failure when the winget invocation itself throws a terminating
error (e.g. it times out or cannot be started). If the User or Sysprep
parameter was passed, also schedules removal for future logins.
.PARAMETER app
The WinGet package ID to uninstall (e.g. 'Microsoft.BingNews').
@@ -119,6 +133,10 @@ function Remove-SelectedApps {
.PARAMETER TimeoutSeconds
Maximum time to allow the foreground WinGet uninstall to run. Defaults
to 120 seconds.
.OUTPUTS
System.Boolean. $true unless the winget invocation threw a terminating error
or any required RunOnce scheduling failed; otherwise $false.
#>
function Remove-WinGetApp {
param(
@@ -131,22 +149,28 @@ function Remove-WinGetApp {
return $false
}
$uninstallSucceeded = $true
$uninstallCommandSucceeded = $true
$exitCode = $null
try {
$uninstallSucceeded = Invoke-NonBlocking -ScriptBlock {
$uninstallResult = Invoke-NonBlocking -ScriptBlock {
param($appId)
$null = & winget uninstall --accept-source-agreements --disable-interactivity --id $appId 2>&1
return $true
$output = @(& winget uninstall --accept-source-agreements --disable-interactivity --id $appId 2>&1)
return [PSCustomObject]@{
ExitCode = $LASTEXITCODE
Output = $output
}
} -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 {
$uninstallSucceeded = $false
$uninstallCommandSucceeded = $false
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 {
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
}
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
warning message box; in CLI mode, prompts via Read-Host. On
confirmation, performs a force-remove of the Edge package.
.OUTPUTS
System.Boolean. $true when Edge is forcefully removed; otherwise $false.
#>
function Request-EdgeForceRemove {
if ($script:GuiWindow) {
$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') {
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') {
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 {
param (
$message,
$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 {
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) {
# 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") }
Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock $importScript -ArgumentObject $regFilePath -PassHiveContext
$succeeded = Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock $importScript -ArgumentObject $regFilePath -PassHiveContext
}
else {
& $importScript $regFilePath $null
$succeeded = & $importScript $regFilePath $null
}
return [bool]$succeeded
}
catch {
$script:RegistryImportFailures++
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
any post-import side effects (e.g., removing companion app packages).
- 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 {
param(
@@ -15,30 +17,32 @@ function Invoke-FeatureApply {
[string]$FeatureId
)
# Resolve feature metadata from Features.json
$feature = $script:Features[$FeatureId]
$applyText = $feature.ApplyText
try {
# Resolve feature metadata from Features.json
$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) {
Import-RegistryFile "> $applyText..." $feature.RegistryKey
if (-not (Import-RegistryFile "> $applyText..." $feature.RegistryKey)) {
return $false
}
# Post-import side effects for specific features
switch ($FeatureId) {
'DisableBing' {
# Also remove the app package for Bing search
Remove-SelectedApps @('Microsoft.BingSearch')
return (Remove-SelectedApps @('Microsoft.BingSearch'))
}
'DisableCopilot' {
# Also remove the app packages for Copilot
Remove-SelectedApps @('Microsoft.Copilot', 'XP9CXNGPPJ97XX')
return (Remove-SelectedApps @('Microsoft.Copilot', 'XP9CXNGPPJ97XX'))
}
'DisableTelemetry' {
# Also disable telemetry scheduled tasks
Disable-TelemetryScheduledTasks
return (Disable-TelemetryScheduledTasks)
}
}
return
return $true
}
# ---- Custom features (no registry backing, or special handling required) ----
@@ -49,30 +53,25 @@ function Invoke-FeatureApply {
if ($appsList.Count -eq 0) {
Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow
Write-Host ""
return
return $true
}
Write-Host "$($appsList.Count) apps selected for removal"
Remove-SelectedApps $appsList
return
return (Remove-SelectedApps $appsList)
}
'RemoveGamingApps' {
$appsList = @('Microsoft.GamingApp', 'Microsoft.XboxGameOverlay', 'Microsoft.XboxGamingOverlay')
Write-Host "> $applyText..."
Remove-SelectedApps $appsList
return
return (Remove-SelectedApps $appsList)
}
'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')
Write-Host "> $applyText..."
Remove-SelectedApps $appsList
return
return (Remove-SelectedApps $appsList)
}
'ForceRemoveEdge' {
Write-Host "> $applyText..."
Invoke-ForceRemoveEdge
return
return (Invoke-ForceRemoveEdge)
}
'DisableWidgets' {
Write-Host "> $applyText..."
@@ -81,76 +80,75 @@ function Invoke-FeatureApply {
Get-Process *Widget* -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
}
Remove-SelectedApps @('Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime')
return
return (Remove-SelectedApps @('Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime'))
}
'EnableWindowsSandbox' {
Write-Host "> $applyText..."
Enable-WindowsFeature "Containers-DisposableClientVM"
Write-Host ""
return
return (Enable-WindowsFeature "Containers-DisposableClientVM")
}
'EnableWindowsSubsystemForLinux' {
Write-Host "> $applyText..."
Enable-WindowsFeature "VirtualMachinePlatform"
Enable-WindowsFeature "Microsoft-Windows-Subsystem-Linux"
Write-Host ""
return
if (-not (Enable-WindowsFeature "VirtualMachinePlatform")) { return $false }
return (Enable-WindowsFeature "Microsoft-Windows-Subsystem-Linux")
}
'ClearStart' {
Write-Host "> $applyText for user $(Get-UserName)..."
$startMenuBinFile = Get-StartMenuBinPathForUser -UserName (Get-UserName)
if (-not [string]::IsNullOrWhiteSpace($startMenuBinFile)) {
Replace-StartMenu -startMenuBinFile $startMenuBinFile
return (Replace-StartMenu -startMenuBinFile $startMenuBinFile)
}
Write-Host ""
return
Write-Warning "Unable to apply '$applyText': the Start menu path for user $(Get-UserName) could not be resolved."
return $false
}
'ReplaceStart' {
Write-Host "> $applyText for user $(Get-UserName)..."
$startMenuBinFile = Get-StartMenuBinPathForUser -UserName (Get-UserName)
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 ""
return
Write-Warning "Unable to apply '$applyText': the Start menu path for user $(Get-UserName) could not be resolved."
return $false
}
'ClearStartAllUsers' {
Replace-StartMenuForAllUsers
return
return (Replace-StartMenuForAllUsers)
}
'ReplaceStartAllUsers' {
Replace-StartMenuForAllUsers -startMenuTemplate $script:Params.Item("ReplaceStartAllUsers")
return
return (Replace-StartMenuForAllUsers -startMenuTemplate $script:Params.Item("ReplaceStartAllUsers"))
}
'DisableStoreSearchSuggestions' {
if ($script:Params.ContainsKey("Sysprep")) {
Write-Host "> Disabling Microsoft Store search suggestions in the start menu for all users..."
Set-StoreSearchSuggestionsDisabledForAllUsers
Write-Host ""
return
return (Set-StoreSearchSuggestionsDisabledForAllUsers)
}
Write-Host "> Disabling Microsoft Store search suggestions for user $(Get-UserName)..."
$storeDb = Get-StoreAppsDatabasePathForUser -UserName (Get-UserName)
if ($storeDb) {
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase $storeDb
return (Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase $storeDb)
}
Write-Host ""
return
Write-Warning "Unable to disable Microsoft Store search suggestions because the Store database for user $(Get-UserName) could not be resolved."
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
Undoes a single feature that has no RegistryUndoKey.
Undoes a single feature.
.DESCRIPTION
Handles undo for features that require custom logic rather than a simple
.reg file import. Features with a RegistryUndoKey are handled directly
via Import-RegistryFile in Invoke-UndoFeatures.
Handles registry-backed undo imports and custom undo logic. Returns
$true when the requested undo succeeds; otherwise writes a warning and
returns $false.
#>
function Invoke-FeatureUndo {
param(
@@ -159,43 +157,65 @@ function Invoke-FeatureUndo {
)
$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) {
'DisableStoreSearchSuggestions' {
if ($script:Params.ContainsKey('Sysprep')) {
Write-Host "> Re-enabling Microsoft Store search suggestions in the start menu for all users..."
Set-StoreSearchSuggestionsEnabledForAllUsers
Write-Host ""
return
$undoText = if ($feature.ApplyUndoText) { $feature.ApplyUndoText } elseif ($feature.UndoLabel) { $feature.UndoLabel } else { $FeatureId }
try {
# ---- Registry-backed features: import undo data, then handle additional tasks ----
if ($feature.RegistryUndoKey) {
if (-not (Import-RegistryFile "> $undoText" (Resolve-UndoRegFilePath $feature.RegistryUndoKey))) {
return $false
}
Write-Host "> Re-enabling Microsoft Store search suggestions for user $(Get-UserName)..."
$storeDb = Get-StoreAppsDatabasePathForUser -UserName (Get-UserName)
if ($storeDb) {
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase $storeDb
switch ($FeatureId) {
'DisableTelemetry' {
# Also re-enable telemetry scheduled tasks.
return (Enable-TelemetryScheduledTasks)
}
}
Write-Host ""
return
return $true
}
'EnableWindowsSandbox' {
Write-Host "> $($feature.ApplyUndoText)..."
Disable-WindowsFeature 'Containers-DisposableClientVM'
Write-Host ""
return
}
'EnableWindowsSubsystemForLinux' {
Write-Host "> $($feature.ApplyUndoText)..."
Disable-WindowsFeature 'Microsoft-Windows-Subsystem-Linux'
Disable-WindowsFeature 'VirtualMachinePlatform'
Write-Host ""
return
}
'DisableTelemetry' {
# Also re-enable telemetry scheduled tasks
Enable-TelemetryScheduledTasks
return
# ---- Custom undo features (no registry backing) ----
switch ($FeatureId) {
'DisableStoreSearchSuggestions' {
if ($script:Params.ContainsKey('Sysprep')) {
Write-Host "> Re-enabling Microsoft Store search suggestions in the start menu for all users..."
return (Set-StoreSearchSuggestionsEnabledForAllUsers)
}
Write-Host "> Re-enabling Microsoft Store search suggestions for user $(Get-UserName)..."
$storeDb = Get-StoreAppsDatabasePathForUser -UserName (Get-UserName)
if ($storeDb) {
return (Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase $storeDb)
}
Write-Warning "Unable to re-enable Microsoft Store search suggestions because the Store database for user $(Get-UserName) could not be resolved."
return $false
}
'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
}
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++
}
}
@@ -262,9 +288,8 @@ function Invoke-ApplyFeatures {
Undoes a list of features, reporting progress for each.
.DESCRIPTION
Iterates through the provided feature IDs. Features with a RegistryUndoKey
are handled by importing the undo .reg file; all others delegate to
Invoke-FeatureUndo for custom undo logic.
Iterates through the provided feature IDs and delegates each to
Invoke-FeatureUndo, which handles registry-backed and custom undo logic.
This is called by Invoke-AllChanges during the undo phase.
#>
function Invoke-UndoFeatures {
@@ -291,11 +316,10 @@ function Invoke-UndoFeatures {
& $script:ApplyProgressCallback $step $TotalSteps $undoText
}
if ($f -and $f.RegistryUndoKey) {
Import-RegistryFile "> $undoText" (Resolve-UndoRegFilePath $f.RegistryUndoKey)
if (-not (Invoke-FeatureUndo -FeatureId $featureId)) {
$script:FeatureFailures++
}
Invoke-FeatureUndo -FeatureId $featureId
Write-Host ""
$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."
}
$script:RegistryImportFailures = 0
$script:AppRemovalFailures = 0
$script:FeatureFailures = 0
$script:AppRemovalVerificationUnavailable = $false
# ---- Gather work items ----
@@ -405,7 +429,11 @@ function Invoke-AllChanges {
}
else {
Write-Host "> Creating a system restore point..."
Invoke-SystemRestorePoint
$restorePointSucceeded = Invoke-SystemRestorePoint
if (-not $restorePointSucceeded) {
if ($script:CancelRequested) { return }
$script:FeatureFailures++
}
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) {
Write-Host ""
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) {
Write-Host ""
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"
$failed = $false
<#
.SYNOPSIS
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
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 {
$enableResult = Invoke-NonBlocking -TimeoutSeconds 90 -ScriptBlock {
try {
@@ -26,7 +41,6 @@ function Invoke-SystemRestorePoint {
}
}
else {
Write-Host ""
$failed = $true
}
}
@@ -79,17 +93,20 @@ function Invoke-SystemRestorePoint {
if ($result -ne "Yes") {
$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
if ($( Read-Host ) -ne 'y') {
$script:CancelRequested = $true
return
return $false
}
}
Write-Host "Warning: Continuing without restore point" -ForegroundColor Yellow
return $false
}
return $true
}
+48 -23
View File
@@ -18,6 +18,9 @@
.EXAMPLE
Replace-StartMenuForAllUsers -startMenuTemplate "C:\CustomLayout.bin"
.OUTPUTS
System.Boolean. $true when all resolved profiles are updated or the change is previewed; otherwise $false.
#>
function Replace-StartMenuForAllUsers {
param (
@@ -29,8 +32,7 @@ function Replace-StartMenuForAllUsers {
# Check if template bin file exists
if (-not (Test-Path $startMenuTemplate)) {
Write-Host "Error: Unable to clear start menu, start2.bin file missing from script folder" -ForegroundColor Red
Write-Host ""
return
return $false
}
# Get path to start menu file for all users
@@ -38,8 +40,11 @@ function Replace-StartMenuForAllUsers {
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
# Go through all users and replace the start menu file
$success = $true
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
@@ -47,19 +52,29 @@ function Replace-StartMenuForAllUsers {
if ($script:Params.ContainsKey("WhatIf")) {
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
if (-not (Test-Path $defaultStartMenuPath)) {
new-item $defaultStartMenuPath -ItemType Directory -Force | Out-Null
Write-Host "Created LocalState folder for default user profile"
try {
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
Replace-StartMenu -startMenuBinFile "$($defaultStartMenuPath)\start2.bin" -startMenuTemplate $startMenuTemplate
Write-Host "Replaced start menu for the default user profile"
Write-Host ""
if (-not (Replace-StartMenu -startMenuBinFile "$($defaultStartMenuPath)\start2.bin" -startMenuTemplate $startMenuTemplate)) {
$success = $false
}
else {
Write-Host "Replaced start menu for the default user profile"
}
return $success
}
@@ -87,6 +102,9 @@ function Replace-StartMenuForAllUsers {
.EXAMPLE
Replace-StartMenu -startMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -startMenuTemplate "C:\CustomLayout.bin"
.OUTPUTS
System.Boolean. $true when the template is valid and copied, or the change is previewed; otherwise $false.
#>
function Replace-StartMenu {
param (
@@ -98,19 +116,19 @@ function Replace-StartMenu {
# Check if template bin file exists
if (-not (Test-Path $startMenuTemplate)) {
Write-Host "Error: Unable to replace start menu, template file not found" -ForegroundColor Red
return
return $false
}
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
return
return $false
}
$userName = Get-StartMenuUserNameFromPath -StartMenuBinFile $startMenuBinFile
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Replace Start Menu for user $userName with template $startMenuTemplate" -ForegroundColor Cyan
return
return $true
}
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
@@ -118,20 +136,27 @@ function Replace-StartMenu {
$startMenuDir = Split-Path $startMenuBinFile -Parent
$backupBinFile = Join-Path $startMenuDir $backupFileName
if (Test-Path $startMenuBinFile) {
# Backup current start menu file
Copy-Item -Path $startMenuBinFile -Destination $backupBinFile -Force
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
New-Item -ItemType File -Path $startMenuBinFile -Force
}
try {
if (Test-Path $startMenuBinFile) {
# Backup current start menu file
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
New-Item -ItemType File -Path $startMenuBinFile -Force -ErrorAction Stop | Out-Null
}
# Copy template file
Copy-Item -Path $startMenuTemplate -Destination $startMenuBinFile -Force
# Copy template file
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"
return $true
}
<#
+78 -25
View File
@@ -10,22 +10,41 @@
.EXAMPLE
DisableStoreSearchSuggestionsForAllUsers
.OUTPUTS
System.Boolean. $true when a profile is processed and all ACL changes succeed; otherwise $false.
#>
function Set-StoreSearchSuggestionsDisabledForAllUsers {
$success = $true
$processedProfiles = 0
# Get path to Store app database for all users
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages"
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
# Go through all users and disable start search suggestions
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
$defaultStoreDbPath = Get-StoreAppsDatabasePathForUser -UserName "Default"
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
DisableStoreSearchSuggestions -StoreAppsDatabase "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
.OUTPUTS
System.Boolean. $true when the database ACL is restricted or previewed; otherwise $false.
#>
function Set-StoreSearchSuggestionsDisabled {
param (
@@ -56,24 +78,22 @@ function Set-StoreSearchSuggestionsDisabled {
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Disable Microsoft Store search suggestions for user $userName by restricting access to ${StoreAppsDatabase}" -ForegroundColor Cyan
return
}
# 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
return $true
}
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
$Acl = Get-Acl -Path $StoreAppsDatabase -ErrorAction Stop
$Ace = [System.Security.AccessControl.FileSystemAccessRule]::new($AccountSid, 'FullControl', 'Deny')
@@ -82,10 +102,11 @@ function Set-StoreSearchSuggestionsDisabled {
}
catch {
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"
return $true
}
<#
@@ -100,22 +121,41 @@ function Set-StoreSearchSuggestionsDisabled {
.EXAMPLE
EnableStoreSearchSuggestionsForAllUsers
.OUTPUTS
System.Boolean. $true when a profile is processed and all ACL changes succeed; otherwise $false.
#>
function Set-StoreSearchSuggestionsEnabledForAllUsers {
$success = $true
$processedProfiles = 0
# Get path to Store app database for all users
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages"
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
# Go through all users and re-enable start search suggestions
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
$defaultStoreDbPath = Get-StoreAppsDatabasePathForUser -UserName "Default"
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
EnableStoreSearchSuggestions -StoreAppsDatabase "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
.OUTPUTS
System.Boolean. $true when the deny ACL is removed, the database is absent, or the change is previewed; otherwise $false.
#>
function Set-StoreSearchSuggestionsEnabled {
param (
@@ -145,23 +188,31 @@ function Set-StoreSearchSuggestionsEnabled {
if ($script:Params.ContainsKey("WhatIf")) {
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)) {
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.
$global:LASTEXITCODE = 0
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
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
try {
$acl = Get-Acl -Path $StoreAppsDatabase
$acl = Get-Acl -Path $StoreAppsDatabase -ErrorAction Stop
$denyRules = @(
$acl.Access | Where-Object {
if ($_.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Deny) { return $false }
@@ -179,7 +230,7 @@ function Set-StoreSearchSuggestionsEnabled {
$null = $acl.RemoveAccessRuleSpecific($denyRule)
}
Set-Acl -Path $StoreAppsDatabase -AclObject $acl | Out-Null
Set-Acl -Path $StoreAppsDatabase -AclObject $acl -ErrorAction Stop | Out-Null
}
catch {
Write-Warning "Failed to normalize ACL for store database '$StoreAppsDatabase': $($_.Exception.Message)"
@@ -188,9 +239,11 @@ function Set-StoreSearchSuggestionsEnabled {
try {
Remove-Item -Path $StoreAppsDatabase -Force -ErrorAction Stop
Write-Host "Re-enabled Microsoft Store search suggestions for user $userName"
return $true
}
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
Disable-TelemetryScheduledTasks
.OUTPUTS
System.Boolean. $true when every task is disabled, absent, already disabled, or previewed; otherwise $false.
#>
function Disable-TelemetryScheduledTasks {
Write-Host "> Disabling telemetry scheduled tasks..."
$tasks = Get-TelemetryScheduledTasks
$success = $true
foreach ($task in $tasks) {
if ($script:CancelRequested) { return }
if ($script:CancelRequested) { return $false }
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Disable Scheduled Task: $($task.Path)$($task.Name)" -ForegroundColor Cyan
continue
}
$result = Invoke-NonBlocking -ScriptBlock {
try {
$result = Invoke-NonBlocking -ScriptBlock {
param($path, $name)
Import-Module ScheduledTasks -ErrorAction SilentlyContinue
$taskObj = Get-ScheduledTask -TaskPath $path -TaskName $name -ErrorAction SilentlyContinue
try {
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) {
return @{ Success = $true; Status = 'NotFound' }
}
@@ -64,17 +77,24 @@ function Disable-TelemetryScheduledTasks {
}
}
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) {
'Disabled' { Write-Host "Disabled Scheduled Task: $($task.Path)$($task.Name)" }
'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 }
'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
Enable-TelemetryScheduledTasks
.OUTPUTS
System.Boolean. $true when every task is enabled, absent, already enabled, or previewed; otherwise $false.
#>
function Enable-TelemetryScheduledTasks {
Write-Host "> Enabling telemetry scheduled tasks..."
$tasks = Get-TelemetryScheduledTasks
$success = $true
foreach ($task in $tasks) {
if ($script:CancelRequested) { return }
if ($script:CancelRequested) { return $false }
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Enable Scheduled Task: $($task.Path)$($task.Name)" -ForegroundColor Cyan
continue
}
$result = Invoke-NonBlocking -ScriptBlock {
try {
$result = Invoke-NonBlocking -ScriptBlock {
param($path, $name)
Import-Module ScheduledTasks -ErrorAction SilentlyContinue
$taskObj = Get-ScheduledTask -TaskPath $path -TaskName $name -ErrorAction SilentlyContinue
try {
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) {
return @{ Success = $true; Status = 'NotFound' }
}
@@ -118,15 +151,22 @@ function Enable-TelemetryScheduledTasks {
}
}
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) {
'Enabled' { Write-Host "Enabled Scheduled Task: $($task.Path)$($task.Name)" }
'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 }
'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 {
param (
[string]$FeatureName
@@ -6,22 +12,51 @@ function Enable-WindowsFeature {
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Enable Windows feature: $FeatureName" -ForegroundColor Cyan
Write-Host ""
return
return $true
}
$result = Invoke-NonBlocking -ScriptBlock {
param($name)
Enable-WindowsOptionalFeature -Online -FeatureName $name -All -NoRestart
} -ArgumentList $FeatureName
$dismResult = @($result) | Where-Object { $_ -is [Microsoft.Dism.Commands.ImageObject] }
if ($dismResult) {
Write-Host ($dismResult | Out-String).Trim()
try {
$result = Invoke-NonBlocking -ScriptBlock {
param($name)
try {
$output = Enable-WindowsOptionalFeature -Online -FeatureName $name -All -NoRestart -ErrorAction Stop
return [PSCustomObject]@{
Success = $true
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 {
param (
[string]$FeatureName
@@ -29,19 +64,42 @@ function Disable-WindowsFeature {
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Disable Windows feature: $FeatureName" -ForegroundColor Cyan
Write-Host ""
return
return $true
}
$result = Invoke-NonBlocking -ScriptBlock {
param($name)
Disable-WindowsOptionalFeature -Online -FeatureName $name -NoRestart
} -ArgumentList $FeatureName
$dismResult = @($result) | Where-Object { $_ -is [Microsoft.Dism.Commands.ImageObject] }
if ($dismResult) {
Write-Host ($dismResult | Out-String).Trim()
try {
$result = Invoke-NonBlocking -ScriptBlock {
param($name)
try {
$output = Disable-WindowsOptionalFeature -Online -FeatureName $name -NoRestart -ErrorAction Stop
return [PSCustomObject]@{
Success = $true
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 {
+4 -2
View File
@@ -233,9 +233,11 @@ function Get-AppRemovalScopeTarget {
switch ($selectedItem.Name) {
"AppRemovalScopeAllUsers" { return 'AllUsers' }
"AppRemovalScopeCurrentUser" { return 'CurrentUser' }
default {
Write-Warning "Unrecognized app-removal scope item '$($selectedItem.Name)'. Skipping app removal."
return $null
}
}
return $null
}
function Invoke-AppPreset {
+3 -3
View File
@@ -369,7 +369,7 @@ function New-DynamicTweakControls {
try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {}
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
}
@@ -389,7 +389,7 @@ function New-DynamicTweakControls {
try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {}
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') {
$feature = $item.Data
@@ -416,7 +416,7 @@ function New-DynamicTweakControls {
try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {}
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 {
Invoke-AllChanges
$registryImportFailureCount = [int]$script:RegistryImportFailures
$appRemovalFailureCount = [int]$script:AppRemovalFailures
$failureCount = $registryImportFailureCount + $appRemovalFailureCount
$failureCount = [int]$script:FeatureFailures + [int]$script:AppRemovalFailures
$appRemovalVerificationUnavailable = [bool]$script:AppRemovalVerificationUnavailable
# Restart explorer if requested
@@ -157,11 +155,7 @@ function Show-ApplyModal {
}
else {
$script:ApplyCompletionTitleEl.Text = "Changes Applied with Errors"
$failureMessages = @()
if ($registryImportFailureCount -gt 0) { $failureMessages += "$registryImportFailureCount registry change(s) failed" }
if ($appRemovalFailureCount -gt 0) { $failureMessages += "$appRemovalFailureCount app removal(s) failed" }
if ($appRemovalVerificationUnavailable) { $failureMessages += "Unable to verify if all apps were uninstalled successfully" }
$script:ApplyCompletionMessageEl.Text = "$($failureMessages -join '; '). See console for details."
$script:ApplyCompletionMessageEl.Text = "$failureCount change(s) failed. See console for details."
}
} else {
Write-Host "All changes have been applied successfully!"
@@ -506,20 +506,15 @@ function Import-Configuration {
return
}
if (-not $config.Version) {
Write-Error "Invalid configuration file format: '$($openDialog.FileName)'"
Show-MessageBox -Message "Invalid configuration file format." -Title 'Invalid Config' -Button 'OK' -Icon 'Error' | Out-Null
$consistencyError = Test-ConfigConsistency -Config $config
if ($consistencyError) {
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
}
$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 ', ')"
$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 (-not (Confirm-UnsafeAppRemoval -SelectedApps $selectedApps -Owner $window)) { return }
$scopeTarget = Get-AppRemovalScopeTarget -AppRemovalScopeCombo $appRemovalScopeCombo -OtherUsernameTextBox $otherUsernameTextBox
if ([string]::IsNullOrWhiteSpace($scopeTarget)) {
Write-Warning 'App removal was cancelled because the selected removal scope is invalid.'
return
}
Add-Parameter 'RemoveApps'
Add-Parameter 'Apps' ($selectedApps -join ',')
$scopeTarget = Get-AppRemovalScopeTarget -AppRemovalScopeCombo $appRemovalScopeCombo -OtherUsernameTextBox $otherUsernameTextBox
if ($scopeTarget) {
Add-Parameter 'AppRemovalTarget' $scopeTarget
}
Add-Parameter 'AppRemovalTarget' $scopeTarget
}
# Apply dynamic tweaks
+1 -4
View File
@@ -39,8 +39,6 @@ param (
[switch]$DisableBing,
[switch]$DisableStoreSearchSuggestions,
[switch]$DisableDesktopSpotlight,
[switch]$HideDesktopSpotlightIcon,
[switch]$EnableDesktopSpotlight,
[switch]$DisableLockscreenTips,
[switch]$DisableSuggestions,
[switch]$DisableLocationServices,
@@ -136,8 +134,7 @@ try {
Invoke-RestMethod $sourceUri -OutFile $tempArchivePath
}
catch {
Write-Host "Unable to fetch required files from GitHub. Please check your internet connection and try again." -ForegroundColor Red
Write-Error -ErrorRecord $_
Write-Host "Error: Unable to fetch required files from GitHub. Please check your internet connection and try again." -ForegroundColor Red
Write-Output ""
Write-Output "Press enter to exit..."
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 {
param(
[Parameter(Mandatory)]
@@ -203,7 +210,7 @@ function Invoke-RegistryOperationsFromRegFile {
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Apply $totalOperations registry changes from '$RegFilePath'" -ForegroundColor Cyan
return
return $true
}
foreach ($operation in $operations) {
@@ -222,5 +229,8 @@ function Invoke-RegistryOperationsFromRegFile {
if ($accessDeniedCount -gt 0) {
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"
}
$consistencyError = Test-ConfigConsistency -Config $configJson
if ($consistencyError) {
throw "Invalid config file '$resolvedConfigPath': $consistencyError"
}
$importedItems = 0
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\Helpers\Add-Parameter.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:SkipRegistryBackupFixturePath = Join-Path $PSScriptRoot 'TestData\JsonFileLoading\ExportedConfig.SkipRegistryBackup.json'
}
@@ -44,3 +45,123 @@ Describe 'Import-ConfigToParams' {
$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' {
BeforeEach {
$script:Params = @{}
$script:RegistryImportFailures = 0
$script:regPath = Join-Path $TestDrive 'feature.reg'
'' | Set-Content -LiteralPath $script:regPath
Mock Get-RegistryFilePathForFeature { $script:regPath }
Mock Invoke-RegistryOperationsFromRegFile {}
Mock Invoke-RegistryOperationsFromRegFile { $true }
Mock Invoke-WithTargetUserHive {}
Mock Invoke-NonBlocking { [PSCustomObject]@{ Output = @(); ExitCode = 0; Error = $null } }
Mock Write-Host {}
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' }
{ 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
}
It 'uses the PowerShell writer only in WhatIf mode' {
$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-NonBlocking -Times 0 -Exactly
}
@@ -41,7 +47,7 @@ Describe 'Import-RegistryFile' {
& $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-RegistryOperationsFromRegFile -Times 1 -Exactly
@@ -51,17 +57,22 @@ Describe 'Import-RegistryFile' {
It 'falls back to the PowerShell writer when reg import fails' {
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 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' {
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-RegistryOperationsFromRegFile -Times 0 -Exactly
$script:RegistryImportFailures | Should -Be 0
}
}
+106 -37
View File
@@ -1,24 +1,24 @@
BeforeAll {
function Import-RegistryFile { param($Message, $path) }
function Remove-SelectedApps { param($Apps) }
function Invoke-ForceRemoveEdge {}
function Disable-TelemetryScheduledTasks {}
function Enable-TelemetryScheduledTasks {}
function Remove-SelectedApps { param($Apps) $true }
function Invoke-ForceRemoveEdge { $true }
function Disable-TelemetryScheduledTasks { $true }
function Enable-TelemetryScheduledTasks { $true }
function Generate-AppsList { @() }
function Get-FriendlyTargetUserName { 'current user' }
function Set-StoreSearchSuggestionsEnabledForAllUsers {}
function Set-StoreSearchSuggestionsEnabled { param($StoreAppsDatabase) }
function Set-StoreSearchSuggestionsEnabledForAllUsers { $true }
function Set-StoreSearchSuggestionsEnabled { param($StoreAppsDatabase) $true }
function Get-StoreAppsDatabasePathForUser { param($UserName) 'store.db' }
function Get-UserName { 'Alice' }
function Disable-WindowsFeature { param($FeatureName) }
function Disable-WindowsFeature { param($FeatureName) $true }
function New-RegistrySettingsBackup { param($ActionableKeys, $ExtraFeatures) }
function Invoke-SystemRestorePoint {}
function Enable-WindowsFeature { param($FeatureName) }
function Enable-WindowsFeature { param($FeatureName) $true }
function Get-StartMenuBinPathForUser { param($UserName) 'start.bin' }
function Replace-StartMenu { param($startMenuBinFile, $startMenuTemplate) }
function Replace-StartMenuForAllUsers { param($startMenuTemplate) }
function Set-StoreSearchSuggestionsDisabledForAllUsers {}
function Set-StoreSearchSuggestionsDisabled { param($StoreAppsDatabase) }
function Replace-StartMenu { param($startMenuBinFile, $startMenuTemplate) $true }
function Replace-StartMenuForAllUsers { param($startMenuTemplate) $true }
function Set-StoreSearchSuggestionsDisabledForAllUsers { $true }
function Set-StoreSearchSuggestionsDisabled { param($StoreAppsDatabase) $true }
. (Join-Path $PSScriptRoot '..\Scripts\Features\Invoke-Changes.ps1')
}
@@ -62,19 +62,19 @@ Describe 'Invoke-FeatureApply' {
ReplaceStartAllUsers = [PSCustomObject]@{ ApplyText = 'Replace Start all users'; RegistryKey = '' }
DisableStoreSearchSuggestions = [PSCustomObject]@{ ApplyText = 'Disable Store suggestions'; RegistryKey = '' }
}
Mock Import-RegistryFile {}
Mock Remove-SelectedApps {}
Mock Invoke-ForceRemoveEdge {}
Mock Disable-TelemetryScheduledTasks {}
Mock Import-RegistryFile { $true }
Mock Remove-SelectedApps { $true }
Mock Invoke-ForceRemoveEdge { $true }
Mock Disable-TelemetryScheduledTasks { $true }
Mock Generate-AppsList { @() }
Mock Get-FriendlyTargetUserName { 'current user' }
Mock Enable-WindowsFeature {}
Mock Enable-WindowsFeature { $true }
Mock Get-StartMenuBinPathForUser { 'start.bin' }
Mock Get-UserName { 'Alice' }
Mock Replace-StartMenu {}
Mock Replace-StartMenuForAllUsers {}
Mock Set-StoreSearchSuggestionsDisabledForAllUsers {}
Mock Set-StoreSearchSuggestionsDisabled {}
Mock Replace-StartMenu { $true }
Mock Replace-StartMenuForAllUsers { $true }
Mock Set-StoreSearchSuggestionsDisabledForAllUsers { $true }
Mock Set-StoreSearchSuggestionsDisabled { $true }
Mock Get-StoreAppsDatabasePathForUser { 'store.db' }
Mock Get-Process { @() }
Mock Stop-Process { param($InputObject) }
@@ -95,6 +95,14 @@ Describe 'Invoke-FeatureApply' {
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' {
Invoke-FeatureApply -FeatureId 'RemoveApps'
@@ -127,6 +135,23 @@ Describe 'Invoke-FeatureApply' {
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 @(
@{ FeatureId = 'RemoveGamingApps'; MinimumCount = 3; ExpectedApp = 'Microsoft.GamingApp' }
@{ FeatureId = 'RemoveHPApps'; MinimumCount = 10; ExpectedApp = 'AD2F1837.myHP' }
@@ -215,7 +240,7 @@ Describe 'Invoke-ApplyFeatures' {
}
$script:progressCalls = New-Object System.Collections.Generic.List[object]
$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' {
@@ -235,6 +260,19 @@ Describe 'Invoke-ApplyFeatures' {
Should -Invoke Invoke-FeatureApply -Times 0 -Exactly
$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' {
@@ -246,14 +284,13 @@ Describe 'Invoke-UndoFeatures' {
CustomUndo = [PSCustomObject]@{ UndoLabel = 'Undo custom'; ApplyUndoText = ''; RegistryUndoKey = '' }
}
Mock Resolve-UndoRegFilePath { param($FileName) "Undo\$FileName" }
Mock Import-RegistryFile {}
Mock Invoke-FeatureUndo {}
Mock Import-RegistryFile { $true }
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
Should -Invoke Import-RegistryFile -Times 1 -Exactly -ParameterFilter { $path -eq 'Undo\undo.reg' }
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
}
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' {
$script:CancelRequested = $true
@@ -283,12 +330,14 @@ Describe 'Invoke-FeatureUndo' {
DisableTelemetry = [PSCustomObject]@{}
DisableStoreSearchSuggestions = [PSCustomObject]@{}
}
Mock Set-StoreSearchSuggestionsEnabledForAllUsers {}
Mock Set-StoreSearchSuggestionsEnabled {}
Mock Set-StoreSearchSuggestionsEnabledForAllUsers { $true }
Mock Set-StoreSearchSuggestionsEnabled { $true }
Mock Get-StoreAppsDatabasePathForUser { 'store.db' }
Mock Get-UserName { 'Alice' }
Mock Disable-WindowsFeature {}
Mock Enable-TelemetryScheduledTasks {}
Mock Disable-WindowsFeature { $true }
Mock Enable-TelemetryScheduledTasks { $true }
Mock Import-RegistryFile { $true }
Mock Resolve-UndoRegFilePath { param($FileName) "Undo\$FileName" }
Mock Write-Host {}
}
@@ -304,16 +353,35 @@ Describe 'Invoke-FeatureUndo' {
It 'disables both WSL optional features in dependency-safe order' {
$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'
$script:disabledFeatures | Should -Be @('Microsoft-Windows-Subsystem-Linux', 'VirtualMachinePlatform')
}
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 'DisableTelemetry'
Should -Invoke Disable-WindowsFeature -Times 1 -Exactly -ParameterFilter { $FeatureName -eq 'Containers-DisposableClientVM' }
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 Resolve-UndoRegFilePath { param($FileName) "Undo\$FileName" }
Mock New-RegistrySettingsBackup {}
Mock Invoke-SystemRestorePoint {}
Mock Invoke-SystemRestorePoint { $true }
Mock Invoke-ApplyFeatures {}
Mock Invoke-UndoFeatures {}
Mock Write-Host {}
@@ -406,20 +474,21 @@ Describe 'Invoke-AllChanges' {
$script:Params = @{ CreateRestorePoint = $true; CustomApply = $true }
$script:UndoParams = @{}
$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') }
Invoke-AllChanges
$script:order | Should -Be @('restore-point', 'apply')
}
It 'reports registry import failures after all requested work completes' {
$script:Params = @{ CustomApply = $true }
It 'counts a restore point failure as a feature failure when the user chooses to continue' {
$script:Params = @{ CreateRestorePoint = $true; CustomApply = $true }
$script:UndoParams = @{}
Mock Invoke-ApplyFeatures { $script:RegistryImportFailures = 2 }
Mock Invoke-SystemRestorePoint { $false }
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' {
+20
View File
@@ -27,6 +27,26 @@ Describe 'Invoke-SystemRestorePoint' {
$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' {
$entryPoint = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\Win11Debloat.ps1') -Raw
$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.Visibility = 'Visible'
$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'
@@ -131,7 +131,7 @@ Describe 'Get-PendingTweakActions' {
$checkBox.Visibility = 'Visible'
$window.RegisterName('LegacyCheckBox', $checkBox)
# 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'
+102 -2
View File
@@ -11,6 +11,7 @@ BeforeAll {
function Resolve-UserProfileContext { param($UserName) $null }
. (Join-Path $PSScriptRoot '..\Scripts\AppRemoval\Remove-SelectedApps.ps1')
. (Join-Path $PSScriptRoot '..\Scripts\AppRemoval\Invoke-ForceRemoveEdge.ps1')
}
Describe 'Remove-SelectedApps' {
@@ -71,9 +72,33 @@ Describe 'Remove-SelectedApps' {
It 'counts a failed WinGet removal' {
Mock Get-AppRemovalMethod { 'WinGet' }
Mock Remove-WinGetApp { $false }
Mock Test-AppInWingetList { $true }
Mock Write-Host {}
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
}
@@ -132,7 +157,7 @@ Describe 'Remove-WinGetApp' {
BeforeEach {
$script:Params = @{}
$script:WingetInstalled = $true
Mock Invoke-NonBlocking { $true }
Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $true; ExitCode = 0; Output = @() } }
Mock Set-RunOnceWingetTask { $true }
Mock Get-UserName { 'Alice' }
Mock Write-Host {}
@@ -174,14 +199,89 @@ Describe 'Remove-WinGetApp' {
It 'reports a timed-out winget uninstall and continues' {
$script:Params = @{ User = 'Alice' }
Mock Invoke-NonBlocking { throw 'Operation timed out after 120 seconds' }
Mock Write-Verbose {}
{ Remove-WinGetApp -app 'One.App' } | Should -Not -Throw
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*'
}
}
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' {
+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 Set-StoreSearchSuggestionsDisabled {}
Mock Set-StoreSearchSuggestionsEnabled {}
Mock Set-StoreSearchSuggestionsDisabled { $true }
Mock Set-StoreSearchSuggestionsEnabled { $true }
Mock Write-Warning {}
}
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 1 -Exactly -ParameterFilter { $StoreAppsDatabase -match 'Users\\Default\\' }
}
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 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
}
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' {
@@ -84,7 +100,7 @@ Describe 'Set-StoreSearchSuggestionsDisabled' {
}
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 Get-Acl -Times 0 -Exactly
@@ -99,7 +115,7 @@ Describe 'Set-StoreSearchSuggestionsDisabled' {
Mock Get-Acl { $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 1 -Exactly -ParameterFilter { $ItemType -eq 'Directory' }
@@ -118,7 +134,7 @@ Describe 'Set-StoreSearchSuggestionsDisabled' {
Mock Get-Acl { $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 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 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-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' {
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 Remove-Item -Times 0 -Exactly
@@ -177,7 +193,7 @@ Describe 'Set-StoreSearchSuggestionsEnabled' {
Mock takeown { throw 'WhatIf should not take ownership.' }
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 takeown -Times 0 -Exactly
@@ -193,7 +209,7 @@ Describe 'Set-StoreSearchSuggestionsEnabled' {
Mock Set-Acl {}
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 icacls -Times 1 -Exactly
@@ -211,14 +227,14 @@ Describe 'Set-StoreSearchSuggestionsEnabled' {
Mock Remove-Item {}
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 Set-Acl -Times 0 -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
Mock Test-Path { $true }
Mock takeown {}
@@ -226,9 +242,9 @@ Describe 'Set-StoreSearchSuggestionsEnabled' {
Mock Get-Acl { $acl }
Mock Set-Acl {}
Mock Remove-Item { throw 'database is locked' }
Mock Write-Warning {}
{
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db'
} | Should -Throw '*Failed to remove*database is locked*'
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match 'Failed to remove.*database is locked' }
}
}
+131
View File
@@ -95,6 +95,69 @@ Describe 'Disable-TelemetryScheduledTasks' {
$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 @(
@{ Status = 'Disabled'; Expected = 'Disabled Scheduled Task' }
@{ Status = 'AlreadyDisabled'; Expected = 'already disabled' }
@@ -108,6 +171,15 @@ Describe 'Disable-TelemetryScheduledTasks' {
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' {
@@ -166,6 +238,56 @@ Describe 'Enable-TelemetryScheduledTasks' {
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 @(
@{ Status = 'Enabled'; Expected = 'Enabled Scheduled Task' }
@{ Status = 'AlreadyEnabled'; Expected = 'already enabled' }
@@ -179,4 +301,13 @@ Describe 'Enable-TelemetryScheduledTasks' {
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' {
BeforeEach {
$script:Params = @{}
Mock Invoke-NonBlocking { @() }
Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $true; Output = $null; Error = $null } }
Mock Write-Host {}
}
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' }
}
@@ -40,7 +40,7 @@ Describe 'Enable-WindowsFeature' {
$script:optionalFeatureBlock = $ScriptBlock
$script:optionalFeatureArguments = $ArgumentList
}
Enable-WindowsFeature -FeatureName 'Feature.One'
Enable-WindowsFeature -FeatureName 'Feature.One' | Should -BeFalse
& $script:optionalFeatureBlock $script:optionalFeatureArguments
$global:OptionalFeatureCalls | Should -HaveCount 1
@@ -51,6 +51,27 @@ Describe 'Enable-WindowsFeature' {
$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' {
$script:Params = @{ WhatIf = $true }
@@ -69,19 +90,19 @@ Describe 'Enable-WindowsFeature' {
Enable-WindowsFeature -FeatureName 'Feature.One'
{ & $script:optionalFeatureBlock $script:optionalFeatureArguments } | Should -Throw 'feature servicing failed'
(& $script:optionalFeatureBlock $script:optionalFeatureArguments).Success | Should -BeFalse
}
}
Describe 'Disable-WindowsFeature' {
BeforeEach {
$script:Params = @{}
Mock Invoke-NonBlocking { @() }
Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $true; Output = $null; Error = $null } }
Mock Write-Host {}
}
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' }
}
@@ -93,7 +114,7 @@ Describe 'Disable-WindowsFeature' {
$script:optionalFeatureBlock = $ScriptBlock
$script:optionalFeatureArguments = $ArgumentList
}
Disable-WindowsFeature -FeatureName 'Feature.One'
Disable-WindowsFeature -FeatureName 'Feature.One' | Should -BeFalse
& $script:optionalFeatureBlock $script:optionalFeatureArguments
$global:OptionalFeatureCalls | Should -HaveCount 1
@@ -104,6 +125,27 @@ Describe 'Disable-WindowsFeature' {
$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' {
$script:Params = @{ WhatIf = $true }
@@ -122,7 +164,7 @@ Describe 'Disable-WindowsFeature' {
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]$DisableSearchHighlights,
[switch]$DisableDesktopSpotlight,
[switch]$HideDesktopSpotlightIcon,
[switch]$EnableDesktopSpotlight,
[switch]$DisableLockscreenTips,
[switch]$DisableSuggestions,
[switch]$DisableLocationServices,
@@ -429,6 +427,7 @@ if (-not $script:WingetInstalled -and -not $Silent) {
. "$PSScriptRoot/Scripts/Helpers/Get-FriendlyTargetUserName.ps1"
. "$PSScriptRoot/Scripts/Helpers/Get-RebootFeatureLabels.ps1"
. "$PSScriptRoot/Scripts/Helpers/Import-ConfigToParams.ps1"
. "$PSScriptRoot/Scripts/Helpers/Test-ConfigConsistency.ps1"
. "$PSScriptRoot/Scripts/Helpers/Get-TargetUserForAppRemoval.ps1"
. "$PSScriptRoot/Scripts/Helpers/Get-RegFileOperations.ps1"
. "$PSScriptRoot/Scripts/Helpers/Test-TargetUserName.ps1"