fix(dashboard): --stop exits 0 when a launchd KeepAlive job respawns the backend
`hermes dashboard --stop` decided its exit code by re-scanning the process table after the kill. On macOS a launchd KeepAlive job brings the backend back on a fresh PID within the grace window, so the re-scan found it and --stop exited 1 right after printing that the stop succeeded and that the job restarts itself. Judge the exit code from the kill result instead: exit 1 only when a matched pid could not be stopped.
This commit is contained in:
+5
-3
@@ -2439,11 +2439,13 @@ def _dashboard_lifecycle_flags(args, token_file) -> None:
|
||||
print("No hermes dashboard processes running.")
|
||||
sys.exit(0)
|
||||
# Reuse the same SIGTERM-grace-SIGKILL path used after `hermes update`;
|
||||
# it prints outcomes itself. Exit 1 only if every pid was unkillable.
|
||||
# it prints outcomes itself. Exit 1 only if a pid was unkillable — judged
|
||||
# from the kill result, not a re-scan: a launchd KeepAlive job respawns
|
||||
# its backend on a fresh PID, which is not a failed stop.
|
||||
from hermes_cli.dashboard_procs import _kill_stale_dashboard_processes
|
||||
|
||||
_kill_stale_dashboard_processes(reason="requested via --stop")
|
||||
sys.exit(1 if _find_stale_dashboard_pids() else 0)
|
||||
result = _kill_stale_dashboard_processes(reason="requested via --stop")
|
||||
sys.exit(1 if result["failed"] else 0)
|
||||
|
||||
|
||||
def _dashboard_validate_serve_args(args, headless_backend, token_file):
|
||||
|
||||
@@ -80,12 +80,15 @@ class TestDashboardStatus:
|
||||
class TestDashboardStop:
|
||||
|
||||
def test_stop_kills_and_exits_zero_when_all_killed(self, capsys):
|
||||
"""After the kill, if the second scan returns empty we exit 0."""
|
||||
# First scan: finds two processes. Second (verification) scan: empty.
|
||||
scans = iter([[12345, 12346], []])
|
||||
"""Every matched pid was killed -> exit 0, even when a scan afterwards would find a
|
||||
process again: a launchd KeepAlive job respawns its backend on a fresh PID, and that
|
||||
respawn is not a failed stop (the kill path already warns about it)."""
|
||||
scans = iter([[12345, 12346], [12347]])
|
||||
with patch("hermes_cli.main._find_stale_dashboard_pids",
|
||||
side_effect=lambda: next(scans)), \
|
||||
patch("hermes_cli.dashboard_procs._kill_stale_dashboard_processes") as mock_kill, \
|
||||
patch("hermes_cli.dashboard_procs._kill_stale_dashboard_processes",
|
||||
return_value={"matched": [12345, 12346], "killed": [12345, 12346],
|
||||
"failed": [], "unrecovered": [12345, 12346]}) as mock_kill, \
|
||||
pytest.raises(SystemExit) as exc:
|
||||
cmd_dashboard(_ns(stop=True))
|
||||
mock_kill.assert_called_once()
|
||||
@@ -98,12 +101,12 @@ class TestDashboardStop:
|
||||
assert exc.value.code == 0
|
||||
|
||||
def test_stop_exits_nonzero_if_kill_leaves_survivors(self):
|
||||
"""If the second scan still finds PIDs, we exit 1 so scripts can
|
||||
detect that the stop didn't succeed (e.g. permission denied)."""
|
||||
scans = iter([[12345], [12345]]) # both scans find the same PID
|
||||
with patch("hermes_cli.main._find_stale_dashboard_pids",
|
||||
side_effect=lambda: next(scans)), \
|
||||
patch("hermes_cli.dashboard_procs._kill_stale_dashboard_processes"), \
|
||||
"""A pid the kill path could not stop (e.g. permission denied) -> exit 1 so
|
||||
scripts can detect that the stop didn't succeed."""
|
||||
with patch("hermes_cli.main._find_stale_dashboard_pids", return_value=[12345]), \
|
||||
patch("hermes_cli.dashboard_procs._kill_stale_dashboard_processes",
|
||||
return_value={"matched": [12345], "killed": [],
|
||||
"failed": [(12345, "Operation not permitted")], "unrecovered": []}), \
|
||||
pytest.raises(SystemExit) as exc:
|
||||
cmd_dashboard(_ns(stop=True))
|
||||
assert exc.value.code == 1
|
||||
|
||||
Reference in New Issue
Block a user