From 054bd42ff0936f6bf9fabf4d569b7a2d997548b7 Mon Sep 17 00:00:00 2001 From: Simone Date: Tue, 9 Jun 2026 22:54:43 +0200 Subject: [PATCH] feat(prepare-win): reboot to cmd shell before toolchain, only if needed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Up front, read Winlogon\Shell; if it isn't already cmd.exe, set it and do a clean reboot before running the toolchain — so the shell is genuinely cmd (not a killed-explorer half-state) for the whole servicing run. Idempotent: when the shell is already cmd (re-runs), skip the reboot. Replaces the earlier kill-explorer approach. --- src/ci_orchestrator/commands/template.py | 32 ++++++++++++++++-------- tests/python/test_commands_template.py | 2 ++ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/ci_orchestrator/commands/template.py b/src/ci_orchestrator/commands/template.py index f10c43d..d4741d2 100644 --- a/src/ci_orchestrator/commands/template.py +++ b/src/ci_orchestrator/commands/template.py @@ -668,18 +668,30 @@ def template_prepare_win( click.echo("[prepare-win] WinRM ready.") try: - # Switch the interactive shell to cmd.exe up front, before the toolchain - # runs, so every reboot in the Windows Update 3010 loop already boots - # without explorer — no shell crash-loop during heavy servicing. Drop the - # live explorer now too (Winlogon relaunches the shell = cmd). Re-affirmed - # before the final reboot and baked into post-install for redundancy. - click.echo("[prepare-win] Setting interactive shell to cmd.exe ...") - transport.run( - "Set-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT" - "\\CurrentVersion\\Winlogon' Shell 'cmd.exe' -Type String -Force\n" - "Stop-Process -Name explorer -Force -ErrorAction SilentlyContinue\n", + # Ensure the interactive shell is cmd.exe — via a clean reboot — before + # the toolchain runs, so explorer never runs during the heavy servicing + # (Windows Update 3010 loop) that would otherwise crash-loop it. Reboot + # only when the shell wasn't already cmd, so re-runs don't reboot need- + # lessly. Re-affirmed before the final reboot and baked into post-install. + shell_res = transport.run( + "(Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT" + "\\CurrentVersion\\Winlogon').Shell", check=False, ) + if "cmd.exe" not in shell_res.stdout.strip().lower(): + click.echo( + f"[prepare-win] Shell is '{shell_res.stdout.strip() or '?'}' — " + "setting cmd.exe and rebooting ..." + ) + transport.run( + "Set-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT" + "\\CurrentVersion\\Winlogon' Shell 'cmd.exe' -Type String -Force", + check=False, + ) + _reboot_and_wait(transport, guest_ip, 5986, winrm_timeout) + click.echo("[prepare-win] Shell is now cmd.exe.") + else: + click.echo("[prepare-win] Shell already cmd.exe — no reboot needed.") # Ensure C:\CI exists transport.run( diff --git a/tests/python/test_commands_template.py b/tests/python/test_commands_template.py index 8ed93e7..9e3a627 100644 --- a/tests/python/test_commands_template.py +++ b/tests/python/test_commands_template.py @@ -434,6 +434,8 @@ def _make_transport() -> MagicMock: def _run(script: str = "", **kwargs: object) -> WinRmResult: if "PercentProcessorTime" in script: # pre-snapshot idle check return WinRmResult(stdout='{"cpu":2.0,"ram":21.0,"top":"idle"}', stderr="", returncode=0) + if ").Shell" in script: # up-front shell check — already cmd, skip reboot + return WinRmResult(stdout="cmd.exe", stderr="", returncode=0) if "ConvertTo-Json" in script: return WinRmResult(stdout=val_json, stderr="", returncode=0) return WinRmResult(stdout="CI_EXITCODE:0", stderr="", returncode=0)