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 |
@@ -1,36 +1,50 @@
|
|||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Forcefully uninstalls Microsoft Edge and removes its leftover shortcuts and autostart entries.
|
Forcefully uninstalls Microsoft Edge and removes its leftover shortcuts and autostart entries.
|
||||||
|
|
||||||
|
.OUTPUTS
|
||||||
|
System.Boolean. $true when Edge is uninstalled and cleanup succeeds; otherwise $false.
|
||||||
#>
|
#>
|
||||||
function Invoke-ForceRemoveEdge {
|
function Invoke-ForceRemoveEdge {
|
||||||
if ($script:Params.ContainsKey("WhatIf")) {
|
if ($script:Params.ContainsKey("WhatIf")) {
|
||||||
Write-Host "[WhatIf] Forcefully uninstall Microsoft Edge" -ForegroundColor Cyan
|
Write-Host "[WhatIf] Forcefully uninstall Microsoft Edge" -ForegroundColor Cyan
|
||||||
Write-Host ""
|
return $true
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "> Forcefully uninstalling Microsoft Edge..."
|
try {
|
||||||
|
Write-Host "> Forcefully uninstalling Microsoft Edge..."
|
||||||
|
|
||||||
$regView = [Microsoft.Win32.RegistryView]::Registry32
|
$regView = [Microsoft.Win32.RegistryView]::Registry32
|
||||||
$hklm = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $regView)
|
$hklm = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $regView)
|
||||||
$hklm.CreateSubKey('SOFTWARE\Microsoft\EdgeUpdateDev').SetValue('AllowUninstall', '')
|
$edgeUpdateKey = $hklm.CreateSubKey('SOFTWARE\Microsoft\EdgeUpdateDev')
|
||||||
|
$edgeUpdateKey.SetValue('AllowUninstall', '')
|
||||||
|
|
||||||
# Create stub (This somehow allows uninstalling Edge)
|
# Create stub (This somehow allows uninstalling Edge)
|
||||||
$edgeStub = "$env:SystemRoot\SystemApps\Microsoft.MicrosoftEdge_8wekyb3d8bbwe"
|
$edgeStub = "$env:SystemRoot\SystemApps\Microsoft.MicrosoftEdge_8wekyb3d8bbwe"
|
||||||
New-Item $edgeStub -ItemType Directory | Out-Null
|
New-Item $edgeStub -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
||||||
New-Item "$edgeStub\MicrosoftEdge.exe" | Out-Null
|
New-Item "$edgeStub\MicrosoftEdge.exe" -ItemType File -Force -ErrorAction Stop | Out-Null
|
||||||
|
|
||||||
# Remove edge
|
# Remove edge
|
||||||
$uninstallRegKey = $hklm.OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft Edge')
|
$uninstallRegKey = $hklm.OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft Edge')
|
||||||
if ($null -ne $uninstallRegKey) {
|
if ($null -eq $uninstallRegKey) {
|
||||||
|
Write-Host "Unable to forcefully uninstall Microsoft Edge, uninstaller could not be found" -ForegroundColor Red
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host "Running uninstaller..."
|
Write-Host "Running uninstaller..."
|
||||||
$uninstallString = $uninstallRegKey.GetValue('UninstallString') + ' --force-uninstall'
|
$uninstallString = $uninstallRegKey.GetValue('UninstallString') + ' --force-uninstall'
|
||||||
Invoke-NonBlocking -ScriptBlock {
|
$exitCode = Invoke-NonBlocking -ScriptBlock {
|
||||||
param($cmd)
|
param($cmd)
|
||||||
Start-Process cmd.exe "/c $cmd" -WindowStyle Hidden -Wait
|
$process = Start-Process cmd.exe "/c $cmd" -WindowStyle Hidden -Wait -PassThru
|
||||||
|
return $process.ExitCode
|
||||||
} -ArgumentList $uninstallString
|
} -ArgumentList $uninstallString
|
||||||
|
if ($exitCode -ne 0) {
|
||||||
|
Write-Warning "Microsoft Edge uninstaller failed with exit code $exitCode."
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host "Removing leftover files..."
|
Write-Host "Removing leftover files..."
|
||||||
|
$cleanupSucceeded = $true
|
||||||
|
|
||||||
$edgePaths = @(
|
$edgePaths = @(
|
||||||
"$env:ProgramData\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk",
|
"$env:ProgramData\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk",
|
||||||
@@ -44,22 +58,89 @@ function Invoke-ForceRemoveEdge {
|
|||||||
|
|
||||||
foreach ($path in $edgePaths) {
|
foreach ($path in $edgePaths) {
|
||||||
if (Test-Path -Path $path) {
|
if (Test-Path -Path $path) {
|
||||||
Remove-Item -Path $path -Force -Recurse -ErrorAction SilentlyContinue
|
try {
|
||||||
Write-Host " Removed $path" -ForegroundColor DarkGray
|
Remove-Item -Path $path -Force -Recurse -ErrorAction Stop
|
||||||
|
Write-Host " Removed $path" -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Failed to remove Edge leftover '$path': $($_.Exception.Message)"
|
||||||
|
$cleanupSucceeded = $false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Cleaning up registry..."
|
Write-Host "Cleaning up registry..."
|
||||||
|
$registryCleanupSucceeded = $true
|
||||||
|
|
||||||
# Remove MS Edge from autostart
|
# Remove MS Edge from autostart. Missing values are already-clean state,
|
||||||
reg delete "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run" /v "MicrosoftEdgeAutoLaunch_A9F6DCE4ABADF4F51CF45CD7129E3C6C" /f *>$null
|
# while failures to inspect or remove an existing value are reported.
|
||||||
reg delete "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run" /v "Microsoft Edge Update" /f *>$null
|
$autostartValues = @(
|
||||||
reg delete "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run" /v "MicrosoftEdgeAutoLaunch_A9F6DCE4ABADF4F51CF45CD7129E3C6C" /f *>$null
|
@{ Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'; Name = 'MicrosoftEdgeAutoLaunch_A9F6DCE4ABADF4F51CF45CD7129E3C6C' },
|
||||||
reg delete "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run" /v "Microsoft Edge Update" /f *>$null
|
@{ Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'; Name = 'Microsoft Edge Update' },
|
||||||
|
@{ Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run'; Name = 'MicrosoftEdgeAutoLaunch_A9F6DCE4ABADF4F51CF45CD7129E3C6C' },
|
||||||
|
@{ Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run'; Name = 'Microsoft Edge Update' }
|
||||||
|
)
|
||||||
|
foreach ($autostartValue in $autostartValues) {
|
||||||
|
if (-not (Remove-EdgeAutostartValue -Path $autostartValue.Path -Name $autostartValue.Name)) {
|
||||||
|
$registryCleanupSucceeded = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $cleanupSucceeded -or -not $registryCleanupSucceeded) {
|
||||||
|
Write-Warning "Microsoft Edge was uninstalled, but some leftover files or autostart entries could not be removed."
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host "Microsoft Edge was uninstalled"
|
Write-Host "Microsoft Edge was uninstalled"
|
||||||
|
return $true
|
||||||
}
|
}
|
||||||
else {
|
catch {
|
||||||
Write-Host "Unable to forcefully uninstall Microsoft Edge, uninstaller could not be found" -ForegroundColor Red
|
Write-Warning "Failed to forcefully uninstall Microsoft Edge: $($_.Exception.Message)"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if ($edgeUpdateKey) { $edgeUpdateKey.Dispose() }
|
||||||
|
if ($uninstallRegKey) { $uninstallRegKey.Dispose() }
|
||||||
|
if ($hklm) { $hklm.Dispose() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Removes an Edge autostart registry value when it exists.
|
||||||
|
|
||||||
|
.OUTPUTS
|
||||||
|
System.Boolean. $true when the value is absent or removed; $false when inspection or removal fails.
|
||||||
|
#>
|
||||||
|
function Remove-EdgeAutostartValue {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Path,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Name
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
$properties = Get-ItemProperty -Path $Path -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch [System.Management.Automation.ItemNotFoundException] {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Failed to inspect Edge autostart entry '$Path\$Name': $($_.Exception.Message)"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $properties.PSObject.Properties[$Name]) {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Remove-ItemProperty -Path $Path -Name $Name -ErrorAction Stop
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Failed to remove Edge autostart entry '$Path\$Name': $($_.Exception.Message)"
|
||||||
|
return $false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,114 +1,120 @@
|
|||||||
# Import & execute regfile
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Imports and executes a registry file.
|
||||||
|
|
||||||
|
.OUTPUTS
|
||||||
|
System.Boolean. $true when the registry file is applied or previewed successfully; otherwise $false.
|
||||||
|
#>
|
||||||
function Import-RegistryFile {
|
function Import-RegistryFile {
|
||||||
param (
|
param (
|
||||||
$message,
|
$message,
|
||||||
$path
|
$path
|
||||||
)
|
)
|
||||||
|
|
||||||
Write-Host $message
|
|
||||||
|
|
||||||
$usesOfflineHive = $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User")
|
|
||||||
$regFilePath = Get-RegistryFilePathForFeature -RegistryKey $path
|
|
||||||
|
|
||||||
if (-not (Test-Path $regFilePath)) {
|
|
||||||
$errorMessage = "Unable to find registry file: $path ($regFilePath)"
|
|
||||||
$script:RegistryImportFailures++
|
|
||||||
Write-Host "Error: $errorMessage" -ForegroundColor Red
|
|
||||||
Write-Host ""
|
|
||||||
throw $errorMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
$importScript = {
|
|
||||||
param($targetRegFilePath, $hiveContext)
|
|
||||||
|
|
||||||
if ($script:Params.ContainsKey("WhatIf")) {
|
|
||||||
Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath
|
|
||||||
Write-Host ""
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
# When the target user's hive is already loaded under their SID, the .reg file's
|
|
||||||
# HKEY_USERS\Default paths won't match. Use the PowerShell registry writer instead,
|
|
||||||
# which remaps Default → SID via Split-RegistryPath.
|
|
||||||
$usePowerShellFallbackOnly = $hiveContext -and [bool]$hiveContext.WasAlreadyLoaded
|
|
||||||
|
|
||||||
if ($usePowerShellFallbackOnly) {
|
|
||||||
Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath
|
|
||||||
Write-Host "The operation completed successfully via PowerShell registry writer."
|
|
||||||
Write-Host ""
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$regResult = Invoke-NonBlocking -ScriptBlock {
|
|
||||||
param($targetRegFilePath)
|
|
||||||
$result = @{
|
|
||||||
Output = @()
|
|
||||||
ExitCode = 0
|
|
||||||
Error = $null
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$global:LASTEXITCODE = 0
|
|
||||||
$output = reg import $targetRegFilePath 2>&1
|
|
||||||
$importExitCode = $LASTEXITCODE
|
|
||||||
|
|
||||||
if ($output) {
|
|
||||||
$result.Output = @($output)
|
|
||||||
}
|
|
||||||
$result.ExitCode = $importExitCode
|
|
||||||
|
|
||||||
if ($importExitCode -ne 0) {
|
|
||||||
throw "Registry import failed with exit code $importExitCode for '$targetRegFilePath'"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
$result.Error = $_.Exception.Message
|
|
||||||
$result.ExitCode = if ($LASTEXITCODE -ne 0) { $LASTEXITCODE } else { 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result
|
|
||||||
} -ArgumentList $targetRegFilePath
|
|
||||||
|
|
||||||
$regOutput = @($regResult.Output)
|
|
||||||
$hasSuccess = ($regResult.ExitCode -eq 0) -and -not $regResult.Error
|
|
||||||
|
|
||||||
if ($regOutput) {
|
|
||||||
foreach ($line in $regOutput) {
|
|
||||||
$lineText = if ($line -is [System.Management.Automation.ErrorRecord]) { $line.Exception.Message } else { $line.ToString() }
|
|
||||||
if ($lineText -and $lineText.Length -gt 0) {
|
|
||||||
if ($hasSuccess) {
|
|
||||||
Write-Host $lineText
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Host $lineText -ForegroundColor Red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $hasSuccess) {
|
|
||||||
$details = if ($regResult.Error) { $regResult.Error } else { "Exit code: $($regResult.ExitCode)" }
|
|
||||||
Write-Warning "reg import failed for '$path'. Falling back to PowerShell registry writer. Details: $details"
|
|
||||||
Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath
|
|
||||||
Write-Host "The operation completed successfully via PowerShell registry writer."
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
Write-Host $message
|
||||||
|
|
||||||
|
$usesOfflineHive = $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User")
|
||||||
|
$regFilePath = Get-RegistryFilePathForFeature -RegistryKey $path
|
||||||
|
|
||||||
|
if (-not (Test-Path $regFilePath)) {
|
||||||
|
$errorMessage = "Unable to find registry file: $path ($regFilePath)"
|
||||||
|
Write-Host "Error: $errorMessage" -ForegroundColor Red
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$importScript = {
|
||||||
|
param($targetRegFilePath, $hiveContext)
|
||||||
|
|
||||||
|
if ($script:Params.ContainsKey("WhatIf")) {
|
||||||
|
return (Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
# When the target user's hive is already loaded under their SID, the .reg file's
|
||||||
|
# HKEY_USERS\Default paths won't match. Use the PowerShell registry writer instead,
|
||||||
|
# which remaps Default → SID via Split-RegistryPath.
|
||||||
|
$usePowerShellFallbackOnly = $hiveContext -and [bool]$hiveContext.WasAlreadyLoaded
|
||||||
|
|
||||||
|
if ($usePowerShellFallbackOnly) {
|
||||||
|
$fallbackSucceeded = Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath
|
||||||
|
if ($fallbackSucceeded) {
|
||||||
|
Write-Host "The operation completed successfully via PowerShell registry writer."
|
||||||
|
}
|
||||||
|
return $fallbackSucceeded
|
||||||
|
}
|
||||||
|
|
||||||
|
$regResult = Invoke-NonBlocking -ScriptBlock {
|
||||||
|
param($targetRegFilePath)
|
||||||
|
$result = @{
|
||||||
|
Output = @()
|
||||||
|
ExitCode = 0
|
||||||
|
Error = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$global:LASTEXITCODE = 0
|
||||||
|
$output = reg import $targetRegFilePath 2>&1
|
||||||
|
$importExitCode = $LASTEXITCODE
|
||||||
|
|
||||||
|
if ($output) {
|
||||||
|
$result.Output = @($output)
|
||||||
|
}
|
||||||
|
$result.ExitCode = $importExitCode
|
||||||
|
|
||||||
|
if ($importExitCode -ne 0) {
|
||||||
|
throw "Registry import failed with exit code $importExitCode for '$targetRegFilePath'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$result.Error = $_.Exception.Message
|
||||||
|
$result.ExitCode = if ($LASTEXITCODE -ne 0) { $LASTEXITCODE } else { 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result
|
||||||
|
} -ArgumentList $targetRegFilePath
|
||||||
|
|
||||||
|
$regOutput = @($regResult.Output)
|
||||||
|
$hasSuccess = ($regResult.ExitCode -eq 0) -and -not $regResult.Error
|
||||||
|
|
||||||
|
if ($regOutput) {
|
||||||
|
foreach ($line in $regOutput) {
|
||||||
|
$lineText = if ($line -is [System.Management.Automation.ErrorRecord]) { $line.Exception.Message } else { $line.ToString() }
|
||||||
|
if ($lineText -and $lineText.Length -gt 0) {
|
||||||
|
if ($hasSuccess) {
|
||||||
|
Write-Host $lineText
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host $lineText -ForegroundColor Red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $hasSuccess) {
|
||||||
|
$details = if ($regResult.Error) { $regResult.Error } else { "Exit code: $($regResult.ExitCode)" }
|
||||||
|
Write-Warning "reg import failed for '$path'. Falling back to PowerShell registry writer. Details: $details"
|
||||||
|
$fallbackSucceeded = Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath
|
||||||
|
if ($fallbackSucceeded) {
|
||||||
|
Write-Host "The operation completed successfully via PowerShell registry writer."
|
||||||
|
}
|
||||||
|
return $fallbackSucceeded
|
||||||
|
}
|
||||||
|
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
if ($usesOfflineHive) {
|
if ($usesOfflineHive) {
|
||||||
# Sysprep targets Default user, User targets the specified user. Logged-in users already have their hive mounted under HKU\<SID>.
|
# Sysprep targets Default user, User targets the specified user. Logged-in users already have their hive mounted under HKU\<SID>.
|
||||||
$targetUserName = if ($script:Params.ContainsKey("Sysprep")) { "Default" } else { $script:Params.Item("User") }
|
$targetUserName = if ($script:Params.ContainsKey("Sysprep")) { "Default" } else { $script:Params.Item("User") }
|
||||||
Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock $importScript -ArgumentObject $regFilePath -PassHiveContext
|
$succeeded = Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock $importScript -ArgumentObject $regFilePath -PassHiveContext
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
& $importScript $regFilePath $null
|
$succeeded = & $importScript $regFilePath $null
|
||||||
}
|
}
|
||||||
|
return [bool]$succeeded
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
$script:RegistryImportFailures++
|
|
||||||
Write-Host $_.Exception.Message -ForegroundColor Red
|
Write-Host $_.Exception.Message -ForegroundColor Red
|
||||||
Write-Host ""
|
return $false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
- Registry-backed: imports the .reg file via Import-RegistryFile, then runs
|
- Registry-backed: imports the .reg file via Import-RegistryFile, then runs
|
||||||
any post-import side effects (e.g., removing companion app packages).
|
any post-import side effects (e.g., removing companion app packages).
|
||||||
- Custom logic: app removal, Windows optional features, start menu
|
- Custom logic: app removal, Windows optional features, start menu
|
||||||
replacement, and other special-case features.
|
replacement, and other special-case features. Returns $true when the
|
||||||
|
feature completes successfully; otherwise writes a warning and returns
|
||||||
|
$false.
|
||||||
#>
|
#>
|
||||||
function Invoke-FeatureApply {
|
function Invoke-FeatureApply {
|
||||||
param(
|
param(
|
||||||
@@ -15,30 +17,32 @@ function Invoke-FeatureApply {
|
|||||||
[string]$FeatureId
|
[string]$FeatureId
|
||||||
)
|
)
|
||||||
|
|
||||||
# Resolve feature metadata from Features.json
|
try {
|
||||||
$feature = $script:Features[$FeatureId]
|
# Resolve feature metadata from Features.json
|
||||||
$applyText = $feature.ApplyText
|
$feature = $script:Features[$FeatureId]
|
||||||
|
$applyText = $feature.ApplyText
|
||||||
|
|
||||||
# ---- Registry-backed features: import .reg file, then handle side effects ----
|
# ---- Registry-backed features: import .reg file, then handle additional tasks ----
|
||||||
if ($feature.RegistryKey) {
|
if ($feature.RegistryKey) {
|
||||||
Import-RegistryFile "> $applyText..." $feature.RegistryKey
|
if (-not (Import-RegistryFile "> $applyText..." $feature.RegistryKey)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
# Post-import side effects for specific features
|
|
||||||
switch ($FeatureId) {
|
switch ($FeatureId) {
|
||||||
'DisableBing' {
|
'DisableBing' {
|
||||||
# Also remove the app package for Bing search
|
# Also remove the app package for Bing search
|
||||||
Remove-SelectedApps @('Microsoft.BingSearch')
|
return (Remove-SelectedApps @('Microsoft.BingSearch'))
|
||||||
}
|
}
|
||||||
'DisableCopilot' {
|
'DisableCopilot' {
|
||||||
# Also remove the app packages for Copilot
|
# Also remove the app packages for Copilot
|
||||||
Remove-SelectedApps @('Microsoft.Copilot', 'XP9CXNGPPJ97XX')
|
return (Remove-SelectedApps @('Microsoft.Copilot', 'XP9CXNGPPJ97XX'))
|
||||||
}
|
}
|
||||||
'DisableTelemetry' {
|
'DisableTelemetry' {
|
||||||
# Also disable telemetry scheduled tasks
|
# Also disable telemetry scheduled tasks
|
||||||
Disable-TelemetryScheduledTasks
|
return (Disable-TelemetryScheduledTasks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return $true
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- Custom features (no registry backing, or special handling required) ----
|
# ---- Custom features (no registry backing, or special handling required) ----
|
||||||
@@ -49,30 +53,25 @@ function Invoke-FeatureApply {
|
|||||||
|
|
||||||
if ($appsList.Count -eq 0) {
|
if ($appsList.Count -eq 0) {
|
||||||
Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow
|
Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow
|
||||||
Write-Host ""
|
return $true
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "$($appsList.Count) apps selected for removal"
|
Write-Host "$($appsList.Count) apps selected for removal"
|
||||||
Remove-SelectedApps $appsList
|
return (Remove-SelectedApps $appsList)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
'RemoveGamingApps' {
|
'RemoveGamingApps' {
|
||||||
$appsList = @('Microsoft.GamingApp', 'Microsoft.XboxGameOverlay', 'Microsoft.XboxGamingOverlay')
|
$appsList = @('Microsoft.GamingApp', 'Microsoft.XboxGameOverlay', 'Microsoft.XboxGamingOverlay')
|
||||||
Write-Host "> $applyText..."
|
Write-Host "> $applyText..."
|
||||||
Remove-SelectedApps $appsList
|
return (Remove-SelectedApps $appsList)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
'RemoveHPApps' {
|
'RemoveHPApps' {
|
||||||
$appsList = @('AD2F1837.HPAIExperienceCenter', 'AD2F1837.HPJumpStarts', 'AD2F1837.HPPCHardwareDiagnosticsWindows', 'AD2F1837.HPPowerManager', 'AD2F1837.HPPrivacySettings', 'AD2F1837.HPSupportAssistant', 'AD2F1837.HPSureShieldAI', 'AD2F1837.HPSystemInformation', 'AD2F1837.HPQuickDrop', 'AD2F1837.HPWorkWell', 'AD2F1837.myHP', 'AD2F1837.HPDesktopSupportUtilities', 'AD2F1837.HPQuickTouch', 'AD2F1837.HPEasyClean', 'AD2F1837.HPConnectedMusic', 'AD2F1837.HPFileViewer', 'AD2F1837.HPRegistration', 'AD2F1837.HPWelcome', 'AD2F1837.HPConnectedPhotopoweredbySnapfish', 'AD2F1837.HPPrinterControl')
|
$appsList = @('AD2F1837.HPAIExperienceCenter', 'AD2F1837.HPJumpStarts', 'AD2F1837.HPPCHardwareDiagnosticsWindows', 'AD2F1837.HPPowerManager', 'AD2F1837.HPPrivacySettings', 'AD2F1837.HPSupportAssistant', 'AD2F1837.HPSureShieldAI', 'AD2F1837.HPSystemInformation', 'AD2F1837.HPQuickDrop', 'AD2F1837.HPWorkWell', 'AD2F1837.myHP', 'AD2F1837.HPDesktopSupportUtilities', 'AD2F1837.HPQuickTouch', 'AD2F1837.HPEasyClean', 'AD2F1837.HPConnectedMusic', 'AD2F1837.HPFileViewer', 'AD2F1837.HPRegistration', 'AD2F1837.HPWelcome', 'AD2F1837.HPConnectedPhotopoweredbySnapfish', 'AD2F1837.HPPrinterControl')
|
||||||
Write-Host "> $applyText..."
|
Write-Host "> $applyText..."
|
||||||
Remove-SelectedApps $appsList
|
return (Remove-SelectedApps $appsList)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
'ForceRemoveEdge' {
|
'ForceRemoveEdge' {
|
||||||
Write-Host "> $applyText..."
|
Write-Host "> $applyText..."
|
||||||
Invoke-ForceRemoveEdge
|
return (Invoke-ForceRemoveEdge)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
'DisableWidgets' {
|
'DisableWidgets' {
|
||||||
Write-Host "> $applyText..."
|
Write-Host "> $applyText..."
|
||||||
@@ -81,76 +80,75 @@ function Invoke-FeatureApply {
|
|||||||
Get-Process *Widget* -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
Get-Process *Widget* -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
Remove-SelectedApps @('Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime')
|
return (Remove-SelectedApps @('Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime'))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
'EnableWindowsSandbox' {
|
'EnableWindowsSandbox' {
|
||||||
Write-Host "> $applyText..."
|
Write-Host "> $applyText..."
|
||||||
Enable-WindowsFeature "Containers-DisposableClientVM"
|
return (Enable-WindowsFeature "Containers-DisposableClientVM")
|
||||||
Write-Host ""
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
'EnableWindowsSubsystemForLinux' {
|
'EnableWindowsSubsystemForLinux' {
|
||||||
Write-Host "> $applyText..."
|
Write-Host "> $applyText..."
|
||||||
Enable-WindowsFeature "VirtualMachinePlatform"
|
if (-not (Enable-WindowsFeature "VirtualMachinePlatform")) { return $false }
|
||||||
Enable-WindowsFeature "Microsoft-Windows-Subsystem-Linux"
|
return (Enable-WindowsFeature "Microsoft-Windows-Subsystem-Linux")
|
||||||
Write-Host ""
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
'ClearStart' {
|
'ClearStart' {
|
||||||
Write-Host "> $applyText for user $(Get-UserName)..."
|
Write-Host "> $applyText for user $(Get-UserName)..."
|
||||||
$startMenuBinFile = Get-StartMenuBinPathForUser -UserName (Get-UserName)
|
$startMenuBinFile = Get-StartMenuBinPathForUser -UserName (Get-UserName)
|
||||||
if (-not [string]::IsNullOrWhiteSpace($startMenuBinFile)) {
|
if (-not [string]::IsNullOrWhiteSpace($startMenuBinFile)) {
|
||||||
Replace-StartMenu -startMenuBinFile $startMenuBinFile
|
return (Replace-StartMenu -startMenuBinFile $startMenuBinFile)
|
||||||
}
|
}
|
||||||
Write-Host ""
|
Write-Warning "Unable to apply '$applyText': the Start menu path for user $(Get-UserName) could not be resolved."
|
||||||
return
|
return $false
|
||||||
}
|
}
|
||||||
'ReplaceStart' {
|
'ReplaceStart' {
|
||||||
Write-Host "> $applyText for user $(Get-UserName)..."
|
Write-Host "> $applyText for user $(Get-UserName)..."
|
||||||
$startMenuBinFile = Get-StartMenuBinPathForUser -UserName (Get-UserName)
|
$startMenuBinFile = Get-StartMenuBinPathForUser -UserName (Get-UserName)
|
||||||
if (-not [string]::IsNullOrWhiteSpace($startMenuBinFile)) {
|
if (-not [string]::IsNullOrWhiteSpace($startMenuBinFile)) {
|
||||||
Replace-StartMenu -startMenuBinFile $startMenuBinFile -startMenuTemplate $script:Params.Item("ReplaceStart")
|
return (Replace-StartMenu -startMenuBinFile $startMenuBinFile -startMenuTemplate $script:Params.Item("ReplaceStart"))
|
||||||
}
|
}
|
||||||
Write-Host ""
|
Write-Warning "Unable to apply '$applyText': the Start menu path for user $(Get-UserName) could not be resolved."
|
||||||
return
|
return $false
|
||||||
}
|
}
|
||||||
'ClearStartAllUsers' {
|
'ClearStartAllUsers' {
|
||||||
Replace-StartMenuForAllUsers
|
return (Replace-StartMenuForAllUsers)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
'ReplaceStartAllUsers' {
|
'ReplaceStartAllUsers' {
|
||||||
Replace-StartMenuForAllUsers -startMenuTemplate $script:Params.Item("ReplaceStartAllUsers")
|
return (Replace-StartMenuForAllUsers -startMenuTemplate $script:Params.Item("ReplaceStartAllUsers"))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
'DisableStoreSearchSuggestions' {
|
'DisableStoreSearchSuggestions' {
|
||||||
if ($script:Params.ContainsKey("Sysprep")) {
|
if ($script:Params.ContainsKey("Sysprep")) {
|
||||||
Write-Host "> Disabling Microsoft Store search suggestions in the start menu for all users..."
|
Write-Host "> Disabling Microsoft Store search suggestions in the start menu for all users..."
|
||||||
Set-StoreSearchSuggestionsDisabledForAllUsers
|
return (Set-StoreSearchSuggestionsDisabledForAllUsers)
|
||||||
Write-Host ""
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "> Disabling Microsoft Store search suggestions for user $(Get-UserName)..."
|
Write-Host "> Disabling Microsoft Store search suggestions for user $(Get-UserName)..."
|
||||||
$storeDb = Get-StoreAppsDatabasePathForUser -UserName (Get-UserName)
|
$storeDb = Get-StoreAppsDatabasePathForUser -UserName (Get-UserName)
|
||||||
if ($storeDb) {
|
if ($storeDb) {
|
||||||
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase $storeDb
|
return (Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase $storeDb)
|
||||||
}
|
}
|
||||||
Write-Host ""
|
Write-Warning "Unable to disable Microsoft Store search suggestions because the Store database for user $(Get-UserName) could not be resolved."
|
||||||
return
|
return $false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Failed to apply '$applyText': $($_.Exception.Message)"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Warning "Unknown feature '$FeatureId' could not be applied."
|
||||||
|
return $false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Undoes a single feature that has no RegistryUndoKey.
|
Undoes a single feature.
|
||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Handles undo for features that require custom logic rather than a simple
|
Handles registry-backed undo imports and custom undo logic. Returns
|
||||||
.reg file import. Features with a RegistryUndoKey are handled directly
|
$true when the requested undo succeeds; otherwise writes a warning and
|
||||||
via Import-RegistryFile in Invoke-UndoFeatures.
|
returns $false.
|
||||||
#>
|
#>
|
||||||
function Invoke-FeatureUndo {
|
function Invoke-FeatureUndo {
|
||||||
param(
|
param(
|
||||||
@@ -159,43 +157,65 @@ function Invoke-FeatureUndo {
|
|||||||
)
|
)
|
||||||
|
|
||||||
$feature = if ($script:Features.ContainsKey($FeatureId)) { $script:Features[$FeatureId] } else { $null }
|
$feature = if ($script:Features.ContainsKey($FeatureId)) { $script:Features[$FeatureId] } else { $null }
|
||||||
|
if (-not $feature) {
|
||||||
|
Write-Warning "Unknown feature '$FeatureId' could not be undone."
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
switch ($FeatureId) {
|
$undoText = if ($feature.ApplyUndoText) { $feature.ApplyUndoText } elseif ($feature.UndoLabel) { $feature.UndoLabel } else { $FeatureId }
|
||||||
'DisableStoreSearchSuggestions' {
|
|
||||||
if ($script:Params.ContainsKey('Sysprep')) {
|
try {
|
||||||
Write-Host "> Re-enabling Microsoft Store search suggestions in the start menu for all users..."
|
# ---- Registry-backed features: import undo data, then handle additional tasks ----
|
||||||
Set-StoreSearchSuggestionsEnabledForAllUsers
|
if ($feature.RegistryUndoKey) {
|
||||||
Write-Host ""
|
if (-not (Import-RegistryFile "> $undoText" (Resolve-UndoRegFilePath $feature.RegistryUndoKey))) {
|
||||||
return
|
return $false
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "> Re-enabling Microsoft Store search suggestions for user $(Get-UserName)..."
|
switch ($FeatureId) {
|
||||||
$storeDb = Get-StoreAppsDatabasePathForUser -UserName (Get-UserName)
|
'DisableTelemetry' {
|
||||||
if ($storeDb) {
|
# Also re-enable telemetry scheduled tasks.
|
||||||
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase $storeDb
|
return (Enable-TelemetryScheduledTasks)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Write-Host ""
|
|
||||||
return
|
return $true
|
||||||
}
|
}
|
||||||
'EnableWindowsSandbox' {
|
|
||||||
Write-Host "> $($feature.ApplyUndoText)..."
|
# ---- Custom undo features (no registry backing) ----
|
||||||
Disable-WindowsFeature 'Containers-DisposableClientVM'
|
switch ($FeatureId) {
|
||||||
Write-Host ""
|
'DisableStoreSearchSuggestions' {
|
||||||
return
|
if ($script:Params.ContainsKey('Sysprep')) {
|
||||||
}
|
Write-Host "> Re-enabling Microsoft Store search suggestions in the start menu for all users..."
|
||||||
'EnableWindowsSubsystemForLinux' {
|
return (Set-StoreSearchSuggestionsEnabledForAllUsers)
|
||||||
Write-Host "> $($feature.ApplyUndoText)..."
|
}
|
||||||
Disable-WindowsFeature 'Microsoft-Windows-Subsystem-Linux'
|
|
||||||
Disable-WindowsFeature 'VirtualMachinePlatform'
|
Write-Host "> Re-enabling Microsoft Store search suggestions for user $(Get-UserName)..."
|
||||||
Write-Host ""
|
$storeDb = Get-StoreAppsDatabasePathForUser -UserName (Get-UserName)
|
||||||
return
|
if ($storeDb) {
|
||||||
}
|
return (Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase $storeDb)
|
||||||
'DisableTelemetry' {
|
}
|
||||||
# Also re-enable telemetry scheduled tasks
|
Write-Warning "Unable to re-enable Microsoft Store search suggestions because the Store database for user $(Get-UserName) could not be resolved."
|
||||||
Enable-TelemetryScheduledTasks
|
return $false
|
||||||
return
|
}
|
||||||
|
'EnableWindowsSandbox' {
|
||||||
|
Write-Host "> $undoText..."
|
||||||
|
return (Disable-WindowsFeature 'Containers-DisposableClientVM')
|
||||||
|
}
|
||||||
|
'EnableWindowsSubsystemForLinux' {
|
||||||
|
Write-Host "> $undoText..."
|
||||||
|
if (-not (Disable-WindowsFeature 'Microsoft-Windows-Subsystem-Linux')) { return $false }
|
||||||
|
return (Disable-WindowsFeature 'VirtualMachinePlatform')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
catch {
|
||||||
|
Write-Warning "Failed to undo '$undoText': $($_.Exception.Message)"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Warning "Feature '$FeatureId' does not support undo."
|
||||||
|
return $false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -251,7 +271,13 @@ function Invoke-ApplyFeatures {
|
|||||||
& $script:ApplyProgressCallback $step $TotalSteps $displayName
|
& $script:ApplyProgressCallback $step $TotalSteps $displayName
|
||||||
}
|
}
|
||||||
|
|
||||||
Invoke-FeatureApply -FeatureId $featureId
|
# Compare app-removal failure counts so a feature that only fails due to
|
||||||
|
# app removal isn't also double-reported as a feature failure.
|
||||||
|
$appRemovalFailuresBefore = $script:AppRemovalFailures
|
||||||
|
if ((-not (Invoke-FeatureApply -FeatureId $featureId)) -and ($script:AppRemovalFailures -eq $appRemovalFailuresBefore)) {
|
||||||
|
$script:FeatureFailures++
|
||||||
|
}
|
||||||
|
Write-Host ""
|
||||||
$step++
|
$step++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,9 +288,8 @@ function Invoke-ApplyFeatures {
|
|||||||
Undoes a list of features, reporting progress for each.
|
Undoes a list of features, reporting progress for each.
|
||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Iterates through the provided feature IDs. Features with a RegistryUndoKey
|
Iterates through the provided feature IDs and delegates each to
|
||||||
are handled by importing the undo .reg file; all others delegate to
|
Invoke-FeatureUndo, which handles registry-backed and custom undo logic.
|
||||||
Invoke-FeatureUndo for custom undo logic.
|
|
||||||
This is called by Invoke-AllChanges during the undo phase.
|
This is called by Invoke-AllChanges during the undo phase.
|
||||||
#>
|
#>
|
||||||
function Invoke-UndoFeatures {
|
function Invoke-UndoFeatures {
|
||||||
@@ -291,11 +316,10 @@ function Invoke-UndoFeatures {
|
|||||||
& $script:ApplyProgressCallback $step $TotalSteps $undoText
|
& $script:ApplyProgressCallback $step $TotalSteps $undoText
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($f -and $f.RegistryUndoKey) {
|
if (-not (Invoke-FeatureUndo -FeatureId $featureId)) {
|
||||||
Import-RegistryFile "> $undoText" (Resolve-UndoRegFilePath $f.RegistryUndoKey)
|
$script:FeatureFailures++
|
||||||
}
|
}
|
||||||
|
Write-Host ""
|
||||||
Invoke-FeatureUndo -FeatureId $featureId
|
|
||||||
$step++
|
$step++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,8 +348,8 @@ function Invoke-AllChanges {
|
|||||||
throw "Win11Debloat is running as the SYSTEM account. Use the '-User' or '-Sysprep' parameter to target a specific user."
|
throw "Win11Debloat is running as the SYSTEM account. Use the '-User' or '-Sysprep' parameter to target a specific user."
|
||||||
}
|
}
|
||||||
|
|
||||||
$script:RegistryImportFailures = 0
|
|
||||||
$script:AppRemovalFailures = 0
|
$script:AppRemovalFailures = 0
|
||||||
|
$script:FeatureFailures = 0
|
||||||
$script:AppRemovalVerificationUnavailable = $false
|
$script:AppRemovalVerificationUnavailable = $false
|
||||||
|
|
||||||
# ---- Gather work items ----
|
# ---- Gather work items ----
|
||||||
@@ -405,7 +429,11 @@ function Invoke-AllChanges {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Write-Host "> Creating a system restore point..."
|
Write-Host "> Creating a system restore point..."
|
||||||
Invoke-SystemRestorePoint
|
$restorePointSucceeded = Invoke-SystemRestorePoint
|
||||||
|
if (-not $restorePointSucceeded) {
|
||||||
|
if ($script:CancelRequested) { return }
|
||||||
|
$script:FeatureFailures++
|
||||||
|
}
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -429,19 +457,20 @@ function Invoke-AllChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ================================================================
|
# ================================================================
|
||||||
# Final: Report registry import and app removal failures
|
# Final: Report failures
|
||||||
# ================================================================
|
# ================================================================
|
||||||
if ($script:RegistryImportFailures -gt 0) {
|
|
||||||
Write-Host ""
|
|
||||||
Write-Warning "$($script:RegistryImportFailures) registry import change(s) failed. See output above for details."
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($script:AppRemovalFailures -gt 0) {
|
if ($script:AppRemovalFailures -gt 0) {
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Warning "$($script:AppRemovalFailures) app removal(s) failed. See output above for details."
|
Write-Warning "$($script:AppRemovalFailures) app removal(s) failed. See output above for details."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($script:FeatureFailures -gt 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Warning "$($script:FeatureFailures) feature change(s) failed. See output above for details."
|
||||||
|
}
|
||||||
|
|
||||||
if ($script:AppRemovalVerificationUnavailable) {
|
if ($script:AppRemovalVerificationUnavailable) {
|
||||||
|
Write-Host ""
|
||||||
Write-Warning "Unable to verify if all apps were uninstalled successfully."
|
Write-Warning "Unable to verify if all apps were uninstalled successfully."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
Write-Host "Created LocalState folder for default user profile"
|
New-Item $defaultStartMenuPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
||||||
|
Write-Host "Created LocalState folder for default user profile"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Failed to create the Default profile Start Menu directory: $($_.Exception.Message)"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Copy template to default profile
|
# Copy template to default profile
|
||||||
Replace-StartMenu -startMenuBinFile "$($defaultStartMenuPath)\start2.bin" -startMenuTemplate $startMenuTemplate
|
if (-not (Replace-StartMenu -startMenuBinFile "$($defaultStartMenuPath)\start2.bin" -startMenuTemplate $startMenuTemplate)) {
|
||||||
Write-Host "Replaced start menu for the default user profile"
|
$success = $false
|
||||||
Write-Host ""
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "Replaced start menu for the default user profile"
|
||||||
|
}
|
||||||
|
return $success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -87,6 +102,9 @@ function Replace-StartMenuForAllUsers {
|
|||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
Replace-StartMenu -startMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -startMenuTemplate "C:\CustomLayout.bin"
|
Replace-StartMenu -startMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -startMenuTemplate "C:\CustomLayout.bin"
|
||||||
|
|
||||||
|
.OUTPUTS
|
||||||
|
System.Boolean. $true when the template is valid and copied, or the change is previewed; otherwise $false.
|
||||||
#>
|
#>
|
||||||
function Replace-StartMenu {
|
function Replace-StartMenu {
|
||||||
param (
|
param (
|
||||||
@@ -98,19 +116,19 @@ function Replace-StartMenu {
|
|||||||
# Check if template bin file exists
|
# Check if template bin file exists
|
||||||
if (-not (Test-Path $startMenuTemplate)) {
|
if (-not (Test-Path $startMenuTemplate)) {
|
||||||
Write-Host "Error: Unable to replace start menu, template file not found" -ForegroundColor Red
|
Write-Host "Error: Unable to replace start menu, template file not found" -ForegroundColor Red
|
||||||
return
|
return $false
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([IO.Path]::GetExtension($startMenuTemplate) -ne ".bin") {
|
if ([IO.Path]::GetExtension($startMenuTemplate) -ne ".bin") {
|
||||||
Write-Host "Error: Unable to replace start menu, template file is not a valid .bin file" -ForegroundColor Red
|
Write-Host "Error: Unable to replace start menu, template file is not a valid .bin file" -ForegroundColor Red
|
||||||
return
|
return $false
|
||||||
}
|
}
|
||||||
|
|
||||||
$userName = Get-StartMenuUserNameFromPath -StartMenuBinFile $startMenuBinFile
|
$userName = Get-StartMenuUserNameFromPath -StartMenuBinFile $startMenuBinFile
|
||||||
|
|
||||||
if ($script:Params.ContainsKey("WhatIf")) {
|
if ($script:Params.ContainsKey("WhatIf")) {
|
||||||
Write-Host "[WhatIf] Replace Start Menu for user $userName with template $startMenuTemplate" -ForegroundColor Cyan
|
Write-Host "[WhatIf] Replace Start Menu for user $userName with template $startMenuTemplate" -ForegroundColor Cyan
|
||||||
return
|
return $true
|
||||||
}
|
}
|
||||||
|
|
||||||
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
|
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
|
||||||
@@ -118,20 +136,27 @@ function Replace-StartMenu {
|
|||||||
$startMenuDir = Split-Path $startMenuBinFile -Parent
|
$startMenuDir = Split-Path $startMenuBinFile -Parent
|
||||||
$backupBinFile = Join-Path $startMenuDir $backupFileName
|
$backupBinFile = Join-Path $startMenuDir $backupFileName
|
||||||
|
|
||||||
if (Test-Path $startMenuBinFile) {
|
try {
|
||||||
# Backup current start menu file
|
if (Test-Path $startMenuBinFile) {
|
||||||
Copy-Item -Path $startMenuBinFile -Destination $backupBinFile -Force
|
# Backup current start menu file
|
||||||
Write-Verbose "Start menu backup for user $userName saved to $backupFileName"
|
Copy-Item -Path $startMenuBinFile -Destination $backupBinFile -Force -ErrorAction Stop
|
||||||
}
|
Write-Verbose "Start menu backup for user $userName saved to $backupFileName"
|
||||||
else {
|
}
|
||||||
Write-Host "Unable to find original start2.bin file for user $userName, no backup was created for this user" -ForegroundColor Yellow
|
else {
|
||||||
New-Item -ItemType File -Path $startMenuBinFile -Force
|
Write-Host "Unable to find original start2.bin file for user $userName, no backup was created for this user" -ForegroundColor Yellow
|
||||||
}
|
New-Item -ItemType File -Path $startMenuBinFile -Force -ErrorAction Stop | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
# Copy template file
|
# Copy template file
|
||||||
Copy-Item -Path $startMenuTemplate -Destination $startMenuBinFile -Force
|
Copy-Item -Path $startMenuTemplate -Destination $startMenuBinFile -Force -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Failed to replace Start Menu for user ${userName}: $($_.Exception.Message)"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host "Replaced start menu for user $userName"
|
Write-Host "Replaced start menu for user $userName"
|
||||||
|
return $true
|
||||||
}
|
}
|
||||||
|
|
||||||
<#
|
<#
|
||||||
@@ -447,4 +472,4 @@ function Restore-StartMenuForAllUsers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return $results
|
return $results
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,22 +10,41 @@
|
|||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
DisableStoreSearchSuggestionsForAllUsers
|
DisableStoreSearchSuggestionsForAllUsers
|
||||||
|
|
||||||
|
.OUTPUTS
|
||||||
|
System.Boolean. $true when a profile is processed and all ACL changes succeed; otherwise $false.
|
||||||
#>
|
#>
|
||||||
function Set-StoreSearchSuggestionsDisabledForAllUsers {
|
function Set-StoreSearchSuggestionsDisabledForAllUsers {
|
||||||
|
$success = $true
|
||||||
|
$processedProfiles = 0
|
||||||
|
|
||||||
# Get path to Store app database for all users
|
# Get path to Store app database for all users
|
||||||
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages"
|
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages"
|
||||||
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
|
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
# Go through all users and disable start search suggestions
|
# Go through all users and disable start search suggestions
|
||||||
foreach ($storeDbPath in $usersStoreDbPaths) {
|
foreach ($storeDbPath in $usersStoreDbPaths) {
|
||||||
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
|
$processedProfiles++
|
||||||
|
if (-not (Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"))) {
|
||||||
|
$success = $false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Also disable start search suggestions for the default user profile
|
# Also disable start search suggestions for the default user profile
|
||||||
$defaultStoreDbPath = Get-StoreAppsDatabasePathForUser -UserName "Default"
|
$defaultStoreDbPath = Get-StoreAppsDatabasePathForUser -UserName "Default"
|
||||||
if ($defaultStoreDbPath) {
|
if ($defaultStoreDbPath) {
|
||||||
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase $defaultStoreDbPath
|
$processedProfiles++
|
||||||
|
if (-not (Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase $defaultStoreDbPath)) {
|
||||||
|
$success = $false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($processedProfiles -eq 0) {
|
||||||
|
Write-Warning 'Unable to disable Microsoft Store search suggestions because no target user profiles could be resolved.'
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
return $success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -44,6 +63,9 @@ function Set-StoreSearchSuggestionsDisabledForAllUsers {
|
|||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
DisableStoreSearchSuggestions -StoreAppsDatabase "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
|
DisableStoreSearchSuggestions -StoreAppsDatabase "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
|
||||||
|
|
||||||
|
.OUTPUTS
|
||||||
|
System.Boolean. $true when the database ACL is restricted or previewed; otherwise $false.
|
||||||
#>
|
#>
|
||||||
function Set-StoreSearchSuggestionsDisabled {
|
function Set-StoreSearchSuggestionsDisabled {
|
||||||
param (
|
param (
|
||||||
@@ -56,24 +78,22 @@ function Set-StoreSearchSuggestionsDisabled {
|
|||||||
|
|
||||||
if ($script:Params.ContainsKey("WhatIf")) {
|
if ($script:Params.ContainsKey("WhatIf")) {
|
||||||
Write-Host "[WhatIf] Disable Microsoft Store search suggestions for user $userName by restricting access to ${StoreAppsDatabase}" -ForegroundColor Cyan
|
Write-Host "[WhatIf] Disable Microsoft Store search suggestions for user $userName by restricting access to ${StoreAppsDatabase}" -ForegroundColor Cyan
|
||||||
return
|
return $true
|
||||||
}
|
}
|
||||||
|
|
||||||
# This file doesn't exist in EEA (No Store app suggestions).
|
try {
|
||||||
if (-not (Test-Path -Path $StoreAppsDatabase))
|
# 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
|
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
|
$storeDbDir = Split-Path -Path $StoreAppsDatabase -Parent
|
||||||
|
if (-not (Test-Path -Path $storeDbDir)) {
|
||||||
|
New-Item -Path $storeDbDir -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
if (-not (Test-Path -Path $storeDbDir)) {
|
New-Item -Path $StoreAppsDatabase -ItemType File -Force -ErrorAction Stop | Out-Null
|
||||||
New-Item -Path $storeDbDir -ItemType Directory -Force | Out-Null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
New-Item -Path $StoreAppsDatabase -ItemType File -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$AccountSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0') # 'EVERYONE' group
|
$AccountSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0') # 'EVERYONE' group
|
||||||
$Acl = Get-Acl -Path $StoreAppsDatabase -ErrorAction Stop
|
$Acl = Get-Acl -Path $StoreAppsDatabase -ErrorAction Stop
|
||||||
$Ace = [System.Security.AccessControl.FileSystemAccessRule]::new($AccountSid, 'FullControl', 'Deny')
|
$Ace = [System.Security.AccessControl.FileSystemAccessRule]::new($AccountSid, 'FullControl', 'Deny')
|
||||||
@@ -82,10 +102,11 @@ function Set-StoreSearchSuggestionsDisabled {
|
|||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
Write-Warning "Failed to restrict ACL for store database '$StoreAppsDatabase': $($_.Exception.Message)"
|
Write-Warning "Failed to restrict ACL for store database '$StoreAppsDatabase': $($_.Exception.Message)"
|
||||||
return
|
return $false
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Disabled Microsoft Store search suggestions for user $userName"
|
Write-Host "Disabled Microsoft Store search suggestions for user $userName"
|
||||||
|
return $true
|
||||||
}
|
}
|
||||||
|
|
||||||
<#
|
<#
|
||||||
@@ -100,22 +121,41 @@ function Set-StoreSearchSuggestionsDisabled {
|
|||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
EnableStoreSearchSuggestionsForAllUsers
|
EnableStoreSearchSuggestionsForAllUsers
|
||||||
|
|
||||||
|
.OUTPUTS
|
||||||
|
System.Boolean. $true when a profile is processed and all ACL changes succeed; otherwise $false.
|
||||||
#>
|
#>
|
||||||
function Set-StoreSearchSuggestionsEnabledForAllUsers {
|
function Set-StoreSearchSuggestionsEnabledForAllUsers {
|
||||||
|
$success = $true
|
||||||
|
$processedProfiles = 0
|
||||||
|
|
||||||
# Get path to Store app database for all users
|
# Get path to Store app database for all users
|
||||||
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages"
|
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages"
|
||||||
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
|
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
# Go through all users and re-enable start search suggestions
|
# Go through all users and re-enable start search suggestions
|
||||||
foreach ($storeDbPath in $usersStoreDbPaths) {
|
foreach ($storeDbPath in $usersStoreDbPaths) {
|
||||||
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
|
$processedProfiles++
|
||||||
|
if (-not (Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"))) {
|
||||||
|
$success = $false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Also re-enable for the default user profile
|
# Also re-enable for the default user profile
|
||||||
$defaultStoreDbPath = Get-StoreAppsDatabasePathForUser -UserName "Default"
|
$defaultStoreDbPath = Get-StoreAppsDatabasePathForUser -UserName "Default"
|
||||||
if ($defaultStoreDbPath) {
|
if ($defaultStoreDbPath) {
|
||||||
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase $defaultStoreDbPath
|
$processedProfiles++
|
||||||
|
if (-not (Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase $defaultStoreDbPath)) {
|
||||||
|
$success = $false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($processedProfiles -eq 0) {
|
||||||
|
Write-Warning 'Unable to re-enable Microsoft Store search suggestions because no target user profiles could be resolved.'
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
return $success
|
||||||
}
|
}
|
||||||
|
|
||||||
<#
|
<#
|
||||||
@@ -133,6 +173,9 @@ function Set-StoreSearchSuggestionsEnabledForAllUsers {
|
|||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
EnableStoreSearchSuggestions -StoreAppsDatabase "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
|
EnableStoreSearchSuggestions -StoreAppsDatabase "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
|
||||||
|
|
||||||
|
.OUTPUTS
|
||||||
|
System.Boolean. $true when the deny ACL is removed, the database is absent, or the change is previewed; otherwise $false.
|
||||||
#>
|
#>
|
||||||
function Set-StoreSearchSuggestionsEnabled {
|
function Set-StoreSearchSuggestionsEnabled {
|
||||||
param (
|
param (
|
||||||
@@ -145,23 +188,31 @@ function Set-StoreSearchSuggestionsEnabled {
|
|||||||
|
|
||||||
if ($script:Params.ContainsKey("WhatIf")) {
|
if ($script:Params.ContainsKey("WhatIf")) {
|
||||||
Write-Host "[WhatIf] Re-enable Microsoft Store search suggestions for user $userName by restoring access to ${StoreAppsDatabase}" -ForegroundColor Cyan
|
Write-Host "[WhatIf] Re-enable Microsoft Store search suggestions for user $userName by restoring access to ${StoreAppsDatabase}" -ForegroundColor Cyan
|
||||||
return
|
return $true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path -Path $StoreAppsDatabase)) {
|
if (-not (Test-Path -Path $StoreAppsDatabase)) {
|
||||||
Write-Host "Store app database not found for user $userName, nothing to undo"
|
Write-Host "Store app database not found for user $userName, nothing to undo"
|
||||||
return
|
return $true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Ensure we can modify/delete the file even if restrictive ACLs were set.
|
# Ensure we can modify/delete the file even if restrictive ACLs were set.
|
||||||
$global:LASTEXITCODE = 0
|
$global:LASTEXITCODE = 0
|
||||||
takeown /F "$StoreAppsDatabase" /A | Out-Null
|
takeown /F "$StoreAppsDatabase" /A | Out-Null
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Warning "Failed to take ownership of store database '$StoreAppsDatabase' while undoing Microsoft Store search suggestions. Exit code: $LASTEXITCODE"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
icacls "$StoreAppsDatabase" /grant *S-1-5-32-544:F /C | Out-Null
|
icacls "$StoreAppsDatabase" /grant *S-1-5-32-544:F /C | Out-Null
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Warning "Failed to grant Administrators access to store database '$StoreAppsDatabase' while undoing Microsoft Store search suggestions. Exit code: $LASTEXITCODE"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
$everyoneSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0') # 'EVERYONE' group
|
$everyoneSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0') # 'EVERYONE' group
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$acl = Get-Acl -Path $StoreAppsDatabase
|
$acl = Get-Acl -Path $StoreAppsDatabase -ErrorAction Stop
|
||||||
$denyRules = @(
|
$denyRules = @(
|
||||||
$acl.Access | Where-Object {
|
$acl.Access | Where-Object {
|
||||||
if ($_.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Deny) { return $false }
|
if ($_.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Deny) { return $false }
|
||||||
@@ -179,7 +230,7 @@ function Set-StoreSearchSuggestionsEnabled {
|
|||||||
$null = $acl.RemoveAccessRuleSpecific($denyRule)
|
$null = $acl.RemoveAccessRuleSpecific($denyRule)
|
||||||
}
|
}
|
||||||
|
|
||||||
Set-Acl -Path $StoreAppsDatabase -AclObject $acl | Out-Null
|
Set-Acl -Path $StoreAppsDatabase -AclObject $acl -ErrorAction Stop | Out-Null
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
Write-Warning "Failed to normalize ACL for store database '$StoreAppsDatabase': $($_.Exception.Message)"
|
Write-Warning "Failed to normalize ACL for store database '$StoreAppsDatabase': $($_.Exception.Message)"
|
||||||
@@ -188,9 +239,11 @@ function Set-StoreSearchSuggestionsEnabled {
|
|||||||
try {
|
try {
|
||||||
Remove-Item -Path $StoreAppsDatabase -Force -ErrorAction Stop
|
Remove-Item -Path $StoreAppsDatabase -Force -ErrorAction Stop
|
||||||
Write-Host "Re-enabled Microsoft Store search suggestions for user $userName"
|
Write-Host "Re-enabled Microsoft Store search suggestions for user $userName"
|
||||||
|
return $true
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
throw "Failed to remove '$StoreAppsDatabase' while undoing Microsoft Store search suggestions for user $userName. $($_.Exception.Message)"
|
Write-Warning "Failed to remove '$StoreAppsDatabase' while undoing Microsoft Store search suggestions for user $userName. $($_.Exception.Message)"
|
||||||
|
return $false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,23 +34,36 @@ function Get-TelemetryScheduledTasks {
|
|||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
Disable-TelemetryScheduledTasks
|
Disable-TelemetryScheduledTasks
|
||||||
|
|
||||||
|
.OUTPUTS
|
||||||
|
System.Boolean. $true when every task is disabled, absent, already disabled, or previewed; otherwise $false.
|
||||||
#>
|
#>
|
||||||
function Disable-TelemetryScheduledTasks {
|
function Disable-TelemetryScheduledTasks {
|
||||||
Write-Host "> Disabling telemetry scheduled tasks..."
|
Write-Host "> Disabling telemetry scheduled tasks..."
|
||||||
$tasks = Get-TelemetryScheduledTasks
|
$tasks = Get-TelemetryScheduledTasks
|
||||||
|
|
||||||
|
$success = $true
|
||||||
foreach ($task in $tasks) {
|
foreach ($task in $tasks) {
|
||||||
if ($script:CancelRequested) { return }
|
if ($script:CancelRequested) { return $false }
|
||||||
|
|
||||||
if ($script:Params.ContainsKey("WhatIf")) {
|
if ($script:Params.ContainsKey("WhatIf")) {
|
||||||
Write-Host "[WhatIf] Disable Scheduled Task: $($task.Path)$($task.Name)" -ForegroundColor Cyan
|
Write-Host "[WhatIf] Disable Scheduled Task: $($task.Path)$($task.Name)" -ForegroundColor Cyan
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = Invoke-NonBlocking -ScriptBlock {
|
try {
|
||||||
|
$result = Invoke-NonBlocking -ScriptBlock {
|
||||||
param($path, $name)
|
param($path, $name)
|
||||||
Import-Module ScheduledTasks -ErrorAction SilentlyContinue
|
try {
|
||||||
$taskObj = Get-ScheduledTask -TaskPath $path -TaskName $name -ErrorAction SilentlyContinue
|
Import-Module ScheduledTasks -ErrorAction Stop
|
||||||
|
$taskObj = Get-ScheduledTask -TaskPath $path -TaskName $name -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
if ($_.Exception -isnot [System.Management.Automation.CommandNotFoundException] -and $_.CategoryInfo.Category -eq [System.Management.Automation.ErrorCategory]::ObjectNotFound) {
|
||||||
|
return @{ Success = $true; Status = 'NotFound' }
|
||||||
|
}
|
||||||
|
return @{ Success = $false; Status = 'Error'; Error = $_.Exception.Message }
|
||||||
|
}
|
||||||
if (-not $taskObj) {
|
if (-not $taskObj) {
|
||||||
return @{ Success = $true; Status = 'NotFound' }
|
return @{ Success = $true; Status = 'NotFound' }
|
||||||
}
|
}
|
||||||
@@ -64,17 +77,24 @@ function Disable-TelemetryScheduledTasks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return @{ Success = $true; Status = 'AlreadyDisabled' }
|
return @{ Success = $true; Status = 'AlreadyDisabled' }
|
||||||
} -ArgumentList @($task.Path, $task.Name)
|
} -ArgumentList @($task.Path, $task.Name)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Failed to disable Scheduled Task: $($task.Path)$($task.Name) - $($_.Exception.Message)"
|
||||||
|
$success = $false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
switch ($result.Status) {
|
switch ($result.Status) {
|
||||||
'Disabled' { Write-Host "Disabled Scheduled Task: $($task.Path)$($task.Name)" }
|
'Disabled' { Write-Host "Disabled Scheduled Task: $($task.Path)$($task.Name)" }
|
||||||
'AlreadyDisabled' { Write-Host "Scheduled Task $($task.Path)$($task.Name) is already disabled" -ForegroundColor DarkGray }
|
'AlreadyDisabled' { Write-Host "Scheduled Task $($task.Path)$($task.Name) is already disabled" -ForegroundColor DarkGray }
|
||||||
'NotFound' { Write-Host "Scheduled Task $($task.Path)$($task.Name) not found" -ForegroundColor DarkGray }
|
'NotFound' { Write-Host "Scheduled Task $($task.Path)$($task.Name) not found" -ForegroundColor DarkGray }
|
||||||
'Error' { Write-Host "Failed to disable Scheduled Task: $($task.Path)$($task.Name) - $($result.Error)" -ForegroundColor Yellow }
|
'Error' { Write-Host "Failed to disable Scheduled Task: $($task.Path)$($task.Name) - $($result.Error)" -ForegroundColor Yellow; $success = $false }
|
||||||
|
default { Write-Warning "Unable to determine the result of disabling Scheduled Task: $($task.Path)$($task.Name)."; $success = $false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host ""
|
return $success
|
||||||
}
|
}
|
||||||
|
|
||||||
<#
|
<#
|
||||||
@@ -88,23 +108,36 @@ function Disable-TelemetryScheduledTasks {
|
|||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
Enable-TelemetryScheduledTasks
|
Enable-TelemetryScheduledTasks
|
||||||
|
|
||||||
|
.OUTPUTS
|
||||||
|
System.Boolean. $true when every task is enabled, absent, already enabled, or previewed; otherwise $false.
|
||||||
#>
|
#>
|
||||||
function Enable-TelemetryScheduledTasks {
|
function Enable-TelemetryScheduledTasks {
|
||||||
Write-Host "> Enabling telemetry scheduled tasks..."
|
Write-Host "> Enabling telemetry scheduled tasks..."
|
||||||
$tasks = Get-TelemetryScheduledTasks
|
$tasks = Get-TelemetryScheduledTasks
|
||||||
|
|
||||||
|
$success = $true
|
||||||
foreach ($task in $tasks) {
|
foreach ($task in $tasks) {
|
||||||
if ($script:CancelRequested) { return }
|
if ($script:CancelRequested) { return $false }
|
||||||
|
|
||||||
if ($script:Params.ContainsKey("WhatIf")) {
|
if ($script:Params.ContainsKey("WhatIf")) {
|
||||||
Write-Host "[WhatIf] Enable Scheduled Task: $($task.Path)$($task.Name)" -ForegroundColor Cyan
|
Write-Host "[WhatIf] Enable Scheduled Task: $($task.Path)$($task.Name)" -ForegroundColor Cyan
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = Invoke-NonBlocking -ScriptBlock {
|
try {
|
||||||
|
$result = Invoke-NonBlocking -ScriptBlock {
|
||||||
param($path, $name)
|
param($path, $name)
|
||||||
Import-Module ScheduledTasks -ErrorAction SilentlyContinue
|
try {
|
||||||
$taskObj = Get-ScheduledTask -TaskPath $path -TaskName $name -ErrorAction SilentlyContinue
|
Import-Module ScheduledTasks -ErrorAction Stop
|
||||||
|
$taskObj = Get-ScheduledTask -TaskPath $path -TaskName $name -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
if ($_.Exception -isnot [System.Management.Automation.CommandNotFoundException] -and $_.CategoryInfo.Category -eq [System.Management.Automation.ErrorCategory]::ObjectNotFound) {
|
||||||
|
return @{ Success = $true; Status = 'NotFound' }
|
||||||
|
}
|
||||||
|
return @{ Success = $false; Status = 'Error'; Error = $_.Exception.Message }
|
||||||
|
}
|
||||||
if (-not $taskObj) {
|
if (-not $taskObj) {
|
||||||
return @{ Success = $true; Status = 'NotFound' }
|
return @{ Success = $true; Status = 'NotFound' }
|
||||||
}
|
}
|
||||||
@@ -118,15 +151,22 @@ function Enable-TelemetryScheduledTasks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return @{ Success = $true; Status = 'AlreadyEnabled' }
|
return @{ Success = $true; Status = 'AlreadyEnabled' }
|
||||||
} -ArgumentList @($task.Path, $task.Name)
|
} -ArgumentList @($task.Path, $task.Name)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Failed to enable Scheduled Task: $($task.Path)$($task.Name) - $($_.Exception.Message)"
|
||||||
|
$success = $false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
switch ($result.Status) {
|
switch ($result.Status) {
|
||||||
'Enabled' { Write-Host "Enabled Scheduled Task: $($task.Path)$($task.Name)" }
|
'Enabled' { Write-Host "Enabled Scheduled Task: $($task.Path)$($task.Name)" }
|
||||||
'AlreadyEnabled' { Write-Host "Scheduled Task $($task.Path)$($task.Name) is already enabled." -ForegroundColor DarkGray }
|
'AlreadyEnabled' { Write-Host "Scheduled Task $($task.Path)$($task.Name) is already enabled." -ForegroundColor DarkGray }
|
||||||
'NotFound' { Write-Host "Scheduled Task $($task.Path)$($task.Name) not found." -ForegroundColor DarkGray }
|
'NotFound' { Write-Host "Scheduled Task $($task.Path)$($task.Name) not found." -ForegroundColor DarkGray }
|
||||||
'Error' { Write-Host "Failed to enable Scheduled Task: $($task.Path)$($task.Name) - $($result.Error)" -ForegroundColor Yellow }
|
'Error' { Write-Host "Failed to enable Scheduled Task: $($task.Path)$($task.Name) - $($result.Error)" -ForegroundColor Yellow; $success = $false }
|
||||||
|
default { Write-Warning "Unable to determine the result of enabling Scheduled Task: $($task.Path)$($task.Name)."; $success = $false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host ""
|
return $success
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
# Enables a Windows optional feature and pipes its output to the console
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Enables a Windows optional feature and pipes its output to the console.
|
||||||
|
|
||||||
|
.OUTPUTS
|
||||||
|
System.Boolean. $true when enabling succeeds or is previewed; otherwise $false.
|
||||||
|
#>
|
||||||
function Enable-WindowsFeature {
|
function Enable-WindowsFeature {
|
||||||
param (
|
param (
|
||||||
[string]$FeatureName
|
[string]$FeatureName
|
||||||
@@ -6,22 +12,51 @@ function Enable-WindowsFeature {
|
|||||||
|
|
||||||
if ($script:Params.ContainsKey("WhatIf")) {
|
if ($script:Params.ContainsKey("WhatIf")) {
|
||||||
Write-Host "[WhatIf] Enable Windows feature: $FeatureName" -ForegroundColor Cyan
|
Write-Host "[WhatIf] Enable Windows feature: $FeatureName" -ForegroundColor Cyan
|
||||||
Write-Host ""
|
return $true
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = Invoke-NonBlocking -ScriptBlock {
|
try {
|
||||||
param($name)
|
$result = Invoke-NonBlocking -ScriptBlock {
|
||||||
Enable-WindowsOptionalFeature -Online -FeatureName $name -All -NoRestart
|
param($name)
|
||||||
} -ArgumentList $FeatureName
|
try {
|
||||||
|
$output = Enable-WindowsOptionalFeature -Online -FeatureName $name -All -NoRestart -ErrorAction Stop
|
||||||
$dismResult = @($result) | Where-Object { $_ -is [Microsoft.Dism.Commands.ImageObject] }
|
return [PSCustomObject]@{
|
||||||
if ($dismResult) {
|
Success = $true
|
||||||
Write-Host ($dismResult | Out-String).Trim()
|
Output = if ($output) { ($output | Out-String).Trim() } else { $null }
|
||||||
|
Error = $null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
Success = $false
|
||||||
|
Output = $null
|
||||||
|
Error = $_.Exception.Message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} -ArgumentList $FeatureName
|
||||||
}
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Failed to enable Windows feature '$FeatureName': $($_.Exception.Message)"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $result -or -not $result.Success) {
|
||||||
|
$details = if ($result -and $result.Error) { ": $($result.Error)" } else { '' }
|
||||||
|
Write-Warning "Failed to enable Windows feature '$FeatureName'$details"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($result.Output) { Write-Host $result.Output }
|
||||||
|
return $true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Disables a Windows optional feature and pipes its output to the console
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Disables a Windows optional feature and pipes its output to the console.
|
||||||
|
|
||||||
|
.OUTPUTS
|
||||||
|
System.Boolean. $true when disabling succeeds or is previewed; otherwise $false.
|
||||||
|
#>
|
||||||
function Disable-WindowsFeature {
|
function Disable-WindowsFeature {
|
||||||
param (
|
param (
|
||||||
[string]$FeatureName
|
[string]$FeatureName
|
||||||
@@ -29,19 +64,42 @@ function Disable-WindowsFeature {
|
|||||||
|
|
||||||
if ($script:Params.ContainsKey("WhatIf")) {
|
if ($script:Params.ContainsKey("WhatIf")) {
|
||||||
Write-Host "[WhatIf] Disable Windows feature: $FeatureName" -ForegroundColor Cyan
|
Write-Host "[WhatIf] Disable Windows feature: $FeatureName" -ForegroundColor Cyan
|
||||||
Write-Host ""
|
return $true
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = Invoke-NonBlocking -ScriptBlock {
|
try {
|
||||||
param($name)
|
$result = Invoke-NonBlocking -ScriptBlock {
|
||||||
Disable-WindowsOptionalFeature -Online -FeatureName $name -NoRestart
|
param($name)
|
||||||
} -ArgumentList $FeatureName
|
try {
|
||||||
|
$output = Disable-WindowsOptionalFeature -Online -FeatureName $name -NoRestart -ErrorAction Stop
|
||||||
$dismResult = @($result) | Where-Object { $_ -is [Microsoft.Dism.Commands.ImageObject] }
|
return [PSCustomObject]@{
|
||||||
if ($dismResult) {
|
Success = $true
|
||||||
Write-Host ($dismResult | Out-String).Trim()
|
Output = if ($output) { ($output | Out-String).Trim() } else { $null }
|
||||||
|
Error = $null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
Success = $false
|
||||||
|
Output = $null
|
||||||
|
Error = $_.Exception.Message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} -ArgumentList $FeatureName
|
||||||
}
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Failed to disable Windows feature '$FeatureName': $($_.Exception.Message)"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $result -or -not $result.Success) {
|
||||||
|
$details = if ($result -and $result.Error) { ": $($result.Error)" } else { '' }
|
||||||
|
Write-Warning "Failed to disable Windows feature '$FeatureName'$details"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($result.Output) { Write-Host $result.Output }
|
||||||
|
return $true
|
||||||
}
|
}
|
||||||
|
|
||||||
function Test-WindowsOptionalFeatureEnabled {
|
function Test-WindowsOptionalFeatureEnabled {
|
||||||
@@ -58,4 +116,4 @@ function Test-WindowsOptionalFeatureEnabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ($feature.State -eq 'Enabled')
|
return ($feature.State -eq 'Enabled')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,9 +233,11 @@ function Get-AppRemovalScopeTarget {
|
|||||||
switch ($selectedItem.Name) {
|
switch ($selectedItem.Name) {
|
||||||
"AppRemovalScopeAllUsers" { return 'AllUsers' }
|
"AppRemovalScopeAllUsers" { return 'AllUsers' }
|
||||||
"AppRemovalScopeCurrentUser" { return 'CurrentUser' }
|
"AppRemovalScopeCurrentUser" { return 'CurrentUser' }
|
||||||
|
default {
|
||||||
|
Write-Warning "Unrecognized app-removal scope item '$($selectedItem.Name)'. Skipping app removal."
|
||||||
|
return $null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Invoke-AppPreset {
|
function Invoke-AppPreset {
|
||||||
|
|||||||
@@ -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,13 +670,15 @@ function Show-MainWindow {
|
|||||||
if ($selectedApps.Count -gt 0) {
|
if ($selectedApps.Count -gt 0) {
|
||||||
if (-not (Confirm-UnsafeAppRemoval -SelectedApps $selectedApps -Owner $window)) { return }
|
if (-not (Confirm-UnsafeAppRemoval -SelectedApps $selectedApps -Owner $window)) { return }
|
||||||
|
|
||||||
|
$scopeTarget = Get-AppRemovalScopeTarget -AppRemovalScopeCombo $appRemovalScopeCombo -OtherUsernameTextBox $otherUsernameTextBox
|
||||||
|
if ([string]::IsNullOrWhiteSpace($scopeTarget)) {
|
||||||
|
Write-Warning 'App removal was cancelled because the selected removal scope is invalid.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Add-Parameter 'RemoveApps'
|
Add-Parameter 'RemoveApps'
|
||||||
Add-Parameter 'Apps' ($selectedApps -join ',')
|
Add-Parameter 'Apps' ($selectedApps -join ',')
|
||||||
|
Add-Parameter 'AppRemovalTarget' $scopeTarget
|
||||||
$scopeTarget = Get-AppRemovalScopeTarget -AppRemovalScopeCombo $appRemovalScopeCombo -OtherUsernameTextBox $otherUsernameTextBox
|
|
||||||
if ($scopeTarget) {
|
|
||||||
Add-Parameter 'AppRemovalTarget' $scopeTarget
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Apply dynamic tweaks
|
# Apply dynamic tweaks
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -427,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