fix(cli): exclude desktop-managed backend from stale-dashboard kill
Fixes #37532
This commit is contained in:
parent
d833b1eff7
commit
192020992d
@ -7363,7 +7363,10 @@ def cmd_gui(args: argparse.Namespace):
|
||||
sys.exit(launch_result.returncode)
|
||||
|
||||
|
||||
def _find_stale_dashboard_pids() -> list[int]:
|
||||
def _find_stale_dashboard_pids(
|
||||
*,
|
||||
exclude_pids: set[int] | None = None,
|
||||
) -> list[int]:
|
||||
"""Return PIDs of ``hermes dashboard`` processes other than ourselves.
|
||||
|
||||
``hermes dashboard`` is a long-lived server process commonly started and
|
||||
@ -7378,6 +7381,15 @@ def _find_stale_dashboard_pids() -> list[int]:
|
||||
it. This helper is just the detection step; see
|
||||
``_kill_stale_dashboard_processes`` for the kill.
|
||||
|
||||
*exclude_pids* is an optional set of PIDs that must never be returned.
|
||||
This is used by the Hermes Desktop Electron app to protect its own
|
||||
backend child process: when the desktop spawns ``hermes dashboard`` as
|
||||
a backend and triggers an auto-update, the update must not kill the
|
||||
dashboard that the desktop itself manages. The desktop sets the
|
||||
environment variable ``HERMES_DESKTOP_CHILD_PID`` on the spawned
|
||||
backend process; ``_kill_stale_dashboard_processes`` reads it and
|
||||
passes it here. (#37532)
|
||||
|
||||
Returns an empty list on any scan error (missing ps/wmic, timeout, etc.).
|
||||
"""
|
||||
patterns = [
|
||||
@ -7453,6 +7465,8 @@ def _find_stale_dashboard_pids() -> list[int]:
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
||||
return []
|
||||
|
||||
if exclude_pids:
|
||||
dashboard_pids = [p for p in dashboard_pids if p not in exclude_pids]
|
||||
return dashboard_pids
|
||||
|
||||
|
||||
@ -7604,7 +7618,18 @@ def _kill_stale_dashboard_processes(
|
||||
launch args (--host, --port, --insecure, --tui, --no-open). The user
|
||||
restarts it manually; a hint is printed.
|
||||
"""
|
||||
pids = _find_stale_dashboard_pids()
|
||||
# When the Hermes Desktop Electron app spawns this dashboard as a
|
||||
# backend child, it sets HERMES_DESKTOP_CHILD_PID so that the update
|
||||
# path can skip killing the desktop-managed process. (#37532)
|
||||
exclude: set[int] | None = None
|
||||
raw_pid = os.environ.get("HERMES_DESKTOP_CHILD_PID")
|
||||
if raw_pid:
|
||||
try:
|
||||
exclude = {int(raw_pid)}
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
pids = _find_stale_dashboard_pids(exclude_pids=exclude)
|
||||
if not pids:
|
||||
return
|
||||
|
||||
|
||||
@ -185,6 +185,48 @@ class TestFindStaleDashboardPids:
|
||||
pids = _find_stale_dashboard_pids()
|
||||
assert pids == [12345]
|
||||
|
||||
def test_exclude_pids_filters_specified_pids(self):
|
||||
"""exclude_pids removes specific PIDs from the result — used by
|
||||
the Desktop Electron app to protect its own backend child. (#37532)
|
||||
"""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(
|
||||
returncode=0,
|
||||
stdout="\n".join([
|
||||
_ps_line(11111, "hermes dashboard --port 9119"),
|
||||
_ps_line(22222, "hermes dashboard --port 9120"),
|
||||
_ps_line(33333, "hermes dashboard --port 9121"),
|
||||
]) + "\n",
|
||||
stderr="",
|
||||
)
|
||||
# Exclude the desktop-managed backend PID
|
||||
pids = _find_stale_dashboard_pids(exclude_pids={22222})
|
||||
assert 11111 in pids
|
||||
assert 22222 not in pids
|
||||
assert 33333 in pids
|
||||
|
||||
def test_exclude_pids_none_is_noop(self):
|
||||
"""Passing exclude_pids=None (the default) changes nothing."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(
|
||||
returncode=0,
|
||||
stdout=_ps_line(12345, "hermes dashboard --port 9119") + "\n",
|
||||
stderr="",
|
||||
)
|
||||
pids = _find_stale_dashboard_pids(exclude_pids=None)
|
||||
assert pids == [12345]
|
||||
|
||||
def test_exclude_all_pids_returns_empty(self):
|
||||
"""If all matched PIDs are excluded, the result is empty."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(
|
||||
returncode=0,
|
||||
stdout=_ps_line(12345, "hermes dashboard --port 9119") + "\n",
|
||||
stderr="",
|
||||
)
|
||||
pids = _find_stale_dashboard_pids(exclude_pids={12345})
|
||||
assert pids == []
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX kill semantics")
|
||||
class TestKillStaleDashboardPosix:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user