mirror of
https://github.com/Raphire/Win11Debloat.git
synced 2026-08-23 08:02:07 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20d206cb9b | ||
|
|
b7f612f36e | ||
|
|
c921730703 | ||
|
|
80eadd531c | ||
|
|
4c29fce469 | ||
|
|
93d77d8034 | ||
|
|
78e1d601b0 | ||
|
|
f763390d53 | ||
|
|
dded2da3a7 | ||
|
|
492a374f5c |
+6
-68
@@ -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
|
||||
|
||||
@@ -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.
@@ -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}"=-
|
||||
@@ -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}"=-
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
<#
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
<#
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 '; ')
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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' {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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' {
|
||||
|
||||
@@ -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' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user