Compare commits

...
10 Commits
Author SHA1 Message Date
Chris Titus 9419b2803e chore: Update generated dev docs and JSON links (#5125) 2026-09-29 12:54:33 -05:00
KristianandChris Titus 92d5a08d49 Fix WaaSMedicSvc restoration using direct registry write (#5096)
* Fix WaaSMedicSvc restoration using direct registry write

Set-Service fails on WaaSMedicSvc with Access Denied since it's a
protected service (LaunchProtected=2). FirstLogon.ps1 silently
swallowed this failure via -ErrorAction SilentlyContinue, permanently
leaving WaaSMedicSvc disabled after setup.

Removes WaaSMedicSvc from the Set-Service restoration loop and
restores it via a direct registry write instead, matching the same
technique already used to disable it in WinUtil-PostInstall.ps1.

Added a test verifying the old Set-Service-based restoration is gone
and the new registry-write fix is present.

Addresses the WaaSMedicSvc portion of #5095; the broader AppX/Store
failure investigation is out of scope for this change.

* Assert exact registry-write contract for WaaSMedicSvc test

Per CodeRabbit review: the previous assertion only checked that
Set-ItemProperty and WaaSMedicSvc appeared near each other, which
would pass even with a wrong -Value or -Type. Now asserts the
complete command including -Value 3 and -Type DWord.

* Surface service restoration failures in FirstLogon.log

BITS, wuauserv, UsoSvc, and WaaSMedicSvc restoration all used
-ErrorAction SilentlyContinue, which suppresses errors before they
reach the output stream FirstLogon.ps1 redirects to its log file
(*>&1 ... >> FirstLogon.log). This meant any restoration failure —
not just the WaaSMedicSvc one already fixed — was invisible even in
the log.

Changed -ErrorAction SilentlyContinue to Continue on both the
Set-Service loop and the WaaSMedicSvc registry write, so failures
still don't halt the script but now actually land in
FirstLogon.log for troubleshooting.

Addresses the logging portion of the reporter's suggestion in #5095.

* Assert -ErrorAction Continue in WaaSMedicSvc test

Per CodeRabbit review: the existing test only checked the
Set-ItemProperty command and value, not the -ErrorAction Continue
change made for logging. Extended the same test to also assert
both the Set-ItemProperty and Set-Service lines use Continue instead
of SilentlyContinue.

* Extend WaaSMedicSvc test to cover full ErrorAction Continue

Per CodeRabbit review: the registry-write assertion stopped at
-Type DWord, so a regression back to -ErrorAction SilentlyContinue
would still pass. Extended the pattern to include -ErrorAction
Continue at the end of the command.

* Exercise FirstLogon service restoration behavior

---------

Co-authored-by: Chris Titus <contact@christitus.com>
2026-09-29 12:46:08 -05:00
eduardodepaivaandChris Titus a3a7c500d4 style(ui): clarify scope in the note (#5053)
* style(ui): clarify scope in the note

- clarify which command affects only the current user and which affects all users
- replace StackPanel with WrapPanel to prevent text clipping at 200% font scaling

* fix(ui): scope AppX installation note to local registration

---------

Co-authored-by: Chris Titus <contact@christitus.com>
2026-09-29 11:24:52 -05:00
eduardodepaivaandChris Titus 40666e1d31 style(ui): replace 'Get' with 'Select' (#5054)
* style(ui): replace 'Get' with 'Select'

To a programmer, 'Get' means read the current state. To a lay user, 'Get' means obtain or download (like the 'Get' button in the Microsoft Store).
'Select' indicates exactly what it does and maintains parallelism with 'Select All'.

* docs: align installed tweak references with selection labels

---------

Co-authored-by: Chris Titus <contact@christitus.com>
2026-09-29 11:20:07 -05:00
eduardodepaivaandChris Titus 4d4e219562 fix(updates): notify before installing downloaded updates (#5105)
* fix(windows update): prevent sudden restart

Microsoft has documented that the `NoAutoRebootWithLoggedOnUsers` registry
key is unreliable and does not work as described in newer versions of Windows.

To effectively prevent unexpected restarts in modern configurations,
this PR updates the `Recommended` settings workflow on the `Updates` tab.

Changes
- Modify `AUOptions` to 3: automatic download and manual installation.
- Remove the obsolete registry key.
- Keep the removal of the registry key in functions that reset Windows Update to default.
- Update UI text and documentation.
- Update Pester tests.

Microsoft documentation:
https://learn.microsoft.com/en-us/windows/deployment/update/waas-restart

Resolves #5093

* Clarify update installation notification behavior

---------

Co-authored-by: Chris Titus <contact@christitus.com>
2026-09-29 11:18:47 -05:00
KristianandChris Titus fc03af421b Add friendly explanations for common DISM exit codes (#5087)
* Add friendly explanations for common DISM exit codes

Invoke-WinUtilISODism threw only a raw exit code on DISM failure
(e.g. "DISM add-driver failed with exit code 112"), leaving users to
look up what the number means themselves.

Adds a $knownExitCode lookup table mapping common Windows/DISM exit
codes (disk full, access denied, file/path not found, file in use,
timeout, etc.) to plain-English explanations. When a failure's exit
code is recognized, the thrown message now includes the explanation
in parentheses; unrecognized codes fall back to the original
plain-number message.

Verified the script still parses correctly after the change.

* Add missing period to fallback DISM error message

* Add tests for DISM known/unknown exit code error messages

Adds a test verifying a known exit code (112) produces the friendly
explanation in the thrown message, alongside the existing test
verifying an unrecognized code still falls back to the plain
numeric message.

Verified: all 35 tests in win11creator.Tests.ps1 pass.

* Document DISM friendly error messages in Win11 Creator troubleshooting

Adds a Troubleshooting table row explaining the new DISM error
message format (exit code + explanation in parentheses), with
guidance for the most common cases and a link to Microsoft's full
error code reference for anything not explained.

* Fix duplicate DISM calls in unmapped exit code test

The unmapped-code test called Invoke-WinUtilISOScript twice — once
via Should -Throw, once to capture the message for the no-parens
check — causing $script:dismCalls to double-count. Consolidated to
a single call, checking both the exit code and the absence of a
parenthesized explanation against one captured exception message.

Verified: all 36 tests in win11creator.Tests.ps1 pass.

* Fix broken DISM error code reference link in docs

Replaced the dead windows-hardware/manufacture link with Microsoft's
actual System Error Codes reference page, which DISM exit codes
correspond to.

* Strengthen DISM fallback regression coverage

---------

Co-authored-by: Chris Titus <contact@christitus.com>
2026-09-29 11:17:51 -05:00
Tokhirjon Yuldoshev 8939ecf4b4 docs: align LTSC FAQ with Windows 11 support (#5111) 2026-09-29 10:59:34 -05:00
Omar 735b59b82f fix(win11creator): update UI labels and screenshot (#5107)
- Update driver injection checkbox tooltip to describe individual package injection instead of a single DISM pass
- Make modification working label conditional so it displays 'Preparing setup media' when driver injection is disabled
- Update documentation screenshot to show the current 3-step wizard layout and vertical status log
2026-09-29 10:58:57 -05:00
Malin Fossum d0d39d6478 Warn instead of logging "tweak completed" after a tweak step error (#5090)
* Warn instead of logging tweak completed after a step error

Invoke-WinUtilTweaks wrote "Apply tweak completed" unconditionally, even
when Invoke-WinUtilScript or another helper had just logged an ERROR for
that tweak. The job layer already counts those errors, so snapshot the
count after the header line and compare at the end: log a WARN line with
the error count when it grew, otherwise the existing completed line.

Two Pester cases run the real logger and script runner against a temp
log file to pin both outcomes.

* Count tweak step errors from the shared log list

The job error counter only increments inside a Start-WinUtilJob worker.
Toggle switches call Invoke-WinUtilTweaks directly on the UI thread, so
a failing toggle still logged "tweak completed" after its ERROR line.

Every ERROR line is added to $sync.LoggedErrors from any runspace, and
Invoke-WinUtilAutoRun already reads it as a before/after delta, so the
tweak runner now does the same. The completion-status tests run without
the worker flag and seed an earlier unrelated error, and a new case
covers a failing UndoScript.

* Count tweak errors on the logging runspace, not the shared list

Diffing $sync.LoggedErrors charged a toggle with errors a concurrent
job logged from its own runspace. Global scope is per runspace, so the
logger now bumps the runspace counter for every headline error and the
tweak status diffs that. Job workers still reset the counter at start
and end, so job results are unchanged.
2026-09-29 10:58:32 -05:00
Kristian 6f1266eb86 Remove redundant DisableFileSyncNGSC registry write in PostInstall script (#5086)
Invoke-WinUtilISOScript.ps1's PostInstall script set DisableFileSyncNGSC
to 1, which FirstLogon.ps1 (from #4409) then immediately overrides back
to 0. The final value was correct, but only because one script undoes
the other's write on every install.

Removes the redundant PostInstall write so FirstLogon.ps1's existing
0 assignment is the only place setting this value.

Verified the script still parses correctly after the change.
2026-09-29 10:57:51 -05:00
36 changed files with 559 additions and 154 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 78 KiB

@@ -10,7 +10,8 @@ This page is generated from [`functions/public/Invoke-WPFPanelAutologin.ps1`](ht
```powershell title="functions/public/Invoke-WPFPanelAutologin.ps1"
function Invoke-WPFPanelAutologin {
Invoke-WebRequest -Uri https://live.sysinternals.com/Autologon.exe -OutFile "$winutildir\autologin.exe"
Start-Process -FilePath "$winutildir\autologin.exe" -ArgumentList /accepteula
$autologonPath = Join-Path $sync.winutildir "autologin.exe"
Invoke-WebRequest -Uri https://live.sysinternals.com/Autologon.exe -OutFile $autologonPath
Start-Process -FilePath $autologonPath -ArgumentList /accepteula
}
```
@@ -21,11 +21,54 @@ function Invoke-WPFSystemRepair {
3. DISM - Repair a corrupted Windows operating system image
#>
Start-Process cmd.exe -ArgumentList "/c chkdsk /scan /perf" -NoNewWindow -Wait
Start-Process cmd.exe -ArgumentList "/c sfc /scannow" -NoNewWindow -Wait
Start-Process cmd.exe -ArgumentList "/c dism /online /cleanup-image /restorehealth" -NoNewWindow -Wait
# SuccessCodes maps the non-zero exits a step treats as success to what they mean. The codes
# are per step because the same number means different things: 1 and 2 are ordinary chkdsk
# outcomes, while 1 from sfc is a failure, and 3010 is a repaired image from DISM only.
$steps = @(
@{
Label = "Checking the disk for errors"
Arguments = "/c chkdsk /scan /perf"
# 3 is left out: the disk could not be checked, or has errors an online scan cannot
# fix, and the steps after this one are not worth running on a disk in that state.
SuccessCodes = @{
1 = "errors were found and fixed"
2 = "cleanup was performed, or was skipped because /f was not given"
}
},
@{
Label = "Scanning protected system files"
Arguments = "/c sfc /scannow"
SuccessCodes = @{}
},
@{
Label = "Repairing the Windows image"
Arguments = "/c dism /online /cleanup-image /restorehealth"
SuccessCodes = @{
3010 = "a restart is needed for the repair to take effect"
}
}
)
Write-Host "==> Finished System Repair"
Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"
$completed = 0
foreach ($step in $steps) {
Step-WinUtilJob -Status "$($step.Label) ($($completed + 1)/$($steps.Count))" -Percent ([int](($completed / $steps.Count) * 100))
Write-WinUtilLog -Component "SystemRepair" -Message $step.Label
# Start-Process does not throw on a nonzero exit, so without this a failed chkdsk, sfc
# or dism run would still be reported as a completed repair
$process = Start-Process cmd.exe -ArgumentList $step.Arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
if ($exitCode -ne 0) {
if ($step.SuccessCodes.ContainsKey($exitCode)) {
# Start-WinUtilJob records WarningRecord output in both the session log and the
# job result, so accepted nonzero outcomes cannot finish with a green checkmark.
Write-Warning "$($step.Label) finished: $($step.SuccessCodes[$exitCode])."
} else {
throw "$($step.Label) failed with exit code $exitCode."
}
}
$completed++
}
}
```
@@ -25,8 +25,5 @@ function Invoke-WPFFixesNTPPool {
Restart-Service w32time
w32tm /resync
Write-Host "================================="
Write-Host "-- NTP Configuration Complete ---"
Write-Host "================================="
}
```
@@ -41,7 +41,7 @@ function Invoke-WPFFixesUpdate {
param($Aggressive = $false)
Write-Progress -Id 0 -Activity "Repairing Windows Update" -PercentComplete 0
Set-WinUtilTaskbaritem -state "Indeterminate" -overlay "logo"
Step-WinUtilJob -State "Indeterminate"
Write-Host "Starting Windows Update Repair..."
# Wait for the first progress bar to show, otherwise the second one won't show
Start-Sleep -Milliseconds 200
@@ -203,24 +203,14 @@ function Invoke-WPFFixesUpdate {
try {
(New-Object -ComObject Microsoft.Update.AutoUpdate).DetectNow()
} catch {
Set-WinUtilTaskbaritem -state "Error" -overlay "warning"
Write-WinUtilLog -Level "ERROR" -Component "Updates" -Message "Failed to create Windows Update COM object: $_"
Write-Warning "Failed to create Windows Update COM object: $_"
}
Start-Process -NoNewWindow -FilePath "wuauclt" -ArgumentList "/resetauthorization", "/detectnow"
Write-Progress -Id 10 -ParentId 0 -Activity "Forcing discovery" -Status "Completed" -PercentComplete 100
Write-Progress -Id 0 -Activity "Repairing Windows Update" -Status "Completed" -PercentComplete 100
Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"
$ButtonType = [System.Windows.MessageBoxButton]::OK
$MessageboxTitle = "Reset Windows Update "
$Messageboxbody = ("Stock settings loaded.`n Please reboot your computer")
$MessageIcon = [System.Windows.MessageBoxImage]::Information
[System.Windows.MessageBox]::Show($Messageboxbody, $MessageboxTitle, $ButtonType, $MessageIcon)
Write-Host "==============================================="
Write-Host "-- Reset All Windows Update Settings to Stock -"
Write-Host "==============================================="
Show-WinUtilMessage -Message "Stock settings loaded.`n Please reboot your computer" -Title "Reset Windows Update" -Button "OK" -Icon "Information" | Out-Null
# Remove the progress bars
Write-Progress -Id 0 -Activity "Repairing Windows Update" -Completed
@@ -18,18 +18,8 @@ function Invoke-WPFFixesWinget {
.DESCRIPTION
BravoNorris for the fantastic idea of a button to reinstall WinGet
#>
# Install Choco if not already present
try {
Set-WinUtilTaskbaritem -state "Indeterminate" -overlay "logo"
Write-Host "==> Starting WinGet Repair"
Install-WinUtilWinget
} catch {
Write-Error "Failed to install WinGet: $_"
Set-WinUtilTaskbaritem -state "Error" -overlay "warning"
} finally {
Write-Host "==> Finished WinGet Repair"
Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"
}
Step-WinUtilJob -Status "Repairing WinGet" -State "Indeterminate"
Install-WinUtilWinget -Force
}
```
@@ -9,19 +9,72 @@ This page is generated from [`functions/private/Invoke-WinUtilInstallPSProfile.p
:::
```powershell title="functions/private/Invoke-WinUtilInstallPSProfile.ps1"
function Get-WinUtilPowerShell7Path {
$command = Get-Command pwsh -CommandType Application -ErrorAction SilentlyContinue
if ($command) { return $command.Source }
foreach ($candidate in @(
"$env:ProgramFiles\PowerShell\7\pwsh.exe",
"$env:LOCALAPPDATA\Microsoft\WindowsApps\pwsh.exe")) {
if (Test-Path -LiteralPath $candidate) { return $candidate }
}
return $null
}
function Invoke-WinUtilInstallPSProfile {
if (-not (Get-Command wt)) {
Write-Host "Windows Terminal not found. Installing..."
<#
.SYNOPSIS
Installs the CTT PowerShell profile
.DESCRIPTION
The profile targets PowerShell 7, so its setup script has to run under pwsh rather than
the runspace this job is on. It runs as a child process with its output captured, so the
job log records what happened instead of it scrolling past in a terminal nobody kept.
#>
$pwshPath = Get-WinUtilPowerShell7Path
if (-not $pwshPath) {
Step-WinUtilJob -Status "Installing PowerShell 7" -State "Indeterminate"
Write-WinUtilLog -Component "Feature" -Message "PowerShell 7 not found, installing it first."
Install-WinUtilWinget
winget install Microsoft.WindowsTerminal --source winget --silent
Install-WinUtilProgramWinget -Action Install -Programs @("Microsoft.PowerShell") | Out-Null
# WinGet updates the persisted PATH, not this already-running process. Resolve the
# standard install locations as well as the current PATH before deciding it failed.
$pwshPath = Get-WinUtilPowerShell7Path
if (-not $pwshPath) {
throw "PowerShell 7 could not be installed, so the profile cannot be set up."
}
}
if (-not (Get-Command pwsh)) {
Write-Host "PowerShell 7 not found. Installing..."
Install-WinUtilWinget
winget install Microsoft.PowerShell --source winget --installer-type wix --silent
Step-WinUtilJob -Status "Running the profile setup" -State "Indeterminate"
$setupUrl = "https://github.com/ChrisTitusTech/powershell-profile/raw/main/setup.ps1"
# Stop in the child, so a setup failure is a nonzero exit rather than a logged error and a
# exit code of zero
$output = & $pwshPath -NoProfile -NonInteractive -Command "`$ErrorActionPreference = 'Stop'; irm '$setupUrl' | iex" 2>&1
$exitCode = $LASTEXITCODE
$failures = 0
foreach ($line in @($output)) {
if ($line -is [System.Management.Automation.ErrorRecord]) {
$failures++
Write-WinUtilErrorRecord -ErrorRecord $line -Component "Feature" -Context "PowerShell profile setup"
} elseif (-not [string]::IsNullOrWhiteSpace($line)) {
Write-WinUtilLog -Component "Feature" -Message ([string]$line).Trim()
}
}
wt new-tab pwsh -NoExit -Command "irm https://github.com/ChrisTitusTech/powershell-profile/raw/main/setup.ps1 | iex"
if ($exitCode -ne 0) {
throw "The profile setup script exited with code $exitCode."
}
if ($failures -gt 0) {
throw "The profile setup script reported $failures error(s); see the log."
}
Write-WinUtilLog -Component "Feature" -Message "CTT PowerShell profile installed. Open a new PowerShell 7 session to use it."
}
```
@@ -10,13 +10,39 @@ This page is generated from [`functions/private/Invoke-WinUtilUninstallPSProfile
```powershell title="functions/private/Invoke-WinUtilUninstallPSProfile.ps1"
function Invoke-WinUtilUninstallPSProfile {
<#
.SYNOPSIS
Restores the PowerShell 7 profile the CTT profile replaced
if (Test-Path ($Profile + ".bak")) {
Move-Item -Path ($Profile + ".bak") -Destination $Profile
} else {
Remove-Item -Path $Profile
.DESCRIPTION
The profile path has to come from pwsh itself. $PROFILE inside this job is the worker's
own Windows PowerShell profile, which is not the file the install wrote.
#>
$pwshPath = Get-WinUtilPowerShell7Path
if (-not $pwshPath) {
throw "PowerShell 7 is not installed, so there is no CTT profile to remove."
}
Write-Host "Successfully uninstalled CTT PowerShell Profile." -ForegroundColor Green
$profilePath = (& $pwshPath -NoProfile -NonInteractive -Command '$PROFILE' | Select-Object -First 1)
if ([string]::IsNullOrWhiteSpace($profilePath)) {
throw "Could not determine the PowerShell 7 profile path."
}
$profilePath = $profilePath.Trim()
$backupPath = "$profilePath.bak"
if (Test-Path $backupPath) {
Move-Item -Path $backupPath -Destination $profilePath -Force
Write-WinUtilLog -Component "Feature" -Message "Restored the profile that was in place before: $profilePath"
return
}
if (Test-Path $profilePath) {
Remove-Item -Path $profilePath -Force
Write-WinUtilLog -Component "Feature" -Message "Removed the CTT PowerShell profile: $profilePath"
return
}
Write-WinUtilLog -Level "WARN" -Component "Feature" -Message "No PowerShell 7 profile found at $profilePath, nothing to remove."
}
```
@@ -13,17 +13,10 @@ function Invoke-WPFSSHServer {
<#
.SYNOPSIS
Invokes the OpenSSH Server install in a runspace
Installs and starts the OpenSSH Server
#>
Invoke-WPFRunspace -ScriptBlock {
Invoke-WinUtilSSHServer
Write-Host "======================================="
Write-Host "-- OpenSSH Server installed! ---"
Write-Host "======================================="
}
Invoke-WinUtilSSHServer
}
```
@@ -9,7 +9,7 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
:::
```json title="config/tweaks.json"
"WPFTweaksDisableLockscreen": {
"WPFToggleDisableLockscreen": {
"Content": "Lock Screen - Disable",
"Description": "Skips the lock screen entirely and goes directly to the sign-in screen on boot and wake.",
"category": "Customize Preferences",
@@ -1,6 +1,6 @@
---
title: "Microsoft Outlook New Version"
description: "This will ensures the classic Outlook application is used."
description: "This will ensure the new Outlook application is used."
editUrl: false
---
@@ -11,7 +11,7 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
```json title="config/tweaks.json"
"WPFToggleNewOutlook": {
"Content": "Microsoft Outlook New Version",
"Description": "This will ensures the classic Outlook application is used.",
"Description": "This will ensure the new Outlook application is used.",
"category": "Customize Preferences",
"panel": "2",
"Type": "Toggle",
@@ -1,6 +1,6 @@
---
title: "Activity History - Disable"
description: "Erases recent docs, clipboard, and run history."
description: "Stops Windows from publishing or uploading user activities while preserving clipboard history."
editUrl: false
---
@@ -11,14 +11,14 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
```json title="config/tweaks.json"
"WPFTweaksActivity": {
"Content": "Activity History - Disable",
"Description": "Erases recent docs, clipboard, and run history.",
"Description": "Stops Windows from publishing or uploading user activities while preserving clipboard history.",
"category": "Essential Tweaks",
"panel": "1",
"registry": [
{
"Path": "HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\System",
"Name": "EnableActivityFeed",
"Value": "0",
"Value": "1",
"Type": "DWord",
"OriginalValue": "<RemoveEntry>"
},
@@ -16,8 +16,10 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
"panel": "1",
"InvokeScript": [
"
Remove-Item -Path \"$Env:Temp\\*\" -Recurse -Force
Remove-Item -Path \"$Env:SystemRoot\\Temp\\*\" -Recurse -Force
# A temp folder always holds files something has open, including this run's own, and
# the job layer counts a logged error as a failed step
Remove-Item -Path \"$Env:Temp\\*\" -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -Path \"$Env:SystemRoot\\Temp\\*\" -Recurse -Force -ErrorAction SilentlyContinue
"
],
}
@@ -15,10 +15,10 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
"category": "Essential Tweaks",
"panel": "1",
"InvokeScript": [
"icacls \"$Env:LocalAppData\\Packages\\Microsoft.WindowsStore_8wekyb3d8bbwe\\LocalState\\store.db\" /deny Everyone:F"
"icacls \"$Env:LocalAppData\\Packages\\Microsoft.WindowsStore_8wekyb3d8bbwe\\LocalState\\store.db\" /deny *S-1-1-0:F"
],
"UndoScript": [
"icacls \"$Env:LocalAppData\\Packages\\Microsoft.WindowsStore_8wekyb3d8bbwe\\LocalState\\store.db\" /grant Everyone:F"
"icacls \"$Env:LocalAppData\\Packages\\Microsoft.WindowsStore_8wekyb3d8bbwe\\LocalState\\store.db\" /grant *S-1-1-0:F"
],
}
```
@@ -1,6 +1,6 @@
---
title: "End Task With Right Click - Enable"
description: "Enables option to end task when right clicking a program in the taskbar."
description: "Enables option to end task when right-clicking a program in the taskbar."
editUrl: false
---
@@ -11,7 +11,7 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
```json title="config/tweaks.json"
"WPFTweaksEndTaskOnTaskbar": {
"Content": "End Task With Right Click - Enable",
"Description": "Enables option to end task when right clicking a program in the taskbar.",
"Description": "Enables option to end task when right-clicking a program in the taskbar.",
"category": "Essential Tweaks",
"panel": "1",
"registry": [
@@ -0,0 +1,46 @@
---
title: "Logitech Download Assistant Auto-Install - Disable"
description: "Blocks the Logi Download Assistant that Windows Update keeps reinstalling with Logitech device drivers. Logitech hardware keeps working without it."
editUrl: false
---
:::note
This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitusTech/winutil/blob/main/config/tweaks.json). Do not edit this page directly.
:::
```json title="config/tweaks.json"
"WPFTweaksLogiBlock": {
"Content": "Logitech Download Assistant Auto-Install - Disable",
"Description": "Blocks the Logi Download Assistant that Windows Update keeps reinstalling with Logitech device drivers. Logitech hardware keeps working without it.",
"category": "z__Advanced Tweaks - CAUTION",
"panel": "1",
"InvokeScript": [
"
Stop-Process -Name \"logi_download_assistant\" -Force -ErrorAction SilentlyContinue
$ProgramFiles64 = if ($Env:ProgramW6432) { $Env:ProgramW6432 } else { $Env:ProgramFiles }
$LogiPath = \"$ProgramFiles64\\LogiDownloadAssistant\"
if (Test-Path $LogiPath) {
Remove-Item $LogiPath\\* -Recurse -Force
} else {
New-Item -Path $LogiPath -ItemType Directory
}
icacls $LogiPath /deny \"*S-1-1-0:(W)\"
if ($LASTEXITCODE -ne 0) { throw \"icacls failed to deny write access on $LogiPath (exit code $LASTEXITCODE)\" }
"
],
"UndoScript": [
"
$ProgramFiles64 = if ($Env:ProgramW6432) { $Env:ProgramW6432 } else { $Env:ProgramFiles }
$LogiPath = \"$ProgramFiles64\\LogiDownloadAssistant\"
if (Test-Path $LogiPath) {
icacls $LogiPath /remove:d \"*S-1-1-0\"
if ($LASTEXITCODE -ne 0) { throw \"icacls failed to remove the write-deny rule on $LogiPath (exit code $LASTEXITCODE)\" }
}
"
],
}
```
@@ -10,53 +10,21 @@ This page is generated from [`functions/public/Invoke-WPFOOSU.ps1`](https://gith
```powershell title="functions/public/Invoke-WPFOOSU.ps1"
function Invoke-WPFOOSU {
if ($sync.ProcessRunning) {
Show-WinUtilMessage -Message "Another process is currently running." -Title "WinUtil" -Button "OK" -Icon "Warning"
return
}
Start-WinUtilJob -Name "OOSU" -Description "Downloading O&O ShutUp10++" -Parameters @{
DownloadPath = Join-Path $sync.winutildir "ooshutup10.exe"
} -ScriptBlock {
param($DownloadPath)
$downloadPath = Join-Path $sync.winutildir "ooshutup10.exe"
$sync.ProcessRunning = $true
Write-WinUtilLog -Component "OOSU" -Message "Downloading O&O ShutUp10++."
Invoke-WPFRunspace -ParameterList @(,("downloadPath", $downloadPath)) -ScriptBlock {
param($downloadPath)
$hasUI = $null -ne $sync.Form -and $null -ne $sync.Form.Dispatcher
try {
Write-WinUtilLog -Component "OOSU" -Message "Downloading O&O ShutUp10++."
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Downloading O&O ShutUp10++ (0%)" -Percent 0
}
Save-WinUtilFile -Uri "https://dl5.oo-software.com/files/ooshutup10/OOSU10.exe" -DestinationPath $downloadPath -ProgressCallback {
param($percent)
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Downloading O&O ShutUp10++ ($percent%)" -Percent $percent
}
}
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Launching O&O ShutUp10++" -Percent 100
}
Start-Process -FilePath $downloadPath
Write-WinUtilLog -Component "OOSU" -Message "O&O ShutUp10++ launched."
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "O&O ShutUp10++ launched" -Percent 100
}
}
catch {
Write-WinUtilLog -Level "ERROR" -Component "OOSU" -Message "O&O ShutUp10++ download failed: $($_.Exception.Message)"
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "O&O ShutUp10++ download failed" -Percent 100
}
Write-Error "Couldn't download O&O ShutUp10. Please make sure you have an active Internet connection."
}
finally {
$sync.ProcessRunning = $false
Save-WinUtilFile -Uri "https://dl5.oo-software.com/files/ooshutup10/OOSU10.exe" -DestinationPath $DownloadPath -ProgressCallback {
param($percent)
Step-WinUtilJob -Status "Downloading O&O ShutUp10++ ($percent%)" -Percent $percent
}
Step-WinUtilJob -Status "Launching O&O ShutUp10++" -Percent 100
Start-Process -FilePath $DownloadPath
Write-WinUtilLog -Component "OOSU" -Message "O&O ShutUp10++ launched."
}
}
```
@@ -40,12 +40,12 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
New-Item -Path $RazerPath -ItemType Directory
}
icacls $RazerPath /deny \"Everyone:(W)\"
icacls $RazerPath /deny \"*S-1-1-0:(W)\"
"
],
"UndoScript": [
"
icacls \"$Env:SystemRoot\\Installer\\Razer\" /remove:d Everyone
icacls \"$Env:SystemRoot\\Installer\\Razer\" /remove:d *S-1-1-0
"
],
}
@@ -17,7 +17,7 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
"InvokeScript": [
"
# Deny permission to remove OneDrive folder
icacls $Env:OneDrive /deny \"Administrators:(D,DC)\"
icacls $Env:OneDrive /deny \"*S-1-5-32-544:(D,DC)\"
Write-Host \"Uninstalling OneDrive...\"
Start-Process -FilePath (Join-Path $Env:SystemRoot \"System32\\OneDriveSetup.exe\") -ArgumentList '/uninstall' -Wait
@@ -31,7 +31,7 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
Remove-Item \"$Env:ProgramData\\Microsoft OneDrive\" -Recurse -Force
# Grant back permission to access OneDrive folder
icacls $Env:OneDrive /grant \"Administrators:(D,DC)\"
icacls $Env:OneDrive /grant \"*S-1-5-32-544:(D,DC)\"
if (-not (Get-ChildItem -Path $Env:OneDrive)) {
Remove-Item -Path $Env:OneDrive -Recurse
@@ -14,6 +14,6 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
"category": "z__Advanced Tweaks - CAUTION",
"panel": "1",
"Type": "Combobox",
"ComboItems": "Default DHCP Google Cloudflare Cloudflare_Malware Cloudflare_Malware_Adult Open_DNS Quad9 AdGuard_Ads_Trackers AdGuard_Ads_Trackers_Malware_Adult Mullvad Mullvad_Ads_Trackers Mullvad_Ads_Trackers_Malware Mullvad_Ads_Trackers_Malware_Social Mullvad_Ads_Trackers_Malware_Adult_Gambling Mullvad_Ads_Trackers_Malware_Adult_Gambling_Social",
"ComboItems": "Default DHCP Fastest Google Cloudflare Cloudflare_Malware Cloudflare_Malware_Adult Open_DNS Quad9 AdGuard_Ads_Trackers AdGuard_Ads_Trackers_Malware_Adult",
}
```
+1 -1
View File
@@ -230,7 +230,7 @@ Yes, WinUtil works on Windows Server editions, though some features may not be a
### Does WinUtil work with Windows LTSC?
Yes, WinUtil works with Windows 10/11 LTSC editions. Some applications may not be available depending on your configuration.
WinUtil currently targets Windows 11. Windows 10 LTSC is not supported, consistent with the Windows 10 support policy above. Some features and applications may vary by Windows 11 edition.
### Can I use WinUtil in a corporate/enterprise environment?
+1 -1
View File
@@ -18,7 +18,7 @@ Use the quick-selection buttons at the top of the Tweaks tab to speed up setup:
* **Minimal**: Selects a smaller, lower-impact set of common tweaks.
* **Advanced**: Selects a focused set of safer advanced tweaks. This preset intentionally skips restore point creation and cleanup tasks to avoid a long runtime.
* **Clear**: Clears all currently selected tweaks.
* **Get Installed Tweaks**: Best-effort detection for tweaks already applied to your system.
* **Select Installed Tweaks**: Best-effort detection for tweaks already applied to your system.
### Run Tweaks
* **Open the Tweaks tab**: Navigate to the **Tweaks** tab in the application.
+1 -1
View File
@@ -27,7 +27,7 @@ Changing modes adjusts system-wide Windows Update behavior. After switching mode
- **Feature updates**: Delayed by **365 days** to reduce the chance of disruption from major Windows changes.
- **Quality updates**: Delayed by **4 days** to allow time for early issues to surface while still keeping the system protected.
- **Drivers**: Excluded from Windows quality updates.
- **Restarts**: Scheduled updates do not automatically restart Windows while a user is signed in. A restart explicitly scheduled by a user still takes precedence.
- **Installation**: Updates download automatically and notify you when they are ready to install. This setting does not control restarts after installation.
- **Availability**: Update deferral policies apply to Windows Pro, Enterprise, and Education editions.
- **Why use it**: This mode offers the best balance between security and stability, which is why it is the recommended option for most PCs.
@@ -144,6 +144,7 @@ When you install Windows 11 from your modified ISO:
| Driver injection seems stuck | WIM servicing can pause at a mount or commit. Allow 10–20 minutes depending on hardware, and check the live log before closing WinUtil |
| "Access Denied" error | Make sure WinUtil is running as Administrator |
| Driver injection warning | Review the live log for the named package. Other compatible packages continue; if none succeed, the ISO is created with the original `install.wim` |
| DISM error with an explanation in parentheses, e.g. "exit code 112 (Disk is full)" | WinUtil now explains common DISM failures in plain language. Free up disk space for a "Disk is full" error, or run WinUtil as Administrator for an "Access denied" error. If the code has no explanation, search the number on Microsoft's [DISM error code reference](https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes) |
| "Setup has failed to validate the product key" | Recreate the ISO with the latest WinUtil. The creator now removes stale `PID.txt`, writes `sources\ei.cfg`, and pins the selected image in `autounattend.xml` so setup does not use an embedded OEM key for a different edition |
---
@@ -2,7 +2,7 @@ function Get-WinUtilTweaksStateReport {
<#
.SYNOPSIS
Groups every config/tweaks.json entry's live applied state by category, reusing the same
detection Invoke-WPFGetInstalled uses to check the "Get Installed Tweaks" checkboxes.
detection Invoke-WPFGetInstalled uses to check the "Select Installed Tweaks" checkboxes.
#>
$categoryFieldNames = [ordered]@{
+2 -1
View File
@@ -356,7 +356,8 @@ function Invoke-WinUtilISOModify {
Invoke-WPFUIThread -ScriptBlock {
$sync["WPFWin11ISOModifyButton"].IsEnabled = $false
}
Set-WinUtilISOStep -Step "Working" -Label "Modifying install.wim"
$workingLabel = if ($InjectDrivers) { "Modifying install.wim" } else { "Preparing setup media" }
Set-WinUtilISOStep -Step "Working" -Label $workingLabel
$modified = $false
try {
+20 -2
View File
@@ -185,6 +185,21 @@ function Invoke-WinUtilISOScript {
return @($survivingFolders)
}
$knownExitCode = @{
112 = "Disk is full"
5 = "Access denied"
2 = "File not found"
3 = "Path not found"
87 = "Invalid parameter"
1168 = "Element not found"
1392 = "File or directory is corrupted"
32 = "File in use / sharing violation"
21 = "Device not ready"
1460 = "Operation timed out"
1223 = "Operation cancelled by user"
50 = "Request not supported"
}
function Invoke-WinUtilISODism {
param (
[Parameter(Mandatory)][string[]]$Arguments,
@@ -199,7 +214,11 @@ function Invoke-WinUtilISOScript {
& $Logger " dism[$Operation]: $line"
}
}
throw "DISM $Operation failed with exit code $exitCode."
if ($knownExitCode.ContainsKey($exitCode)) {
throw "DISM $Operation failed with exit code $exitCode ($($knownExitCode[$exitCode]))."
} else {
throw "DISM $Operation failed with exit code $exitCode."
}
}
if ($Operation -ne 'metadata') {
& $Logger "DISM $Operation completed."
@@ -538,7 +557,6 @@ $appxList
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager' 'ShippedWithReserves' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\CurrentControlSet\Control\BitLocker' 'PreventDeviceEncryption' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Chat' 'ChatIcon' 'REG_DWORD' '3'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\OneDrive' 'DisableFileSyncNGSC' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\DataCollection' 'AllowTelemetry' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\CurrentControlSet\Services\dmwappushservice' 'Start' 'REG_DWORD' '4'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsCopilot' 'TurnOffWindowsCopilot' 'REG_DWORD' '1'
+9 -1
View File
@@ -23,6 +23,9 @@ function Invoke-WinUtilTweaks {
$action = if ($undo) { "Undo" } else { "Apply" }
Write-WinUtilLog -Component "Tweaks" -Message "$action tweak: $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
if ($undo) {
$Values = @{
@@ -81,5 +84,10 @@ function Invoke-WinUtilTweaks {
Remove-WinUtilProvisionedAPPX -PackageList $sync.configs.tweaks.$CheckBox.appx
}
}
Write-WinUtilLog -Component "Tweaks" -Message "$action tweak completed: $CheckBox"
$errorCount = [int]$global:WinUtilJobErrorCount - $errorsBefore
if ($errorCount -gt 0) {
Write-WinUtilLog -Level "WARN" -Component "Tweaks" -Message "$action tweak finished with $errorCount error(s): $CheckBox"
} else {
Write-WinUtilLog -Component "Tweaks" -Message "$action tweak completed: $CheckBox"
}
}
+4 -1
View File
@@ -42,7 +42,10 @@ function Write-WinUtilLog {
$null = $sync.LoggedErrors.Add("[$Component] $Message")
}
if ($Level -eq "ERROR" -and -not $Detail -and $global:WinUtilIsJobWorker) {
# Global scope is per runspace, so this counter only ever sees errors logged by the
# runspace that owns it: a job worker reads its own, and a tweak on the UI thread reads the
# UI thread's
if ($Level -eq "ERROR" -and -not $Detail) {
$global:WinUtilJobErrorCount++
}
@@ -8,7 +8,7 @@ function Invoke-WPFUpdatessecurity {
1. Disables driver offering through Windows Update
2. Defers feature updates for 365 days
3. Defers quality updates for 4 days
4. Prevents automatic restarts while a user is signed in
4. Configures automatic updates to notify when downloaded updates are ready to install
#>
@@ -67,13 +67,16 @@ function Invoke-WPFUpdatessecurity {
Remove-ItemProperty -Path $legacySettingsPath -Name $legacyValue -ErrorAction SilentlyContinue
}
Write-Host "Preventing automatic restarts while users are signed in..."
Write-WinUtilLog -Component "Updates" -Message "Configuring scheduled automatic updates without restarting while users are signed in."
Write-Host "Configuring automatic updates to download and notify before installation..."
Write-WinUtilLog -Component "Updates" -Message "Configuring automatic updates to download and notify before installation."
New-Item -Path $automaticUpdatePolicyPath -Force
# NoAutoRebootWithLoggedOnUsers only applies when automatic updates use option 4.
Set-ItemProperty -Path $automaticUpdatePolicyPath -Name "AUOptions" -Type DWord -Value 4
Set-ItemProperty -Path $automaticUpdatePolicyPath -Name "NoAutoRebootWithLoggedOnUsers" -Type DWord -Value 1
# Remove the previous scheduled-install reboot policy when switching to download-and-notify.
Remove-ItemProperty -Path $automaticUpdatePolicyPath -Name "NoAutoRebootWithLoggedOnUsers" -ErrorAction SilentlyContinue
# AUOptions 3 downloads updates and notifies before installation; it does not control restarts.
Set-ItemProperty -Path $automaticUpdatePolicyPath -Name "AUOptions" -Type DWord -Value 3
Set-ItemProperty -Path $automaticUpdatePolicyPath -Name "AUPowerManagement" -Type DWord -Value 0
Write-WinUtilLog -Component "Updates" -Message "Recommended Windows Update settings workflow completed."
+3 -3
View File
@@ -131,7 +131,7 @@ Describe "Write-WinUtilLog" {
Should -Invoke -CommandName Write-Warning -Times 0 -Exactly
}
It "counts only headline errors written by the active job worker" {
It "counts headline errors in the logging runspace whether or not it is a job worker" {
$script:sync = [hashtable]::Synchronized(@{
winutildir = $script:testRoot
LoggedErrors = [System.Collections.ArrayList]::Synchronized([System.Collections.ArrayList]::new())
@@ -142,9 +142,9 @@ Describe "Write-WinUtilLog" {
Write-WinUtilLog -Level "ERROR" -Component "Test" -Message "job error"
Write-WinUtilLog -Level "ERROR" -Detail -Component "Test" -Message "error detail"
$global:WinUtilIsJobWorker = $false
Write-WinUtilLog -Level "ERROR" -Component "UI" -Message "unrelated error"
Write-WinUtilLog -Level "ERROR" -Component "UI" -Message "toggle error"
$global:WinUtilJobErrorCount | Should -Be 1
$global:WinUtilJobErrorCount | Should -Be 2
$script:sync.LoggedErrors.Count | Should -Be 2
}
+81
View File
@@ -179,6 +179,87 @@ Describe "Invoke-WinUtilTweaks" {
}
}
Describe "Invoke-WinUtilTweaks completion status" {
BeforeAll {
. (Join-Path $script:repoRoot "functions\private\Write-WinUtilLog.ps1")
. (Join-Path $script:repoRoot "functions\private\Invoke-WinUtilScript.ps1")
}
BeforeEach {
$script:testRoot = Join-Path ([System.IO.Path]::GetTempPath()) "winutil-tweaks-$([guid]::NewGuid())"
$script:logPath = Join-Path $script:testRoot "logs\winutil_2026-09-15_12-00-00.log"
$script:sync = [Hashtable]::Synchronized(@{
logPath = $script:logPath
configs = @{
tweaks = [pscustomobject]@{
WPFTweaksFailing = [pscustomobject]@{
InvokeScript = @("throw 'simulated icacls failure'")
UndoScript = @("throw 'simulated icacls undo failure'")
}
WPFTweaksClean = [pscustomobject]@{
InvokeScript = @("Write-Output 'apply tweak'")
}
# A job worker logging an error from its own runspace lands in the shared
# list without passing through this runspace's logger
WPFTweaksDuringJob = [pscustomobject]@{
InvokeScript = @("`$null = `$sync.LoggedErrors.Add('[Job] error from a concurrent job'); Write-Output 'apply tweak'")
}
}
}
# Seeded with an earlier error: only errors logged during this tweak may count
LoggedErrors = [System.Collections.ArrayList]::Synchronized([System.Collections.ArrayList]::new(@("[UI] earlier unrelated failure")))
})
# Toggle switches run the tweak on the UI thread, outside any job worker. The runspace
# counter starts non-zero: only errors logged during this tweak may count
Remove-Variable -Name WinUtilIsJobWorker -Scope Global -ErrorAction SilentlyContinue
$global:WinUtilJobErrorCount = 3
Mock Write-Host { }
Mock Write-Warning { }
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name WinUtilJobErrorCount -Scope Global -ErrorAction SilentlyContinue
Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
}
It "warns instead of reporting completion when a tweak step logged an error" {
Invoke-WinUtilTweaks -CheckBox "WPFTweaksFailing"
$log = Get-Content -Path $script:logPath -Raw
$log | Should -Match "\[ERROR\] \[Script\] Runtime exception while running script for WPFTweaksFailing"
$log | Should -Match "\[WARN\] \[Tweaks\] Apply tweak finished with 1 error\(s\): WPFTweaksFailing"
$log | Should -Not -Match "tweak completed: WPFTweaksFailing"
}
It "warns when an undo step logged an error" {
Invoke-WinUtilTweaks -CheckBox "WPFTweaksFailing" -undo $true
$log = Get-Content -Path $script:logPath -Raw
$log | Should -Match "\[ERROR\] \[Script\] Runtime exception while running script for WPFTweaksFailing"
$log | Should -Match "\[WARN\] \[Tweaks\] Undo tweak finished with 1 error\(s\): WPFTweaksFailing"
$log | Should -Not -Match "tweak completed: WPFTweaksFailing"
}
It "reports completion when every tweak step succeeded" {
Invoke-WinUtilTweaks -CheckBox "WPFTweaksClean"
$log = Get-Content -Path $script:logPath -Raw
$log | Should -Match "\[INFO\] \[Tweaks\] Apply tweak completed: WPFTweaksClean"
$log | Should -Not -Match "\[WARN\]"
$log | Should -Not -Match "\[ERROR\]"
}
It "ignores an error another runspace logged while the tweak ran" {
Invoke-WinUtilTweaks -CheckBox "WPFTweaksDuringJob"
$log = Get-Content -Path $script:logPath -Raw
$log | Should -Match "\[INFO\] \[Tweaks\] Apply tweak completed: WPFTweaksDuringJob"
$log | Should -Not -Match "tweak finished with"
}
}
Describe "Invoke-WPFtweaksbutton" {
BeforeEach {
$script:sync = [Hashtable]::Synchronized(@{
+6 -8
View File
@@ -333,7 +333,7 @@ Describe "Invoke-WPFUpdatessecurity" {
}
}
It "sets recommended update deferral and auto-reboot policy values" {
It "sets recommended update deferral and installation notification policy values" {
Invoke-WPFUpdatessecurity
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
@@ -360,17 +360,15 @@ Describe "Invoke-WPFUpdatessecurity" {
$Type -eq "DWord" -and
$Value -eq 4
}
Should -Invoke -CommandName Remove-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -and
$Name -eq "NoAutoRebootWithLoggedOnUsers"
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -and
$Name -eq "AUOptions" -and
$Type -eq "DWord" -and
$Value -eq 4
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -and
$Name -eq "NoAutoRebootWithLoggedOnUsers" -and
$Type -eq "DWord" -and
$Value -eq 1
$Value -eq 3
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -and
+182
View File
@@ -950,6 +950,109 @@ Describe "Win11 Creator setup media" {
}
}
It "reports a friendly explanation when DISM fails with a known exit code" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoMountFailureKnownCode_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$script:dismCalls = [System.Collections.Generic.List[string]]::new()
function dism.exe {
param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments)
$script:dismCalls.Add(($Arguments -join '|'))
if ($Arguments -contains '/Get-WimInfo') {
$global:LASTEXITCODE = 0
'Languages : en-US'
'Installation : Client'
'Edition : Professional'
'ProductSuite : Terminal Server'
'ProductType : WinNT'
} elseif ($Arguments -contains '/Mount-Image') {
$global:LASTEXITCODE = 112
'Mount failed'
} elseif ($Arguments -contains '/Get-MountedImageInfo') {
$global:LASTEXITCODE = 0
"Mount Dir : $(Join-Path (Split-Path -Path $contentRoot -Parent) 'wim_mount')"
} elseif ($Arguments -contains '/Export-Driver') {
$global:LASTEXITCODE = 0
Export-WinUtilTestDriverPackage -Arguments $Arguments -Fixtures @(
@{ Path = 'storage_pkg'; Name = 'iaStorAC.inf'; Class = 'System' }
)
} else {
$global:LASTEXITCODE = 0
}
}
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
{ Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' } |
Should -Throw '*112*Disk is full*'
@($script:dismCalls | Where-Object { $_ -match '/Get-MountedImageInfo' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Discard' }).Count | Should -Be 1
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "falls back to the plain exit code for an unmapped DISM failure" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoMountFailureUnknownCode_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$script:dismCalls = [System.Collections.Generic.List[string]]::new()
function dism.exe {
param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments)
$script:dismCalls.Add(($Arguments -join '|'))
if ($Arguments -contains '/Get-WimInfo') {
$global:LASTEXITCODE = 0
'Languages : en-US'
'Installation : Client'
'Edition : Professional'
'ProductSuite : Terminal Server'
'ProductType : WinNT'
} elseif ($Arguments -contains '/Mount-Image') {
$global:LASTEXITCODE = 999
'Mount failed'
} elseif ($Arguments -contains '/Get-MountedImageInfo') {
$global:LASTEXITCODE = 0
"Mount Dir : $(Join-Path (Split-Path -Path $contentRoot -Parent) 'wim_mount')"
} elseif ($Arguments -contains '/Export-Driver') {
$global:LASTEXITCODE = 0
Export-WinUtilTestDriverPackage -Arguments $Arguments -Fixtures @(
@{ Path = 'storage_pkg'; Name = 'iaStorAC.inf'; Class = 'System' }
)
} else {
$global:LASTEXITCODE = 0
}
}
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
$thrown = $null
try {
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional'
} catch {
$thrown = $_.Exception.Message
}
$thrown | Should -Be 'DISM mount failed with exit code 999.'
@($script:dismCalls | Where-Object { $_ -match '/Get-MountedImageInfo' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Discard' }).Count | Should -Be 1
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "does not add driver setup artifacts when injection is disabled" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoNoDrivers_$([guid]::NewGuid())"
@@ -1017,4 +1120,83 @@ Describe "Win11 Creator setup media" {
$exportRunIndex | Should -BeGreaterThan $exportDialogIndex
$script:exportFunction | Should -Match ([regex]::Escape('return'))
}
Context "FirstLogon update service restoration" {
BeforeAll {
[xml]$unattend = Get-Content -LiteralPath $script:autoUnattendPath -Raw
$firstLogon = $unattend.SelectSingleNode("//*[local-name()='File' and @path='C:\Windows\Setup\Scripts\FirstLogon.ps1']").InnerText
$tokens = $null
$parseErrors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseInput($firstLogon, [ref]$tokens, [ref]$parseErrors)
if ($parseErrors.Count) { throw 'FirstLogon script failed to parse.' }
$blocks = @($ast.FindAll({
param($node)
$node -is [System.Management.Automation.Language.ScriptBlockExpressionAst] -and
$node.ScriptBlock.Find({
param($command)
$command -is [System.Management.Automation.Language.CommandAst] -and
$command.GetCommandName() -eq 'Set-Service'
}, $false)
}, $true))
if ($blocks.Count -ne 1) { throw 'Expected exactly one service restoration block.' }
# Never execute the surrounding FirstLogon cleanup, downloads, or installer.
$commands = $blocks[0].ScriptBlock.FindAll({
param($node)
$node -is [System.Management.Automation.Language.CommandAst]
}, $true)
foreach ($command in $commands) {
if ($command.GetCommandName() -notin @('reg.exe', 'Set-Service', 'Set-ItemProperty')) {
throw "Unexpected command in restoration block: $($command.Extent.Text)"
}
}
$script:restoreServices = $blocks[0].ScriptBlock.GetScriptBlock()
function reg.exe { param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments) }
}
BeforeEach {
Mock reg.exe { }
Mock Set-Service { }
Mock Set-ItemProperty { }
}
It "restores ordinary services and writes the protected Medic startup value directly" {
& $script:restoreServices
Should -Invoke Set-Service -Times 3 -Exactly
Should -Invoke Set-Service -Times 1 -Exactly -ParameterFilter { $Name -eq 'BITS' -and $StartupType -eq 'Manual' -and $ErrorAction -eq 'Continue' }
Should -Invoke Set-Service -Times 1 -Exactly -ParameterFilter { $Name -eq 'wuauserv' -and $StartupType -eq 'Manual' -and $ErrorAction -eq 'Continue' }
Should -Invoke Set-Service -Times 1 -Exactly -ParameterFilter { $Name -eq 'UsoSvc' -and $StartupType -eq 'Automatic' -and $ErrorAction -eq 'Continue' }
Should -Invoke Set-Service -Times 0 -Exactly -ParameterFilter { $Name -eq 'WaaSMedicSvc' }
Should -Invoke Set-ItemProperty -Times 1 -Exactly
Should -Invoke Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq 'HKLM:\SYSTEM\CurrentControlSet\Services\WaaSMedicSvc' -and
$Name -eq 'Start' -and $Value -eq 3 -and $Type -eq 'DWord' -and $ErrorAction -eq 'Continue'
}
}
It "keeps failures observable while attempting the remaining restoration work" {
$script:restorationCalls = [System.Collections.Generic.List[string]]::new()
Mock Set-Service {
param($Name, $ErrorAction)
$script:restorationCalls.Add($Name)
if ($script:restorationCalls.Count -eq 1) {
Write-Error 'simulated service restoration failure' -ErrorAction $ErrorAction
}
}
Mock Set-ItemProperty {
param($ErrorAction)
$script:restorationCalls.Add('Medic registry')
Write-Error 'simulated Medic registry failure' -ErrorAction $ErrorAction
}
$output = @(& $script:restoreServices 2>&1)
$failures = @($output | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] })
$failures.Count | Should -Be 2
$failures[0].Exception.Message | Should -Be 'simulated service restoration failure'
$failures[1].Exception.Message | Should -Be 'simulated Medic registry failure'
$script:restorationCalls.Count | Should -Be 4
@($script:restorationCalls | Select-Object -First 3 | Sort-Object) | Should -Be @('BITS', 'UsoSvc', 'wuauserv')
$script:restorationCalls[3] | Should -Be 'Medic registry'
}
}
}
+3 -2
View File
@@ -453,10 +453,11 @@ $scripts = @(
reg.exe delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config" /v DODownloadMode /f;
reg.exe add "HKLM\Software\Policies\Microsoft\Windows\OneDrive" /v DisableFileSyncNGSC /t REG_DWORD /d 0 /f;
reg.exe add "HKCU\Software\Microsoft\Windows\CurrentVersion\GameDVR" /v AppCaptureEnabled /t REG_DWORD /d 0 /f;
$services = @{ BITS = 'Manual'; wuauserv = 'Manual'; UsoSvc = 'Automatic'; WaaSMedicSvc = 'Manual' };
$services = @{ BITS = 'Manual'; wuauserv = 'Manual'; UsoSvc = 'Automatic' };
foreach ($name in $services.Keys) {
Set-Service -Name $name -StartupType $services[$name] -ErrorAction SilentlyContinue;
Set-Service -Name $name -StartupType $services[$name] -ErrorAction Continue;
}
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\WaaSMedicSvc' -Name 'Start' -Value 3 -Type DWord -ErrorAction Continue;
};
{
reg.exe add "HKLM\SOFTWARE\Microsoft\PolicyManager\current\device\Education" /f;
+8 -8
View File
@@ -1423,7 +1423,7 @@
<Button Name="WPFminimal" Content=" Minimal " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFAdvanced" Content=" Advanced " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFClearTweaksSelection" Content=" Clear " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFGetInstalledTweaks" Content=" Get Installed Tweaks " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFGetInstalledTweaks" Content=" Select Installed Tweaks " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFAppxRemoval" Content=" AppX Removal " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
</WrapPanel>
</StackPanel>
@@ -1503,7 +1503,7 @@
<TextBlock Text="- Defers feature updates for 365 days" TextWrapping="Wrap" Margin="0,0,0,7" Foreground="{DynamicResource MainForegroundColor}"/>
<TextBlock Text="- Defers quality updates for 4 days" TextWrapping="Wrap" Margin="0,0,0,7" Foreground="{DynamicResource MainForegroundColor}"/>
<TextBlock Text="- Excludes drivers from quality updates" TextWrapping="Wrap" Margin="0,0,0,7" Foreground="{DynamicResource MainForegroundColor}"/>
<TextBlock Text="- Prevents automatic restarts while a user is signed in" TextWrapping="Wrap" Margin="0,0,0,12" Foreground="{DynamicResource MainForegroundColor}"/>
<TextBlock Text="- Notifies when downloaded updates are ready to install" TextWrapping="Wrap" Margin="0,0,0,12" Foreground="{DynamicResource MainForegroundColor}"/>
<TextBlock Text="Available on Windows Pro, Enterprise, and Education editions."
FontSize="11"
FontStyle="Italic"
@@ -1966,7 +1966,7 @@
Foreground="{DynamicResource MainForegroundColor}"
IsChecked="False"
Cursor="Hand"
ToolTip="Stages boot-storage drivers for Setup and adds all exported drivers to the selected install.wim edition in one DISM pass."/>
ToolTip="Injects boot-storage drivers for Setup and adds exported drivers to the selected install.wim edition individually, skipping incompatible packages."/>
</StackPanel>
</Border>
@@ -2215,7 +2215,7 @@
<Label Content="Selections:" FontSize="{DynamicResource FontSize}" VerticalAlignment="Center" Margin="2"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,2,0,0">
<Button Name="WPFDefaultAppxSelection" Content=" Default " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFGetInstalledAppx" Content=" Get Installed " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFGetInstalledAppx" Content=" Select Installed " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFSelectAllAppx" Content=" Select All " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFClearAppxSelection" Content=" Clear Selection " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
</StackPanel>
@@ -2225,13 +2225,13 @@
</Grid>
<Border Grid.Row="2" Style="{StaticResource BorderStyle}" Margin="5,15,5,5">
<StackPanel Background="{DynamicResource MainBackgroundColor}" Orientation="Horizontal" HorizontalAlignment="Left">
<WrapPanel Background="{DynamicResource MainBackgroundColor}" Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Padding="10" TextWrapping="Wrap" Foreground="{DynamicResource MainForegroundColor}">
Note: Select the Windows AppX packages you wish to install or remove.
<LineBreak/>Install Selected registers a local manifest when available, then falls back to the Microsoft Store.
<LineBreak/>Remove Selected removes packages for the current user and all new user profiles.
<LineBreak/>'Install Selected' registers a local manifest for the current user when available, then falls back to the Microsoft Store.
<LineBreak/>'Remove Selected' removes packages for all users and stops new profiles from getting them by default.
</TextBlock>
</StackPanel>
</WrapPanel>
</Border>
</Grid>
</ScrollViewer>