fix(process): kill a re-adopted PID whose start time is unreadable

This commit is contained in:
Hermes Agent
2026-09-24 23:46:48 -05:00
committed by brooklyn!
parent 7550800d8b
commit 627bf49baa
2 changed files with 30 additions and 6 deletions

View File

@@ -100,6 +100,28 @@ class TestCheckpointReadopt:
proc.kill()
proc.wait()
def test_kill_terminates_readopted_pid_when_start_probe_fails(
self, registry, monkeypatch
):
"""kill must reach a re-adopted live child with an unreadable start
time, not report already_exited and leave it running unsupervised."""
proc = _sleep_proc()
try:
start = ProcessRegistry._safe_host_start_time(proc.pid)
assert start is not None
session = _detach(registry, proc, sid="proc_kill_probe", start=start)
monkeypatch.setattr(ProcessRegistry, "_safe_host_start_time", staticmethod(lambda _pid: None))
result = registry.kill_process(session.id)
assert result["status"] == "killed", result
proc.wait(timeout=10)
assert session.exited is True
finally:
if proc.poll() is None:
proc.kill()
proc.wait()
def test_gone_pid_is_pruned_without_a_false_completion(self, registry):
session = ProcessSession(
id="proc_gone",

View File

@@ -1174,7 +1174,7 @@ class ProcessRegistry(ProcessCheckpointMixin):
if pty_alive:
survivors.append(session.pid)
if session.pid_scope == "host" and session.pid:
if self._host_pid_is_ours(session.pid, session.host_start_time):
if self._detached_host_fate(session.pid, session.host_start_time) == "running":
if session.pid not in survivors:
survivors.append(session.pid)
survivors.extend(
@@ -2290,13 +2290,13 @@ class ProcessRegistry(ProcessCheckpointMixin):
elif session.env_ref and session.pid:
session.env_ref.execute(f"kill {session.pid} 2>/dev/null", timeout=5)
elif session.detached and session.pid_scope == "host" and session.pid:
# Identity check, not bare liveness: a gone/recycled PID means our
# process exited — never tree-kill the stranger. Still stop an owned
# scope: a daemonized descendant may survive the wrapper PID.
# Same fate as poll/list: a gone or reused PID means our process is
# gone — never tree-kill the stranger — but a live PID with an
# unreadable start time is still ours and must really be killed.
# If this recovered session also carries an owned systemd scope, stop that scope before
# returning: a daemonized descendant may still be alive there even though the wrapper PID exited
# or was recycled across the gateway restart (#70716, teknium1 review).
if not self._host_pid_is_ours(session.pid, session.host_start_time):
if self._detached_host_fate(session.pid, session.host_start_time) != "running":
if session.systemd_unit:
_stop_systemd_unit(session.systemd_unit)
with session._lock:
@@ -2308,7 +2308,9 @@ class ProcessRegistry(ProcessCheckpointMixin):
# whose start time does not match.
self._close_reused_detached(session)
return {"status": "already_exited", "exit_code": session.exit_code, **output}
self._terminate_host_pid(session.pid, session.host_start_time)
# Identity was just proven above. Re-passing the start time would make
# an unreadable probe refuse the kill and leave the re-adopted child running.
self._terminate_host_pid(session.pid)
else:
return {
# Reject non-positive timeouts — the schema declares minimum=1, but not every caller