From 2489f15040b3234ec637365e325bfc704ebd73db Mon Sep 17 00:00:00 2001 From: salch-cred <141555468+salch-cred@users.noreply.github.com> Date: Wed, 9 Sep 2026 20:26:04 +0530 Subject: [PATCH] fix(serve): ensure parent-death watchdog detects exited Desktop parent (#80204) --- hermes_cli/web_server_lifecycle.py | 19 ++++-- .../hermes_cli/test_serve_parent_watchdog.py | 63 +++++++++++++++++++ 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/hermes_cli/web_server_lifecycle.py b/hermes_cli/web_server_lifecycle.py index 14b554628c..ccb26d425c 100644 --- a/hermes_cli/web_server_lifecycle.py +++ b/hermes_cli/web_server_lifecycle.py @@ -75,7 +75,7 @@ def _process_start_marker(pid: int) -> str: marker = result.stdout.strip() if result.returncode == 0 and marker: return f"ps:{marker}" - if result.returncode == 1 and not marker: + if result.returncode != 0 and (not marker or "no such process" in result.stderr.lower() or "not found" in result.stderr.lower()): raise ProcessLookupError(pid) raise OSError(f"ps could not inspect PID {pid}: {result.stderr.strip()}") @@ -278,12 +278,19 @@ def _is_serve_orphaned( try: if expected_start_marker is not None: probe = process_start_marker or _process_start_marker - actual_marker = probe(int(desktop_pid)) - if _parent_start_markers_match(actual_marker, expected_start_marker): - return False - if _parent_start_marker_mismatch_is_conclusive(actual_marker, expected_start_marker): + try: + actual_marker = probe(int(desktop_pid)) + except ProcessLookupError: return True - # Inconclusive marker: degrade to PID liveness instead of exiting. + except Exception: + actual_marker = None + + if actual_marker is not None: + if _parent_start_markers_match(actual_marker, expected_start_marker): + return False + if _parent_start_marker_mismatch_is_conclusive(actual_marker, expected_start_marker): + return True + # Inconclusive marker: degrade to PID liveness instead of exiting. if pid_exists is None: from gateway.status import _pid_exists diff --git a/tests/hermes_cli/test_serve_parent_watchdog.py b/tests/hermes_cli/test_serve_parent_watchdog.py index 0b146a2a80..1f23d02b7d 100644 --- a/tests/hermes_cli/test_serve_parent_watchdog.py +++ b/tests/hermes_cli/test_serve_parent_watchdog.py @@ -175,3 +175,66 @@ def test_parent_watchdog_warns_when_disarmed_by_unusable_marker(monkeypatch, cap class _NoThread: def start(self): raise AssertionError("watchdog thread must not start") + + +def test_parent_watchdog_detects_dead_parent_when_start_marker_probe_raises_oserror(): + """#80204: when marker probe fails with OSError on a dead parent, watchdog + must degrade to PID liveness check and reap the orphan, not return False.""" + def broken_marker_probe(_pid: int) -> str: + raise OSError("ps could not inspect PID 4242: ps: 4242: No such process") + + # Parent is dead: pid_exists returns False -> must return True (orphaned) + assert ( + _is_serve_orphaned( + 4242, + "ps:Thu Aug 20 22:33:11 2026", + pid_exists=lambda _pid: False, + process_start_marker=broken_marker_probe, + ) + is True + ) + + # Parent is still alive: pid_exists returns True -> fail-safe False + assert ( + _is_serve_orphaned( + 4242, + "ps:Thu Aug 20 22:33:11 2026", + pid_exists=lambda _pid: True, + process_start_marker=broken_marker_probe, + ) + is False + ) + + +def test_parent_watchdog_detects_dead_parent_on_process_lookup_error(): + """#80204: ProcessLookupError from probe immediately signals parent is gone.""" + def lookup_error_probe(pid: int) -> str: + raise ProcessLookupError(pid) + + assert ( + _is_serve_orphaned( + 4242, + "ps:Thu Aug 20 22:33:11 2026", + pid_exists=lambda _pid: True, + process_start_marker=lookup_error_probe, + ) + is True + ) + + +def test_ps_process_start_marker_raises_process_lookup_error_on_missing_process(monkeypatch): + """#80204: _process_start_marker raises ProcessLookupError when ps exits non-zero with no marker.""" + import subprocess + import pytest + from hermes_cli import web_server_lifecycle + + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess(args=args, returncode=1, stdout="", stderr="ps: 4242: No such process") + + monkeypatch.setattr(subprocess, "run", fake_run) + monkeypatch.setattr(web_server_lifecycle.sys, "platform", "darwin") + monkeypatch.setattr(web_server_lifecycle.os, "name", "posix") + + with pytest.raises(ProcessLookupError): + web_server_lifecycle._process_start_marker(4242) +