mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
fix(install): use CREATE_NO_WINDOW instead of DETACHED_PROCESS on Windows (#2527)
## Description On Windows, the detached agent process spawned by `install hook ensure` (and the `install restart` self-spawn) pops up a visible black console window repeatedly, because `DETACHED_PROCESS` makes `CREATE_NO_WINDOW` a no-op per the Win32 process-creation-flags docs. #2521 ## Type of Change - [x] Bug fix (non-breaking change which fixes an issue) ## Changes Made - `headroom/install/runtime.py`: `start_detached_agent()` now uses `CREATE_NO_WINDOW` instead of `DETACHED_PROCESS`, combined with `CREATE_NEW_PROCESS_GROUP` (unchanged detach/isolation semantics, window hidden). - `headroom/install/runtime.py`: `_spawn_detached_restart()` now also sets `CREATE_NO_WINDOW` on Windows (previously had no `creationflags` at all on that platform). - `tests/test_install/test_runtime.py`: updated the Windows branch of `test_start_detached_agent_and_run_foreground` to assert the actual `creationflags` value passed to `Popen`, instead of just monkeypatching an unused `DETACHED_PROCESS` attribute. ## Testing - [x] Added/updated tests - [x] Ran full local test suite ``` $ python -m pytest tests/test_install -q ======================= 137 passed, 1 skipped in 48.68s ======================= $ ruff check headroom/install/runtime.py tests/test_install/test_runtime.py All checks passed! $ ruff format --check headroom/install/runtime.py tests/test_install/test_runtime.py 2 files already formatted $ mypy headroom --ignore-missing-imports Success: no issues found in 506 source files ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.13.11, headroom repo local checkout - Exact command / steps: `python -m pytest tests/test_install/test_runtime.py -q`, plus manual read of `subprocess` Windows creation-flag semantics (`DETACHED_PROCESS` + child console allocation vs `CREATE_NO_WINDOW`) - Observed result: all 25 tests in `test_runtime.py` pass, including the updated assertion that `creationflags == CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP` on the Windows code path - Not tested: did not reproduce the original visible-console-popup repro end-to-end via live Claude Code hook invocation (no environment with the full hook-triggered respawn loop set up in this session); relying on the Win32 docs and the reporter's own local verification of the same flag swap ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
d50cfabedc
commit
045f3dfe6f
2 changed files with 18 additions and 6 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue