From 0dba3316b2987f2ba8a3dad879a457d41cdf985b Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Fri, 14 Aug 2026 20:53:12 -0700 Subject: [PATCH] fix(gateway): generalize supervised-gateway exemption in orphan reaper to all platforms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compose the service-PID exclusion (#85743, RelaxJonh) and the recorded-PID + parent-chain exemption (#86100, arccat-114) into one cross-platform rule: - _get_service_pids() exclusion now runs unconditionally, not only under is_macos() — it is the authoritative "supervised" signal for launchd and any systemd unit visible on a host that got past the systemd gate. - The recorded-healthy-gateway (get_running_pid()) + parent-chain exemption now runs on every platform, not only Windows. A recorded, liveness-verified gateway is by definition not an orphan "the pidfile/runtime record can't see", so the reaper must never target it — this covers Windows Scheduled Task / Startup VBS supervision, standalone launcher-started gateways (the case #85743 alone would miss), and macOS/WSL equivalents. True orphans (no service registration, no valid runtime record) are still found and reaped, preserving the #51325/#75936 duplicate-port protection. Existing macOS regression tests updated to pin get_running_pid to None for their scenario; Windows regression tests from #86100 carry over unchanged. Bug class: #83683 (root), #86287, #86098, #85738, #85368, #85344, #85044, #84855, #84824, #84200. --- hermes_cli/gateway.py | 68 ++++++++++++++++---------------- tests/hermes_cli/test_gateway.py | 3 ++ 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index 594e651506..e8634ea12d 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -1555,44 +1555,42 @@ def _reap_unsupervised_gateway_orphans(extra_exclude: set | None = None) -> bool own = {os.getpid()} if extra_exclude: own |= extra_exclude - # On macOS, exclude the launchd-managed gateway PID so the orphan reaper - # doesn't kill a supervised gateway when Hermes Desktop opens (the serve - # process calls this on startup). supports_systemd_services() returns - # False on macOS, so without this the launchd gateway looks like an - # unsupervised orphan and gets SIGTERM'd, causing launchd to restart it. - if is_macos(): - try: - own |= _get_service_pids() - except Exception: - pass - # On Windows there is no systemd/launchd service query to fall back on + # Service-managed gateways are not orphans — never reap them. This + # covers macOS launchd (supports_systemd_services() is False there, so + # without this the launchd gateway looks like an unsupervised orphan and + # gets SIGTERM'd, causing launchd to restart it — or leaving it down + # under KeepAlive.SuccessfulExit=false) and any systemd unit reachable + # from a host that got past the gate above (#83683, #85344). + try: + own |= _get_service_pids() + except Exception: + pass + # On Windows there is no systemd/launchd service query at all # (_get_service_pids() returns an empty set), so a gateway supervised by # a Scheduled Task / Startup VBS looks like an unsupervised orphan to the - # process scan. Exempt the recorded healthy gateway PID and its parent - # chain: the Scheduled Task launches a ``gateway run`` bootstrap whose - # argv matches the gateway scan, and killing that bootstrap takes the - # detached gateway it spawned down with it (#86098). - if is_windows(): - try: - from gateway.status import get_running_pid - - recorded = get_running_pid() - if recorded and recorded > 0: - own.add(recorded) - try: - import psutil # type: ignore - - parent = psutil.Process(recorded).parent() - while parent is not None: - own.add(parent.pid) - parent = parent.parent() - except Exception: - pass - except Exception: - pass + # process scan (#86098). The same holds on every platform for a healthy + # gateway launched standalone (no service registration) whose PID the + # runtime record can see (#83683). Exempt the recorded healthy gateway + # PID and its parent chain: a recorded, liveness-verified gateway is by + # definition not an orphan "the pidfile/runtime record can't see", and + # the Scheduled-Task bootstrap's argv (``gateway run``) matches the + # gateway scan — killing that bootstrap takes the detached gateway it + # spawned down with it. try: - # launchd/systemd-supervised gateways are not orphans — never reap them. - own |= _get_service_pids() + from gateway.status import get_running_pid + + recorded = get_running_pid() + if recorded and recorded > 0: + own.add(recorded) + try: + import psutil # type: ignore + + parent = psutil.Process(recorded).parent() + while parent is not None: + own.add(parent.pid) + parent = parent.parent() + except Exception: + pass except Exception: pass try: diff --git a/tests/hermes_cli/test_gateway.py b/tests/hermes_cli/test_gateway.py index bc841e0ff9..d47c647506 100644 --- a/tests/hermes_cli/test_gateway.py +++ b/tests/hermes_cli/test_gateway.py @@ -447,6 +447,8 @@ class TestReapUnsupervisedGatewayOrphansMacOS: # _get_service_pids returns the launchd-managed gateway PID. monkeypatch.setattr(gateway, "_get_service_pids", lambda: {launchd_pid}) + # No pidfile-recorded gateway in this scenario. + monkeypatch.setattr("gateway.status.get_running_pid", lambda: None) # find_gateway_pids returns the launchd PID plus a real orphan. # The reaper should only kill the orphan, not the launchd PID. @@ -478,6 +480,7 @@ class TestReapUnsupervisedGatewayOrphansMacOS: monkeypatch.setattr(gateway, "is_macos", lambda: True) monkeypatch.setattr(gateway, "supports_systemd_services", lambda: False) monkeypatch.setattr(gateway, "_get_service_pids", lambda: {launchd_pid}) + monkeypatch.setattr("gateway.status.get_running_pid", lambda: None) # find_gateway_pids would return the launchd PID, but it's excluded. monkeypatch.setattr(