Compare commits

...
13 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
Omar 48885d7c1e docs(win11creator): correct architecture and guide (#5106)
* docs(win11creator): correct architecture and guide

Update documentation to match actual Win11 Creator implementation:

- Clarify customizations run at first logon via autounattend.xml rather
  than offline WIM modification
- Correct removed bloat AppX count from 40+ to 19 packages
- Update USB partition layout to single FAT32 partition with WIM split
- Clarify OneDrive uninstall timing during first logon
- Document missing helpers including USB functions and oscdimg helpers
- Document edition pinning, $OEM$ fallback scripts, and WIM metadata validation
- Update logging location and remove offline registry tweak wording

* docs(win11creator): clarify timing and media limits
2026-09-28 13:08:11 -05:00
Omar abcbc23144 fix: inject Setup storage into boot.wim (#5102)
$WinpeDriver$ matched INF names, so RST companions reached Setup. Add SCSIAdapter and HDC packages to boot.wim index 2 with DISM.
2026-09-28 12:35:50 -05:00
dependabot[bot] 311fde6f31 chore(deps): bump astral-sh/setup-uv from 10.1.0 to 10.2.0 (#5120)
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 10.1.0 to 10.2.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v10.1.0...v10.2.0)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 10.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-28 12:31:43 -05:00
39 changed files with 846 additions and 320 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
persist-credentials: false
- name: Install uv and Python
uses: astral-sh/setup-uv@v10.1.0
uses: astral-sh/setup-uv@v10.2.0
with:
version: "0.12.0"
python-version: "3.13"
+1 -1
View File
@@ -183,7 +183,7 @@ When the user corrects an agent approach, add or tighten one concrete rule here
- Have each Pester file load the assemblies and dot-source the functions it needs; several passed only because an earlier file in alphabetical order happened to load them.
- Log install/uninstall package names and package-manager IDs before queuing background runspace work; do not rely on runspace host output for the package identity.
- For Win11 Creator, start each new ISO modification in a fresh `WinUtil_Win11ISO_*` temp directory; existing-work detection is only for resuming/exporting already modified media.
- For Win11 Creator driver injection, keep offline WIM servicing to one mount and one commit: add each root package folder with its own `/Add-Driver /Recurse` so a single bad driver cannot fail the rest, and skip any folder whose ancestor is already in the set, since that ancestor's `/Recurse` covers it. Warn per failure and commit only when at least one package was added; when none were, warn and discard rather than throwing, so the run still produces an ISO. The discard in the cleanup block carries both orphaned mounts and that intentional zero-added case; keep it. Do not export editions or run unrelated WIM cleanup, and reject damaged metadata before ISO export. Use `-LiteralPath` for driver export paths, since `%TEMP%` can contain wildcard characters.
- For Win11 Creator driver injection, inject storage controllers (`SCSIAdapter` / `HDC`) into `boot.wim` index 2 with DISM, not `$WinpeDriver$`. Keep `install.wim` on its own per-package `/Add-Driver` retry loop. For each image, add each root package folder with its own `/Add-Driver /Recurse` so a single bad driver cannot fail the rest, and skip any folder whose ancestor is already in the set, since that ancestor's `/Recurse` covers it. Warn per failure and commit only when at least one package was added; when none were, warn and discard rather than throwing, so the run still produces an ISO. The discard in the cleanup block carries both orphaned mounts and that intentional zero-added case; keep it. Do not export editions or run unrelated WIM cleanup, and reject damaged metadata before ISO export. Use `-LiteralPath` for driver export paths, since `%TEMP%` can contain wildcard characters.
- For Script Analyzer cleanup, fix actionable source warnings first and do not globally suppress accepted convention warnings such as plural names, `ShouldProcess` on UI helpers, `$global:sync`, or compile-time cross-file false positives.
- For DNS DHCP reset, keep the cmdlet reset and explicitly set IPv4 and IPv6 DNS source to DHCP.
- Public pull-request diffs may be sent to configured external review services without a separate privacy approval; do not block the review loop on upload authorization for this public repository.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 78 KiB

@@ -135,21 +135,31 @@ The **Win11 Creator** is a specialized subsystem within Winutil that creates cus
### Win11 Creator Components
**Core Functions** (`functions/private/`):
- `Invoke-WinUtilISO.ps1`: Main orchestrator containing all Win11 Creator functions
- `Invoke-WinUtilISO.ps1`: Main orchestrator containing ISO workflow and helper functions
- `Invoke-WinUtilISOBrowse`: ISO file selection dialog
- `Invoke-WinUtilISOMountAndVerify`: Validates and mounts ISO, verifies it is an official Windows 11 ISO
- `Invoke-WinUtilISOModify`: Launches modification in background runspace
- `Invoke-WinUtilISOExport`: Handles ISO and USB export
- `Invoke-WinUtilISOExport`: Builds bootable ISO via `oscdimg.exe`
- `Invoke-WinUtilISOCheckExistingWork`: Recovers incomplete work sessions
- `Invoke-WinUtilISOCleanAndReset`: Cleans up temp directories and resets UI
- `Write-WinUtilISOLog`: Posts log messages to UI status box and session log
- `Set-WinUtilISOStep`: Controls wizard navigation state
- `Get-WinUtilEditionIdFromName`: Maps edition display names to setup edition IDs
- `Invoke-WinUtilRobocopy`: Copies files with exit code verification
- `Find-WinUtilOscdimg` / `Get-WinUtilOscdimgPath`: Locates or installs `oscdimg.exe`
- `Invoke-WinUtilISOScript.ps1`: Applies modifications to mounted install.wim
- Removes provisioned AppX packages (40+ bloatware apps)
- Injects drivers (optional) from the current system
- Removes OneDrive setup files
- Applies offline registry tweaks (hardware bypass, privacy, telemetry, OOBE)
- Deletes telemetry scheduled task definitions
- Pre-stages setup scripts from autounattend.xml
- `Invoke-WinUtilISOUSB.ps1`: USB drive detection and formatting
- `Invoke-WinUtilISORefreshUSBDrives`: Enumerates USB drives
- `Get-WinUtilFreeDriveLetter`: Finds available drive letters
- `Invoke-WinUtilISOWriteUSB`: Formats USB drive as GPT/FAT32, splits WIM if needed, and copies files
- `Invoke-WinUtilISOScript.ps1`: Prepares setup media and customizations
- Stages AppX removal (19 bloatware packages) into `autounattend.xml` for first logon
- Applies 50+ registry tweaks during Windows Setup and first logon; removes scheduled tasks at first logon
- Triggers OneDrive uninstall during first logon
- Stages setup script fallbacks to `sources\$OEM$\$$\Setup\Scripts\`
- Pins selected edition in `autounattend.xml` and writes `sources\ei.cfg`
- Injects eligible current system drivers into WIM images if enabled (WIM mount is only used for driver servicing)
### Win11 Creator Data Flow
@@ -169,18 +179,19 @@ User optionally enables the Driver Injection checkbox
↓
Invoke-WinUtilISOModify (runs in background runspace)
├─ Create work directory: ~WinUtil_Win11ISO_[timestamp]
├─ Copy ISO contents to disk (~5-6 GB)
├─ Mount install.wim at selected edition/index
├─ Copy ISO contents to disk via robocopy (~5-6 GB)
├─ Invoke-WinUtilISOScript:
│ ├─ Remove 40+ bloat AppX packages
│ ├─ Export and inject drivers (if enabled)
│ ├─ Remove OneDrive setup
│ ├─ Load offline registry hives
│ ├─ Apply 50+ registry tweaks (hardware bypass, privacy, telemetry, OOBE, etc.)
│ ├─ Delete telemetry scheduled task files
│ ├─ Pre-stage setup scripts from autounattend.xml to C:\Windows\Setup\Scripts\
│ └─ Unload registry hives
├─ Dismount and save the modified install.wim (~10+ minutes, slowest step)
│ ├─ Generate autounattend.xml with Windows PE and specialize safeguards
│ ├─ Add first-logon script (19 AppX removals, registry tweaks, OneDrive uninstall)
│ ├─ Pin selected edition index in autounattend.xml (/IMAGE/INDEX)
│ ├─ Stage setup script fallbacks to sources\$OEM$\$$\Setup\Scripts\
│ ├─ Write sources\ei.cfg and remove stale sources\PID.txt
│ └─ If driver injection enabled:
│ ├─ Require install.wim; install.esd cannot accept driver injection
│ ├─ Export current system drivers via DISM
│ ├─ Exclude stale duplicate packages
│ ├─ Inject storage drivers into boot.wim index 2 and eligible drivers into install.wim
│ └─ Validate WIM metadata before and after injection
├─ Dismount source ISO
└─ Report completion, enable export options
↓
@@ -191,12 +202,15 @@ Invoke-WinUtilISOExport (user chooses output)
│
└─ Option 2: Write to USB
├─ Format USB as GPT
├─ Create 512 MB EFI partition
├─ Copy modified ISO contents
├─ Create single FAT32 partition (capped at 32 GB)
├─ Split install.wim into .swm files if > 3.8 GB
├─ Reject install.esd files of 4 GB or more
├─ Copy files via robocopy
└─ Output: Bootable USB (minimum 8 GB)
↓
Invoke-WinUtilISOCleanAndReset (optional)
└─ Delete temp working directory (~10-15 GB)
├─ Dismount any open WIM mounts with discard
├─ Delete temp working directory (~10-15 GB)
└─ Reset UI to initial state
```
@@ -208,20 +222,30 @@ Invoke-WinUtilISOCleanAndReset (optional)
- Checks image metadata for "Windows 11" string
- Rejects custom, modified, or non-Windows 11 ISOs
**WIM Metadata Validation**:
- During driver injection, `Assert-WinUtilISOWimMetadata` validates critical fields (`Languages`, `Installation`, `Edition`, `ProductSuite`, `ProductType`) before and after WIM servicing
- Driver injection stops before export if the required metadata is missing or changes
- Without driver injection, WinUtil preserves the original installation image and skips WIM metadata validation
**Edition Pinning & Setup Fallback**:
- Writes `sources\ei.cfg` and removes `sources\PID.txt` so setup does not use mismatched OEM product keys
- Pins the selected edition index in `autounattend.xml` (`/IMAGE/INDEX`)
- Stages fallback setup scripts under `sources\$OEM$\$$\Setup\Scripts\` with `UseConfigurationSet` enabled
**Work Session Recovery**:
- Auto-detects incomplete work from previous sessions
- Allows resuming the export step without re-running selection and modification
- Prevents redundant modifications
**Modification Safety**:
- All registry changes are documented in a script (reversible)
- Windows PE and specialize set hardware bypass and setup safeguards; `WinUtil-PostInstall.ps1` applies additional registry changes at first logon
- Original ISO never modified; only working copy
- Logged to `WinUtil_Win11ISO.log` for debugging
- DISM handles image dismount with automatic cleanup on error
- Logged to the WinUtil session log and the live UI status panel
- DISM handles image dismount with automatic cleanup and discard on error
### Win11 Creator Registry Tweaks
The `Invoke-WinUtilISOScript` function applies **50+ offline registry tweaks**:
The `Invoke-WinUtilISOScript` function applies **50+ registry tweaks** during setup and first logon:
**Hardware Bypass**:
- TPM 2.0 check bypass
@@ -259,9 +283,9 @@ The `Invoke-WinUtilISOScript` function applies **50+ offline registry tweaks**:
### Driver Injection Feature
**Optional Enhancement**: When enabled, exports all drivers from the running system and injects them into both:
- `install.wim` (main OS image)
- `boot.wim` index 2 (Windows Setup PE environment)
**Optional Enhancement**: When enabled, exports all drivers from the running system and injects them into:
- `install.wim` (main OS image), excluding stale duplicates
- `boot.wim` index 2 (Windows Setup PE environment), storage controllers only (`SCSIAdapter` / `HDC`)
**Use Case**: Enables offline installation on systems with missing drivers.
@@ -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.
+16 -7
View File
@@ -45,8 +45,8 @@ Once the ISO is verified, WinUtil moves to this step and shows the mounted drive
Then click **Run Windows ISO Modification and Creator** to start the customization process. WinUtil will:
**App & Component Removal:**
- **Remove 40+ bloat apps** — Clipchamp, Teams, Copilot, Dev Home, new Outlook, Bing apps, Solitaire, and more
- **Delete OneDrive setup** from the image
- **Remove 19 bloat apps** — Clipchamp, Teams, Dev Home, new Outlook, Bing apps, Solitaire, and more
- **Uninstall OneDrive** during first logon
**System Customization:**
- **Bypass hardware checks** — removes TPM, Secure Boot, CPU, RAM, and storage requirement enforcement so the ISO installs on unsupported hardware
@@ -67,9 +67,13 @@ Then click **Run Windows ISO Modification and Creator** to start the customizati
- **Disable Copilot and search box suggestions**
**Optional: Driver Injection**
- If enabled, WinUtil exports the drivers from your current system, stages boot-storage drivers for Windows Setup, and injects eligible packages into the selected `install.wim` image. Stale duplicate packages are excluded before injection. Each remaining package is added separately, so one incompatible package does not stop the others. If a package fails, WinUtil discards the partial mount and retries the remaining packages against the original image. Check the live log for warnings: if every package fails, WinUtil keeps the original `install.wim` and still produces the ISO.
- If enabled, WinUtil exports the drivers from your current system, injects boot-storage drivers into `boot.wim` (Windows Setup, index 2), and injects eligible packages into the selected `install.wim` image. Stale duplicate packages are excluded before injection. Each remaining package is added separately, so one incompatible package does not stop the others. If a package fails, WinUtil discards the partial mount and retries the remaining packages against the original image. Check the live log for warnings: if every package fails, WinUtil keeps the original `install.wim` and still produces the ISO.
A live log shows progress as each step completes. This stage usually takes **10–30 minutes** depending on disk speed. The WIM dismount near the end is the slowest part, so do not close WinUtil while it is running.
:::note
Driver injection requires `sources\install.wim`. If the ISO uses `sources\install.esd`, leave this option off.
:::
A live log shows progress as each step completes. Without driver injection, WinUtil writes the setup changes in a few seconds after the ISO copy. With driver injection, this stage usually takes **10–20 minutes**, depending on hardware. Keep WinUtil open until the log reports completion.
:::note
The resulting ISO is close to the size of the source ISO. WinUtil does not remove the unused editions from `install.wim`; it selects your edition through `sources\ei.cfg` and `autounattend.xml` so Windows Setup installs the right one.
@@ -95,7 +99,11 @@ Once the modification is complete, choose how to save your image:
1. Click **Write Directly to a USB Drive**.
2. Select your USB drive from the dropdown (click **Refresh** if it doesn't appear).
3. Click **Erase & Write to USB** and confirm the warning — **all data on the drive will be permanently erased**.
4. WinUtil formats the drive as GPT with a 512 MB EFI partition and copies the modified Windows files.
4. WinUtil formats the drive as GPT with a single FAT32 partition (capped at 32 GB, splitting `install.wim` into `.swm` files if larger than 3.8 GB) and copies the modified Windows files.
:::note
WinUtil cannot split `install.esd`. If that file is 4 GB or larger, save an ISO instead of writing a FAT32 USB drive.
:::
:::danger
Double-check you have selected the correct drive before confirming. This operation cannot be undone.
@@ -130,12 +138,13 @@ When you install Windows 11 from your modified ISO:
| Problem | Fix |
|---------|-----|
| "install.wim not found" | Not a valid Windows 11 ISO — download a fresh one from Microsoft |
| "install.wim / install.esd was not found" | The ISO has no Windows installation image — download a fresh official ISO from Microsoft |
| "oscdimg.exe not found" | Run `winget install -e --id Microsoft.OSCDIMG` then retry |
| USB drive not showing up | Plug it in, wait a few seconds, then click **Refresh** |
| Modification seems stuck | The WIM dismount step is slow — wait at least 10 minutes before assuming it's frozen |
| 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]@{
+3 -2
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 {
@@ -401,7 +402,7 @@ function Invoke-WinUtilISOModify {
if ($driversInjected.Value) {
Step-WinUtilJob -Status "Finalizing install image..." -Percent 70
Write-WinUtilISOLog "Added current-system drivers to $sourceImageFileName index $SelectedWimIndex with one mount and commit."
Write-WinUtilISOLog "Added current-system drivers to $sourceImageFileName index $SelectedWimIndex."
} elseif ($InjectDrivers) {
Step-WinUtilJob -Status "Preserving install image..." -Percent 70
Write-WinUtilISOLog "No current-system drivers were injected into $sourceImageFileName index $SelectedWimIndex; install.wim was left unchanged. Review the warning log entries for details."
+133 -109
View File
@@ -6,7 +6,8 @@ function Invoke-WinUtilISOScript {
.DESCRIPTION
Stages WinUtil's AppX removal, registry tweaks, and scheduled-task cleanup
in the answer file for first logon, writes sources\ei.cfg for the selected
edition, and optionally adds current-system drivers to one install.wim index.
edition, and optionally adds current-system drivers to boot.wim index 2 and
one install.wim index.
.PARAMETER ISOContentsDir
Root directory of the copied ISO contents.
@@ -51,31 +52,9 @@ function Invoke-WinUtilISOScript {
)
$DriversInjected.Value = $false
function Copy-WinUtilISODriverFolder {
param (
[Parameter(Mandatory)][string]$Source,
[Parameter(Mandatory)][string]$Destination
)
$folderName = Split-Path $Source -Leaf
$targetPath = Join-Path $Destination $folderName
$suffix = 1
while (Test-Path -LiteralPath $targetPath) {
$targetPath = Join-Path $Destination "${folderName}_$suffix"
$suffix++
}
Copy-Item -LiteralPath $Source -Destination $targetPath -Recurse -Force -ErrorAction Stop
return $targetPath
}
function Test-WinUtilISOStorageDriver {
param ([Parameter(Mandatory)][System.IO.FileInfo]$InfFile)
if ($InfFile.BaseName -match '(?i)(iaahci|iastor|vmd|irst|rst)') {
return $true
}
try {
return (Get-Content -LiteralPath $InfFile.FullName -Raw -ErrorAction Stop) -match '(?im)^\s*Class\s*=\s*(SCSIAdapter|HDC)\s*(?:;.*)?$'
} catch {
@@ -206,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,
@@ -220,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."
@@ -267,6 +265,93 @@ function Invoke-WinUtilISOScript {
return @(& dism.exe /English /Get-MountedImageInfo 2>$null) -match [regex]::Escape($Path)
}
function Get-WinUtilISODriverFolderName {
param ([Parameter(Mandatory)][string]$DriverFolder)
if ($DriverFolder.StartsWith($driverExportRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
return $DriverFolder.Substring($driverExportRoot.Length).TrimStart('\')
}
return $DriverFolder
}
function Get-WinUtilISORootDriverFolders {
param ([Parameter(Mandatory)][AllowEmptyCollection()][string[]]$DriverFolders)
return @($DriverFolders | Where-Object {
$candidate = $_
-not ($DriverFolders | Where-Object { $candidate.StartsWith("$_\", [System.StringComparison]::OrdinalIgnoreCase) })
})
}
function Add-WinUtilISODriversToImage {
param (
[Parameter(Mandatory)][string]$ImagePath,
[Parameter(Mandatory)][int]$ImageIndex,
[Parameter(Mandatory)][string]$MountDir,
[Parameter(Mandatory)][AllowEmptyCollection()][string[]]$DriverFolders,
[Parameter(Mandatory)][string]$ImageLabel,
[Parameter(Mandatory)][ref]$ImageMounted
)
if ($DriverFolders.Count -eq 0) {
& $Logger "No driver packages to add to ${ImageLabel}."
return 0
}
Set-ItemProperty -LiteralPath $ImagePath -Name IsReadOnly -Value $false
New-Item -Path $MountDir -ItemType Directory -Force | Out-Null
$remainingDriverFolders = @($DriverFolders)
while ($remainingDriverFolders.Count -gt 0) {
& $Logger "Mounting $ImageLabel index $ImageIndex for driver injection..."
Invoke-WinUtilISODism -Arguments @('/English', '/Mount-Image', "/ImageFile:$ImagePath", "/Index:$ImageIndex", "/MountDir:$MountDir") -Operation 'mount' | Out-Null
$ImageMounted.Value = $true
$failedDriverFolder = $null
$driverName = $null
foreach ($driverFolder in $remainingDriverFolders) {
$driverName = Get-WinUtilISODriverFolderName -DriverFolder $driverFolder
try {
Invoke-WinUtilISODism -Arguments @('/English', "/Image:$MountDir", '/Add-Driver', "/Driver:$driverFolder", '/Recurse') -Operation "add-driver:$driverName" | Out-Null
} catch {
& $Logger "Warning: failed to add driver package '$driverName': $_"
$failedDriverFolder = $driverFolder
break
}
}
if (-not $failedDriverFolder) {
break
}
& $Logger "Discarding the potentially partial $ImageLabel mount before continuing without '$driverName'."
try {
Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$MountDir", '/Discard') -Operation 'discard' | Out-Null
$ImageMounted.Value = $false
} catch {
throw "Failed to discard the potentially partial $ImageLabel mount after driver package '$driverName' failed: $_"
}
$remainingDriverFolders = @($remainingDriverFolders | Where-Object { $_ -ne $failedDriverFolder })
}
$addedCount = $remainingDriverFolders.Count
if ($addedCount -eq 0) {
if ($ImageLabel -eq 'install.wim') {
& $Logger "Warning: none of the $($DriverFolders.Count) exported driver packages could be added; continuing with an unmodified install.wim."
} else {
& $Logger "Warning: none of the $($DriverFolders.Count) driver packages could be added to ${ImageLabel}."
}
return 0
}
& $Logger "Added $addedCount of $($DriverFolders.Count) driver packages to ${ImageLabel}."
& $Logger "Committing the driver-only $ImageLabel change..."
Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$MountDir", '/Commit') -Operation 'commit' | Out-Null
$ImageMounted.Value = $false
return $addedCount
}
if ([IO.Path]::GetExtension($InstallImagePath) -ne '.wim') {
throw 'Current-system driver injection requires install.wim; install.esd cannot be serviced in place.'
}
@@ -287,7 +372,7 @@ function Invoke-WinUtilISOScript {
$imageMounted = $false
try {
& $Logger "Exporting current system drivers before modifying install.wim..."
& $Logger "Exporting current system drivers before WIM driver injection..."
$dismLog = Join-Path $env:TEMP "WinUtil_DismDriverExport_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
Invoke-WinUtilISODism -Arguments @('/English', '/Online', '/Export-Driver', "/Destination:$driverExportRoot", "/LogPath:$dismLog") -Operation 'export-driver' | Out-Null
@@ -296,32 +381,6 @@ function Invoke-WinUtilISOScript {
throw 'DISM exported no driver INF files.'
}
$driverFolders = @($driverInfs | Group-Object { $_.Directory.FullName })
$winpeDriverDir = Join-Path $ContentRoot '$WinpeDriver$'
$storageCount = 0
$copyFailures = 0
foreach ($driverFolderGroup in $driverFolders) {
$driverFolder = [string]$driverFolderGroup.Name
$storageInfs = @($driverFolderGroup.Group | Where-Object { Test-WinUtilISOStorageDriver -InfFile $_ })
if ($storageInfs.Count -eq 0) {
continue
}
try {
New-Item -Path $winpeDriverDir -ItemType Directory -Force | Out-Null
$winpeTarget = Copy-WinUtilISODriverFolder -Source $driverFolder -Destination $winpeDriverDir
$storageCount++
& $Logger "Staged boot-storage package '$driverFolder' for WinPE as '$winpeTarget'."
} catch {
$copyFailures++
& $Logger "Warning: failed to stage boot-storage package '$driverFolder': $_"
}
}
if ($copyFailures -gt 0) {
throw "Failed to stage $copyFailures boot-storage driver package folders."
}
$stagedDriverFolders = @(Select-WinUtilISOStagedDriverPackages -DriverFolderGroups $driverFolders -Logger $Logger)
$metadataBefore = Get-WinUtilISOWimMetadata -ImagePath $InstallImagePath -Index $InstallImageIndex
Assert-WinUtilISOWimMetadata -Before $metadataBefore
@@ -352,67 +411,33 @@ function Invoke-WinUtilISOScript {
}
}
& $Logger "Exported $($stagedDriverFolders.Count) of $($driverFolders.Count) driver packages ($storageCount staged for WinPE, $($excludedDriverFolderGroups.Count) excluded)."
& $Logger "Exported $($stagedDriverFolders.Count) of $($driverFolders.Count) driver packages ($($excludedDriverFolderGroups.Count) excluded)."
Set-ItemProperty -LiteralPath $InstallImagePath -Name IsReadOnly -Value $false
New-Item -Path $mountDir -ItemType Directory -Force | Out-Null
# Storage for Setup comes from the same surviving set as install.wim, so stale
# duplicates never reach boot.wim.
$storageFolders = @(
$driverFolders |
Where-Object { $_.Name -in $stagedDriverFolders } |
Where-Object { @($_.Group | Where-Object { Test-WinUtilISOStorageDriver -InfFile $_ }).Count -gt 0 } |
ForEach-Object { [string]$_.Name }
)
$storageRootFolders = @(Get-WinUtilISORootDriverFolders -DriverFolders $storageFolders)
$rootPackageFolders = @(Get-WinUtilISORootDriverFolders -DriverFolders $stagedDriverFolders)
$imageMountedRef = [ref]$imageMounted
# Add each package separately so one bad driver cannot fail the rest. Because
# /Recurse covers descendants, only the highest surviving folder in each tree
# needs its own DISM call.
$rootPackageFolders = @($stagedDriverFolders | Where-Object {
$candidate = $_
-not ($stagedDriverFolders | Where-Object { $candidate.StartsWith("$_\", [System.StringComparison]::OrdinalIgnoreCase) })
})
& $Logger "Adding $($rootPackageFolders.Count) root driver packages to install.wim."
$remainingDriverFolders = @($rootPackageFolders)
while ($remainingDriverFolders.Count -gt 0) {
& $Logger "Mounting install.wim index $InstallImageIndex for driver injection..."
Invoke-WinUtilISODism -Arguments @('/English', '/Mount-Image', "/ImageFile:$InstallImagePath", "/Index:$InstallImageIndex", "/MountDir:$mountDir") -Operation 'mount' | Out-Null
$imageMounted = $true
$failedDriverFolder = $null
foreach ($driverFolder in $remainingDriverFolders) {
$driverName = $driverFolder
if ($driverFolder.StartsWith($driverExportRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
$driverName = $driverFolder.Substring($driverExportRoot.Length).TrimStart('\')
}
try {
Invoke-WinUtilISODism -Arguments @('/English', "/Image:$mountDir", '/Add-Driver', "/Driver:$driverFolder", '/Recurse') -Operation "add-driver:$driverName" | Out-Null
} catch {
& $Logger "Warning: failed to add driver package '$driverName': $_"
$failedDriverFolder = $driverFolder
break
}
$bootWim = Join-Path $ContentRoot 'sources\boot.wim'
if ($storageRootFolders.Count -gt 0) {
if (Test-Path -LiteralPath $bootWim) {
& $Logger "Adding $($storageRootFolders.Count) root storage driver packages to boot.wim."
$null = Add-WinUtilISODriversToImage -ImagePath $bootWim -ImageIndex 2 -MountDir $mountDir -DriverFolders $storageRootFolders -ImageLabel 'boot.wim' -ImageMounted $imageMountedRef
} else {
& $Logger 'Warning: boot.wim was not found; Windows Setup will not have injected storage drivers.'
}
if (-not $failedDriverFolder) {
break
}
& $Logger "Discarding the potentially partial install.wim mount before continuing without '$driverName'."
try {
Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$mountDir", '/Discard') -Operation 'discard' | Out-Null
$imageMounted = $false
} catch {
throw "Failed to discard the potentially partial install.wim mount after driver package '$driverName' failed: $_"
}
$remainingDriverFolders = @($remainingDriverFolders | Where-Object { $_ -ne $failedDriverFolder })
}
$addedCount = $remainingDriverFolders.Count
if ($addedCount -eq 0) {
# Boot-storage drivers staged for WinPE remain available to Windows Setup.
& $Logger "Warning: none of the $($rootPackageFolders.Count) exported driver packages could be added; continuing with an unmodified install.wim."
} else {
& $Logger "Added $addedCount of $($rootPackageFolders.Count) driver packages to install.wim."
& $Logger 'Committing the driver-only install.wim change...'
Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$mountDir", '/Commit') -Operation 'commit' | Out-Null
$imageMounted = $false
& $Logger "Adding $($rootPackageFolders.Count) root driver packages to install.wim."
$addedCount = Add-WinUtilISODriversToImage -ImagePath $InstallImagePath -ImageIndex $InstallImageIndex -MountDir $mountDir -DriverFolders $rootPackageFolders -ImageLabel 'install.wim' -ImageMounted $imageMountedRef
if ($addedCount -gt 0) {
$metadataAfter = Get-WinUtilISOWimMetadata -ImagePath $InstallImagePath -Index $InstallImageIndex
Assert-WinUtilISOWimMetadata -Before $metadataBefore -After $metadataAfter
& $Logger 'Driver injection complete; install.wim metadata validation passed.'
@@ -423,7 +448,7 @@ function Invoke-WinUtilISOScript {
try {
Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$mountDir", '/Discard') -Operation 'discard' | Out-Null
} catch {
& $Logger "Warning: could not discard the failed install.wim mount: $_"
& $Logger "Warning: could not discard the failed WIM mount: $_"
}
}
Remove-Item -LiteralPath $mountDir -Recurse -Force -ErrorAction SilentlyContinue
@@ -532,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
+284 -19
View File
@@ -214,7 +214,7 @@ Describe "Win11 Creator setup media" {
$script:modifyFunction | Should -Match 'if \(\$m -like "Warning:\*"\)[\s\S]*Write-WinUtilISOLog -Level "WARN" -Message \$m -SkipSessionLog[\s\S]*Write-Warning \$m'
}
It "keeps WIM servicing limited to one driver-only mount and commit" {
It "keeps WIM servicing limited to DISM Add-Driver without image export or cleanup" {
$isoScriptContent = Get-Content -Path $script:isoScriptPath -Raw
foreach ($expectedText in @(
@@ -222,7 +222,8 @@ Describe "Win11 Creator setup media" {
"'/Add-Driver'",
"'/Commit'",
"`$mountDir = Join-Path (Split-Path -Path `$ContentRoot -Parent) 'wim_mount'",
'install.wim metadata validation passed'
'install.wim metadata validation passed',
"Join-Path `$ContentRoot 'sources\boot.wim'"
)) {
$isoScriptContent | Should -Match ([regex]::Escape($expectedText))
}
@@ -233,17 +234,19 @@ Describe "Win11 Creator setup media" {
'Export-WindowsImage',
'Set-WindowsImage',
'/ResetBase',
'/Cleanup-Image'
'/Cleanup-Image',
'$WinpeDriver$'
)) {
$isoScriptContent | Should -Not -Match ([regex]::Escape($forbiddenText))
}
}
It "stages only boot-storage drivers in WinPE" {
It "injects only SCSIAdapter or HDC storage drivers into boot.wim" {
$isoScriptContent = Get-Content -Path $script:isoScriptPath -Raw
$isoScriptContent | Should -Match ([regex]::Escape("Join-Path `$ContentRoot '`$WinpeDriver$'"))
$isoScriptContent | Should -Match 'SCSIAdapter\|HDC'
$isoScriptContent | Should -Match ([regex]::Escape("Join-Path `$ContentRoot 'sources\boot.wim'"))
$isoScriptContent | Should -Not -Match ([regex]::Escape("Join-Path `$ContentRoot '`$WinpeDriver$'"))
$isoScriptContent | Should -Not -Match ([regex]::Escape('sources\$OEM$\$$\Drivers'))
$isoScriptContent | Should -Not -Match ([regex]::Escape('WinUtil-InstallDrivers.ps1'))
$isoScriptContent | Should -Not -Match ([regex]::Escape('SetupComplete.cmd'))
@@ -435,7 +438,7 @@ Describe "Win11 Creator setup media" {
It "stages storage drivers for WinPE and adds all drivers to one install.wim index" {
It "adds eligible drivers to one install.wim index and does not create `$WinpeDriver$" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoDrivers_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
@@ -443,7 +446,7 @@ Describe "Win11 Creator setup media" {
New-WinUtilDriverExportHarness -Fixtures @(
@{ Path = 'system_pkg'; Name = 'chipset.inf'; Class = 'System' },
@{ Path = 'storage_pkg'; Name = 'iaStorAC.inf'; Class = 'System' },
@{ Path = 'storage_pkg'; Name = 'iaStorAC.inf'; Class = 'SCSIAdapter' },
@{ Path = 'scsi_pkg'; Name = 'controller.inf'; Class = 'SCSIAdapter' },
@{ Path = 'net_pkg'; Name = 'network.inf'; Class = 'Net' },
@{ Path = 'group_a\duplicate'; Name = 'audio.inf'; Class = 'Media' },
@@ -465,12 +468,8 @@ Describe "Win11 Creator setup media" {
$logs.Add([string]$message)
}
$winpeDriverRoot = Join-Path $contentRoot '$WinpeDriver$'
@(Get-ChildItem -Path $winpeDriverRoot -Directory).Count | Should -Be 2
Test-Path (Join-Path $winpeDriverRoot 'system_pkg\chipset.inf') | Should -BeFalse
Test-Path (Join-Path $winpeDriverRoot 'storage_pkg\iaStorAC.inf') | Should -BeTrue
Test-Path (Join-Path $winpeDriverRoot 'scsi_pkg\controller.inf') | Should -BeTrue
Test-Path (Join-Path $winpeDriverRoot 'net_pkg\network.inf') | Should -BeFalse
Test-Path (Join-Path $contentRoot '$WinpeDriver$') | Should -BeFalse
($logs -join '|') | Should -Match 'Warning: boot.wim was not found'
@($script:dismCalls | Where-Object { $_ -match '/Mount-Image' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 9
@@ -488,7 +487,7 @@ Describe "Win11 Creator setup media" {
$nsMgr = New-Object System.Xml.XmlNamespaceManager($answerFile.NameTable)
$nsMgr.AddNamespace('sg', 'https://schneegans.de/windows/unattend-generator/')
$answerFile.SelectSingleNode('//sg:File[@path="C:\Windows\Setup\Scripts\WinUtil-InstallDrivers.ps1"]', $nsMgr) | Should -BeNullOrEmpty
($logs -join '|') | Should -Match 'Exported 10 of 11 driver packages \(2 staged for WinPE, 1 excluded\)'
($logs -join '|') | Should -Match 'Exported 10 of 11 driver packages \(1 excluded\)'
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*ntprint\.inf_x86_7426e1b60aa62272' \(DriverVer 1/1/2023,10\.0\.26100\.8875\) superseded by '.*ntprint\.inf_x86_58e7118cdecb935e' \(DriverVer 6/1/2024,10\.0\.26100\.9168\)"
($logs -join '|') | Should -Match 'install.wim metadata validation passed'
($logs -join '|') | Should -Match 'DISM mount completed.'
@@ -505,6 +504,91 @@ Describe "Win11 Creator setup media" {
}
}
It "injects SCSIAdapter storage drivers into boot.wim index 2 and not `$WinpeDriver`$" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoBootWim_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$bootWim = Join-Path $contentRoot 'sources\boot.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
New-WinUtilDriverExportHarness -Fixtures @(
@{ Path = 'net_pkg'; Name = 'network.inf'; Class = 'Net' },
@{ Path = 'scsi_pkg'; Name = 'controller.inf'; Class = 'SCSIAdapter' },
@{ Path = 'hdc_pkg'; Name = 'ide.inf'; Class = 'HDC' },
@{ Path = 'name_only_pkg'; Name = 'iaStorAC.inf'; Class = 'System' },
@{ Path = 'iastorhsacomponent.inf_amd64_1b2a068a8496b6a2'; Name = 'iaStorHsaComponent.inf'; Class = 'SoftwareComponent' },
@{ Path = 'iastorhsa_ext.inf_amd64_ba71359697f80d4e'; Name = 'iaStorHsa_Ext.inf'; Class = 'Extension' }
)
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
Set-Content -Path $bootWim -Value 'mock-boot'
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -Log {
param($message)
$logs.Add([string]$message)
}
Test-Path (Join-Path $contentRoot '$WinpeDriver$') | Should -BeFalse
@($script:dismCalls | Where-Object { $_ -match '/Mount-Image' -and $_ -match 'boot\.wim' -and $_ -match '/Index:2' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Mount-Image' -and $_ -match 'install\.wim' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Commit' }).Count | Should -Be 2
$bootMountCall = $script:dismCalls | Where-Object { $_ -match '/Mount-Image' -and $_ -match 'boot\.wim' } | Select-Object -First 1
$installMountCall = $script:dismCalls | Where-Object { $_ -match '/Mount-Image' -and $_ -match 'install\.wim' } | Select-Object -First 1
$bootMountIndex = [Array]::IndexOf($script:dismCalls.ToArray(), $bootMountCall)
$installMountIndex = [Array]::IndexOf($script:dismCalls.ToArray(), $installMountCall)
$bootAdds = @($script:dismCalls[$bootMountIndex..($installMountIndex - 1)] | Where-Object { $_ -match '/Add-Driver' })
$bootAdds.Count | Should -Be 2
($bootAdds -join "`n") | Should -Match ([regex]::Escape('scsi_pkg'))
($bootAdds -join "`n") | Should -Match ([regex]::Escape('hdc_pkg'))
($bootAdds -join "`n") | Should -Not -Match ([regex]::Escape('net_pkg'))
($bootAdds -join "`n") | Should -Not -Match 'iastorhsa'
($bootAdds -join "`n") | Should -Not -Match ([regex]::Escape('name_only_pkg'))
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 8
($logs -join '|') | Should -Match 'Added 2 of 2 driver packages to boot.wim'
($logs -join '|') | Should -Match 'Added 6 of 6 driver packages to install.wim'
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "injects only the newest duplicate storage package into boot.wim" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoBootWimDedup_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$bootWim = Join-Path $contentRoot 'sources\boot.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
New-WinUtilDriverExportHarness -Fixtures @(
@{ Path = 'iastorvd.inf_amd64_11111111aaaaaaaa'; Name = 'iaStorVD.inf'; Class = 'SCSIAdapter'; DriverVer = '1/1/2023,20.2.1.1016' },
@{ Path = 'iastorvd.inf_amd64_22222222bbbbbbbb'; Name = 'iaStorVD.inf'; Class = 'SCSIAdapter'; DriverVer = '6/1/2024,20.2.8.1028' }
)
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
Set-Content -Path $bootWim -Value 'mock-boot'
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -Log {
param($message)
$logs.Add([string]$message)
}
$bootAdds = @($script:dismCalls | Where-Object { $_ -match '/Add-Driver' })
@($bootAdds | Where-Object { $_ -match '22222222bbbbbbbb' }).Count | Should -Be 2
@($bootAdds | Where-Object { $_ -match '11111111aaaaaaaa' }).Count | Should -Be 0
($logs -join '|') | Should -Match 'Added 1 of 1 driver packages to boot.wim'
($logs -join '|') | Should -Match 'Added 1 of 1 driver packages to install.wim'
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "injects Class=Extension driver packages like any other export" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoExtensionInject_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
@@ -528,7 +612,7 @@ Describe "Win11 Creator setup media" {
}
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 3
($logs -join '|') | Should -Match 'Exported 3 of 3 driver packages \(0 staged for WinPE, 0 excluded\)'
($logs -join '|') | Should -Match 'Exported 3 of 3 driver packages \(0 excluded\)'
$driversInjected.Value | Should -BeTrue
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'ext_pkg_lower')
@@ -628,7 +712,7 @@ Describe "Win11 Creator setup media" {
}
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 2
($logs -join '|') | Should -Match 'Exported 2 of 2 driver packages \(0 staged for WinPE, 0 excluded\)'
($logs -join '|') | Should -Match 'Exported 2 of 2 driver packages \(0 excluded\)'
($logs -join '|') | Should -Not -Match 'Excluding stale duplicate driver package'
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'device.inf_amd64_11111111aaaaaaaa')
@@ -669,7 +753,7 @@ Describe "Win11 Creator setup media" {
}
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 3
($logs -join '|') | Should -Match 'Exported 3 of 7 driver packages \(0 staged for WinPE, 4 excluded\)'
($logs -join '|') | Should -Match 'Exported 3 of 7 driver packages \(4 excluded\)'
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*ntprint\.inf_x86_7426e1b60aa62272' \(DriverVer 1/1/2023,10\.0\.26100\.8875\) superseded by '.*ntprint\.inf_x86_58e7118cdecb935e'"
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*ntprint\.inf_x86_6688e7b66f8d9fb5' \(DriverVer 1/1/2024,10\.0\.26100\.8972\) superseded by '.*ntprint\.inf_x86_58e7118cdecb935e'"
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*sample\.inf_amd64_11111111aaaaaaaa' \(DriverVer unknown\) superseded by '.*sample\.inf_amd64_22222222bbbbbbbb' \(DriverVer 3/1/2024,1\.2\.3\.4\)"
@@ -753,8 +837,7 @@ Describe "Win11 Creator setup media" {
($logs -join '|') | Should -Match "none of the $script:expectedRootPackages exported driver packages could be added"
($logs -join '|') | Should -Not -Match "Added 0 of $script:expectedRootPackages"
# WinPE staging is independent of WIM servicing, so it must survive the failure.
@(Get-ChildItem -Path (Join-Path $contentRoot '$WinpeDriver$') -Directory).Count | Should -Be 2
Test-Path (Join-Path $contentRoot '$WinpeDriver$') | Should -BeFalse
$driversInjected.Value | Should -BeFalse
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
@@ -867,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())"
@@ -934,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>