From c9469a2f5f0aec2553cd3086ab9b9cc4f577a84b Mon Sep 17 00:00:00 2001 From: HCL Date: Tue, 21 Apr 2026 11:02:32 +0800 Subject: [PATCH] fix(gateway): exclude CLI invoker ancestor chain in _append_unique_pid Reporter @yes999zc in #13242: `hermes gateway run` from an interactive CLI session got false-positive 'already running' and shut down immediately. The `ps aux` scan in `_scan_gateway_pids` matches generic substrings like 'hermes_cli.main gateway', which picks up the invoking CLI process AND its ancestor chain (parent shell, tmux session, launcher). `_append_unique_pid` already filtered `os.getpid()` but not the ancestor chain. The repo already has an ancestor-detection helper (`_is_pid_ancestor_of_current_process`) used by `_request_gateway_self_restart`; extend `_append_unique_pid` to reuse it, so CLI-invoker self-matches are filtered at the same point that covers `find_gateway_pids`, service probes, and scan results. Fixes #13242 --- hermes_cli/gateway.py | 8 ++++++++ tests/hermes_cli/test_gateway.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index bc809cadf93a..ef628a2c99c3 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -180,6 +180,14 @@ def _append_unique_pid(pids: list[int], pid: int | None, exclude_pids: set[int]) return if pid == os.getpid() or pid in exclude_pids or pid in pids: return + # Exclude the CLI invoker's ancestor chain (parent shell, launcher, tmux + # session, etc.). Otherwise `hermes gateway run` from an interactive CLI + # gets a self false-positive: `ps aux` matches the current hermes CLI + # and/or its ancestor, and the gateway decides it's already running and + # exits immediately. The same ancestor-detection helper is already used + # by `_request_gateway_self_restart`. (#13242) + if _is_pid_ancestor_of_current_process(pid): + return pids.append(pid) diff --git a/tests/hermes_cli/test_gateway.py b/tests/hermes_cli/test_gateway.py index 07265b2c3ac4..91f3a44b594a 100644 --- a/tests/hermes_cli/test_gateway.py +++ b/tests/hermes_cli/test_gateway.py @@ -253,6 +253,10 @@ def test_find_gateway_pids_falls_back_to_pid_file_when_process_scan_fails(monkey monkeypatch.setattr(gateway, "_get_service_pids", lambda: set()) monkeypatch.setattr(gateway, "is_windows", lambda: False) monkeypatch.setattr("gateway.status.get_running_pid", lambda: 321) + # _append_unique_pid now also excludes ancestor PIDs (#13242). Stub the + # ancestor check so this fallback test doesn't shell out to `ps -o ppid=` + # for every candidate pid (321 is a fake service pid, not a real ancestor). + monkeypatch.setattr(gateway, "_is_pid_ancestor_of_current_process", lambda pid: False) def fake_run(cmd, **kwargs): if cmd[:4] == ["ps", "-A", "eww", "-o"]: @@ -264,6 +268,34 @@ def fake_run(cmd, **kwargs): assert gateway.find_gateway_pids() == [321] +def test_append_unique_pid_excludes_current_process_ancestors(monkeypatch): + """#13242: `hermes gateway run` from an interactive CLI saw the CLI + process itself in `ps aux` and concluded 'already running'. The fix + extends `_append_unique_pid` to exclude not just `os.getpid()` but the + entire ancestor chain (parent shell, launcher, tmux/screen, etc.). + """ + # Simulate: current pid = 5000, its parent = 4000 (a "hermes" CLI process + # that ps picked up and would have been returned). `_append_unique_pid` + # must reject BOTH 5000 (current) and 4000 (ancestor). + monkeypatch.setattr("os.getpid", lambda: 5000) + + parent_chain = {5000: 4000, 4000: 1} # pid 4000 is the direct parent + + def fake_parent(pid): + return parent_chain.get(pid, 0) + + monkeypatch.setattr(gateway, "_get_parent_pid", fake_parent) + + pids: list[int] = [] + gateway._append_unique_pid(pids, 5000, set()) # current + gateway._append_unique_pid(pids, 4000, set()) # ancestor + gateway._append_unique_pid(pids, 7777, set()) # unrelated real gateway + assert pids == [7777], ( + "_append_unique_pid must exclude current pid AND its ancestor chain " + "so the CLI invoker doesn't self-detect as a running gateway (#13242)" + ) + + # --------------------------------------------------------------------------- # _wait_for_gateway_exit # ---------------------------------------------------------------------------