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