fix(herdr): detach daemon bootstrap into own process group via python spawner
- Replace 'nohup ... server & disown' with portable Python 'Popen(..., start_new_session=True)' in lib.sh shim generator - Isolate headless herdr server in its own SID/PGID to protect against parent process-group signal broadcast (SIGTERM/SIGINT) - Add regression test test_h26_daemon_spawn_uses_process_group_detachment to verify child PGID detachment (Closes #3 Item 1)
This commit is contained in:
@@ -550,3 +550,49 @@ def test_h25_new_session_idle_pane_preserves_env_and_ws_id(mam_sandbox, mock_her
|
||||
split_calls = [c for c in new_calls if len(c) >= 2 and c[0] == "pane" and c[1] == "split"]
|
||||
assert not split_calls, f"Expected no pane split call when an idle pane was available: {split_calls}"
|
||||
|
||||
|
||||
def test_h26_daemon_spawn_uses_process_group_detachment(mam_sandbox, mock_herdr, mock_agents):
|
||||
"""H-26 (Issue #3): the headless herdr daemon bootstrap must fully detach into
|
||||
its own process group/session rather than relying on nohup+disown, which only
|
||||
masks SIGHUP and drops bash job-table tracking - it does NOT change PGID/SID,
|
||||
so the daemon previously died on any process-group signal broadcast."""
|
||||
shim_path = mam_sandbox / ".mam" / "shim" / "herdr"
|
||||
res = _run_lib(mam_sandbox, "true")
|
||||
assert res.returncode == 0, res.stderr
|
||||
shim = shim_path.read_text()
|
||||
|
||||
assert 'nohup "$REAL_HERDR" --session "$_MAM_SESSION" server' not in shim
|
||||
assert "start_new_session=True" in shim
|
||||
|
||||
# Extract the actual shipped spawner statement (not a re-implementation) and
|
||||
# run it standalone against a fake REAL_HERDR that just sleeps, to prove the
|
||||
# spawned process ends up in a DIFFERENT process group than the caller shell.
|
||||
start_marker = "_mam_server_pid=$(python3 -c '"
|
||||
end_marker = "' \"$REAL_HERDR\" --session \"$_MAM_SESSION\" server)"
|
||||
start = shim.index(start_marker)
|
||||
end = shim.index(end_marker, start) + len(end_marker)
|
||||
spawner_stmt = shim[start:end]
|
||||
assert "start_new_session=True" in spawner_stmt
|
||||
|
||||
fake_herdr = mam_sandbox / "fake_herdr.sh"
|
||||
fake_herdr.write_text("#!/usr/bin/env bash\nexec sleep 30\n")
|
||||
fake_herdr.chmod(0o755)
|
||||
|
||||
script = f"""
|
||||
set -euo pipefail
|
||||
REAL_HERDR="{fake_herdr}"
|
||||
_MAM_SESSION="testsess"
|
||||
{spawner_stmt}
|
||||
echo "PID=$_mam_server_pid"
|
||||
echo "SHELL_PGID=$(ps -o pgid= -p $$ | tr -d ' ')"
|
||||
sleep 0.3
|
||||
echo "CHILD_PGID=$(ps -o pgid= -p $_mam_server_pid | tr -d ' ')"
|
||||
kill "$_mam_server_pid" 2>/dev/null || true
|
||||
"""
|
||||
res2 = subprocess.run(["bash", "-c", script], capture_output=True, text=True)
|
||||
assert res2.returncode == 0, f"stderr: {res2.stderr}\nstdout: {res2.stdout}"
|
||||
fields = dict(line.split("=", 1) for line in res2.stdout.strip().splitlines())
|
||||
assert fields["CHILD_PGID"] != fields["SHELL_PGID"], (
|
||||
f"daemon child stayed in the caller's process group (not detached): {fields}"
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user