diff --git a/lib/images/windows/templates/generalize.nix b/lib/images/windows/templates/generalize.nix index b9456ea..b883d89 100644 --- a/lib/images/windows/templates/generalize.nix +++ b/lib/images/windows/templates/generalize.nix @@ -225,19 +225,47 @@ in # Fires at the real account's first logon (HKLM RunOnce = next user to log # on), i.e. after the reboot below, once the bootstrap is no longer in use. Set-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce' ` - -Name vmixUserCleanup -Value 'cmd /c C:\vmix-user-cleanup.cmd' -Type String + -Name vmixFinalize -Value 'cmd /c C:\vmix-finalize.cmd' -Type String ''; - # Runs once as the real account (RunOnce, after the hand-over reboot): retire - # the bootstrap account and its profile, and apply the per-user desktop tint - # (which the bootstrap ran against on the first boot, before this account - # existed). Bootstrap is idle here, so its profile is safe to remove. - userCleanupScript = pkgs.writeText "vmix-user-cleanup.cmd" '' + # Runs once as the real account (RunOnce, after the hand-over reboot), so the + # fresh machine SID and the real profile already exist. This is where activation + # belongs -- on that fresh SID, not the throwaway bootstrap's. It then finishes + # per-user setup, retires the bootstrap, unmounts the config CD for good, and + # wipes every vmix artifact off C:\ so the running machine carries no leftover + # setup files. Self-deletes last. + finalizeScript = pkgs.writeText "vmix-finalize.cmd" '' @echo off + :: 1) Activate Windows on the real account's fresh SID (TSforge, offline). + cscript //nologo C:\Windows\System32\slmgr.vbs /ipk M7XTQ-FN8P6-TTKYV-9D4CC-J462D + cscript //nologo C:\Windows\System32\slmgr.vbs /rilc + net stop sppsvc /y 2>nul + net start sppsvc + ping -n 8 127.0.0.1 >nul + if exist C:\MAS_AIO.cmd ( echo. | call C:\MAS_AIO.cmd /Z-Windows ) + if exist "C:\Program Files\Microsoft Office\root\Office16\WINWORD.EXE" ( + if exist C:\MAS_AIO.cmd ( echo. | call C:\MAS_AIO.cmd /Ohook ) + ) + :: 2) Per-user desktop tint, now that the real account is logged in. + powershell -NoProfile -ExecutionPolicy Bypass -File C:\vmix-apply-config.ps1 >nul 2>&1 + :: 3) Retire the bootstrap account and its profile (idle now). powershell -NoProfile -ExecutionPolicy Bypass -Command "Get-CimInstance Win32_UserProfile | Where-Object { $_.LocalPath -like '*\${username}' } | Remove-CimInstance -ErrorAction SilentlyContinue" net user ${username} /delete >nul 2>&1 - powershell -NoProfile -ExecutionPolicy Bypass -File C:\vmix-apply-config.ps1 >nul 2>&1 - del /q C:\vmix-user-cleanup.cmd 2>nul + :: 4) Unmount the config CD and keep it unmounted -- drop its letter and stop + :: the mount manager auto-lettering it (D: keeps its explicit assignment). + powershell -NoProfile -ExecutionPolicy Bypass -Command "Get-CimInstance Win32_Volume -Filter 'DriveType=5 AND DriveLetter IS NOT NULL' | ForEach-Object { $_.DriveLetter=$null; [void]($_ | Set-CimInstance) }" + > C:\Windows\Temp\vmix-am.txt echo automount disable + >> C:\Windows\Temp\vmix-am.txt echo automount scrub + diskpart /s C:\Windows\Temp\vmix-am.txt >nul 2>&1 + del /q C:\Windows\Temp\vmix-am.txt 2>nul + :: 5) Wipe every vmix setup artifact from C:\. + del /q C:\MAS_AIO.cmd C:\vmix-config.ps1 C:\vmix-data-initialized 2>nul + del /q C:\vmix-load-config.cmd C:\vmix-apply-config.ps1 C:\vmix-create-user.ps1 2>nul + del /q C:\vmix-init-data-disk.cmd C:\vmix-data-profile.cmd C:\vmix-heal-profile.ps1 2>nul + del /q C:\vmix-static-ip.cmd C:\vmix-static-ip.ps1 2>nul + del /q C:\oobe-unattend.xml C:\post-oobe.cmd 2>nul + :: 6) Self-delete. + (goto) 2>nul & del "%~f0" ''; # Thin launcher, so the scheduled task has a cmd to point at. @@ -513,6 +541,7 @@ in powershell -Command "Get-AppxPackage *MicrosoftEdgeDevToolsClient* | Remove-AppxPackage -ErrorAction SilentlyContinue" + ${lib.optionalString (!configMedium) '' :: Re-install product key and licenses to restore activation IDs after sysprep cscript //nologo C:\Windows\System32\slmgr.vbs /ipk M7XTQ-FN8P6-TTKYV-9D4CC-J462D cscript //nologo C:\Windows\System32\slmgr.vbs /rilc @@ -531,6 +560,9 @@ in ) ) del /q C:\MAS_AIO.cmd 2>nul + ''} + :: configMedium: activation is deferred to the boot-2 finalize, so it runs + :: on the real account's fresh SID. MAS_AIO.cmd is kept for it until then. ${lib.optionalString enableRDP '' :: Enable RDP @@ -556,7 +588,7 @@ in powercfg /hibernate off ''} - ${lib.optionalString (staticIP != null || configMedium) '' + ${lib.optionalString (staticIP != null && !configMedium) '' :: This VM's only NIC sits on a macvtap bridged to the host's LAN, so its :: address is a LAN address that nothing hands out -- the guest asserts it. :: Registered to run at every boot rather than applied here. OOBE runs in @@ -568,10 +600,20 @@ in schtasks /create /tn "vmix-static-ip" /tr "C:\vmix-static-ip.cmd" /sc onstart /ru SYSTEM /rl HIGHEST /f > nul 2>&1 ''} - ${lib.optionalString (dataDisk != null) '' + ${lib.optionalString configMedium '' + :: configMedium (sealed images) run OOBE on the real target NIC, so the + :: address is set here once and left in the persistent store -- it survives + :: reboots on its own, no per-boot task and no script left on disk. Runs on + :: this bootstrap boot so the real account is already reachable on boot 2. + powershell -NoProfile -ExecutionPolicy Bypass -File C:\vmix-static-ip.ps1 > C:\Windows\Temp\vmix-static-ip.log 2>&1 + ''} + + ${lib.optionalString (dataDisk != null && !configMedium) '' :: Ensures D: is assigned on every boot -- the image ships without a :: persisted letter for the data disk -- and heals a profile that went :: temporary before D: was ready. Onstart / SYSTEM, like the address task. + :: configMedium does not need this: the letter persists via MountedDevices + :: in the overlay after the first boot, so there is nothing to re-assert. schtasks /create /tn "vmix-data-profile" /tr "C:\vmix-data-profile.cmd" /sc onstart /ru SYSTEM /rl HIGHEST /f > nul 2>&1 ''} @@ -727,7 +769,7 @@ in { { source = loadConfigScript; dest = "/vmix-load-config.cmd"; } { source = applyConfigScript; dest = "/vmix-apply-config.ps1"; } { source = createUserScript; dest = "/vmix-create-user.ps1"; } - { source = userCleanupScript; dest = "/vmix-user-cleanup.cmd"; } + { source = finalizeScript; dest = "/vmix-finalize.cmd"; } ] ++ lib.optionals (staticIP != null || configMedium) [ { source = staticIPScript; dest = "/vmix-static-ip.cmd"; }