fix(gateway): generalize supervised-gateway exemption in orphan reaper to all platforms

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.
This commit is contained in:
Teknium
2026-08-14 20:53:12 -07:00
parent 102369c5f6
commit 0dba3316b2
2 changed files with 36 additions and 35 deletions
+33 -35
View File
@@ -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:
+3
View File
@@ -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(