test: reap session-leaked Hermes daemons on macOS too

The session-end reaper (78eff4628a, #106742) found leftover processes by
reading /proc/<pid>/environ, so on macOS it scanned nothing. There, e2e
tests that make a client start its own gateway (e.g. the C7 two-tenant
Desktop backend) leave a `gateway run --idle-exit` daemon running as a
launchd job, outside the pytest process tree. The test homes configure
an API server, so the gateway's idle exit keeps reporting
"messaging ['api_server']" and the daemon never exits: ~320 MB per run.
One dev Mac had 22 of them, 11-26 h old.

Read each process's exec-time environment through psutil on every POSIX
host instead, and run the reaper's own test on macOS. That test times
out on base on macOS and passes here; a real C7 run leaves no gateway
behind afterwards.
This commit is contained in:
kshitijk4poor authored and kshitij committed 2026-11-05 17:07:37 +05:30
1 parent 8e7cf55eb6
commit d0001c5ba2
2 files changed
+14 -13

No files matched your search

+12 -11
View File
@@ -1106,30 +1106,31 @@ def _remove_relocated_basetemp(config) -> None:
def _processes_with_hermes_home_under(roots) -> list[int]:
"""PIDs (never this process) whose environment names a ``HERMES_HOME`` inside one of *roots*.
Linux only (``/proc/<pid>/environ``; unreadable entries are another user's and skipped).
Elsewhere the scan is empty: a test-spawned daemon there is the per-test guard's problem.
POSIX (psutil's exec-time environment; another user's processes are unreadable and skipped).
Windows is out of scope: no leak observed there, and SIGTERM there is TerminateProcess.
"""
if not sys.platform.startswith("linux") or not os.path.isdir("/proc"):
if os.name != "posix":
return []
import psutil
resolved = [Path(r).resolve() for r in roots if r]
me = os.getpid()
found = []
for entry in os.scandir("/proc"):
if not entry.name.isdigit() or int(entry.name) == os.getpid():
for proc in psutil.process_iter():
if proc.pid == me:
continue
try:
with open(f"/proc/{entry.name}/environ", "rb") as handle:
environ = handle.read().split(b"\0")
except OSError:
home = proc.environ().get("HERMES_HOME")
except (psutil.Error, OSError):
continue
home = next((item[12:] for item in environ if item.startswith(b"HERMES_HOME=")), None)
if not home:
continue
try:
path = Path(os.fsdecode(home)).resolve()
path = Path(home).resolve()
except (OSError, ValueError):
continue
if any(path == root or path.is_relative_to(root) for root in resolved):
found.append(int(entry.name))
found.append(proc.pid)
return found
+2 -2
View File
@@ -23,7 +23,7 @@ def _detached(env_home, marker):
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
@pytest.mark.platforms("linux")
@pytest.mark.platforms("linux", "macos")
@pytest.mark.live_system_guard_bypass # signals only the two children this test spawned
def test_session_teardown_terminates_daemons_under_the_session_basetemp_only(tmp_path):
basetemp, elsewhere = tmp_path / "basetemp", tmp_path / "elsewhere"
@@ -32,7 +32,7 @@ def test_session_teardown_terminates_daemons_under_the_session_basetemp_only(tmp
inside = _detached(basetemp / "t0" / "hermes", "inside")
outside = _detached(elsewhere, "outside")
try:
# Both children have exec'd with their HERMES_HOME (what the reaper reads from /proc).
# Both children have exec'd with their HERMES_HOME (what the reaper reads via psutil environ()).
wait_until(lambda: {inside.pid, outside.pid} <= set(suite_conftest._processes_with_hermes_home_under(
[basetemp, elsewhere])), timeout=10, what='both detached children started')
config = SimpleNamespace(_tmp_path_factory=SimpleNamespace(_basetemp=basetemp))