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:
2026-08-31 10:30:42 +09:00
parent 50a9f32773
commit c49ee3bdd8
2 changed files with 61 additions and 3 deletions
+46
View File
@@ -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}"
)