diff --git a/headroom/install/runtime.py b/headroom/install/runtime.py index 43bf89243..0736e0a33 100644 --- a/headroom/install/runtime.py +++ b/headroom/install/runtime.py @@ -280,7 +280,10 @@ def start_detached_agent(profile: str) -> subprocess.Popen[str]: kwargs: dict[str, Any] = {"stdout": log_file, "stderr": log_file} if _is_windows(): - kwargs["creationflags"] = getattr(subprocess, "DETACHED_PROCESS", 0) | getattr( + # DETACHED_PROCESS makes CREATE_NO_WINDOW a no-op (per Win32 docs), so a + # detached console child pops up a visible window. Use CREATE_NO_WINDOW + # instead; it still detaches from the parent's console. + kwargs["creationflags"] = getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000) | getattr( subprocess, "CREATE_NEW_PROCESS_GROUP", 0 ) else: @@ -413,7 +416,9 @@ def _spawn_detached_restart(profile: str) -> None: """ command = [*resolve_headroom_command(), "install", "restart", "--profile", profile] popen_kwargs: dict[str, Any] = {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL} - if not _is_windows(): + if _is_windows(): + popen_kwargs["creationflags"] = getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000) + else: popen_kwargs["start_new_session"] = True subprocess.Popen(command, **popen_kwargs) diff --git a/tests/test_install/test_runtime.py b/tests/test_install/test_runtime.py index efdc8c494..ce654d859 100644 --- a/tests/test_install/test_runtime.py +++ b/tests/test_install/test_runtime.py @@ -417,15 +417,22 @@ def test_run_foreground_and_detached_helpers(monkeypatch, tmp_path: Path) -> Non monkeypatch.setattr("headroom.install.runtime.resolve_headroom_command", lambda: ["headroom"]) monkeypatch.setattr("headroom.install.runtime.sys.platform", "win32") - monkeypatch.setattr("headroom.install.runtime.subprocess.DETACHED_PROCESS", 1, raising=False) + monkeypatch.setattr("headroom.install.runtime.subprocess.CREATE_NO_WINDOW", 4, raising=False) monkeypatch.setattr( "headroom.install.runtime.subprocess.CREATE_NEW_PROCESS_GROUP", 2, raising=False ) + nt_calls: list[tuple[list[str], dict]] = [] fake_proc_nt = FakeProc() - monkeypatch.setattr( - "headroom.install.runtime.subprocess.Popen", lambda command, **kwargs: fake_proc_nt - ) + + def fake_popen_nt(command: list[str], **kwargs): + nt_calls.append((command, kwargs)) + return fake_proc_nt + + monkeypatch.setattr("headroom.install.runtime.subprocess.Popen", fake_popen_nt) assert start_detached_agent("demo") is fake_proc_nt + # DETACHED_PROCESS is not used: it makes CREATE_NO_WINDOW a no-op on + # Windows, so a detached console child would pop up a visible window. + assert nt_calls[0][1]["creationflags"] == 4 | 2 monkeypatch.setattr("headroom.install.runtime.sys.platform", "linux") fake_proc_posix = FakeProc()