fix(process-registry): keep CLI workers off controlling tty
This commit is contained in:
parent
bcdfdd51e5
commit
ff5dfdecef
|
|
@ -1649,6 +1649,10 @@ class TestSystemdCgroupIsolation:
|
|||
ENTIRE gateway cgroup, taking down the messaging control plane.
|
||||
"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _mark_gateway_process(self, monkeypatch):
|
||||
monkeypatch.setenv("_HERMES_GATEWAY", "1")
|
||||
|
||||
def _fake_popen_capture(self):
|
||||
"""Return (fake_popen, captured) where captured["argv"] gets the
|
||||
argv passed to subprocess.Popen."""
|
||||
|
|
@ -1780,6 +1784,63 @@ class TestSystemdCgroupIsolation:
|
|||
assert argv == ["/bin/bash", "-lic", "set +m; echo hello"], argv
|
||||
assert captured["start_new_session"] is True
|
||||
|
||||
def test_inherited_systemd_marker_does_not_scope_interactive_cli(
|
||||
self, registry, monkeypatch
|
||||
):
|
||||
"""A CLI inside a supervised terminal must keep workers off its tty."""
|
||||
fake_popen, captured = self._fake_popen_capture()
|
||||
|
||||
monkeypatch.setenv("INVOCATION_ID", "herdr-service-inherited-marker")
|
||||
monkeypatch.delenv("_HERMES_GATEWAY", raising=False)
|
||||
monkeypatch.setattr("tools.process_registry._find_shell", lambda: "/bin/bash")
|
||||
monkeypatch.setattr(
|
||||
"tools.process_registry._systemd_run_user_scope_available",
|
||||
lambda: True,
|
||||
)
|
||||
monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/systemd-run")
|
||||
|
||||
with patch("subprocess.Popen", side_effect=fake_popen), \
|
||||
patch("threading.Thread", return_value=MagicMock()), \
|
||||
patch.object(registry, "_write_checkpoint"):
|
||||
registry.spawn_local("echo hello", cwd="/tmp")
|
||||
|
||||
assert captured["argv"] == [
|
||||
"/bin/bash",
|
||||
"-lic",
|
||||
"set +m; echo hello",
|
||||
]
|
||||
assert captured["start_new_session"] is True
|
||||
|
||||
def test_inherited_systemd_marker_does_not_scope_interactive_cli_pty(
|
||||
self, registry, monkeypatch
|
||||
):
|
||||
"""The same gateway-identity gate applies to PTY-backed workers."""
|
||||
from ptyprocess import PtyProcess
|
||||
|
||||
fake_pty = MagicMock()
|
||||
fake_pty.pid = 4321
|
||||
|
||||
monkeypatch.setenv("INVOCATION_ID", "herdr-service-inherited-marker")
|
||||
monkeypatch.delenv("_HERMES_GATEWAY", raising=False)
|
||||
monkeypatch.setattr("tools.process_registry._find_shell", lambda: "/bin/bash")
|
||||
monkeypatch.setattr(
|
||||
"tools.process_registry._systemd_run_user_scope_available",
|
||||
lambda: True,
|
||||
)
|
||||
monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/systemd-run")
|
||||
|
||||
with patch.object(PtyProcess, "spawn", return_value=fake_pty) as pty_spawn, \
|
||||
patch("threading.Thread", return_value=MagicMock()), \
|
||||
patch.object(registry, "_write_checkpoint"):
|
||||
session = registry.spawn_local("codex", cwd="/tmp", use_pty=True)
|
||||
|
||||
assert pty_spawn.call_args.args[0] == [
|
||||
"/bin/bash",
|
||||
"-lic",
|
||||
"set +m; codex",
|
||||
]
|
||||
assert session.systemd_unit == ""
|
||||
|
||||
def test_systemd_post_spawn_failure_never_kills_gateway_process_group(
|
||||
self, registry, monkeypatch
|
||||
):
|
||||
|
|
|
|||
|
|
@ -245,6 +245,27 @@ def _systemd_run_user_scope_available() -> bool:
|
|||
return available
|
||||
|
||||
|
||||
def _is_supervised_gateway_process() -> bool:
|
||||
"""Return whether this process is in a supervised Hermes gateway runtime.
|
||||
|
||||
Supervisor markers such as systemd's ``INVOCATION_ID`` are inherited by
|
||||
every descendant. An interactive CLI launched from a supervised terminal
|
||||
manager therefore cannot use that marker alone: its login-shell workers
|
||||
would stay in the CLI's session and could take ownership of the controlling
|
||||
tty. ``gateway.run`` adds ``_HERMES_GATEWAY`` to the gateway process tree;
|
||||
unrelated supervised terminal managers do not.
|
||||
"""
|
||||
if os.environ.get("_HERMES_GATEWAY") != "1":
|
||||
return False
|
||||
|
||||
try:
|
||||
from gateway.restart import is_gateway_supervisor_process
|
||||
|
||||
return is_gateway_supervisor_process()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _build_systemd_scope_argv(
|
||||
shell_argv: List[str],
|
||||
unit_suffix: str,
|
||||
|
|
@ -991,18 +1012,12 @@ class ProcessRegistry:
|
|||
# Cgroup isolation for PTY mode (#70716, reviewer gap #1):
|
||||
# Wrap the PTY command in a systemd scope so interactive
|
||||
# executors get their own cgroup, same as pipe mode.
|
||||
pty_use_systemd_scope = False
|
||||
try:
|
||||
from gateway.restart import is_gateway_supervisor_process
|
||||
|
||||
pty_under_supervisor = is_gateway_supervisor_process()
|
||||
pty_use_systemd_scope = (
|
||||
not _IS_WINDOWS
|
||||
and pty_under_supervisor
|
||||
and _systemd_run_user_scope_available()
|
||||
)
|
||||
except Exception:
|
||||
pty_use_systemd_scope = False
|
||||
pty_under_supervisor = _is_supervised_gateway_process()
|
||||
pty_use_systemd_scope = (
|
||||
not _IS_WINDOWS
|
||||
and pty_under_supervisor
|
||||
and _systemd_run_user_scope_available()
|
||||
)
|
||||
|
||||
if pty_use_systemd_scope:
|
||||
pty_argv = _build_systemd_scope_argv(
|
||||
|
|
@ -1081,18 +1096,12 @@ class ProcessRegistry:
|
|||
# for both pipe mode and the PTY path above.
|
||||
shell_argv = [user_shell, "-lic", f"set +m; {safe_command}"]
|
||||
use_systemd_scope = False
|
||||
under_supervisor = False
|
||||
try:
|
||||
from gateway.restart import is_gateway_supervisor_process
|
||||
|
||||
under_supervisor = is_gateway_supervisor_process()
|
||||
use_systemd_scope = (
|
||||
not _IS_WINDOWS
|
||||
and under_supervisor
|
||||
and _systemd_run_user_scope_available()
|
||||
)
|
||||
except Exception:
|
||||
use_systemd_scope = False
|
||||
under_supervisor = _is_supervised_gateway_process()
|
||||
use_systemd_scope = (
|
||||
not _IS_WINDOWS
|
||||
and under_supervisor
|
||||
and _systemd_run_user_scope_available()
|
||||
)
|
||||
|
||||
if use_systemd_scope:
|
||||
unit_suffix = (
|
||||
|
|
|
|||
Loading…
Reference in New Issue