diff --git a/headroom/cli/init.py b/headroom/cli/init.py index a9b5d9da9..b9d71d33e 100644 --- a/headroom/cli/init.py +++ b/headroom/cli/init.py @@ -9,6 +9,8 @@ import shlex import shutil import subprocess import sys +from collections.abc import Iterator +from contextlib import contextmanager, redirect_stderr, redirect_stdout from hashlib import sha1 from pathlib import Path from typing import Any @@ -552,31 +554,54 @@ def _install_copilot_marketplace() -> None: ) +@contextmanager +def _suppress_hook_output() -> Iterator[None]: + """Keep best-effort hook recovery from emitting invalid hook output.""" + stdout_fd = os.dup(1) + stderr_fd = os.dup(2) + try: + with open(os.devnull, "w", encoding="utf-8") as devnull: + sys.stdout.flush() + sys.stderr.flush() + os.dup2(devnull.fileno(), 1) + os.dup2(devnull.fileno(), 2) + with redirect_stdout(devnull), redirect_stderr(devnull): + yield + finally: + sys.stdout.flush() + sys.stderr.flush() + os.dup2(stdout_fd, 1) + os.dup2(stderr_fd, 2) + os.close(stdout_fd) + os.close(stderr_fd) + + def _ensure_profile_running(profile: str) -> None: manifest = load_manifest(profile) if manifest is None: return - if wait_ready(manifest, timeout_seconds=1): - return - try: - with acquire_runtime_start_lock(manifest.profile) as acquired: - if not acquired: - return - if wait_ready(manifest, timeout_seconds=1): - return - if runtime_status(manifest) == "running": - if wait_ready(manifest, timeout_seconds=_STARTUP_READY_TIMEOUT_SECONDS): + with _suppress_hook_output(): + if wait_ready(manifest, timeout_seconds=1): + return + try: + with acquire_runtime_start_lock(manifest.profile) as acquired: + if not acquired: return - stop_runtime(manifest) - if manifest.preset == InstallPreset.PERSISTENT_DOCKER.value: - start_persistent_docker(manifest) - elif manifest.supervisor_kind == SupervisorKind.SERVICE.value: - start_supervisor(manifest) - else: - start_detached_agent(manifest.profile) - wait_ready(manifest, timeout_seconds=45) - except Exception: - return + if wait_ready(manifest, timeout_seconds=1): + return + if runtime_status(manifest) == "running": + if wait_ready(manifest, timeout_seconds=_STARTUP_READY_TIMEOUT_SECONDS): + return + stop_runtime(manifest) + if manifest.preset == InstallPreset.PERSISTENT_DOCKER.value: + start_persistent_docker(manifest) + elif manifest.supervisor_kind == SupervisorKind.SERVICE.value: + start_supervisor(manifest) + else: + start_detached_agent(manifest.profile) + wait_ready(manifest, timeout_seconds=45) + except Exception: + return def _probe_init_targets(global_scope: bool) -> list[tuple[str, str | None]]: diff --git a/tests/test_cli/test_init_cli.py b/tests/test_cli/test_init_cli.py index 7cd76f28b..99a4ce5dc 100644 --- a/tests/test_cli/test_init_cli.py +++ b/tests/test_cli/test_init_cli.py @@ -2,6 +2,7 @@ from __future__ import annotations import importlib import json +import os import sys import types from contextlib import contextmanager @@ -842,6 +843,33 @@ def test_ensure_profile_running_covers_runtime_modes(monkeypatch) -> None: assert ("docker-profile", 45) in wait_calls +def test_ensure_profile_running_suppresses_hook_recovery_output(monkeypatch, capfd) -> None: + init_cli, _ = _load_init_module(monkeypatch) + manifest = SimpleNamespace( + preset=init_cli.InstallPreset.PERSISTENT_TASK.value, + supervisor_kind=init_cli.SupervisorKind.SERVICE.value, + profile="service-profile", + ) + + monkeypatch.setattr(init_cli, "load_manifest", lambda profile: manifest) + monkeypatch.setattr(init_cli, "wait_ready", lambda manifest, timeout_seconds: False) + + def noisy_start_supervisor(manifest) -> None: + print("python stdout") + print("python stderr", file=sys.stderr) + os.write(1, b"fd stdout\n") + os.write(2, b"fd stderr\n") + raise RuntimeError("not permitted") + + monkeypatch.setattr(init_cli, "start_supervisor", noisy_start_supervisor) + + init_cli._ensure_profile_running("service-profile") + + captured = capfd.readouterr() + assert captured.out == "" + assert captured.err == "" + + def test_ensure_profile_running_returns_when_ready_or_on_exception(monkeypatch) -> None: init_cli, _ = _load_init_module(monkeypatch) manifest = SimpleNamespace(