mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
fix(init): suppress hook recovery output (#760)
## Summary - silence best-effort profile recovery while `headroom init hook ensure` runs from installed hooks - suppress both Python-level stdout/stderr and child process file-descriptor output so SessionStart hooks do not emit invalid JSON - add a regression test for noisy supervisor recovery failures ## Verification - `python3 -m py_compile headroom/cli/init.py` - live local hook probe: `headroom init hook ensure --profile default --marker headroom-init-codex` exits 0 with empty output - targeted pytest was not runnable locally because `uv.lock` currently fails to parse due to an inconsistent GitPython wheel version entry
This commit is contained in:
parent
d76a7296df
commit
b4395993ae
2 changed files with 73 additions and 20 deletions
|
|
@ -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]]:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue