Files
wehub-resource-sync 3e779be6f3
CI / lint (push) Failing after 13m4s
CI / test (3.11, ubuntu-latest) (push) Failing after 2m4s
CI / test (3.13, ubuntu-latest) (push) Successful in 13m30s
CI / test (3.14, ubuntu-latest) (push) Successful in 17m21s
CI / test (3.12, ubuntu-latest) (push) Successful in 17m55s
CI / discover-apps-ps (push) Successful in 1m56s
CI / test (3.9, ubuntu-latest) (push) Successful in 13m17s
CI / test (3.10, ubuntu-latest) (push) Successful in 26m21s
CI / audit (push) Successful in 13m38s
Deploy site / deploy (push) Has been cancelled
CI / test (3.14, ubuntu-24.04-arm) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:37 +08:00

763 lines
43 KiB
Batchfile

@echo off
REM First-boot OEM setup for winpodx Windows guest. Runs once during dockur's unattended install. Every action must stay idempotent - there is no guest-side re-run channel in 0.1.6 (push/exec bridge planned for a later release).
set WINPODX_OEM_VERSION=28
echo [WinPodX] Starting post-install configuration (version %WINPODX_OEM_VERSION%)...
REM ---------------------------------------------------------------------------
REM Windows Defender exclusions - ABSOLUTE FIRST STEP.
REM
REM Background: kernalix7's fresh installs 2026-05-02 through 2026-05-04
REM consistently died at line ~248 (PS Expand-Archive of the rdprrap zip
REM under C:\OEM\). install.log ended at "--- extract attempt 1" with no
REM follow-up; setup.log was never created; agent.ps1 never copied; agent
REM never came up. 3 disconnected User sessions accumulated in qwinsta on
REM every attempt. v0.3.0 (OEM v12) on the same hardware worked.
REM
REM Diff against v0.3.0 install.bat: the install.bat code itself is
REM essentially unchanged in the pre-extract section. The rdprrap zip
REM blob hash is identical. What changed: (1) the OEM bundle grew from 6
REM files to 13+ (added rdprrap-activate.ps1, hidden-launcher.vbs,
REM launch_uwp.ps1/vbs, agent-respawn.ps1, agent/agent.ps1); (2) the
REM dockur image was pinned to a specific Windows 11 digest in PR #83
REM (v0.3.1), and that newer Windows build ships a stricter Defender
REM real-time policy. The combination - more PS/VBS files staged in
REM C:\OEM\ alongside rdprrap-installer.exe (which patches termsrv.dll
REM and is naturally classifier-flagged as PUP) - triggers Defender
REM real-time scanning of C:\OEM\ during install.bat's first PS call.
REM Defender locks one of the files; PS Expand-Archive blocks waiting on
REM the lock; install.bat blocks waiting on PS; whole thing deadlocks
REM until something kicks the autologon session.
REM
REM Excluding C:\OEM and C:\winpodx from real-time scanning at the very
REM start of install.bat means none of our staged files ever get
REM scanned. This is safe because install.bat is the only thing writing
REM there, the contents are SHA-pinned to the bundle, and the user
REM workload runs from C:\Users\... (still scanned). It also keeps
REM rdprrap-installer.exe itself out of Defender's process list.
REM
REM Add-MpPreference is idempotent - re-running install.bat (e.g., on
REM container recreate) just re-asserts the exclusion silently.
REM C:\Users\Public\winpodx is where register-apps.ps1 stages the
REM reverse-open shim + its per-slug .exe copies (#425). That tiny,
REM stripped, unsigned Rust binary trips Defender's ML heuristic
REM (Trojan:Win32/Rafvartar!rfn -- a false positive), so it gets
REM quarantined and reverse-open silently breaks. Exclude the path (and
REM the shim process) here, first-step, so the exclusion is in place
REM before per-user logon stages the files. Add-MpPreference accepts a
REM not-yet-existent path, so registering it early is fine.
echo [WinPodX] Adding Windows Defender exclusions for C:\OEM, C:\winpodx, C:\Users\Public\winpodx, and the winpodx helper processes...
powershell -NoProfile -Command "try { Add-MpPreference -ExclusionPath 'C:\OEM','C:\winpodx','C:\Users\Public\winpodx' -ErrorAction Stop; Add-MpPreference -ExclusionProcess 'rdprrap-installer.exe','winpodx-reverse-open-shim.exe' -ErrorAction Stop } catch { Write-Output ('defender-exclusion: ' + $_.Exception.Message) }" >nul 2>&1
echo [WinPodX] Setting DNS...
netsh interface ip set dns "Ethernet" static 1.1.1.1
netsh interface ip add dns "Ethernet" 1.0.0.1 index=2
echo [WinPodX] Enabling Remote Desktop...
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server" /v fDenyTSConnections /t REG_DWORD /d 0 /f
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server" /v fSingleSessionPerUser /t REG_DWORD /d 0 /f
REM NLA off for automated FreeRDP; SecurityLayer=2 keeps TLS on the RDP channel.
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" /v UserAuthentication /t REG_DWORD /d 0 /f
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" /v SecurityLayer /t REG_DWORD /d 2 /f
echo [WinPodX] Enabling RemoteApp...
reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Terminal Server\TSAppAllowList" /v fDisabledAllowList /t REG_DWORD /d 1 /f
REM Without fInheritInitialProgram, Windows ignores /shell: and /app: from the RDP client.
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" /v fInheritInitialProgram /t REG_DWORD /d 1 /f
REM v0.2.1: cap bumped 10 -> 50. cfg.pod.max_sessions still controls the
REM actual desired count via _apply_max_sessions; this is just the
REM ceiling so the cfg value isn't silently clamped at OEM time.
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" /v MaxInstanceCount /t REG_DWORD /d 50 /f
REM Bug B (v0.1.9 / OEM v7): host suspend / long idle commonly leaves Windows
REM with TermService stalled or the virtual NIC in power-save, breaking RDP
REM while VNC keeps working. Two preventive measures so the host-side
REM recover_rdp_if_needed() helper has less to do.
echo [WinPodX] Disabling NIC power-save...
powershell -NoProfile -Command "Get-NetAdapter -ErrorAction SilentlyContinue | Where-Object {$_.Status -ne 'Disabled'} | Set-NetAdapterPowerManagement -AllowComputerToTurnOffDevice $false -ErrorAction SilentlyContinue" >nul 2>&1
echo [WinPodX] Configuring TermService recovery actions...
REM 3 attempts at 5s spacing, 24h reset window - Windows itself recovers
REM TermService crashes without needing host intervention.
sc.exe failure TermService reset= 86400 actions= restart/5000/restart/5000/restart/5000 >nul 2>&1
REM v0.1.9.1: RDP session timeout + keep-alive. v0.2.1 adjusts the
REM disconnection-time semantics:
REM * MaxIdleTime 0 = no idle timeout (active sessions never auto-disconnect)
REM * MaxConnectionTime 0 = no max session duration
REM * MaxDisconnectionTime 30000 ms = 30 sec - disconnected sessions
REM auto-LOGOFF after 30 s. Previously this was 0 ("never logoff"),
REM which left zombie disconnected sessions accumulating every time
REM the user closed a FreeRDP window. The next launch then triggered
REM "Select a session to reconnect to" dialog because Windows saw
REM the user had N old disconnected sessions. rdprrap allows
REM concurrent sessions but doesn't suppress that prompt - only
REM auto-logoff does.
REM Both the machine policy and the RDP-Tcp WinStation keys are set;
REM whichever Windows consults first finds the saner default.
echo [WinPodX] Disabling RDP session timeouts + enabling keep-alive...
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" /v MaxIdleTime /t REG_DWORD /d 0 /f >nul 2>&1
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" /v MaxDisconnectionTime /t REG_DWORD /d 30000 /f >nul 2>&1
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" /v MaxConnectionTime /t REG_DWORD /d 0 /f >nul 2>&1
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" /v KeepAliveEnable /t REG_DWORD /d 1 /f >nul 2>&1
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" /v KeepAliveInterval /t REG_DWORD /d 1 /f >nul 2>&1
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" /v MaxIdleTime /t REG_DWORD /d 0 /f >nul 2>&1
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" /v MaxDisconnectionTime /t REG_DWORD /d 30000 /f >nul 2>&1
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" /v MaxConnectionTime /t REG_DWORD /d 0 /f >nul 2>&1
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" /v KeepAliveTimeout /t REG_DWORD /d 1 /f >nul 2>&1
echo [WinPodX] Configuring firewall...
REM Delete-then-add keeps the rule idempotent; plain add creates duplicates on re-run.
netsh advfirewall firewall set rule group="Remote Desktop" new enable=yes 2>nul
netsh advfirewall firewall delete rule name="RDP TCP" >nul 2>&1
netsh advfirewall firewall delete rule name="RDP UDP" >nul 2>&1
netsh advfirewall firewall add rule name="RDP TCP" dir=in action=allow protocol=tcp localport=3389 2>nul
netsh advfirewall firewall add rule name="RDP UDP" dir=in action=allow protocol=udp localport=3389 2>nul
REM Allow inbound on the winpodx agent port. QEMU's user-mode NAT forwards
REM container:8765 -> Windows VM 10.0.2.15:8765, which Windows Firewall
REM blocks by default - kernalix7 saw curl timeout from the host on
REM 2026-04-30 even with agent.ps1 bound to 0.0.0.0:8765 because the SYN
REM never made it past the firewall. RDP got auto-allowed via the
REM "Remote Desktop" group rule above; 8765 needs an explicit rule.
netsh advfirewall firewall delete rule name="winpodx-agent" >nul 2>&1
netsh advfirewall firewall add rule name="winpodx-agent" dir=in action=allow protocol=tcp localport=8765 2>nul
echo [WinPodX] Optimizing for RDP...
reg add "HKCU\Control Panel\Desktop" /v DragFullWindows /t REG_SZ /d 0 /f
reg add "HKCU\Control Panel\Desktop" /v MenuShowDelay /t REG_SZ /d 0 /f
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects" /v VisualFXSetting /t REG_DWORD /d 2 /f
REM Pin Windows build so termsrv.dll stays stable; feature/build upgrades come via winpodx releases only.
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v NoAutoRebootWithLoggedOnUsers /t REG_DWORD /d 1 /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v TargetReleaseVersion /t REG_DWORD /d 1 /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v ProductVersion /t REG_SZ /d "Windows 11" /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v TargetReleaseVersionInfo /t REG_SZ /d "25H2" /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v DeferFeatureUpdates /t REG_DWORD /d 1 /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v DeferFeatureUpdatesPeriodInDays /t REG_DWORD /d 365 /f
REM Timezone is handled upstream now: winpodx sets the dockur TZ env var
REM in compose.yaml from cfg.pod.timezone (or host autodetect), and
REM dockur writes the <TimeZone> element into the Sysprep unattend.xml
REM on first boot. No tzutil call needed here -- Windows reads the
REM Sysprep value during OOBE. Leaving this block as a no-op marker so
REM the OEM script structure stays predictable across releases.
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v AllowCortana /t REG_DWORD /d 0 /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\DataCollection" /v AllowTelemetry /t REG_DWORD /d 0 /f
sc config WSearch start= disabled
net stop WSearch 2>nul
sc config SysMain start= disabled
net stop SysMain 2>nul
powercfg /h off
REM Print Spooler stays enabled for RDP printer redirection.
sc config WerSvc start= disabled
net stop WerSvc 2>nul
sc config DiagTrack start= disabled
net stop DiagTrack 2>nul
powercfg /setactive 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
REM Force every idle timeout to "never" so the RDP service / virtio NIC
REM don't get suspended out from under the host while the user is on a
REM coffee break. Without this the host pod_status() probe sees RDP
REM unreachable on a healthy container and shows "starting" forever
REM (#TBD, observed by kernalix7).
powercfg /change standby-timeout-ac 0
powercfg /change standby-timeout-dc 0
powercfg /change hibernate-timeout-ac 0
powercfg /change hibernate-timeout-dc 0
powercfg /change monitor-timeout-ac 0
powercfg /change monitor-timeout-dc 0
powercfg /change disk-timeout-ac 0
powercfg /change disk-timeout-dc 0
REM Modern Standby (S0 low-power idle) -- even with the timeouts above
REM it can drop the NIC. The platform role registry key forces
REM Desktop class so Modern Standby logic is bypassed.
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Power" /v PlatformAoAcOverride /t REG_DWORD /d 0 /f
REM Belt-and-braces: disable hardware-initiated wake / sleep transitions
REM via the AC profile too.
powercfg /setdcvalueindex SCHEME_CURRENT SUB_SLEEP STANDBYIDLE 0 2>nul
powercfg /setacvalueindex SCHEME_CURRENT SUB_SLEEP STANDBYIDLE 0 2>nul
powercfg /setactive SCHEME_CURRENT 2>nul
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Serialize" /v StartupDelayInMSec /t REG_DWORD /d 0 /f
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\BackgroundAccessApplications" /v GlobalUserDisabled /t REG_DWORD /d 1 /f
echo [WinPodX] Home folder is available at \\tsclient\home via RDP drive redirection
REM dockur's base image ships a "Shared" desktop item pointing to \\host.lan\Data (SMB) that we don't use; replace with Home/USB shortcuts to the RDP redirections.
echo [WinPodX] Creating desktop shortcuts to tsclient shares...
powershell -NoProfile -ExecutionPolicy Bypass -Command "$d=[Environment]::GetFolderPath('Desktop'); foreach($n in 'Shared','Shared.lnk'){ $p=Join-Path $d $n; if(Test-Path -LiteralPath $p){ Remove-Item -Force -Recurse -LiteralPath $p -ErrorAction SilentlyContinue } }; $s=New-Object -ComObject WScript.Shell; foreach($x in @(@('Home','\\tsclient\home'), @('USB','\\tsclient\media'))){ $l=$s.CreateShortcut((Join-Path $d ($x[0]+'.lnk'))); $l.TargetPath=$x[1]; $l.Save() }"
REM C:\winpodx is the persistent run dir for runtime-delivered helpers
REM (e.g. agent-keepalive.ps1, copied here by the keep-alive task). USB
REM media is reachable in every session via the \\tsclient\media RDP
REM redirection (the desktop "USB" shortcut above points at it); a real
REM block device is available through USB passthrough. There is no
REM drive-letter auto-mapper -- it couldn't surface a letter reliably in
REM RemoteApp sessions and kept destabilizing first boot (#613/#638).
mkdir C:\winpodx 2>nul
REM Clean up any legacy OEM updater task / file from pre-0.1.6 installs.
schtasks /delete /tn "WinpodxOEMUpdate" /f >nul 2>&1
if exist C:\winpodx\oem_updater.ps1 del /F /Q C:\winpodx\oem_updater.ps1 >nul 2>&1
REM Parenthesized echo strips the trailing space that `echo X > file` leaves behind.
(echo %WINPODX_OEM_VERSION%)>C:\winpodx\oem_version.txt
echo [WinPodX] Installing multi-session RDP (rdprrap) - offline bundle...
REM Bundle ships under config/oem/ and is staged into C:\OEM\ by dockur's unattended install.
REM The pin file (version / filename / sha256) lives next to the zip. No network
REM access is required - everything is copied straight from the staged folder.
set "RDPRRAP_PIN="
if exist "C:\OEM\rdprrap_version.txt" set "RDPRRAP_PIN=C:\OEM\rdprrap_version.txt"
set "RDPRRAP_VERSION="
set "RDPRRAP_FILENAME="
set "RDPRRAP_SHA256="
if defined RDPRRAP_PIN (
for /f "usebackq tokens=1,* delims==" %%A in ("%RDPRRAP_PIN%") do (
if /I "%%A"=="version" set "RDPRRAP_VERSION=%%B"
if /I "%%A"=="filename" set "RDPRRAP_FILENAME=%%B"
if /I "%%A"=="sha256" set "RDPRRAP_SHA256=%%B"
)
)
if not defined RDPRRAP_VERSION goto :rdprrap_skip
if not defined RDPRRAP_FILENAME goto :rdprrap_skip
if not defined RDPRRAP_SHA256 goto :rdprrap_skip
set "RDPRRAP_DIR=C:\winpodx\rdprrap"
set "RDPRRAP_INSTALLED=%RDPRRAP_DIR%\.installed_version"
set "RDPRRAP_CUR="
if exist "%RDPRRAP_INSTALLED%" (
for /f "usebackq delims=" %%V in ("%RDPRRAP_INSTALLED%") do set "RDPRRAP_CUR=%%V"
)
REM Check sits on its own line so %RDPRRAP_CUR% expands AFTER the for-loop above (no delayed expansion needed).
if defined RDPRRAP_CUR if /I "%RDPRRAP_CUR%"=="%RDPRRAP_VERSION%" (
echo [WinPodX] rdprrap %RDPRRAP_VERSION% already installed, skipping.
goto :rdprrap_done
)
REM Locate the bundled zip inside the OEM staging folder.
set "RDPRRAP_ZIP_SRC="
if exist "C:\OEM\%RDPRRAP_FILENAME%" set "RDPRRAP_ZIP_SRC=C:\OEM\%RDPRRAP_FILENAME%"
if not defined RDPRRAP_ZIP_SRC (
echo [WinPodX] WARNING: bundled %RDPRRAP_FILENAME% not found at C:\winpodx or C:\OEM; staying single-session.
goto :rdprrap_done
)
REM certutil prints 3 lines; line 2 has the hex digest with spaces between bytes.
set "RDPRRAP_GOT="
for /f "usebackq skip=1 delims=" %%H in (`certutil -hashfile "%RDPRRAP_ZIP_SRC%" SHA256 ^| findstr /R "^[0-9a-fA-F ]*$"`) do (
if not defined RDPRRAP_GOT set "RDPRRAP_GOT=%%H"
)
set "RDPRRAP_GOT=%RDPRRAP_GOT: =%"
if /I not "%RDPRRAP_GOT%"=="%RDPRRAP_SHA256%" (
echo [WinPodX] WARNING: rdprrap sha256 mismatch on bundle; staying single-session.
echo [WinPodX] expected %RDPRRAP_SHA256%
echo [WinPodX] got %RDPRRAP_GOT%
goto :rdprrap_done
)
mkdir "%RDPRRAP_DIR%" 2>nul
REM --- Diagnostic logs ------------------------------------------------------
REM Status marker (one-line classification, machine-readable):
REM enabled / extract-failed / installer-failed / not-activated / missing-bundle
REM Detailed log (full timestamps + retry-by-retry stderr/stdout) so when
REM something fails users / `winpodx pod apply-fixes` have something to
REM root-cause from. The marker fits a single grep, the log is the deep dive.
set "RDPRRAP_STATUS=C:\winpodx\rdprrap\.activation_status"
set "RDPRRAP_LOG=C:\winpodx\rdprrap\install.log"
(echo === rdprrap install log === %DATE% %TIME%) > "%RDPRRAP_LOG%"
(echo version=%RDPRRAP_VERSION%)>>"%RDPRRAP_LOG%"
(echo bundle=%RDPRRAP_ZIP_SRC%)>>"%RDPRRAP_LOG%"
REM --- Extract with retries + per-attempt log ------------------------------
REM Expand-Archive occasionally fails on first-boot Sysprep with file-lock /
REM antivirus interference. Retry up to 3 times. Capture stderr each
REM attempt so a final extract-failed has actionable diagnostics.
REM v0.4.0 (post-rc1): tar -xf instead of PS Expand-Archive.
REM
REM Background: PS Expand-Archive deadlocked on every fresh install
REM 2026-05-02 through 2026-05-04. install.log ended at "extract attempt
REM 1" with no follow-up - neither "extract OK" nor "extract failed" -
REM meaning the PowerShell call never returned. The 3 disconnected
REM User sessions in qwinsta on every attempt were the eventual kicks
REM (host-side migrate's password probe at 12:45 UTC) that finally
REM killed the hanging install.bat. PS Expand-Archive was hanging on
REM Defender's real-time scan of C:\OEM\ + the rdprrap zip extraction
REM target - one of the staged PS / EXE files (rdprrap-installer.exe
REM is naturally PUP-flagged because it patches termsrv.dll) was
REM getting locked, and Expand-Archive blocked waiting on the lock.
REM
REM tar (Windows 10 1803+, ships in System32\tar.exe) bypasses the
REM PowerShell engine entirely - no module load, no .NET assembly
REM resolution, no AMSI hookpoints. It's a syscall-direct extraction
REM that Defender's PS-script analysis layer can't intercept. The
REM Defender exclusions added at the top of install.bat (Add-MpPreference
REM -ExclusionPath C:\OEM,C:\winpodx) are also in effect by this point,
REM so even the EXE path is exempt.
REM
REM Output flatten: tar's -C strips the archive's leading directory by
REM default for our zip layout, so the inner rdprrap-<version>/ folder
REM that Expand-Archive used to leave behind isn't present after tar
REM extraction. We still defensively check + flatten via cmd's MOVE /Y
REM in case the archive layout changes.
echo [WinPodX] Extracting rdprrap %RDPRRAP_VERSION%...
set "RDPRRAP_EXTRACTED="
for %%T in (1 2 3) do (
if not defined RDPRRAP_EXTRACTED (
(echo --- extract attempt %%T %DATE% %TIME%)>>"%RDPRRAP_LOG%"
"%SystemRoot%\System32\tar.exe" -xf "%RDPRRAP_ZIP_SRC%" -C "%RDPRRAP_DIR%" >>"%RDPRRAP_LOG%" 2>&1
if not errorlevel 1 set "RDPRRAP_EXTRACTED=1"
if not defined RDPRRAP_EXTRACTED (
(echo extract attempt %%T failed; sleeping 2s)>>"%RDPRRAP_LOG%"
echo [WinPodX] extract attempt %%T failed, retrying after 2s...
REM Plain timeout instead of `powershell Start-Sleep` - PS
REM call here would re-introduce the very engine load that
REM tar is bypassing.
timeout /t 2 /nobreak >nul 2>&1
)
)
)
if not defined RDPRRAP_EXTRACTED (
(echo FINAL: extract-failed)>>"%RDPRRAP_LOG%"
echo [WinPodX] WARNING: rdprrap extraction failed after 3 attempts; staying single-session.
echo [WinPodX] diagnostic log: %RDPRRAP_LOG%
(echo extract-failed)>"%RDPRRAP_STATUS%"
goto :rdprrap_done
)
(echo extract OK via tar)>>"%RDPRRAP_LOG%"
REM Defensive flatten: if tar left an inner rdprrap-<version>/ folder
REM (depends on the zip's layout convention), move its contents up to
REM RDPRRAP_DIR. Single shot, no PS - pure cmd. Idempotent: the for
REM loop simply finds no match if there's no inner dir.
for /d %%D in ("%RDPRRAP_DIR%\rdprrap-*") do (
(echo flattening inner dir: %%~nxD)>>"%RDPRRAP_LOG%"
xcopy /E /Y /Q "%%D\*" "%RDPRRAP_DIR%\" >>"%RDPRRAP_LOG%" 2>&1
rmdir /S /Q "%%D" >>"%RDPRRAP_LOG%" 2>&1
)
set "RDPRRAP_EXE=%RDPRRAP_DIR%\rdprrap-installer.exe"
if not exist "%RDPRRAP_EXE%" (
(echo FINAL: extract-failed - rdprrap-installer.exe missing after extract)>>"%RDPRRAP_LOG%"
echo [WinPodX] WARNING: rdprrap-installer.exe missing after extract; staying single-session.
echo [WinPodX] diagnostic log: %RDPRRAP_LOG%
(echo extract-failed)>"%RDPRRAP_STATUS%"
goto :rdprrap_done
)
REM --- Delegate install / TermService cycle / verify to rdprrap-activate.ps1
REM Single source of truth - same script `winpodx pod multi-session on`
REM uses for runtime activation. install.bat used to inline ~80 lines of
REM installer-retry / TermService-cycle / ServiceDll-verify / marker logic;
REM that code now lives in rdprrap-activate.ps1 so a fix to the activation
REM flow benefits both OEM-time and runtime paths without drift.
REM
REM Safety at OEM time: install.bat runs from FirstLogonCommands in the
REM local console session - TermService manages RDP sessions only, so the
REM cycle inside the script doesn't tear down our cmd.exe parent.
REM
REM The script writes the same .activation_status / install.log markers
REM install.bat used to write directly. Idempotency marker
REM (.installed_version) stays install.bat's responsibility - only OEM
REM time has the SHA-pinned bundle context to safely stamp it.
echo [WinPodX] Activating rdprrap (delegating to rdprrap-activate.ps1)...
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0rdprrap-activate.ps1" >>"%RDPRRAP_LOG%" 2>&1
set "RDPRRAP_RC=%ERRORLEVEL%"
(echo rdprrap-activate.ps1 exit=%RDPRRAP_RC%)>>"%RDPRRAP_LOG%"
if not "%RDPRRAP_RC%"=="0" (
echo [WinPodX] WARNING: rdprrap-activate.ps1 exited with code %RDPRRAP_RC%; staying single-session.
echo [WinPodX] diagnostic log: %RDPRRAP_LOG%
goto :rdprrap_done
)
(echo %RDPRRAP_VERSION%)>"%RDPRRAP_INSTALLED%"
echo [WinPodX] rdprrap %RDPRRAP_VERSION% installed and activated.
goto :rdprrap_done
:rdprrap_skip
echo [WinPodX] rdprrap_version.txt not found or incomplete; staying single-session.
:rdprrap_done
REM ---------------------------------------------------------------------
REM v0.2.2-rev1: winpodx guest HTTP agent
REM ---------------------------------------------------------------------
echo [WinPodX] Installing WinPodX guest agent...
copy /Y "%~dp0agent\agent.ps1" "C:\OEM\agent.ps1" 2>nul
mkdir C:\OEM\agent-runs 2>nul
REM Hidden launchers go under C:\Users\Public\winpodx\launchers\ rather than
REM C:\OEM\. Public is universally writable for Authenticated Users, so the
REM agent (which runs as User, non-admin) can later overwrite these files
REM during a `winpodx pod apply-fixes` migration without needing UAC. C:\OEM\
REM is SYSTEM-owned and rejects User writes by default.
REM
REM Diagnostic log (`C:\winpodx\setup.log`) records each copy + verify
REM step. Pre-OEM-v20 the copies were silent (`2>nul`), so when a
REM Sysprep-time failure (network share blip, AV interference) skipped a
REM file, the missing-file symptom only surfaced later as a "Cannot find
REM script file" wscript dialog at HKCU\Run firing time -- with no
REM breadcrumbs. With this log + per-file existence check below, a
REM staging failure now writes a single line we can grep from the host
REM via the agent.
mkdir C:\winpodx 2>nul
set "SETUP_LOG=C:\winpodx\setup.log"
(echo === setup log === %DATE% %TIME%) > "%SETUP_LOG%"
(echo OEM_VERSION=%WINPODX_OEM_VERSION%)>>"%SETUP_LOG%"
(echo install.bat dir=%~dp0)>>"%SETUP_LOG%"
mkdir "C:\Users\Public\winpodx" 2>nul
mkdir "C:\Users\Public\winpodx\launchers" 2>nul
REM Each launcher: copy, verify the file exists at its destination, log
REM the outcome. Continuing on failure (so a single bad copy doesn't
REM abort the rest of install.bat) but recording it for diagnostics.
REM `LAUNCHERS_OK=1` after the loop means hidden-launcher.vbs (the
REM critical wrapper) made it to its destination -- gates the wscript
REM reg add below.
set "LAUNCHERS_OK="
for %%F in (
"hidden-launcher.vbs"
"launch_uwp.vbs"
"launch_uwp.ps1"
"agent-respawn.ps1"
"agent-keepalive.ps1"
"rdprrap-activate.ps1"
) do (
copy /Y "%~dp0%%~F" "C:\Users\Public\winpodx\launchers\%%~F" >nul 2>>"%SETUP_LOG%"
if exist "C:\Users\Public\winpodx\launchers\%%~F" (
(echo launcher OK: %%~F)>>"%SETUP_LOG%"
) else (
(echo launcher FAILED: %%~F src=%~dp0%%~F not staged or copy denied)>>"%SETUP_LOG%"
)
)
if exist "C:\Users\Public\winpodx\launchers\hidden-launcher.vbs" set "LAUNCHERS_OK=1"
(echo launchers gate: LAUNCHERS_OK=%LAUNCHERS_OK%)>>"%SETUP_LOG%"
REM Pre-register the URL ACL for agent.ps1's HttpListener prefix.
REM
REM agent.ps1 binds ``http://+:8765/`` (all interfaces inside the
REM Windows VM) - NOT 127.0.0.1. dockur's user-mode QEMU NAT forwards
REM from container:8765 to the VM's slirp interface (10.0.2.15:8765,
REM NOT 127.0.0.1:8765); a 127.0.0.1-only listener would mean slirp's
REM forwarded packets hit a closed port (kernalix7 saw "Connection
REM reset by peer" on 2026-04-30 from exactly this). Binding to + is
REM safe because the agent stays externally unreachable: compose's
REM 127.0.0.1:8765:8765/tcp mapping is loopback on the host, and the
REM QEMU slirp net is private to the container.
REM
REM HttpListener.Start() needs a urlacl entry to bind ``+``; without
REM it the bind fails with "conflicts with an existing registration on
REM the machine" because a non-admin process (the autologon User the
REM agent runs as) cannot reserve a strong-wildcard prefix without a
REM pre-existing reservation it is permitted to use.
REM
REM #269 (ismikes, Kubuntu 26.04): agent.log showed the bind failing
REM with exactly that conflict on every boot even though install.bat
REM ran to completion. Root cause: the prior `user=Everyone` form +
REM hidden `>nul 2>&1` masked a failed / locale-dependent reservation.
REM Hardened below:
REM - sddl=D:(A;;GX;;;WD) reserves for the World (Everyone) SID
REM S-1-1-0 directly, locale-independent (the literal string
REM "Everyone" is localized on non-English Windows and the netsh
REM parse silently no-ops there). GX = GENERIC_EXECUTE = the
REM register/listen right HttpListener needs.
REM - delete every overlapping 8765 reservation (strong +, weak *,
REM loopback) first so a stale differently-owned entry can't win.
REM - results logged to setup.log (not >nul) so a failure is visible.
REM - show urlacl after, so the actual post-state is in the log.
echo [agent-install] step=urlacl status=enter>>"%SETUP_LOG%"
netsh http delete urlacl url=http://127.0.0.1:8765/ >>"%SETUP_LOG%" 2>&1
netsh http delete urlacl url=http://*:8765/ >>"%SETUP_LOG%" 2>&1
netsh http delete urlacl url=http://+:8765/ >>"%SETUP_LOG%" 2>&1
netsh http add urlacl url=http://+:8765/ sddl="D:(A;;GX;;;WD)" >>"%SETUP_LOG%" 2>&1
echo [agent-install] urlacl add rc=%ERRORLEVEL%>>"%SETUP_LOG%"
netsh http show urlacl url=http://+:8765/ >>"%SETUP_LOG%" 2>&1
echo [agent-install] step=urlacl status=exit>>"%SETUP_LOG%"
REM Register HKCU\Run via PowerShell instead of `reg add`. Three reasons
REM the cmd-quoting path bit users pre-OEM-v20:
REM 1. `reg add /d "...\\\"path\\\"..."` survives cmd parsing but
REM reg.exe stores literal backslash-quote pairs in the registry,
REM and CommandLineToArgvW at logon evaluates them as escaped
REM quotes -- not always argv-equivalent to the intended quoted
REM argument. PS's Set-ItemProperty takes a real .NET string and
REM stores it byte-exact.
REM 2. PS can verify hidden-launcher.vbs exists *before* registering
REM the wscript-wrapped value -- if the launcher staging copy
REM failed (logged via setup.log above, LAUNCHERS_OK unset), we
REM fall back to direct `powershell.exe -WindowStyle Hidden` for
REM WinpodxAgent. That fallback flashes briefly but at least
REM starts the agent; without it, HKCU\Run fired wscript on a
REM missing file -> "Cannot find script file" dialog blocking
REM the user session indefinitely (kernalix7 saw this on a fresh
REM install 2026-05-02 18:48).
REM 3. Single PS round-trip writes the WinpodxAgent entry.
echo [WinPodX] Registering HKCU\Run entries...
echo [agent-install] step=hkcu-run-register status=enter>>"%SETUP_LOG%"
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$wrap = 'C:\Users\Public\winpodx\launchers\hidden-launcher.vbs';" ^
"$haveWrap = Test-Path -LiteralPath $wrap;" ^
"Write-Output (\"reg-add: haveWrap=$haveWrap\");" ^
"$key = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run';" ^
"if ($haveWrap) {" ^
" $agent = 'wscript.exe \"' + $wrap + '\" \"powershell.exe\" \"-NoProfile\" \"-ExecutionPolicy\" \"Bypass\" \"-File\" \"C:\OEM\agent.ps1\"';" ^
"} else {" ^
" Write-Output 'reg-add: hidden-launcher.vbs missing -> fallback to direct powershell (brief flash)';" ^
" $agent = 'powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File C:\OEM\agent.ps1';" ^
"}" ^
"try {" ^
" Set-ItemProperty -Path $key -Name 'WinpodxAgent' -Value $agent -Force;" ^
" Write-Output ('reg-add: WinpodxAgent=' + $agent);" ^
"} catch {" ^
" Write-Output ('reg-add: ERROR ' + $_.Exception.GetType().FullName + ': ' + $_.Exception.Message);" ^
"}" >>"%SETUP_LOG%" 2>&1
echo [agent-install] step=hkcu-run-register status=exit rc=%ERRORLEVEL%>>"%SETUP_LOG%"
REM Start the agent NOW (install.bat-time spawn) -- not just register it
REM in HKCU\Run for future sessions. Reasoning: HKCU\Run fires once per
REM user logon, and the autologon User session has *already* logged in
REM by the time install.bat (FirstLogonCommands) executes. Registering
REM HKCU\Run here only takes effect on the NEXT session, so without
REM this explicit spawn the agent sits idle until the user (or a host
REM RDP probe) opens a new session. install.sh's wait-ready phase 3
REM was timing out at /health waiting for an agent that wasn't going
REM to start until much later. spawn here -> agent /health up before
REM install.bat exits, phase 3 succeeds cleanly, migrate's apply chain
REM runs against a healthy agent (no FreeRDP-fallback cascades).
REM
REM Same wscript+hidden-launcher.vbs wrapper / direct-PS fallback
REM split as the HKCU\Run registration above. Spawned via
REM Start-Process detached so install.bat doesn't block on the agent's
REM event loop.
echo [WinPodX] Starting guest agent...
echo [agent-install] step=spawn status=enter>>"%SETUP_LOG%"
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$wrap = 'C:\Users\Public\winpodx\launchers\hidden-launcher.vbs';" ^
"$agentScript = 'C:\OEM\agent.ps1';" ^
"Write-Output ('agent-spawn: wrap=' + $wrap + ' wrapExists=' + (Test-Path -LiteralPath $wrap));" ^
"Write-Output ('agent-spawn: agentScript=' + $agentScript + ' scriptExists=' + (Test-Path -LiteralPath $agentScript));" ^
"try {" ^
" if (Test-Path -LiteralPath $wrap) {" ^
" $startArgs = @($wrap, 'powershell.exe', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $agentScript);" ^
" $p = Start-Process wscript.exe -ArgumentList $startArgs -WindowStyle Hidden -PassThru;" ^
" Write-Output ('agent-spawn: wscript+hidden-launcher.vbs pid=' + $p.Id);" ^
" } else {" ^
" $p = Start-Process powershell.exe -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $agentScript) -WindowStyle Hidden -PassThru;" ^
" Write-Output ('agent-spawn: direct-powershell-fallback pid=' + $p.Id);" ^
" }" ^
"} catch {" ^
" Write-Output ('agent-spawn: ERROR ' + $_.Exception.GetType().FullName + ': ' + $_.Exception.Message);" ^
"}" >>"%SETUP_LOG%" 2>&1
echo [agent-install] step=spawn status=exit rc=%ERRORLEVEL%>>"%SETUP_LOG%"
REM Quick post-spawn health probe -- give the agent 5s to bind 8765,
REM then log whether the listener is up. If it's not, we know agent.ps1
REM either failed to start or crashed before HttpListener.Start(), and
REM the user / next debugger has a clear breadcrumb without needing
REM to chase Start-Process exit codes.
echo [agent-install] step=post-spawn-probe status=enter>>"%SETUP_LOG%"
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"Start-Sleep -Seconds 5;" ^
"$listener = Get-NetTCPConnection -LocalPort 8765 -State Listen -ErrorAction SilentlyContinue;" ^
"if ($listener) {" ^
" Write-Output ('post-spawn-probe: 8765 listener up (PID ' + ($listener | Select-Object -First 1 -ExpandProperty OwningProcess) + ')');" ^
"} else {" ^
" Write-Output 'post-spawn-probe: 8765 NOT listening 5s after spawn';" ^
" Get-ChildItem C:\OEM\agent.log -ErrorAction SilentlyContinue | ForEach-Object { Write-Output ('post-spawn-probe: agent.log size=' + $_.Length) };" ^
" Get-Content C:\OEM\agent.log -Tail 20 -ErrorAction SilentlyContinue | ForEach-Object { Write-Output ('agent.log: ' + $_) };" ^
"}" >>"%SETUP_LOG%" 2>&1
echo [agent-install] step=post-spawn-probe status=exit rc=%ERRORLEVEL%>>"%SETUP_LOG%"
REM ---------------------------------------------------------------------
REM Agent keep-alive scheduled task (WinpodxAgentKeepAlive).
REM
REM HKCU\Run fires the agent exactly once per interactive logon. When the
REM autologon session is torn down (RDP single-session enforcement kicks
REM it when a FreeRDP connection arrives before rdprrap multi-session is
REM active, or a TermService cycle during rdprrap (re)activation), the
REM agent process dies with the session and HKCU\Run does NOT re-fire --
REM the agent stays dead until the pod reboots. This task is the
REM persistent watchdog HKCU\Run never was.
REM
REM Principal: the INTERACTIVE autologon user, NOT SYSTEM / S4U. The
REM agent's /exec runs PowerShell in the user context (Start Menu / per-
REM user app discovery, per-user reverse-open HKCU registration); a
REM SYSTEM principal would change HKCU + Start Menu out from under those
REM callers. A user-context task only runs while a session exists, which
REM covers crash-but-alive (1-min repetition) and re-logon (AtLogOn). The
REM session-kick-with-no-relogon case is handled by keeping rdprrap
REM activation idempotent so the kick does not happen (see
REM _apply_multi_session / rdprrap-activate.ps1).
REM
REM agent-keepalive.ps1 is staged to C:\winpodx (survives the C:\OEM wipe
REM on classic VMs, same as power-monitor.ps1) and runs through the
REM wscript hidden-launcher wrapper so the 1-min wakeups never flash a
REM console. Registered via the ScheduledTasks cmdlets so we can attach
REM BOTH an AtLogOn trigger AND a 1-minute repetition (schtasks.exe can
REM only set one schedule per task).
echo [WinPodX] Registering agent keep-alive scheduled task...
echo [agent-install] step=keepalive-task status=enter>>"%SETUP_LOG%"
if exist "C:\Users\Public\winpodx\launchers\agent-keepalive.ps1" (
if not exist C:\winpodx mkdir C:\winpodx
copy /Y "C:\Users\Public\winpodx\launchers\agent-keepalive.ps1" C:\winpodx\agent-keepalive.ps1 >nul 2>&1
)
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$wrap = 'C:\Users\Public\winpodx\launchers\hidden-launcher.vbs';" ^
"$ka = 'C:\winpodx\agent-keepalive.ps1';" ^
"if (-not (Test-Path -LiteralPath $ka)) { Write-Output 'keepalive: agent-keepalive.ps1 not staged; skipping'; exit 0 };" ^
"if (Test-Path -LiteralPath $wrap) {" ^
" $exe = 'wscript.exe';" ^
" $arg = '\"' + $wrap + '\" \"powershell.exe\" \"-NoProfile\" \"-ExecutionPolicy\" \"Bypass\" \"-File\" \"' + $ka + '\"';" ^
"} else {" ^
" $exe = 'powershell.exe';" ^
" $arg = '-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File \"' + $ka + '\"';" ^
"}" ^
"$act = New-ScheduledTaskAction -Execute $exe -Argument $arg;" ^
"$tLogon = New-ScheduledTaskTrigger -AtLogOn;" ^
"$tRepeat = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 1);" ^
"$me = \"$env:USERDOMAIN\$env:USERNAME\";" ^
"$prin = New-ScheduledTaskPrincipal -UserId $me -LogonType Interactive -RunLevel Limited;" ^
"$set = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 2);" ^
"try {" ^
" Register-ScheduledTask -TaskName 'WinpodxAgentKeepAlive' -Action $act -Trigger @($tLogon,$tRepeat) -Principal $prin -Settings $set -Force | Out-Null;" ^
" Write-Output ('keepalive: registered for ' + $me);" ^
"} catch {" ^
" Write-Output ('keepalive: ERROR ' + $_.Exception.Message);" ^
"}" >>"%SETUP_LOG%" 2>&1
echo [agent-install] step=keepalive-task status=exit rc=%ERRORLEVEL%>>"%SETUP_LOG%"
REM Token is delivered via the OEM bind mount - no \\tsclient\home copy
REM needed. Setup stages it to {oem_dir}/agent_token.txt before container
REM creation; dockur lays the OEM directory contents into C:\OEM\.
echo [WinPodX] Guest agent installed.
REM Stage power-monitor.ps1 under C:\winpodx (C:\OEM is wiped after
REM first boot, so anything we want to keep needs a copy outside it).
REM Then register a SYSTEM-level scheduled task that runs the monitor
REM at boot; the script subscribes to Win32_PowerManagementEvent and
REM cycles TermService when the host suspends/resumes (which the guest
REM only sees as a wall-clock jump, leaving the RDP TCP listener
REM stale -- kernalix7's recurring "GUI stuck on starting after host
REM wake" symptom).
if exist C:\OEM\power-monitor.ps1 (
if not exist C:\winpodx mkdir C:\winpodx
copy /Y C:\OEM\power-monitor.ps1 C:\winpodx\power-monitor.ps1 >nul 2>&1
schtasks /create /tn "winpodx-power-monitor" ^
/tr "powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File C:\winpodx\power-monitor.ps1" ^
/sc onstart /ru SYSTEM /rl HIGHEST /f >nul 2>&1
REM Kick the task immediately so the WMI subscription is live for
REM this session too -- otherwise the very first host suspend
REM after install (before the next guest reboot) goes unhandled.
schtasks /run /tn "winpodx-power-monitor" >nul 2>&1
echo [WinPodX] Guest power monitor scheduled.
)
REM Sentinel lives under C:\winpodx so it survives past the one-shot C:\OEM stage.
(echo done)>C:\winpodx\setup_done.txt
echo [WinPodX] Post-install configuration complete (version %WINPODX_OEM_VERSION%)!
echo [WinPodX] RDP is now enabled. You can connect with FreeRDP.
REM ---------------------------------------------------------------------------
REM TermService cycle -- ABSOLUTELY LAST STEP.
REM
REM This restarts TermService so the running process picks up the
REM termwrap.dll patched into HKLM:\...\TermService\Parameters\ServiceDll
REM by rdprrap-activate.ps1 (synchronous OEM-mode call earlier in this
REM script). rdprrap-activate.ps1 deliberately skips the cycle in OEM
REM mode (marker = patched-pending-cycle) BECAUSE this cycle is what
REM has been killing install.bat mid-script -- in dockur's setup the
REM autologon User session is itself managed through TermService, so
REM `net stop TermService /y` takes our cmd.exe down with it.
REM
REM By doing the cycle as the very last action, we have already
REM committed:
REM - launcher staging into C:\Users\Public\winpodx\launchers\
REM - HKCU\Run\WinpodxAgent (registered via PowerShell)
REM - inline agent spawn (so it ran with the now-stale TermService;
REM it'll die with this cycle, then HKCU\Run brings it back in
REM the autologon-retry session)
REM - C:\winpodx\setup_done.txt sentinel
REM - rdprrap-installer registry patch (done by rdprrap-activate.ps1)
REM
REM So even when this cycle kills install.bat's session, the autologon
REM retry creates a fresh user session with multi-session active and
REM HKCU\Run firing -- that brings the agent up cleanly.
REM
REM We still try to update the .activation_status marker after the
REM cycle so apply-fixes doesn't have to re-trigger activation. If
REM cmd.exe dies before reaching that, the marker stays at
REM `patched-pending-cycle` and the host's `_apply_multi_session`
REM ServiceDll cross-check (PR #85) will reconcile it to `enabled` on
REM next apply-fixes.
echo [WinPodX] Cycling TermService to load termwrap.dll (final step)...
net stop TermService /y >nul 2>&1
net start TermService >nul 2>&1
REM Best-effort marker update. May not run if the cycle killed our
REM session; that's fine -- apply-fixes reconciles via ServiceDll check.
(echo enabled)>C:\winpodx\rdprrap\.activation_status 2>nul
REM ---------------------------------------------------------------------------
REM Scheduled reboot -- ABSOLUTELY FINAL.
REM
REM Several registry edits set above (PlatformAoAcOverride for Modern
REM Standby, NIC binding, etc.) only take effect on the next Windows
REM boot. Without this reboot, a fresh-install guest is technically
REM running with the *old* Modern Standby state for the first session;
REM the host then sees the long-idle stall the powercfg /change
REM timeouts can't prevent on their own. The TermService cycle above
REM picks up termwrap.dll; the reboot below picks up everything that
REM needs a clean boot.
REM
REM Sequence:
REM 1. install.bat writes ``C:\winpodx\oem_reboot_pending.txt``.
REM 2. ``shutdown /r /t 15`` queues the reboot with a 15s grace
REM window so cmd.exe can finish + the autologon User session
REM doesn't fight the cycle.
REM 3. On the next Windows boot, the RunOnce key below fires
REM ``del oem_reboot_pending.txt`` so the host can poll the
REM marker's absence as the "reboot pass complete" signal.
REM 4. ``winpodx pod wait-ready`` adds a [4/4] step that polls
REM the marker via the agent transport (or `\\tsclient\home`
REM fallback) until it's gone.
REM
REM If shutdown fails for any reason, the marker is left behind --
REM apply-fixes treats this as "still need second-pass reboot" and
REM offers to retry. Failure mode is detectable, not silent.
REM -- winpodx bare-metal disguise: prune unused virtio driver service keys --
REM al-khaser flags Services\{viostor,vioscsi,BalloonService} as VM tells. The
REM virtio-win bundle installs them even with no matching device. The helper
REM removes only the keys whose device is absent (guarded so a virtio-boot or
REM ballooned guest keeps its driver). Runs before the reboot so the change is
REM applied on the clean boot below. No-op when the helper isn't shipped.
if exist "%~dp0disguise-cleanup.ps1" (
echo [WinPodX] Pruning unused virtio driver service keys...
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0disguise-cleanup.ps1" >nul 2>&1
)
echo [WinPodX] Scheduling reboot to apply registry / power settings...
(echo pending)>C:\winpodx\oem_reboot_pending.txt 2>nul
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" ^
/v WinpodxClearOemRebootMarker /t REG_SZ ^
/d "cmd.exe /c del /q C:\winpodx\oem_reboot_pending.txt" /f >nul
shutdown /r /t 15 /c "WinPodX: applying registry / power settings"