Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contributors/emails/ilovethevikings@yahoo.com
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
insane66613
32 changes: 24 additions & 8 deletions hermes_cli/web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -17696,9 +17696,26 @@ def _open():
threading.Thread(target=_open, daemon=True).start()


def _is_serve_orphaned(original_ppid: int, getppid=os.getppid) -> bool:
"""True when this process lost its original spawning parent (ppid changed)."""
return getppid() != original_ppid
def _is_serve_orphaned(desktop_pid: int, pid_exists=None) -> bool:
"""True when the Desktop process that owns this serve backend is gone.

``HERMES_PARENT_PID`` is the Electron Desktop PID, not necessarily this
Python process's immediate PPID. On Windows the venv ``hermes.exe`` launcher
introduces one or more shim processes, so comparing ``os.getppid()`` to the
Electron PID incorrectly treats a healthy backend as orphaned and exits 0.
Probe the recorded Desktop PID directly instead.

Any liveness-probe failure is fail-safe: keep serving rather than killing a
backend whose owner could not be conclusively shown to be dead.
"""
try:
if pid_exists is None:
from gateway.status import _pid_exists

pid_exists = _pid_exists
return not bool(pid_exists(int(desktop_pid)))
except Exception:
return False


def _start_parent_death_watchdog() -> None:
Expand All @@ -17716,7 +17733,7 @@ def _start_parent_death_watchdog() -> None:
if not raw:
return
try:
original_ppid = int(raw)
desktop_pid = int(raw)
except (TypeError, ValueError):
return
try:
Expand All @@ -17725,17 +17742,16 @@ def _start_parent_death_watchdog() -> None:
poll = 2.0

def _loop() -> None:
while not _is_serve_orphaned(original_ppid):
while not _is_serve_orphaned(desktop_pid):
time.sleep(poll)
os._exit(0)

threading.Thread(target=_loop, daemon=True, name="serve-parent-watchdog").start()


def _demo() -> None:
# orphan iff current ppid differs from the recorded spawning parent
assert _is_serve_orphaned(999999999, getppid=lambda: 1) is True
assert _is_serve_orphaned(42, getppid=lambda: 42) is False
assert _is_serve_orphaned(999999999, pid_exists=lambda _pid: False) is True
assert _is_serve_orphaned(42, pid_exists=lambda _pid: True) is False
print("web_server parent-death watchdog self-check: OK")


Expand Down
17 changes: 17 additions & 0 deletions tests/hermes_cli/test_serve_parent_watchdog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Regression tests for Desktop-owned ``hermes serve`` lifecycle tracking."""

from hermes_cli.web_server import _is_serve_orphaned


def test_parent_watchdog_tracks_recorded_desktop_pid_not_immediate_ppid():
"""Windows venv launch shims must not make a live Desktop look orphaned."""

assert _is_serve_orphaned(4242, pid_exists=lambda pid: pid == 4242) is False
assert _is_serve_orphaned(4242, pid_exists=lambda _pid: False) is True


def test_parent_watchdog_fails_safe_when_liveness_probe_errors():
def broken_probe(_pid: int) -> bool:
raise OSError("process table temporarily unavailable")

assert _is_serve_orphaned(4242, pid_exists=broken_probe) is False
Loading