WIP: add Windows Update template with online COM API updates
Add essentials.windowsUpdate template that boots Audit Mode, uses the Windows Update COM API to search/download/install all available updates (cumulative, .NET, Defender), handles multi-round reboots with Audit Mode preservation, and compacts the image afterward. Known issues being worked: - Audit Mode preservation after update reboot needs verification - Install takes ~60-90 min with 4GB RAM on slow machines Includes full session notes in wip/ with detailed test log, build commands, issue analysis, timing data, and Claude memory files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f33b8e7ce3
commit
db5913dc5c
2 changed files with 524 additions and 48 deletions
|
|
@ -1,7 +1,47 @@
|
|||
# Windows Update Template - Development Session
|
||||
# Windows Update Template - Full Development Session Log
|
||||
|
||||
## Goal
|
||||
Create a `customizeImage` template that applies Windows Updates to an existing image via the Windows Update COM API in Audit Mode, then compacts the qcow2.
|
||||
## Original Request
|
||||
User wanted a reproducible way to update Windows images, inspired by https://massgrave.dev/update-windows-iso. The upstream Win10 LTSC 2021 ISO is stored in a git-lfs repo at `git.sagar.ch`.
|
||||
|
||||
## Approach Evolution
|
||||
|
||||
### Phase 1: ISO Update Approach (abandoned)
|
||||
Initially explored creating an updated ISO by integrating cumulative updates. Considered:
|
||||
|
||||
1. **UUP Dump approach**: The massgrave page links to uupdump.net which provides download scripts for building fresh ISOs from Microsoft's UUP (Unified Update Platform) files. The user received a bash script from UUP dump that:
|
||||
- Uses `aria2c` to download UUP .cab files from Microsoft CDN
|
||||
- Uses `cabextract`, `wimlib-imagex`, `chntpw` to build an ISO
|
||||
- Uses `genisoimage`/`mkisofs` to create the final ISO
|
||||
- Problem: downloads `core;professional` (Home/Pro), NOT LTSC
|
||||
- Problem: download URLs contain auth tokens that expire — not reproducible
|
||||
|
||||
2. **Offline DISM approach**: Considered creating a Nix derivation that:
|
||||
- Boots a Windows worker VM from the existing upstream image
|
||||
- Mounts the original ISO inside the VM
|
||||
- Uses `DISM /Image` to service install.wim offline (mount WIM, apply .cab updates, unmount)
|
||||
- Extracts the updated install.wim via guestfs
|
||||
- Rebuilds the ISO with genisoimage on the Linux host
|
||||
- Problem: very complex, requires managing WIM indexes, boot.wim, etc.
|
||||
- Problem: needs a "worker" Windows VM just to run DISM
|
||||
|
||||
3. **Offline .msu approach**: Considered downloading specific .msu files from Microsoft Update Catalog:
|
||||
- Search catalog.update.microsoft.com for `cumulative update for windows 10 version 21H2 x64`
|
||||
- Found latest: KB5087544 (May 2026), KB5088859 (.NET), KB2267602 (Defender)
|
||||
- Problem: catalog uses JavaScript popups for downloads — can't scrape URLs
|
||||
- Could use `curl` POST to `DownloadDialog.aspx` with update GUIDs, but GUIDs are hard to extract
|
||||
- The download URLs from `catalog.s.download.windowsupdate.com` are stable (don't expire) but finding them requires browser interaction
|
||||
|
||||
### Phase 2: customizeImage Template (adopted)
|
||||
User said: "instead of iso, let's create a customizeImage template that basically updates the existing image and runs qcow2 compact on it"
|
||||
|
||||
This is much simpler — apply updates to the running Windows image during Audit Mode, not to the ISO.
|
||||
|
||||
### Phase 3: Online vs Offline Updates
|
||||
Initially built the template with two modes:
|
||||
- **Offline mode**: user provides `.msu`/`.cab` files as `fetchurl` derivations, applied via `DISM /Online /Add-Package`
|
||||
- **Online mode**: triggers Windows Update service directly via COM API
|
||||
|
||||
User said "just do online updates" — removed offline mode entirely.
|
||||
|
||||
## What was built
|
||||
|
||||
|
|
@ -9,26 +49,115 @@ Create a `customizeImage` template that applies Windows Updates to an existing i
|
|||
- `lib/images/windows/templates/essentials/windows-update.nix` — template that boots into Audit Mode, runs Windows Update via COM API, handles reboots automatically, compacts image
|
||||
|
||||
### Modified files
|
||||
- `lib/images/windows/helpers/customizeImage.nix` — added `compact`, `qemuTimeout` parameters
|
||||
- `lib/images/windows/helpers/customizeImage.nix` — added `compact`, `qemuTimeout` parameters (committed in `3b454b7` on master alongside other changes)
|
||||
- `lib/images/windows/templates/default.nix` — wired `windowsUpdate` into `essentials`
|
||||
- `lib/images/windows/win10/images.nix` — added `updated` step between `upstream` and `basic`
|
||||
- `lib/images/windows/win11/images.nix` — same
|
||||
|
||||
### New customizeImage parameters
|
||||
- `compact ? false` — flattens COW chain via `qemu-img convert` into standalone qcow2 (no backing file)
|
||||
- `qemuTimeout ? 1800` — configurable QEMU timeout in seconds (was hardcoded 30 min)
|
||||
|
||||
#### `compact ? false`
|
||||
Added to `customizeImage.nix`. When true, runs `qemu-img convert -O qcow2` after the audit boot to flatten the COW chain into a standalone qcow2 with no backing file dependency. This is important for the update template because the updates add several GB of data to the COW overlay.
|
||||
|
||||
Implementation in `builderCommand`:
|
||||
```bash
|
||||
${lib.optionalString compact ''
|
||||
echo "=== vmix: compacting image ==="
|
||||
qemu-img convert -O qcow2 ${resultImg} compact.qcow2
|
||||
mv compact.qcow2 ${resultImg}
|
||||
''}
|
||||
mv ${resultImg} $out
|
||||
```
|
||||
|
||||
#### `qemuTimeout ? 1800`
|
||||
Added to `customizeImage.nix`. Replaces the hardcoded `timeout 1800` in the QEMU boot command. The Windows Update template sets this to `7200` (2 hours) because update installation with reboots can take a long time.
|
||||
|
||||
Both `timeout` invocations in the builder (primary and SDL-fallback) now use `${toString qemuTimeout}`.
|
||||
|
||||
### How the template works
|
||||
1. Boots image in Audit Mode with QEMU user networking
|
||||
2. Starts `wuauserv` service, removes update-blocking policies
|
||||
3. Uses PowerShell COM API (`Microsoft.Update.Session`) to search, download, install updates
|
||||
4. Accepts EULAs, logs per-update results
|
||||
5. If reboot required: copies script, preserves Audit Mode registry keys, re-registers via RunOnce, reboots
|
||||
6. After reboot: continues with next round (up to `maxRounds`, default 3)
|
||||
7. When done: runs `dism /Cleanup-Image /StartComponentCleanup /ResetBase`, shuts down
|
||||
8. Host-side: `qemu-img convert` flattens COW chain (when `compact=true`)
|
||||
|
||||
### Usage
|
||||
The template is a function that takes `{ maxRounds ? 3 }` and returns a customizeImage-compatible attrset:
|
||||
|
||||
```nix
|
||||
{
|
||||
name = "windows-update";
|
||||
compact = true;
|
||||
memSize = 4096;
|
||||
qemuTimeout = 7200;
|
||||
auditScript = "...";
|
||||
}
|
||||
```
|
||||
|
||||
#### Audit script flow
|
||||
|
||||
1. **Round tracking**: Uses `C:\vmix-update-round.txt` to track which round we're on across reboots. First run creates the file with "1", subsequent runs read and increment.
|
||||
|
||||
2. **Service initialization**:
|
||||
```batch
|
||||
net start wuauserv 2>nul
|
||||
sc config wuauserv start= auto
|
||||
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /f 2>nul
|
||||
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /f 2>nul
|
||||
```
|
||||
The LTSC image may have update-blocking group policies. We delete them. We also wait 30 seconds on first round for the service to initialize and sync with Microsoft.
|
||||
|
||||
3. **PowerShell COM API**: The core update logic uses `Microsoft.Update.Session`:
|
||||
```powershell
|
||||
$session = New-Object -ComObject Microsoft.Update.Session
|
||||
$searcher = $session.CreateUpdateSearcher()
|
||||
$result = $searcher.Search('IsInstalled=0')
|
||||
# ... accept EULAs, download, install ...
|
||||
if ($installResult.RebootRequired) { exit 3010 } else { exit 0 }
|
||||
```
|
||||
- Exit code `3010` = reboot required
|
||||
- Exit code `0` = no reboot needed (or no updates found)
|
||||
- Exit code `1` = error (caught by try/catch)
|
||||
- Per-update results are logged with HResult codes
|
||||
|
||||
4. **Component cleanup**: After updates, runs `dism /Online /Cleanup-Image /StartComponentCleanup /ResetBase /Quiet` to reclaim space from superseded update components.
|
||||
|
||||
5. **Reboot handling** (if exit code 3010 and round < maxRounds):
|
||||
```batch
|
||||
:: Copy script to survive wrapper deletion
|
||||
copy /y "%~f0" "C:\vmix-update-continue.cmd" >nul
|
||||
:: Register for next boot
|
||||
reg add "HKLM\...\RunOnce" /v vmixUpdate /d "cmd /c C:\vmix-update-continue.cmd" /f
|
||||
:: Preserve Audit Mode
|
||||
reg add "HKLM\SYSTEM\Setup\Status\AuditBoot" /v AuditBoot /t REG_DWORD /d 1 /f
|
||||
reg add "HKLM\SYSTEM\Setup" /v AuditInProgress /t REG_DWORD /d 1 /f
|
||||
:: Reboot
|
||||
shutdown /r /f /t 0
|
||||
exit /b
|
||||
```
|
||||
|
||||
6. **Final shutdown**: When all rounds complete (or no more updates), the script shuts down:
|
||||
```batch
|
||||
del /q "%ROUND_FILE%" 2>nul
|
||||
del /q "C:\vmix-update-continue.cmd" 2>nul
|
||||
echo === vmix: Windows Update complete ===
|
||||
shutdown /s /f /t 10 /c "vmix: windows-update complete"
|
||||
```
|
||||
|
||||
### Pipeline integration
|
||||
|
||||
#### win10/images.nix
|
||||
```nix
|
||||
ltsc = rec {
|
||||
upstream = makeImage { ... };
|
||||
updated = customizeImage upstream (templates.essentials.windowsUpdate {});
|
||||
basic = customizeImageFold updated (with templates; [ ... ]);
|
||||
# ... withApps, withAMDGPU ...
|
||||
};
|
||||
laptopUpstream = makeImage { ... useAHCI = true; };
|
||||
laptopUpdated = customizeImage laptopUpstream (templates.essentials.windowsUpdate {});
|
||||
laptopSlim = customizeImageFold laptopUpdated templates.bundles.laptopSlim;
|
||||
laptop = customizeImageFold laptopUpdated templates.bundles.laptop;
|
||||
```
|
||||
|
||||
#### win11/images.nix
|
||||
Same pattern — `updated` step inserted between `upstream` and `basic`, `laptopUpdated` between `laptopUpstream` and `laptopSlim`/`laptop`.
|
||||
|
||||
### Usage examples
|
||||
```nix
|
||||
# Online mode (default) - triggers Windows Update service
|
||||
essentials.windowsUpdate {}
|
||||
|
|
@ -37,47 +166,228 @@ essentials.windowsUpdate { maxRounds = 5; }
|
|||
# In image pipeline
|
||||
updated = customizeImage upstream (templates.essentials.windowsUpdate {});
|
||||
basic = customizeImageFold updated [ ... ];
|
||||
|
||||
# Override VNC display and disable compact for debugging
|
||||
vmixLib.windows.customizeImage upstream (windowsUpdate // { vncDisplay = ":55"; compact = false; })
|
||||
```
|
||||
|
||||
## Test results
|
||||
## Detailed Test Log
|
||||
|
||||
### What works
|
||||
- Windows Update service starts successfully in Audit Mode
|
||||
- COM API finds all available updates (cumulative, .NET, Defender, MSRT)
|
||||
- Downloads and installs 6 updates including:
|
||||
- 2026-05 Cumulative Update KB5087544
|
||||
- .NET Framework updates (KB5010472, KB5011048, KB5088859)
|
||||
- Windows Malicious Software Removal Tool (KB890830)
|
||||
- Microsoft Defender definitions (KB2267602)
|
||||
- `compact` flag works — produces standalone qcow2 without backing file
|
||||
- Image grows from 5.02 GB to ~8.33 GB with updates
|
||||
### Build 1: First attempt (no VNC, wrong attr path)
|
||||
```bash
|
||||
nix build --print-build-logs --impure --option sandbox relaxed --expr '
|
||||
let vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
|
||||
in vmixLib.images.win10.ltsc.updated
|
||||
'
|
||||
```
|
||||
**Result**: Error — `attribute 'images' missing`. The correct path is `vmixLib.windows.images.win10.ltsc.updated`, not `vmixLib.images.win10.ltsc.updated`.
|
||||
|
||||
### Issues found and fixed during session
|
||||
### Build 2: Correct path, VNC :1, original script (no wuauserv start)
|
||||
```bash
|
||||
nix build ... --expr '
|
||||
let vmixLib = ...;
|
||||
upstream = vmixLib.windows.images.win10.ltsc.upstream;
|
||||
windowsUpdate = vmixLib.windows.templates.essentials.windowsUpdate {};
|
||||
in vmixLib.windows.customizeImage upstream (windowsUpdate // { vncDisplay = ":1"; })
|
||||
'
|
||||
```
|
||||
**Result**: Build "succeeded" but image size identical to upstream (5.03 GB vs 5.02 GB). The COM API found 0 updates because:
|
||||
- Windows Update service (`wuauserv`) wasn't running in Audit Mode
|
||||
- No time given for service to initialize
|
||||
- Possible update-blocking policies active
|
||||
|
||||
1. **First build (no service start)**: COM API found 0 updates because `wuauserv` wasn't running
|
||||
- Fix: added `net start wuauserv` + `sc config wuauserv start= auto` + remove blocking policies + 30s wait
|
||||
**Build log showed**: QEMU booted, script ran, shut down — no errors visible in nix log (VNC output isn't captured).
|
||||
|
||||
2. **Wrapper deletes script before reboot**: After round 1, the wrapper's `del /q C:\vmix-audit-script.cmd` runs before reboot completes, so RunOnce points to deleted file
|
||||
- Fix: script copies itself to `C:\vmix-update-continue.cmd` before rebooting; registers that path in RunOnce
|
||||
### Build 3: Added wuauserv start, policy removal, 30s wait
|
||||
Updated `windows-update.nix` to add:
|
||||
```batch
|
||||
net start wuauserv 2>nul
|
||||
sc config wuauserv start= auto
|
||||
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /f 2>nul
|
||||
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /f 2>nul
|
||||
timeout /t 30 /nobreak >nul
|
||||
```
|
||||
|
||||
3. **Audit Mode lost after reboot**: Cumulative updates can reset the Audit Mode flag, causing Windows to enter OOBE instead of Audit Mode after reboot
|
||||
- Fix: explicitly set `AuditBoot=1` and `AuditInProgress=1` registry keys before rebooting
|
||||
Also added EULA acceptance and try/catch error handling to the PowerShell block.
|
||||
|
||||
4. **No shutdown after round 2**: When script runs from RunOnce (not wrapper), no shutdown command executes
|
||||
- Fix: script always calls `shutdown /s /f /t 10` when done, regardless of invocation method
|
||||
**Build with VNC :55, chained virtio tools first**:
|
||||
```bash
|
||||
nix build ... --expr '
|
||||
let vmixLib = ...;
|
||||
upstream = vmixLib.windows.images.win10.ltsc.upstream;
|
||||
withVirtio = vmixLib.windows.customizeImage upstream vmixLib.windows.templates.essentials.virtioTools;
|
||||
windowsUpdate = vmixLib.windows.templates.essentials.windowsUpdate {};
|
||||
in vmixLib.windows.customizeImage withVirtio (windowsUpdate // { vncDisplay = ":55"; compact = false; })
|
||||
'
|
||||
```
|
||||
|
||||
### Remaining TODO (for next session on faster machine)
|
||||
- [ ] Verify the Audit Mode preservation fix (AuditBoot + AuditInProgress registry keys) actually works — last build was still in round 1 install when session ended
|
||||
- [ ] The cumulative update install takes ~60-90 min with 4GB RAM — consider using 8GB for faster builds
|
||||
- [ ] Test round 2 → round 3 flow (does it find additional updates after cumulative?)
|
||||
- [ ] Test with `compact = true` to verify final image is standalone
|
||||
- [ ] Test full pipeline: `upstream → updated → basic → generalize`
|
||||
- [ ] Test win11 images too
|
||||
- [ ] Consider: should `windowsUpdate` go before or after `virtioTools` in the pipeline? Currently it's before (applied to raw upstream), but having virtio drivers might help with disk I/O performance during update install
|
||||
- [ ] The template is impure (downloads from Microsoft during build) — document this clearly
|
||||
- [ ] Consider adding a `maxRounds = 1` fast mode that skips the reboot cycle (just installs whatever doesn't need reboot)
|
||||
**VNC observations** (screenshots taken with `gvnccapture localhost:55 /tmp/screenshot.png`):
|
||||
|
||||
## Build command for testing
|
||||
- **T+2min**: Boot screen "Please wait"
|
||||
- **T+3min**: CMD window showing:
|
||||
```
|
||||
=== vmix audit: windows-update ===
|
||||
=== vmix: Windows Update round 1 of 3 ===
|
||||
The Windows Update service is starting.
|
||||
The Windows Update service was started successfully.
|
||||
[SC] ChangeServiceConfig SUCCESS
|
||||
Waiting for Windows Update service to initialize...
|
||||
Searching for updates...
|
||||
```
|
||||
- **T+7min**: Found 6 updates:
|
||||
```
|
||||
Found 6 updates
|
||||
- 2022-02 Cumulative Update Preview for .NET Framework 3.5 and 4.8 for Windows 10 Version 21H2 for x64 (KB5010472)
|
||||
- Microsoft .NET Framework 4.8.1 for Windows 10 Version 21H2 for x64 (KB5011048)
|
||||
- Windows Malicious Software Removal Tool x64 - v5.141 (KB890830)
|
||||
- 2026-05 Cumulative Update for .NET Framework 3.5, 4.8 and 4.8.1 for Windows 10 Version 21H2 for x64 (KB5088859)
|
||||
- Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.451.326.0) - Current Channel (Broad)
|
||||
- 2026-05 Cumulative Update for Windows 10 Version 21H2 for x64-based Systems (KB5087544)
|
||||
Downloading...
|
||||
```
|
||||
- **T+12min**: "Installing..."
|
||||
- **T+20min through T+55min**: Still "Installing..." — KB5087544 is a massive cumulative update spanning Oct 2021 → May 2026
|
||||
- **T+58min**: Sysprep dialog showing on desktop — the machine rebooted after update install and landed in Audit Mode desktop, but the round 2 script didn't run
|
||||
|
||||
**Issue discovered**: The wrapper script (`vmix-audit-wrapper.cmd`) deletes `C:\vmix-audit-script.cmd` after the audit script returns but before the reboot completes. The RunOnce entry points to the deleted file.
|
||||
|
||||
### Build 4: Added script copy fix + shutdown fix
|
||||
|
||||
Updated script to:
|
||||
1. Copy itself to `C:\vmix-update-continue.cmd` before rebooting
|
||||
2. Register `cmd /c C:\vmix-update-continue.cmd` in RunOnce (not the original path)
|
||||
3. Always call `shutdown /s /f /t 10` when done (handles both wrapper and RunOnce invocation)
|
||||
|
||||
**Same build command as Build 3.**
|
||||
|
||||
**VNC observations**:
|
||||
- **T+3min**: Round 1 started, same 6 updates found
|
||||
- **T+7min**: Downloading...
|
||||
- **T+12min**: Installing...
|
||||
- **T+55min**: TianoCore UEFI boot screen — machine rebooted!
|
||||
- **T+57min**: "Working on updates — 90% complete" — Windows finalizing update installation after reboot
|
||||
- **T+62min**: "Working on updates — 19% complete" (different counter — this is the post-reboot finalization)
|
||||
- **T+70min**: Sysprep dialog... but NO round 2 script running
|
||||
|
||||
**Issue discovered**: After reboot, Windows exited Audit Mode and entered OOBE ("Choose your keyboard layout" screen) instead of staying in Audit Mode. The cumulative update reset the Audit Mode flags during its finalization pass.
|
||||
|
||||
Wait — actually on this build we saw the Sysprep dialog (which IS Audit Mode). The issue was the RunOnce not firing. On a subsequent build, we saw the keyboard layout screen (OOBE). The behavior is inconsistent.
|
||||
|
||||
### Build 5: Added Audit Mode preservation
|
||||
|
||||
Added registry keys before reboot to preserve Audit Mode:
|
||||
```batch
|
||||
reg add "HKLM\SYSTEM\Setup\Status\AuditBoot" /v AuditBoot /t REG_DWORD /d 1 /f
|
||||
reg add "HKLM\SYSTEM\Setup" /v AuditInProgress /t REG_DWORD /d 1 /f
|
||||
```
|
||||
|
||||
**Same build command. VNC observations**:
|
||||
- **T+3min**: Round 1 started, same 6 updates found
|
||||
- **T+70min**: Still on "Installing..." — round 1 didn't finish in 70 min on this machine
|
||||
- **T+100min**: "Working on updates — 19% complete" — rebooted, finalizing
|
||||
- **T+110min**: "Choose your keyboard layout" — OOBE screen again!
|
||||
|
||||
**Session ended here** — the Audit Mode preservation fix hasn't been verified as working yet. The machine was moved to a faster machine for continued testing.
|
||||
|
||||
## Detailed Issue Analysis
|
||||
|
||||
### Issue 1: Windows Update service not running
|
||||
**Root cause**: In Audit Mode, the Windows Update service (`wuauserv`) has `Start=3` (manual) and isn't auto-started. The COM API requires the service to be running to search for updates.
|
||||
|
||||
**Fix**: Explicitly start the service and set it to auto-start:
|
||||
```batch
|
||||
net start wuauserv 2>nul
|
||||
sc config wuauserv start= auto
|
||||
```
|
||||
|
||||
Also remove any group policies that block updates (LTSC may have these pre-configured):
|
||||
```batch
|
||||
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /f 2>nul
|
||||
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /f 2>nul
|
||||
```
|
||||
|
||||
And wait 30 seconds for the service to initialize and sync with Microsoft servers on first round.
|
||||
|
||||
### Issue 2: Wrapper deletes script before reboot completes
|
||||
**Root cause**: The `customizeImage.nix` wrapper script flow:
|
||||
```
|
||||
call C:\vmix-audit-script.cmd ← our update script
|
||||
del /q C:\vmix-audit-script.cmd ← DELETE happens here
|
||||
shutdown /s /t 5 ← may override our shutdown /r
|
||||
del /q C:\vmix-audit-wrapper.cmd
|
||||
```
|
||||
|
||||
When our script calls `shutdown /r /f /t 0` and then `exit /b`, control returns to the wrapper. The wrapper deletes the script file. Even though `shutdown /r /t 0` was called, the batch file continues executing and the delete happens before the system actually reboots.
|
||||
|
||||
The RunOnce entry points to `C:\vmix-audit-script.cmd` which no longer exists after reboot.
|
||||
|
||||
Additionally, the wrapper's `shutdown /s /t 5` may override our `shutdown /r /t 0` (though in practice the `/t 0` reboot usually wins).
|
||||
|
||||
**Fix**: Copy the script to a separate path before rebooting:
|
||||
```batch
|
||||
copy /y "%~f0" "C:\vmix-update-continue.cmd" >nul
|
||||
reg add "HKLM\...\RunOnce" /v vmixUpdate /d "cmd /c C:\vmix-update-continue.cmd" /f
|
||||
```
|
||||
|
||||
The wrapper only knows about `C:\vmix-audit-script.cmd` and `C:\vmix-audit-wrapper.cmd`. It doesn't know about `C:\vmix-update-continue.cmd`, so that file survives.
|
||||
|
||||
### Issue 3: Audit Mode lost after update reboot
|
||||
**Root cause**: Windows Audit Mode is maintained by registry keys:
|
||||
- `HKLM\SYSTEM\Setup\Status\AuditBoot` (AuditBoot = 1)
|
||||
- `HKLM\SYSTEM\Setup` (AuditInProgress = 1)
|
||||
|
||||
Some cumulative updates include "specialize" or "generalize" passes during their finalization that can clear these keys, causing Windows to transition from Audit Mode to OOBE on the next boot.
|
||||
|
||||
**Fix**: Explicitly set the Audit Mode registry keys right before rebooting:
|
||||
```batch
|
||||
reg add "HKLM\SYSTEM\Setup\Status\AuditBoot" /v AuditBoot /t REG_DWORD /d 1 /f
|
||||
reg add "HKLM\SYSTEM\Setup" /v AuditInProgress /t REG_DWORD /d 1 /f
|
||||
```
|
||||
|
||||
**Status**: NOT YET VERIFIED. The last build with this fix was still in the install phase when the session ended. The fix may or may not be sufficient — some updates may clear these keys AFTER the reboot during the "Working on updates X% complete" phase, which would happen after our registry writes.
|
||||
|
||||
**Alternative approaches if the fix doesn't work**:
|
||||
1. Use `C:\Windows\System32\Sysprep\sysprep.exe /audit /reboot /quiet` instead of `shutdown /r` — this explicitly tells Windows to re-enter Audit Mode
|
||||
2. Set up a `SetupComplete.cmd` or `Specialize` unattend pass that forces Audit Mode
|
||||
3. Use a scheduled task instead of RunOnce (scheduled tasks persist across mode transitions)
|
||||
4. Accept single-round updates only (`maxRounds = 1`) — no reboot needed if updates don't require it
|
||||
|
||||
### Issue 4: No shutdown after round 2
|
||||
**Root cause**: When the script is invoked via RunOnce (round 2+), it runs directly — not through the wrapper. The wrapper is what normally calls `shutdown /s /t 5` after the script completes. Without the wrapper, the script finishes and the machine just sits at the Audit Mode desktop indefinitely (until the 2-hour QEMU timeout).
|
||||
|
||||
**Fix**: The script itself calls `shutdown /s /f /t 10` when all rounds are complete. This is harmless when called from the wrapper (two shutdown commands — the second one either fails silently or is a no-op since shutdown is already pending).
|
||||
|
||||
## All Build Commands Used
|
||||
|
||||
### Quick evaluation (check attribute paths)
|
||||
```bash
|
||||
nix eval --impure --expr '
|
||||
let vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
|
||||
in builtins.attrNames vmixLib.windows.images.win10.ltsc
|
||||
'
|
||||
```
|
||||
|
||||
### Build updated image directly (no VNC, with compact)
|
||||
```bash
|
||||
nix build --print-build-logs --impure --option sandbox relaxed --expr '
|
||||
let
|
||||
vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
|
||||
in vmixLib.windows.images.win10.ltsc.updated
|
||||
'
|
||||
```
|
||||
|
||||
### Build with VNC monitoring on port 5901 (display :1)
|
||||
```bash
|
||||
nix build --print-build-logs --impure --option sandbox relaxed --expr '
|
||||
let
|
||||
vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
|
||||
upstream = vmixLib.windows.images.win10.ltsc.upstream;
|
||||
windowsUpdate = vmixLib.windows.templates.essentials.windowsUpdate {};
|
||||
in vmixLib.windows.customizeImage upstream (windowsUpdate // { vncDisplay = ":1"; })
|
||||
'
|
||||
```
|
||||
|
||||
### Build with virtio tools + VNC :55 + no compact (debugging)
|
||||
```bash
|
||||
nix build --print-build-logs --impure --option sandbox relaxed --expr '
|
||||
let
|
||||
|
|
@ -89,7 +399,132 @@ nix build --print-build-logs --impure --option sandbox relaxed --expr '
|
|||
'
|
||||
```
|
||||
|
||||
Monitor via VNC on port 5955 (`localhost:55`):
|
||||
### Build with more RAM (8GB) for faster installs
|
||||
```bash
|
||||
gvnccapture localhost:55 /tmp/screenshot.png
|
||||
nix build --print-build-logs --impure --option sandbox relaxed --expr '
|
||||
let
|
||||
vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
|
||||
upstream = vmixLib.windows.images.win10.ltsc.upstream;
|
||||
windowsUpdate = vmixLib.windows.templates.essentials.windowsUpdate {};
|
||||
in vmixLib.windows.customizeImage upstream (windowsUpdate // { vncDisplay = ":55"; memSize = 8192; })
|
||||
'
|
||||
```
|
||||
|
||||
### Build with more rounds
|
||||
```bash
|
||||
nix build --print-build-logs --impure --option sandbox relaxed --expr '
|
||||
let
|
||||
vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
|
||||
upstream = vmixLib.windows.images.win10.ltsc.upstream;
|
||||
windowsUpdate = vmixLib.windows.templates.essentials.windowsUpdate { maxRounds = 5; };
|
||||
in vmixLib.windows.customizeImage upstream (windowsUpdate // { vncDisplay = ":55"; })
|
||||
'
|
||||
```
|
||||
|
||||
### Full pipeline test (upstream → updated → basic → generalize)
|
||||
```bash
|
||||
nix build --print-build-logs --impure --option sandbox relaxed --expr '
|
||||
let
|
||||
vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
|
||||
in vmixLib.windows.images.win10.ltsc.basic.generalize {
|
||||
username = "User"; password = ""; hostname = "WIN-VM";
|
||||
vncDisplay = ":55";
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
### VNC monitoring commands
|
||||
```bash
|
||||
# Take a screenshot
|
||||
nix-shell -p gtk-vnc --run 'gvnccapture localhost:55 /tmp/screenshot.png'
|
||||
|
||||
# Connect with a VNC viewer
|
||||
vncviewer localhost:5955
|
||||
|
||||
# Check if QEMU is running
|
||||
ps aux | grep 'qemu-system' | grep -v grep
|
||||
|
||||
# Check nix build log for a store path
|
||||
nix log /nix/store/HASH-windows-update-....qcow2
|
||||
|
||||
# Check image info
|
||||
nix-shell -p qemu --run 'qemu-img info /nix/store/HASH-....qcow2'
|
||||
```
|
||||
|
||||
## Image sizes observed
|
||||
| Image | disk size | virtual size |
|
||||
|-------|-----------|-------------|
|
||||
| upstream (win10-ltsc-2021) | 5.02 GB | 64 GB |
|
||||
| updated (killed build, round 1 only) | 8.33 GB | 64 GB |
|
||||
| upstream (compacted, no updates) | 5.03 GB | 64 GB |
|
||||
|
||||
## Updates found by Windows Update (Win10 LTSC 2021 → May 2026)
|
||||
1. **2022-02 Cumulative Update Preview for .NET Framework 3.5 and 4.8** (KB5010472)
|
||||
2. **Microsoft .NET Framework 4.8.1** for Windows 10 21H2 x64 (KB5011048)
|
||||
3. **Windows Malicious Software Removal Tool x64 v5.141** (KB890830)
|
||||
4. **2026-05 Cumulative Update for .NET Framework 3.5, 4.8 and 4.8.1** (KB5088859)
|
||||
5. **Security Intelligence Update for Microsoft Defender Antivirus** (KB2267602, Version 1.451.326.0+)
|
||||
6. **2026-05 Cumulative Update for Windows 10 Version 21H2** (KB5087544) — the big one, ~870 MB
|
||||
|
||||
## Timing observations (4 vCPU, 4GB RAM machine)
|
||||
- Nix evaluation: ~30 seconds
|
||||
- VirtIO tools step (cached): instant
|
||||
- Windows boot to Audit Mode desktop: ~60 seconds
|
||||
- Windows Update search: ~2-3 minutes
|
||||
- Download all 6 updates: ~3-5 minutes
|
||||
- Install all updates (especially KB5087544): **50-70 minutes**
|
||||
- Reboot + "Working on updates" finalization: ~10-15 minutes
|
||||
- Total for round 1: ~70-90 minutes
|
||||
- DISM cleanup: ~2-5 minutes
|
||||
- qemu-img convert (compact): ~2-3 minutes
|
||||
|
||||
## Remaining TODO (for next session on faster machine)
|
||||
- [ ] **CRITICAL**: Verify the Audit Mode preservation fix works (AuditBoot + AuditInProgress registry keys)
|
||||
- [ ] If Audit Mode fix doesn't work, try `sysprep /audit /reboot /quiet` instead of `shutdown /r`
|
||||
- [ ] Test round 2 → round 3 flow (find additional updates after cumulative? likely Defender definition updates)
|
||||
- [ ] Test with `compact = true` to verify final standalone image
|
||||
- [ ] Test full pipeline: `upstream → updated → basic → generalize`
|
||||
- [ ] Test win11 images
|
||||
- [ ] Consider using 8GB RAM (`memSize = 8192`) for faster update installs
|
||||
- [ ] Consider: should `windowsUpdate` go before or after `virtioTools`? Currently before in the pipeline, but the explicit build command chains virtio first for better I/O
|
||||
- [ ] The template is impure (downloads from Microsoft during build) — this is intentional but should be documented
|
||||
- [ ] Consider `maxRounds = 1` mode for builds where you only want non-reboot updates
|
||||
- [ ] The `compact` step doesn't compress — consider adding `-c` flag to `qemu-img convert` for compressed qcow2
|
||||
- [ ] The wrapper's `shutdown /s /t 5` may still race with the script's `shutdown /r /t 0` — consider using `shutdown /a` (abort) before `shutdown /r` to cancel any pending shutdown
|
||||
|
||||
## Architecture notes
|
||||
|
||||
### How customizeImage works (for context)
|
||||
1. Creates a COW overlay on the original image: `qemu-img create -f qcow2 -b ${originalImage} -F qcow2 disk.qcow2`
|
||||
2. Optionally resizes: `qemu-img resize disk.qcow2 ${diskSize}`
|
||||
3. Merges offline registry entries via `virt-win-reg --merge`
|
||||
4. Injects audit script + wrapper via `virt-customize --upload`
|
||||
5. Adds RunOnce registry entry for the wrapper
|
||||
6. Boots QEMU with the image (user networking, UEFI, VirtIO or AHCI)
|
||||
7. The wrapper runs the audit script, then shuts down
|
||||
8. Optionally compacts: `qemu-img convert -O qcow2`
|
||||
9. Moves result to `$out`
|
||||
|
||||
### The wrapper problem
|
||||
The wrapper (`vmix-audit-wrapper.cmd`) is designed for simple, single-boot templates:
|
||||
```batch
|
||||
call C:\vmix-audit-script.cmd
|
||||
del /q C:\vmix-audit-script.cmd
|
||||
shutdown /s /t 5
|
||||
del /q C:\vmix-audit-wrapper.cmd
|
||||
```
|
||||
|
||||
This is fine for templates that don't reboot. But the Windows Update template needs multiple reboots, which conflicts with the wrapper's assumptions. The workarounds (copy script, self-shutdown) are necessary because modifying the wrapper would affect all templates.
|
||||
|
||||
A future improvement might be to add a `multiboot` flag to customizeImage that changes the wrapper behavior for templates that need reboots.
|
||||
|
||||
### Why QEMU user networking works for Windows Update
|
||||
QEMU's `-nic user` (SLIRP) provides NAT networking. The guest gets DHCP and can reach the internet via the host. Windows Update uses HTTPS to Microsoft's servers, which works through NAT. No special firewall rules or port forwarding needed.
|
||||
|
||||
### Why VirtIO tools are chained before updates (in test builds)
|
||||
The upstream image uses VirtIO storage (`if=virtio`) but doesn't have VirtIO guest tools installed. The update template doesn't need guest tools to work, but having them improves:
|
||||
- Disk I/O performance (VirtIO balloon, better driver)
|
||||
- Memory management
|
||||
- Guest agent for monitoring
|
||||
|
||||
In the pipeline (`win10/images.nix`), updates are applied directly to `upstream` without virtio tools, because virtio tools installation is part of the `basic` step. For testing, we chain them explicitly.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue