diff --git a/gateway/status.py b/gateway/status.py index 74763332c86c5..88239f247740b 100644 --- a/gateway/status.py +++ b/gateway/status.py @@ -216,10 +216,11 @@ def _cleanup_invalid_pid_path(pid_path: Path, *, cleanup_stale: bool) -> None: if not cleanup_stale: return try: - if pid_path == _get_pid_path(): - remove_pid_file() - else: - pid_path.unlink(missing_ok=True) + # Invalid/stale PID records must be removed unconditionally. + # Do NOT call remove_pid_file() here: that helper intentionally keeps + # PID files that belong to a different process during graceful + # handoffs, which is the opposite of what stale cleanup needs. + pid_path.unlink(missing_ok=True) except Exception: pass diff --git a/tests/gateway/test_status.py b/tests/gateway/test_status.py index 6c371cfbead62..4626e104e8bbb 100644 --- a/tests/gateway/test_status.py +++ b/tests/gateway/test_status.py @@ -51,6 +51,26 @@ def test_get_running_pid_rejects_live_non_gateway_pid(self, tmp_path, monkeypatc assert status.get_running_pid() is None assert not pid_path.exists() + def test_get_running_pid_removes_stale_pid_from_different_process(self, tmp_path, monkeypatch): + """Regression: stale PID file owned by a dead *different* PID must be removed. + + Previously, stale cleanup called remove_pid_file() for the default PID path, + but that helper intentionally refuses to delete files that don't belong to + the current process. Result: a dead foreign PID could leave gateway.pid + behind forever and block startup with a PID-file race. + """ + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + pid_path = tmp_path / "gateway.pid" + pid_path.write_text(json.dumps({"pid": 99999})) + + def _dead_pid(pid, sig): + raise ProcessLookupError + + monkeypatch.setattr(status.os, "kill", _dead_pid) + + assert status.get_running_pid() is None + assert not pid_path.exists() + def test_get_running_pid_accepts_gateway_metadata_when_cmdline_unavailable(self, tmp_path, monkeypatch): monkeypatch.setenv("HERMES_HOME", str(tmp_path)) pid_path = tmp_path / "gateway.pid"