mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-10-23 05:21:08 +00:00
Fix bugs found in a code audit (#5188)
* fix(packages): install every ID in semicolon-separated choco entries applications.json githubdesktop has choco "git;github-desktop", but Get-WinUtilSelectedPackages passed the whole string to choco as one package name, so install and uninstall always failed under the Choco preference. Detection in Invoke-WinUtilCurrentSystem already splits on ";". Split in Add-PackageId so install and detection agree. * fix(tweaks): undo services only when still at applied type KeepServiceStartup skipped a service on apply when its startup type was changed by the user, but undo forced the static OriginalType back regardless, overwriting the user setting that apply deliberately left alone. Undo now uses the same guard in reverse: only restore OriginalType when the service still sits at the applied StartupType. * fix(iso): treat a failed source-ISO dismount as an error Two Dismount-DiskImage calls in the modify path lacked -ErrorAction Stop, unlike the existing call sites at lines 185 and 291. A non-terminating dismount failure (device busy) was swallowed, $modified was set anyway, and the finally-block dismount cleanup was skipped, leaving the source ISO mounted with no feedback. * fix(features): continue past a failed optional feature Enable-WindowsOptionalFeature ran with -ErrorAction Stop and no per-feature catch, so one feature missing on the current edition (e.g. Windows Sandbox on Home) aborted the whole selection. Each feature now fails independently and logs an error. * fix(tweaks): drop nonexistent wermgr service, use REG_SZ types Set-Service wermgr targets a service that does not exist. Invoke-WinUtilScript runs the script block with -ErrorAction Stop, so the failing Set-Service aborted the rest: apply never set POWERSHELL_TELEMETRY_OPTOUT or removed PeriodInNanoSeconds, and undo never restored them. Also fix MouseSpeed/MouseThreshold1/MouseThreshold2 and KeyboardDelay to Type String: Windows stores these Control Panel values as REG_SZ, and writing them as DWord changed the value type, which the settings UI can ignore. * fix(tweaks): guard diagtrack, drop taskbar keys from Visual Effects WPFTweaksTelemetry disabled diagtrack through InvokeScript/UndoScript, bypassing the KeepServiceStartup guard that service entries get, and undo forced Automatic regardless of what the user had set. diagtrack is now a service block entry, so apply and undo go through Set-WinUtilService with the guard. WPFTweaksDisplay (Visual Effects - Set to Best Performance) also set ShowTaskViewButton and SearchboxTaskbarMode to 0 with OriginalValue 1, while WPFToggleTaskView/WPFToggleTaskbarSearch own the same values with the opposite originals: one undo path always wrote a guessed value over the user setup, and taskbar visibility is not a visual effect. Removed both entries from WPFTweaksDisplay; the toggles remain the sole owners. Adds a configs test asserting no two tweaks claim the same registry value with conflicting OriginalValue or Type. * fix(tweaks): guard Set-Service calls in config scripts WPFTweaksWindowsAI and WPFTweaksRemoveOneDrive called Set-Service unguarded inside scripts that Invoke-WinUtilScript runs with -ErrorAction Stop. WSAIFabricSvc only exists on some Windows 11 builds, so on machines without it the failing Set-Service aborted the block and Disable-WindowsOptionalFeature for Recall never ran. Each Set-Service is now wrapped in a Get-Service existence check, and a configs test keeps future config scripts guarded. * fix(appx): report an install with no source as a failed result Install-WinUtilAPPX threw when a package had no local manifest and no StoreId, and Invoke-WPFAppxInstall had no per-package catch: selecting any of the three appx.json entries without a StoreId aborted the whole job, skipped every remaining selected package, and never reached Complete-WinUtilPackageRun. The no-source case now returns a Failed result like the choco and winget paths, so the batch completes, the summary names the package, and Complete-WinUtilPackageRun still fails the job. Unexpected errors continue to surface to the job layer. * Document partial failures and service undo limits accurately * Keep dependencies installed when uninstalling their app * Skip dependent scripts when a Windows feature fails * Check OneSyncSvc template existence through the registry --------- Co-authored-by: foxbyte <198093090+adiraj66132@users.noreply.github.com> Co-authored-by: Chris Titus <contact@christitus.com>
This commit is contained in:
17 files changed
+451
-53
No files matched your search
+23
-29
@@ -449,6 +449,11 @@
|
||||
"category": "Essential Tweaks",
|
||||
"panel": "1",
|
||||
"service": [
|
||||
{
|
||||
"Name": "diagtrack",
|
||||
"StartupType": "Disabled",
|
||||
"OriginalType": "Automatic"
|
||||
},
|
||||
{
|
||||
"Name": "WerSvc",
|
||||
"StartupType": "Disabled",
|
||||
@@ -553,9 +558,6 @@
|
||||
# Disable Defender Auto Sample Submission
|
||||
Set-MpPreference -SubmitSamplesConsent 2
|
||||
|
||||
# Disable (Connected User Experiences and Telemetry) Service
|
||||
Set-Service -Name diagtrack -StartupType Disabled
|
||||
|
||||
# Disable PowerShell 7 telemetry
|
||||
[Environment]::SetEnvironmentVariable('POWERSHELL_TELEMETRY_OPTOUT', '1', 'Machine')
|
||||
"
|
||||
@@ -565,9 +567,6 @@
|
||||
# Enable Defender Auto Sample Submission
|
||||
Set-MpPreference -SubmitSamplesConsent 1
|
||||
|
||||
# Enable (Connected User Experiences and Telemetry) Service
|
||||
Set-Service -Name diagtrack -StartupType Automatic
|
||||
|
||||
# Enable PowerShell 7 telemetry
|
||||
[Environment]::SetEnvironmentVariable('POWERSHELL_TELEMETRY_OPTOUT', '', 'Machine')
|
||||
"
|
||||
@@ -696,8 +695,11 @@
|
||||
[Environment]::SetEnvironmentVariable('OneDrive', $null, 'User')
|
||||
}
|
||||
|
||||
# Disable OneSyncSvc
|
||||
Set-Service -Name OneSyncSvc -StartupType Disabled
|
||||
# OneSyncSvc is a per-user template; Get-Service only exposes user instances.
|
||||
# Skip the operation if this Windows installation has no template.
|
||||
if (Test-Path -LiteralPath 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\OneSyncSvc') {
|
||||
Set-Service -Name OneSyncSvc -StartupType Disabled
|
||||
}
|
||||
"
|
||||
],
|
||||
"UndoScript": [
|
||||
@@ -705,8 +707,10 @@
|
||||
Write-Host \"Installing OneDrive\"
|
||||
winget install Microsoft.Onedrive --source winget
|
||||
|
||||
# Enabled OneSyncSvc
|
||||
Set-Service -Name OneSyncSvc -StartupType Automatic
|
||||
# Enable OneSyncSvc when the per-user service template exists
|
||||
if (Test-Path -LiteralPath 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\OneSyncSvc') {
|
||||
Set-Service -Name OneSyncSvc -StartupType Automatic
|
||||
}
|
||||
"
|
||||
],
|
||||
"link": "https://winutil.christitus.com/code-reference/tweaks/z--advanced-tweaks---caution/removeonedrive"
|
||||
@@ -772,7 +776,7 @@
|
||||
"Path": "HKCU:\\Control Panel\\Keyboard",
|
||||
"Name": "KeyboardDelay",
|
||||
"Value": "0",
|
||||
"Type": "DWord",
|
||||
"Type": "String",
|
||||
"OriginalValue": "1"
|
||||
},
|
||||
{
|
||||
@@ -816,20 +820,6 @@
|
||||
"Value": "0",
|
||||
"Type": "DWord",
|
||||
"OriginalValue": "1"
|
||||
},
|
||||
{
|
||||
"Path": "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced",
|
||||
"Name": "ShowTaskViewButton",
|
||||
"Value": "0",
|
||||
"Type": "DWord",
|
||||
"OriginalValue": "1"
|
||||
},
|
||||
{
|
||||
"Path": "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Search",
|
||||
"Name": "SearchboxTaskbarMode",
|
||||
"Value": "0",
|
||||
"Type": "DWord",
|
||||
"OriginalValue": "1"
|
||||
}
|
||||
],
|
||||
"InvokeScript": [
|
||||
@@ -948,7 +938,11 @@
|
||||
Remove-AppxPackage $Appx
|
||||
}
|
||||
|
||||
Set-Service -Name WSAIFabricSvc -StartupType Disabled
|
||||
# WSAIFabricSvc only exists on some Windows 11 builds; without the guard a
|
||||
# missing service aborts the rest of this script block and Recall stays enabled
|
||||
if (Get-Service -Name WSAIFabricSvc -ErrorAction SilentlyContinue) {
|
||||
Set-Service -Name WSAIFabricSvc -StartupType Disabled
|
||||
}
|
||||
# Store/MSIX PowerShell 7 can hang or fail COM activation in DISM.
|
||||
$RecallCommand = {
|
||||
Disable-WindowsOptionalFeature -FeatureName Recall -Online -NoRestart -ErrorAction Stop | Out-Null
|
||||
@@ -1565,7 +1559,7 @@
|
||||
"Path": "HKCU:\\Control Panel\\Mouse",
|
||||
"Name": "MouseSpeed",
|
||||
"Value": "1",
|
||||
"Type": "DWord",
|
||||
"Type": "String",
|
||||
"OriginalValue": "0",
|
||||
"DefaultState": "true"
|
||||
},
|
||||
@@ -1573,7 +1567,7 @@
|
||||
"Path": "HKCU:\\Control Panel\\Mouse",
|
||||
"Name": "MouseThreshold1",
|
||||
"Value": "6",
|
||||
"Type": "DWord",
|
||||
"Type": "String",
|
||||
"OriginalValue": "0",
|
||||
"DefaultState": "true"
|
||||
},
|
||||
@@ -1581,7 +1575,7 @@
|
||||
"Path": "HKCU:\\Control Panel\\Mouse",
|
||||
"Name": "MouseThreshold2",
|
||||
"Value": "10",
|
||||
"Type": "DWord",
|
||||
"Type": "String",
|
||||
"OriginalValue": "0",
|
||||
"DefaultState": "true"
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@ If you have trouble finding an application, press `Ctrl + F` and search for its
|
||||
Before uninstalling or upgrading apps, close any running programs first. Some packages may still prompt for input or fail if their source is unavailable.
|
||||
:::
|
||||
|
||||
When an app lists multiple package IDs, installation includes its listed dependencies. Uninstall removes only the selected app; a dependency is removed only when selected separately.
|
||||
|
||||
For an existing stable PowerShell 7 MSI installation, selecting PowerShell or using **Upgrade All** first checks the official PowerShell release and uses a signed MSI matching the installed executable architecture. MSI failures are reported separately; Upgrade All continues with the other WinGet packages. Installations without an MSI registration retain the normal WinGet behavior.
|
||||
|
||||
The direct MSI workaround conservatively skips PowerShell if WinGet reports any configured pins, or pin state cannot be checked. This avoids bypassing blocking or version-gating policies; Upgrade All still runs its normal WinGet operation, which honors those policies.
|
||||
|
||||
@@ -60,3 +60,5 @@ Only enable this if you intend to use remote shell access. After turning it on,
|
||||
Because WinUtil runs elevated, the account it sets up is an administrator, and sshd reads administrator keys from `C:\ProgramData\ssh\administrators_authorized_keys` rather than from your profile. WinUtil creates that file and restricts it to Administrators and SYSTEM, which is what sshd requires. Add your public keys there. If an earlier WinUtil version changed `sshd_config` to read administrator keys from `%USERPROFILE%\.ssh\authorized_keys`, that is undone and any keys in it are copied across, so key auth keeps working.
|
||||
|
||||
Non-administrator accounts keep using `%USERPROFILE%\.ssh\authorized_keys` and need no extra setup.
|
||||
|
||||
If an optional Windows feature is unavailable or fails to enable, WinUtil logs the failure and continues with the remaining optional features. Dependent setup scripts for that selection are skipped when an optional feature fails. The final job status still reports errors.
|
||||
@@ -40,12 +40,16 @@ Some tweaks take effect immediately, while others may require Explorer to restar
|
||||
* **Select Tweaks to Remove**: Choose the tweaks you want to disable or remove.
|
||||
* **Undo Tweaks**: Click **Undo Selected Tweaks** at the bottom of the screen to apply the changes.
|
||||
|
||||
Undo restores a service default only when its current startup type matches the type applied by the tweak. Other current startup types are preserved. WinUtil does not track whether a matching startup type was set before the tweak, so undo can also reset a service that already had that type.
|
||||
|
||||
### AppX Packages
|
||||
|
||||
Open **AppX Removal** from the Tweaks tab to manage the listed Windows apps. Select one or more packages, then choose **Install Selected** or **Remove Selected**.
|
||||
|
||||
When installing a selected package, WinUtil first registers an existing local `AppxManifest.xml`. If no usable local manifest remains, WinUtil installs the package from the Microsoft Store through WinGet when a Store product ID is available.
|
||||
|
||||
A package without a usable local manifest or Store product ID is reported as failed. The remaining selected packages are still attempted, and the final summary lists failures.
|
||||
|
||||
During AppX installation or removal, the window-level progress bar shows the current package, completed package count, and overall progress. The Windows taskbar also reflects progress and the final success or failure state.
|
||||
|
||||
### Essential Tweaks
|
||||
|
||||
@@ -5,7 +5,10 @@ function Get-WinUtilSelectedPackages {
|
||||
[object] $PackageList,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string] $Preference
|
||||
[string] $Preference,
|
||||
|
||||
[ValidateSet("Install", "Uninstall")]
|
||||
[string] $Action = "Install"
|
||||
)
|
||||
|
||||
# A single package has no meaningful percentage to show
|
||||
@@ -26,12 +29,22 @@ function Get-WinUtilSelectedPackages {
|
||||
$PackageId
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace([string]$PackageId) -or $PackageId -eq "na") {
|
||||
return
|
||||
# A config field may hold several IDs separated by ";" (e.g. a dependency;the package).
|
||||
# Detection splits the same way, so install and detection agree on what gets installed.
|
||||
$ids = [string]$PackageId -split ";"
|
||||
if ($Action -eq "Uninstall") {
|
||||
# Detection identifies the last ID as the app; preceding IDs are dependencies.
|
||||
$ids = @($ids[-1])
|
||||
}
|
||||
foreach ($id in $ids) {
|
||||
$id = $id.Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($id) -or $id -eq "na") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not $Target.Contains($PackageId)) {
|
||||
$null = $Target.Add($PackageId)
|
||||
if (-not $Target.Contains($id)) {
|
||||
$null = $Target.Add($id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,11 +75,19 @@ function Install-WinUtilAPPX {
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($StoreId)) {
|
||||
# A package with no local manifest and no Store ID cannot be installed. Report it as a
|
||||
# result rather than throwing so the remaining selected packages still run and the job
|
||||
# summary names this one; Complete-WinUtilPackageRun fails the job on the Failed outcome.
|
||||
$errorMessage = "Unable to install $Name because no local manifest or Microsoft Store ID is available."
|
||||
Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message $errorMessage
|
||||
$exception = [System.InvalidOperationException]::new($errorMessage)
|
||||
$exception.Data["WinUtilErrorReported"] = $true
|
||||
throw $exception
|
||||
return [pscustomobject]@{
|
||||
Package = $Name
|
||||
Manager = "appx"
|
||||
Action = "Install"
|
||||
ExitCode = 1
|
||||
Outcome = "Failed"
|
||||
Detail = $errorMessage
|
||||
}
|
||||
}
|
||||
|
||||
Write-WinUtilLog -Component "AppX" -Message "No usable local manifest found for $Name. Installing Microsoft Store product $StoreId."
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
function Invoke-WinUtilFeatureInstall ($CheckBox) {
|
||||
Write-WinUtilLog -Component "Feature" -Message "Applying feature action: $CheckBox"
|
||||
|
||||
$featureFailed = $false
|
||||
if ($sync.configs.feature.$CheckBox.feature) {
|
||||
foreach ($feature in $sync.configs.feature.$CheckBox.feature) {
|
||||
Write-Host "Installing $feature"
|
||||
Write-WinUtilLog -Component "Feature" -Message "Enabling Windows optional feature: $feature"
|
||||
Enable-WindowsOptionalFeature -Online -FeatureName $feature -All -NoRestart -ErrorAction Stop
|
||||
Write-WinUtilLog -Component "Feature" -Message "Enabled Windows optional feature: $feature"
|
||||
try {
|
||||
Enable-WindowsOptionalFeature -Online -FeatureName $feature -All -NoRestart -ErrorAction Stop
|
||||
Write-WinUtilLog -Component "Feature" -Message "Enabled Windows optional feature: $feature"
|
||||
} catch {
|
||||
$featureFailed = $true
|
||||
# A feature missing on this edition (e.g. Windows Sandbox on Home) must not
|
||||
# abort the remaining selected features
|
||||
Write-Warning "Unable to enable feature $feature`: $($_.Exception.Message)"
|
||||
Write-WinUtilLog -Level "ERROR" -Component "Feature" -Message "Unable to enable feature $feature`: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($sync.configs.feature.$CheckBox.InvokeScript) {
|
||||
if ($featureFailed) {
|
||||
Write-WinUtilLog -Level "WARN" -Component "Feature" -Message "Skipping dependent scripts after an optional feature failed: $CheckBox"
|
||||
} elseif ($sync.configs.feature.$CheckBox.InvokeScript) {
|
||||
foreach ($script in $sync.configs.feature.$CheckBox.InvokeScript) {
|
||||
Write-Host "Running Script for $CheckBox"
|
||||
Write-WinUtilLog -Component "Feature" -Message "Running feature script for: $CheckBox"
|
||||
|
||||
@@ -455,7 +455,7 @@ function Invoke-WinUtilISOModify {
|
||||
|
||||
Step-WinUtilJob -Status "Dismounting source ISO..." -Percent 80
|
||||
Write-WinUtilISOLog "Dismounting original ISO..."
|
||||
Dismount-DiskImage -ImagePath $isoPath
|
||||
Dismount-DiskImage -ImagePath $isoPath -ErrorAction Stop
|
||||
|
||||
$sync["Win11ISOWorkDir"] = $workDir
|
||||
$sync["Win11ISOContentsDir"] = $isoContents
|
||||
@@ -480,7 +480,7 @@ function Invoke-WinUtilISOModify {
|
||||
$mountedISO = Get-DiskImage -ImagePath $isoPath
|
||||
if ($mountedISO -and $mountedISO.Attached) {
|
||||
Write-WinUtilISOLog "Cleaning up: dismounting source ISO..."
|
||||
Dismount-DiskImage -ImagePath $isoPath
|
||||
Dismount-DiskImage -ImagePath $isoPath -ErrorAction Stop
|
||||
}
|
||||
} catch { Write-WinUtilISOLog -Level "WARN" -Message "Could not dismount ISO during cleanup: $_" }
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ function Invoke-WinUtilTweaks {
|
||||
|
||||
$action = if ($undo) { "Undo" } else { "Apply" }
|
||||
Write-WinUtilLog -Component "Tweaks" -Message "$action tweak: $CheckBox"
|
||||
# The next status read must hit the registry, not a value cached before this change
|
||||
if ($sync.ToggleStatusCache) {
|
||||
$sync.ToggleStatusCache.Remove($CheckBox)
|
||||
}
|
||||
# The counter lives in this runspace, so an error a concurrent job logs from its own
|
||||
# runspace cannot be charged to a toggle flipped on the UI thread
|
||||
$errorsBefore = [int]$global:WinUtilJobErrorCount
|
||||
@@ -46,13 +50,18 @@ function Invoke-WinUtilTweaks {
|
||||
$sync.configs.tweaks.$CheckBox.service | ForEach-Object {
|
||||
$changeservice = $true
|
||||
|
||||
# The check for !($undo) is required, without it the script will throw an error for accessing unavailable member, which's the 'OriginalService' Property
|
||||
if ($KeepServiceStartup -AND !($undo)) {
|
||||
# KeepServiceStartup protects a service the user (or another tool) changed away from
|
||||
# the value this tweak expects. Undo uses the same rule in reverse: only restore
|
||||
# OriginalType when the service still sits at the StartupType this tweak applies.
|
||||
# This compares current state, not history: a service already at StartupType
|
||||
# before apply is indistinguishable from one changed by this tweak.
|
||||
$expectedType = if ($undo) { $psitem.StartupType } else { $psitem.$($values.OriginalService) }
|
||||
if ($KeepServiceStartup) {
|
||||
$serviceName = $psitem.Name
|
||||
try {
|
||||
# Check if the service exists
|
||||
$service = Get-Service -Name $serviceName -ErrorAction Stop
|
||||
if(!($service.StartType.ToString() -eq $psitem.$($values.OriginalService))) {
|
||||
if(!($service.StartType.ToString() -eq $expectedType)) {
|
||||
$changeservice = $false
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -35,7 +35,7 @@ function Invoke-WPFUnInstall {
|
||||
} -ScriptBlock {
|
||||
param($PackagesToUninstall, $ManagerPreference)
|
||||
|
||||
$packagesSorted = Get-WinUtilSelectedPackages -PackageList $PackagesToUninstall -Preference $ManagerPreference
|
||||
$packagesSorted = Get-WinUtilSelectedPackages -PackageList $PackagesToUninstall -Preference $ManagerPreference -Action Uninstall
|
||||
$packagesWinget = $packagesSorted['Winget']
|
||||
$packagesChoco = $packagesSorted['Choco']
|
||||
$totalPackages = @($packagesWinget).Count + @($packagesChoco).Count
|
||||
|
||||
+47
-3
@@ -204,12 +204,15 @@ Describe "Install-WinUtilAPPX" {
|
||||
Should -Invoke -CommandName Install-WinUtilProgramWinget -Times 1 -Exactly
|
||||
}
|
||||
|
||||
It "throws after logging an error when neither install method is available" {
|
||||
It "returns a failed result when neither install method is available" {
|
||||
Mock powershell.exe { $global:LASTEXITCODE = 0 }
|
||||
|
||||
{ Install-WinUtilAPPX -Name "Example.Package" } |
|
||||
Should -Throw "Unable to install Example.Package because no local manifest or Microsoft Store ID is available."
|
||||
$result = @(Install-WinUtilAPPX -Name "Example.Package")
|
||||
|
||||
$result.Count | Should -Be 1
|
||||
$result[0].Outcome | Should -Be "Failed"
|
||||
$result[0].Manager | Should -Be "appx"
|
||||
$result[0].Detail | Should -Be "Unable to install Example.Package because no local manifest or Microsoft Store ID is available."
|
||||
Should -Invoke -CommandName Install-WinUtilProgramWinget -Times 0 -Exactly
|
||||
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
|
||||
$Level -eq "ERROR" -and
|
||||
@@ -464,6 +467,47 @@ Describe "Invoke-WPFAppxInstall" {
|
||||
{ & $script:capturedAppxInstallScriptBlock @jobParameters } | Should -Throw "Install failed"
|
||||
}
|
||||
|
||||
It "keeps installing the remaining packages when one returns a failed result" {
|
||||
$script:sync.selectedAppx.Add("WPFAppxExample")
|
||||
$script:sync.configs.appxHashtable.WPFAppxNoSource = [pscustomobject]@{
|
||||
Content = "No Source App"
|
||||
PackageId = "No.Source.Package"
|
||||
}
|
||||
$script:sync.selectedAppx.Add("WPFAppxNoSource")
|
||||
Mock Install-WinUtilAPPX {
|
||||
if ($Name -eq "No.Source.Package") {
|
||||
[pscustomobject]@{
|
||||
Package = $Name
|
||||
Manager = "appx"
|
||||
Action = "Install"
|
||||
ExitCode = 1
|
||||
Outcome = "Failed"
|
||||
Detail = "no install source"
|
||||
}
|
||||
} else {
|
||||
[pscustomobject]@{
|
||||
Package = $Name
|
||||
Manager = "appx"
|
||||
Action = "Install"
|
||||
ExitCode = 0
|
||||
Outcome = "Succeeded"
|
||||
Detail = "exit code 0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Invoke-WPFAppxInstall
|
||||
$jobParameters = $script:capturedAppxInstallParameters
|
||||
& $script:capturedAppxInstallScriptBlock @jobParameters
|
||||
|
||||
Should -Invoke -CommandName Install-WinUtilAPPX -Times 2 -Exactly
|
||||
Should -Invoke Complete-WinUtilPackageRun -Times 1 -Exactly -ParameterFilter {
|
||||
$Action -eq "Install" -and $Results.Count -eq 2 -and
|
||||
@($Results | Where-Object Outcome -EQ "Failed").Count -eq 1 -and
|
||||
@($Results | Where-Object Outcome -EQ "Succeeded").Count -eq 1
|
||||
}
|
||||
}
|
||||
|
||||
It "passes a skipped Store result to package completion and reports it as skipped" {
|
||||
$script:sync.selectedAppx.Add("WPFAppxExample")
|
||||
Mock Install-WinUtilAPPX {
|
||||
|
||||
@@ -218,6 +218,41 @@ Describe "Tweaks config" {
|
||||
}
|
||||
}
|
||||
|
||||
It "does not claim the same registry value with conflicting originals or types" -TestCases $testCase {
|
||||
param([string]$Path)
|
||||
|
||||
$tweaks = Get-Content -Path $Path -Raw | ConvertFrom-Json
|
||||
$claims = @{}
|
||||
|
||||
foreach ($tweak in $tweaks.PSObject.Properties) {
|
||||
foreach ($registryEntry in @($tweak.Value.registry)) {
|
||||
if ($null -eq $registryEntry -or $registryEntry.Values) { continue }
|
||||
|
||||
$key = "$($registryEntry.Path)|$($registryEntry.Name)"
|
||||
if (-not $claims.ContainsKey($key)) {
|
||||
$claims[$key] = @{}
|
||||
}
|
||||
$claims[$key][$tweak.Name] = [pscustomobject]@{
|
||||
OriginalValue = [string]$registryEntry.OriginalValue
|
||||
Type = [string]$registryEntry.Type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$conflicts = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($key in $claims.Keys) {
|
||||
$variants = @($claims[$key].Values | Select-Object -Unique -Property OriginalValue, Type)
|
||||
if ($variants.Count -gt 1) {
|
||||
$owners = ($claims[$key].Keys | Sort-Object) -join ", "
|
||||
$conflicts.Add("$key claimed with conflicting originals/types by: $owners")
|
||||
}
|
||||
}
|
||||
|
||||
if ($conflicts.Count -gt 0) {
|
||||
throw ($conflicts -join "`n")
|
||||
}
|
||||
}
|
||||
|
||||
It "keeps the location service disabled" -TestCases $testCase {
|
||||
param([string]$Path)
|
||||
|
||||
@@ -239,6 +274,14 @@ Describe "Tweaks config" {
|
||||
$errorReporting[0].StartupType | Should -Be "Disabled"
|
||||
$errorReporting[0].OriginalType | Should -Be "Manual"
|
||||
|
||||
# Telemetry and error reporting must coexist after merging their independent fixes.
|
||||
$diagnostics = @($telemetry.service | Where-Object Name -eq "diagtrack")
|
||||
$diagnostics | Should -HaveCount 1
|
||||
$diagnostics[0].StartupType | Should -Be "Disabled"
|
||||
$diagnostics[0].OriginalType | Should -Be "Automatic"
|
||||
((@($telemetry.InvokeScript) + @($telemetry.UndoScript)) -join "`n") |
|
||||
Should -Not -Match 'Set-Service\s+-Name\s+diagtrack'
|
||||
|
||||
# wermgr is an executable, not a service, so Set-Service on it failed on every run (#5145)
|
||||
$scripts = (@($telemetry.InvokeScript) + @($telemetry.UndoScript)) -join "`n"
|
||||
$scripts | Should -Not -Match "wermgr"
|
||||
@@ -292,6 +335,32 @@ Describe "Tweaks config" {
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Config scripts changing services" {
|
||||
It "guards every Set-Service call in a config script with a service or template existence check" {
|
||||
$unguarded = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
foreach ($configFile in (Get-ChildItem -Path $script:configRoot -Filter *.json)) {
|
||||
$config = Get-Content -Path $configFile.FullName -Raw | ConvertFrom-Json
|
||||
foreach ($entry in $config.PSObject.Properties) {
|
||||
foreach ($field in @("InvokeScript", "UndoScript")) {
|
||||
if (-not (Test-WinUtilHasProperty -Object $entry.Value -Name $field)) { continue }
|
||||
|
||||
foreach ($scriptText in @($entry.Value.$field)) {
|
||||
if ([string]$scriptText -notmatch 'Set-Service') { continue }
|
||||
if ([string]$scriptText -notmatch 'Get-Service|Test-Path -LiteralPath.*CurrentControlSet') {
|
||||
$unguarded.Add("$($configFile.Name):$($entry.Name).$field")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($unguarded.Count -gt 0) {
|
||||
throw ("Unguarded Set-Service calls abort the rest of their script block when the service is missing:`n" + ($unguarded -join "`n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Preset config" {
|
||||
It "references existing config entries or supported actions" {
|
||||
$preset = Get-WinUtilConfigObject -Name "preset"
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
#===========================================================================
|
||||
# Tests - Feature Installation
|
||||
#===========================================================================
|
||||
|
||||
BeforeAll {
|
||||
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||
. (Join-Path $script:repoRoot "functions\private\Invoke-WinUtilFeatureInstall.ps1")
|
||||
|
||||
function Enable-WindowsOptionalFeature {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$Online,
|
||||
[string]$FeatureName,
|
||||
[switch]$All,
|
||||
[switch]$NoRestart
|
||||
)
|
||||
}
|
||||
function Write-WinUtilLog {
|
||||
param($Message, $Level, $Component)
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Invoke-WinUtilFeatureInstall" {
|
||||
BeforeEach {
|
||||
$script:sync = [Hashtable]::Synchronized(@{
|
||||
configs = @{
|
||||
feature = [pscustomobject]@{
|
||||
WPFFeaturesMulti = [pscustomobject]@{
|
||||
feature = @("FeatureA", "FeatureB")
|
||||
}
|
||||
WPFFeaturesScriptOnly = [pscustomobject]@{
|
||||
InvokeScript = @("Write-Output 'feature script'")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Mock Enable-WindowsOptionalFeature { }
|
||||
Mock Write-Host { }
|
||||
Mock Write-Warning { }
|
||||
Mock Write-WinUtilLog { }
|
||||
}
|
||||
|
||||
AfterEach {
|
||||
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
It "enables every selected feature" {
|
||||
Invoke-WinUtilFeatureInstall -CheckBox "WPFFeaturesMulti"
|
||||
|
||||
Should -Invoke -CommandName Enable-WindowsOptionalFeature -Times 1 -Exactly -ParameterFilter {
|
||||
$FeatureName -eq "FeatureA"
|
||||
}
|
||||
Should -Invoke -CommandName Enable-WindowsOptionalFeature -Times 1 -Exactly -ParameterFilter {
|
||||
$FeatureName -eq "FeatureB"
|
||||
}
|
||||
}
|
||||
|
||||
It "continues with the remaining features when one fails" {
|
||||
Mock Enable-WindowsOptionalFeature { throw "simulated missing feature" } -ParameterFilter {
|
||||
$FeatureName -eq "FeatureA"
|
||||
}
|
||||
|
||||
{ Invoke-WinUtilFeatureInstall -CheckBox "WPFFeaturesMulti" } | Should -Not -Throw
|
||||
|
||||
Should -Invoke -CommandName Enable-WindowsOptionalFeature -Times 1 -Exactly -ParameterFilter {
|
||||
$FeatureName -eq "FeatureA"
|
||||
}
|
||||
Should -Invoke -CommandName Enable-WindowsOptionalFeature -Times 1 -Exactly -ParameterFilter {
|
||||
$FeatureName -eq "FeatureB"
|
||||
}
|
||||
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
|
||||
$Level -eq "ERROR" -and $Message -like "*FeatureA*"
|
||||
}
|
||||
}
|
||||
|
||||
It "skips dependent scripts after an enable failure and allows the next selection" {
|
||||
$script:sync.configs.feature.WPFFeaturesMulti | Add-Member -NotePropertyName InvokeScript -NotePropertyValue @("throw 'dependent command unavailable'")
|
||||
Mock Enable-WindowsOptionalFeature { throw "simulated missing feature" } -ParameterFilter {
|
||||
$FeatureName -eq "FeatureA"
|
||||
}
|
||||
|
||||
$output = @(
|
||||
Invoke-WinUtilFeatureInstall -CheckBox "WPFFeaturesMulti"
|
||||
Invoke-WinUtilFeatureInstall -CheckBox "WPFFeaturesScriptOnly"
|
||||
)
|
||||
|
||||
$output | Should -Be @('feature script')
|
||||
Should -Invoke Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
|
||||
$Level -eq 'ERROR' -and $Message -like '*FeatureA*'
|
||||
}
|
||||
Should -Invoke Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
|
||||
$Level -eq 'WARN' -and $Message -like '*Skipping dependent scripts*'
|
||||
}
|
||||
}
|
||||
|
||||
It "runs the configured feature scripts" {
|
||||
Invoke-WinUtilFeatureInstall -CheckBox "WPFFeaturesScriptOnly"
|
||||
|
||||
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
|
||||
$Message -eq "Feature action completed: WPFFeaturesScriptOnly"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ BeforeAll {
|
||||
param($ArgumentList, $ParameterList, [scriptblock]$ScriptBlock)
|
||||
}
|
||||
function Get-WinUtilSelectedPackages {
|
||||
param($PackageList, [string]$Preference)
|
||||
param($PackageList, [string]$Preference, [string]$Action)
|
||||
}
|
||||
function Install-WinUtilWinget { }
|
||||
function Install-WinUtilChoco { }
|
||||
@@ -358,6 +358,8 @@ Describe "Invoke-WPFUnInstall job body" {
|
||||
$jobParameters = $script:capturedUninstallJob.Parameters
|
||||
& $script:capturedUninstallJob.ScriptBlock @jobParameters
|
||||
|
||||
Should -Invoke Get-WinUtilSelectedPackages -Times 1 -Exactly -ParameterFilter { $Action -eq "Uninstall" }
|
||||
|
||||
Should -Invoke -CommandName Install-WinUtilProgramWinget -Times 1 -Exactly -ParameterFilter {
|
||||
$Action -eq "Uninstall" -and @($Programs)[0] -eq "Git.Git"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
BeforeAll {
|
||||
$tweaks = Get-Content (Join-Path $PSScriptRoot '../config/tweaks.json') -Raw | ConvertFrom-Json
|
||||
$script:oneDrive = $tweaks.WPFTweaksRemoveOneDrive
|
||||
# Isolate only the template guard, never the destructive OneDrive script around it.
|
||||
function Get-OneSyncGuard {
|
||||
param($Action)
|
||||
$source = [scriptblock]::Create($script:oneDrive.$Action -join "`n")
|
||||
$guards = @($source.Ast.FindAll({
|
||||
param($node)
|
||||
$node -is [System.Management.Automation.Language.IfStatementAst] -and
|
||||
$node.Extent.Text -match 'Set-Service -Name OneSyncSvc'
|
||||
}, $true))
|
||||
$guards.Count | Should -Be 1
|
||||
[scriptblock]::Create($guards[0].Extent.Text)
|
||||
}
|
||||
function Set-Service {
|
||||
param($Name, $StartupType)
|
||||
throw "Set-Service must be mocked: $Name $StartupType"
|
||||
}
|
||||
function Get-Service {
|
||||
param($Name)
|
||||
throw "Get-Service cannot enumerate the template: $Name"
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'OneDrive per-user service template' {
|
||||
BeforeEach {
|
||||
Mock Set-Service {}
|
||||
Mock Get-Service { throw 'No running service instance is visible' }
|
||||
}
|
||||
|
||||
It 'configures an existing template without querying a running instance (<Action>)' -TestCases @(
|
||||
@{ Action = 'InvokeScript'; ExpectedType = 'Disabled' }
|
||||
@{ Action = 'UndoScript'; ExpectedType = 'Automatic' }
|
||||
) {
|
||||
param($Action, $ExpectedType)
|
||||
$guard = Get-OneSyncGuard -Action $Action
|
||||
Mock Test-Path { $true }
|
||||
|
||||
{ & $guard } | Should -Not -Throw
|
||||
|
||||
Should -Invoke Test-Path -Times 1 -Exactly -ParameterFilter {
|
||||
$LiteralPath -eq 'HKLM:\SYSTEM\CurrentControlSet\Services\OneSyncSvc'
|
||||
}
|
||||
Should -Invoke Set-Service -Times 1 -Exactly -ParameterFilter {
|
||||
$Name -eq 'OneSyncSvc' -and $StartupType -eq $ExpectedType
|
||||
}
|
||||
Should -Invoke Get-Service -Times 0 -Exactly
|
||||
}
|
||||
|
||||
It 'skips the operation when the template is absent (<Action>)' -TestCases @(
|
||||
@{ Action = 'InvokeScript' }
|
||||
@{ Action = 'UndoScript' }
|
||||
) {
|
||||
param($Action)
|
||||
$guard = Get-OneSyncGuard -Action $Action
|
||||
Mock Test-Path { $false }
|
||||
|
||||
{ & $guard } | Should -Not -Throw
|
||||
|
||||
Should -Invoke Test-Path -Times 1 -Exactly -ParameterFilter {
|
||||
$LiteralPath -eq 'HKLM:\SYSTEM\CurrentControlSet\Services\OneSyncSvc'
|
||||
}
|
||||
Should -Invoke Set-Service -Times 0 -Exactly
|
||||
Should -Invoke Get-Service -Times 0 -Exactly
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,27 @@ Describe "Get-WinUtilSelectedPackages" {
|
||||
(@($result["Winget"]) -join "|") | Should -Be "VideoLAN.VLC|Mozilla.Firefox"
|
||||
}
|
||||
|
||||
It "splits multi-ID values on ';' so every ID is installed" {
|
||||
$packages = @(
|
||||
[pscustomobject]@{ winget = "GitHub.GitHubDesktop"; choco = "git;github-desktop" }
|
||||
)
|
||||
|
||||
$result = Get-WinUtilSelectedPackages -PackageList $packages -Preference "Choco"
|
||||
|
||||
(@($result["Choco"]) -join "|") | Should -Be "git|github-desktop"
|
||||
}
|
||||
|
||||
It "does not uninstall an app dependency unless it was separately selected" {
|
||||
$desktop = [pscustomobject]@{ winget = "GitHub.GitHubDesktop"; choco = "git;github-desktop" }
|
||||
$git = [pscustomobject]@{ winget = "Git.Git"; choco = "git" }
|
||||
|
||||
$desktopOnly = Get-WinUtilSelectedPackages -PackageList @($desktop) -Preference Choco -Action Uninstall
|
||||
@($desktopOnly.Choco) | Should -Be @('github-desktop')
|
||||
|
||||
$both = Get-WinUtilSelectedPackages -PackageList @($desktop, $git) -Preference Choco -Action Uninstall
|
||||
@($both.Choco) | Should -Be @('github-desktop', 'git')
|
||||
}
|
||||
|
||||
It "skips blank, na, and missing package IDs" {
|
||||
$packages = @(
|
||||
[pscustomobject]@{ winget = ""; choco = "" }
|
||||
|
||||
+51
-3
@@ -12,6 +12,10 @@ BeforeAll {
|
||||
function Set-WinUtilService {
|
||||
param($Name, $StartupType)
|
||||
}
|
||||
function Get-Service {
|
||||
[CmdletBinding()]
|
||||
param($Name)
|
||||
}
|
||||
function Set-WinUtilRegistry {
|
||||
param($Name, $Path, $Type, $Value)
|
||||
}
|
||||
@@ -136,9 +140,16 @@ Describe "Invoke-WinUtilTweaks" {
|
||||
}
|
||||
|
||||
It "uses original registry values and service startup types in undo mode" {
|
||||
Mock Get-Service {
|
||||
[pscustomobject]@{
|
||||
Name = "DiagTrack"
|
||||
StartType = "Disabled"
|
||||
}
|
||||
} -ParameterFilter { $Name -eq "DiagTrack" }
|
||||
|
||||
Invoke-WinUtilTweaks -CheckBox "WPFTweaksExample" -undo $true
|
||||
|
||||
Should -Invoke -CommandName Get-Service -Times 0 -Exactly
|
||||
Should -Invoke -CommandName Get-Service -Times 1 -Exactly
|
||||
Should -Invoke -CommandName Set-WinUtilService -Times 1 -Exactly -ParameterFilter {
|
||||
$Name -eq "DiagTrack" -and $StartupType -eq "Automatic"
|
||||
}
|
||||
@@ -169,6 +180,43 @@ Describe "Invoke-WinUtilTweaks" {
|
||||
Should -Invoke -CommandName Set-WinUtilService -Times 0 -Exactly
|
||||
}
|
||||
|
||||
It "leaves a service alone on undo when its startup type is not the applied value" {
|
||||
Mock Get-Service {
|
||||
[pscustomobject]@{
|
||||
Name = "DiagTrack"
|
||||
StartType = "Manual"
|
||||
}
|
||||
} -ParameterFilter { $Name -eq "DiagTrack" }
|
||||
|
||||
Invoke-WinUtilTweaks -CheckBox "WPFTweaksServiceOnly" -undo $true
|
||||
|
||||
Should -Invoke -CommandName Get-Service -Times 1 -Exactly
|
||||
Should -Invoke -CommandName Set-WinUtilService -Times 0 -Exactly
|
||||
}
|
||||
|
||||
It "forces the service back to its original startup type on undo when it still matches the applied value" {
|
||||
Mock Get-Service {
|
||||
[pscustomobject]@{
|
||||
Name = "DiagTrack"
|
||||
StartType = "Disabled"
|
||||
}
|
||||
} -ParameterFilter { $Name -eq "DiagTrack" }
|
||||
|
||||
Invoke-WinUtilTweaks -CheckBox "WPFTweaksServiceOnly" -undo $true
|
||||
|
||||
Should -Invoke -CommandName Set-WinUtilService -Times 1 -Exactly -ParameterFilter {
|
||||
$Name -eq "DiagTrack" -and $StartupType -eq "Automatic"
|
||||
}
|
||||
}
|
||||
|
||||
It "drops the cached toggle status so the next status read hits the registry" {
|
||||
$script:sync.ToggleStatusCache = @{ WPFTweaksServiceOnly = $true }
|
||||
|
||||
Invoke-WinUtilTweaks -CheckBox "WPFTweaksServiceOnly"
|
||||
|
||||
$script:sync.ToggleStatusCache.ContainsKey("WPFTweaksServiceOnly") | Should -BeFalse
|
||||
}
|
||||
|
||||
It "forces a service startup type when KeepServiceStartup is disabled" {
|
||||
Invoke-WinUtilTweaks -CheckBox "WPFTweaksServiceOnly" -KeepServiceStartup $false
|
||||
|
||||
@@ -188,7 +236,7 @@ Describe "Invoke-WinUtilTweaks" {
|
||||
) + $script:sync.configs.tweaks.WPFTweaksExample.service
|
||||
|
||||
Mock Get-Service {
|
||||
$exception = [Microsoft.PowerShell.Commands.ServiceCommandException]::new("Cannot find any service with service name '$Name'.")
|
||||
$exception = [System.Exception]::new("Cannot find any service with service name '$Name'.")
|
||||
$errorRecord = [System.Management.Automation.ErrorRecord]::new(
|
||||
$exception,
|
||||
"NoServiceFoundForGivenName,Microsoft.PowerShell.Commands.GetServiceCommand",
|
||||
@@ -221,7 +269,7 @@ Describe "Invoke-WinUtilTweaks" {
|
||||
|
||||
It "rethrows service lookup errors that do not identify a missing service" {
|
||||
Mock Get-Service {
|
||||
$exception = [Microsoft.PowerShell.Commands.ServiceCommandException]::new("Service lookup failed.")
|
||||
$exception = [System.Exception]::new("Service lookup failed.")
|
||||
$errorRecord = [System.Management.Automation.ErrorRecord]::new(
|
||||
$exception,
|
||||
"ServiceLookupFailed",
|
||||
|
||||
Reference in new issue
Block a user