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 See wip/win10-update.session.md for full context and TODOs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3b454b749a
commit
f33b8e7ce3
5 changed files with 222 additions and 6 deletions
|
|
@ -20,6 +20,7 @@ in rec {
|
||||||
bestPerformance = import ./essentials/best-performance.nix args;
|
bestPerformance = import ./essentials/best-performance.nix args;
|
||||||
clearFileAssociations = import ./essentials/clear-file-associations.nix args;
|
clearFileAssociations = import ./essentials/clear-file-associations.nix args;
|
||||||
virtioDrivers = import ./essentials/virtio-drivers.nix args;
|
virtioDrivers = import ./essentials/virtio-drivers.nix args;
|
||||||
|
windowsUpdate = import ./essentials/windows-update.nix args;
|
||||||
};
|
};
|
||||||
|
|
||||||
# Applications
|
# Applications
|
||||||
|
|
|
||||||
114
lib/images/windows/templates/essentials/windows-update.nix
Normal file
114
lib/images/windows/templates/essentials/windows-update.nix
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
# Apply all available Windows Updates via the Windows Update COM API in Audit Mode.
|
||||||
|
# Handles reboots automatically — re-registers via RunOnce and continues updating.
|
||||||
|
# Compacts the image afterward to flatten the COW chain.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# essentials.windowsUpdate {}
|
||||||
|
# essentials.windowsUpdate { maxRounds = 5; }
|
||||||
|
{ pkgs, lib, ... }:
|
||||||
|
{ maxRounds ? 3 }:
|
||||||
|
{
|
||||||
|
name = "windows-update";
|
||||||
|
compact = true;
|
||||||
|
memSize = 4096;
|
||||||
|
qemuTimeout = 7200;
|
||||||
|
auditScript = ''
|
||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
:: Track update round via a counter file
|
||||||
|
set "ROUND_FILE=C:\vmix-update-round.txt"
|
||||||
|
set "MAX_ROUNDS=${toString maxRounds}"
|
||||||
|
|
||||||
|
if exist "%ROUND_FILE%" (
|
||||||
|
set /p ROUND=<"%ROUND_FILE%"
|
||||||
|
) else (
|
||||||
|
set "ROUND=1"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo === vmix: Windows Update round %ROUND% of %MAX_ROUNDS% ===
|
||||||
|
|
||||||
|
:: Ensure Windows Update service is running
|
||||||
|
net start wuauserv 2>nul
|
||||||
|
sc config wuauserv start= auto
|
||||||
|
|
||||||
|
:: Remove any update-blocking policies (LTSC may have these)
|
||||||
|
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /f 2>nul
|
||||||
|
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /f 2>nul
|
||||||
|
|
||||||
|
:: Give the service time to initialize on first round
|
||||||
|
if "%ROUND%"=="1" (
|
||||||
|
echo Waiting for Windows Update service to initialize...
|
||||||
|
timeout /t 30 /nobreak >nul
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Run Windows Update via PowerShell COM API
|
||||||
|
powershell -ExecutionPolicy Bypass -Command ^
|
||||||
|
"try {" ^
|
||||||
|
" $session = New-Object -ComObject Microsoft.Update.Session;" ^
|
||||||
|
" $searcher = $session.CreateUpdateSearcher();" ^
|
||||||
|
" Write-Host 'Searching for updates...';" ^
|
||||||
|
" $result = $searcher.Search('IsInstalled=0');" ^
|
||||||
|
" $count = $result.Updates.Count;" ^
|
||||||
|
" Write-Host \"Found $count updates\";" ^
|
||||||
|
" if ($count -eq 0) { exit 0 };" ^
|
||||||
|
" foreach ($u in $result.Updates) { Write-Host \" - $($u.Title)\" };" ^
|
||||||
|
" $updatesToInstall = New-Object -ComObject Microsoft.Update.UpdateColl;" ^
|
||||||
|
" foreach ($u in $result.Updates) {" ^
|
||||||
|
" if ($u.EulaAccepted -eq $false) { $u.AcceptEula() };" ^
|
||||||
|
" $updatesToInstall.Add($u) | Out-Null" ^
|
||||||
|
" };" ^
|
||||||
|
" $downloader = $session.CreateUpdateDownloader();" ^
|
||||||
|
" $downloader.Updates = $updatesToInstall;" ^
|
||||||
|
" Write-Host 'Downloading...';" ^
|
||||||
|
" $downloader.Download() | Out-Null;" ^
|
||||||
|
" $installer = $session.CreateUpdateInstaller();" ^
|
||||||
|
" $installer.Updates = $updatesToInstall;" ^
|
||||||
|
" Write-Host 'Installing...';" ^
|
||||||
|
" $installResult = $installer.Install();" ^
|
||||||
|
" Write-Host \"Result: $($installResult.ResultCode)\";" ^
|
||||||
|
" for ($i = 0; $i -lt $updatesToInstall.Count; $i++) {" ^
|
||||||
|
" $hr = $installResult.GetUpdateResult($i).HResult;" ^
|
||||||
|
" Write-Host \" $($updatesToInstall.Item($i).Title): code=$hr\"" ^
|
||||||
|
" };" ^
|
||||||
|
" if ($installResult.RebootRequired) { exit 3010 } else { exit 0 }" ^
|
||||||
|
"} catch {" ^
|
||||||
|
" Write-Host \"ERROR: $_\";" ^
|
||||||
|
" exit 1" ^
|
||||||
|
"}"
|
||||||
|
|
||||||
|
set "WU_EXIT=%ERRORLEVEL%"
|
||||||
|
echo Windows Update exit code: %WU_EXIT%
|
||||||
|
|
||||||
|
:: Cleanup component store
|
||||||
|
echo Cleaning up component store...
|
||||||
|
dism /Online /Cleanup-Image /StartComponentCleanup /ResetBase /Quiet 2>nul
|
||||||
|
|
||||||
|
:: Check if we need to reboot and continue
|
||||||
|
set /a "NEXT_ROUND=%ROUND%+1"
|
||||||
|
|
||||||
|
if "%WU_EXIT%"=="3010" (
|
||||||
|
if %ROUND% LSS %MAX_ROUNDS% (
|
||||||
|
echo Reboot required, scheduling round %NEXT_ROUND%...
|
||||||
|
echo %NEXT_ROUND% > "%ROUND_FILE%"
|
||||||
|
:: Copy script to a path the wrapper won't delete
|
||||||
|
copy /y "%~f0" "C:\vmix-update-continue.cmd" >nul
|
||||||
|
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" /v vmixUpdate /t REG_SZ /d "cmd /c C:\vmix-update-continue.cmd" /f
|
||||||
|
:: Preserve Audit Mode across reboot (updates can reset it)
|
||||||
|
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
|
||||||
|
:: Immediate reboot (preempts wrapper shutdown)
|
||||||
|
shutdown /r /f /t 0
|
||||||
|
exit /b
|
||||||
|
) else (
|
||||||
|
echo Max update rounds reached.
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Done — clean up and shutdown
|
||||||
|
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"
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,9 @@ rec {
|
||||||
upstreamISO = upstreamISOs.win10-ltsc-2021;
|
upstreamISO = upstreamISOs.win10-ltsc-2021;
|
||||||
productKey = "M7XTQ-FN8P6-TTKYV-9D4CC-J462D";
|
productKey = "M7XTQ-FN8P6-TTKYV-9D4CC-J462D";
|
||||||
};
|
};
|
||||||
basic = customizeImageFold upstream (with templates; [
|
# Apply all available Windows Updates (cumulative, .NET, Defender)
|
||||||
|
updated = customizeImage upstream (templates.essentials.windowsUpdate {});
|
||||||
|
basic = customizeImageFold updated (with templates; [
|
||||||
essentials.virtioTools
|
essentials.virtioTools
|
||||||
essentials.removeIE
|
essentials.removeIE
|
||||||
essentials.removeWMP
|
essentials.removeWMP
|
||||||
|
|
@ -38,8 +40,9 @@ rec {
|
||||||
productKey = "M7XTQ-FN8P6-TTKYV-9D4CC-J462D";
|
productKey = "M7XTQ-FN8P6-TTKYV-9D4CC-J462D";
|
||||||
useAHCI = true;
|
useAHCI = true;
|
||||||
};
|
};
|
||||||
|
laptopUpdated = customizeImage laptopUpstream (templates.essentials.windowsUpdate {});
|
||||||
|
|
||||||
laptopSlim = customizeImageFold laptopUpstream templates.bundles.laptopSlim;
|
laptopSlim = customizeImageFold laptopUpdated templates.bundles.laptopSlim;
|
||||||
|
|
||||||
laptop = customizeImageFold laptopUpstream templates.bundles.laptop;
|
laptop = customizeImageFold laptopUpdated templates.bundles.laptop;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ rec {
|
||||||
windowsVersionForVirtioDrivers = "w11";
|
windowsVersionForVirtioDrivers = "w11";
|
||||||
};
|
};
|
||||||
|
|
||||||
basic = customizeImageFold upstream (with templates; [
|
updated = customizeImage upstream (templates.essentials.windowsUpdate {});
|
||||||
|
basic = customizeImageFold updated (with templates; [
|
||||||
essentials.virtioTools
|
essentials.virtioTools
|
||||||
essentials.removeIE
|
essentials.removeIE
|
||||||
essentials.removeWMP
|
essentials.removeWMP
|
||||||
|
|
@ -45,9 +46,11 @@ rec {
|
||||||
windowsVersionForVirtioDrivers = "w11";
|
windowsVersionForVirtioDrivers = "w11";
|
||||||
};
|
};
|
||||||
|
|
||||||
laptopSlim = customizeImageFold laptopUpstream
|
laptopUpdated = customizeImage laptopUpstream (templates.essentials.windowsUpdate {});
|
||||||
|
|
||||||
|
laptopSlim = customizeImageFold laptopUpdated
|
||||||
(templates.bundles.laptopSlim ++ [ templates.reg.disableUCPD ]);
|
(templates.bundles.laptopSlim ++ [ templates.reg.disableUCPD ]);
|
||||||
|
|
||||||
laptop = customizeImageFold laptopUpstream
|
laptop = customizeImageFold laptopUpdated
|
||||||
(templates.bundles.laptop ++ [ templates.reg.disableUCPD ]);
|
(templates.bundles.laptop ++ [ templates.reg.disableUCPD ]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
95
wip/win10-update.session.md
Normal file
95
wip/win10-update.session.md
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
# Windows Update Template - Development Session
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## What was built
|
||||||
|
|
||||||
|
### New files
|
||||||
|
- `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/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)
|
||||||
|
|
||||||
|
### 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
|
||||||
|
```nix
|
||||||
|
# Online mode (default) - triggers Windows Update service
|
||||||
|
essentials.windowsUpdate {}
|
||||||
|
essentials.windowsUpdate { maxRounds = 5; }
|
||||||
|
|
||||||
|
# In image pipeline
|
||||||
|
updated = customizeImage upstream (templates.essentials.windowsUpdate {});
|
||||||
|
basic = customizeImageFold updated [ ... ];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test results
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
### Issues found and fixed during session
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
|
||||||
|
## Build command for testing
|
||||||
|
```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;
|
||||||
|
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; })
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
Monitor via VNC on port 5955 (`localhost:55`):
|
||||||
|
```bash
|
||||||
|
gvnccapture localhost:55 /tmp/screenshot.png
|
||||||
|
```
|
||||||
Loading…
Add table
Add a link
Reference in a new issue