fix(dashboard): list --port 0 serve backends in --status
--status probed the port parsed from argv, so a backend launched with --port 0 (Desktop SSH backends) was always skipped while --stop still killed it. Use the bind the spawn ledger records after listening.
This commit is contained in:
@@ -108,6 +108,23 @@ def _scan_dashboard_processes(*, exclude_pids: set[int] | None = None) -> list[t
|
||||
return found
|
||||
|
||||
|
||||
def _ledger_serve_binds() -> dict[int, tuple[str, int]]:
|
||||
"""``pid -> (host, port)`` recorded in the spawn ledger for live serve/dashboard backends.
|
||||
|
||||
The entry is written after the bind, so it carries the real port where argv only says
|
||||
``--port 0`` (Desktop SSH backends ask the OS for a port). Empty when the ledger is unavailable.
|
||||
"""
|
||||
binds: dict[int, tuple[str, int]] = {}
|
||||
with contextlib.suppress(Exception):
|
||||
from hermes_cli.process_identity import ledger_entries
|
||||
for entry in ledger_entries():
|
||||
pid, port = entry.get("pid"), entry.get("port")
|
||||
if (entry.get("purpose") in ("serve", "dashboard") and isinstance(pid, int)
|
||||
and isinstance(port, int) and port > 0):
|
||||
binds[pid] = (str(entry.get("host") or ""), port)
|
||||
return binds
|
||||
|
||||
|
||||
def _pid_environ(pid: int) -> dict[str, str] | None:
|
||||
"""Exec-time environment of *pid* (psutil, then /proc); ``None`` when unreadable."""
|
||||
with contextlib.suppress(Exception):
|
||||
|
||||
@@ -503,16 +503,21 @@ def _report_dashboard_status() -> int:
|
||||
``--status`` let an operator kill what they couldn't see.
|
||||
|
||||
Ledger-registered serves (profiled launches the argv scan can't match) surface via the spawn-ledger
|
||||
augmentation in _scan_dashboard_processes. See #81564.
|
||||
augmentation in _scan_dashboard_processes, and the ledger's recorded bind replaces the argv port so
|
||||
``--port 0`` backends are probed on the port the OS actually gave them. See #81564.
|
||||
"""
|
||||
from hermes_cli.dashboard_procs import _scan_dashboard_processes
|
||||
from hermes_cli.dashboard_procs import _ledger_serve_binds, _scan_dashboard_processes
|
||||
from gateway.status import _pid_exists
|
||||
binds = _ledger_serve_binds()
|
||||
live: list[tuple[int, str, str]] = []
|
||||
for pid, command in _scan_dashboard_processes():
|
||||
runtime = _parse_dashboard_runtime(command)
|
||||
if runtime is None:
|
||||
continue
|
||||
mode, host, port = runtime
|
||||
if pid in binds:
|
||||
ledger_host, port = binds[pid]
|
||||
host = ledger_host or host
|
||||
if port <= 0 or not _pid_exists(pid) or not _dashboard_listening(host, port):
|
||||
continue
|
||||
live.append((pid, command, mode))
|
||||
|
||||
@@ -56,6 +56,31 @@ class TestDashboardStatus:
|
||||
assert "PID 12346" in out
|
||||
assert "PID 12347" in out and "[serve]" in out
|
||||
|
||||
def test_status_lists_os_assigned_port_serve_that_stop_targets(self, capsys, monkeypatch):
|
||||
"""A ``--port 0`` serve (Desktop SSH backend) is listed on the port the ledger recorded,
|
||||
so ``--status`` shows every backend ``--stop`` would kill (#81564)."""
|
||||
import socket
|
||||
|
||||
from hermes_cli import dashboard_procs, process_identity
|
||||
from hermes_cli.main_dashboard import _find_stale_dashboard_pids
|
||||
|
||||
pid = 424242
|
||||
with socket.socket() as listener:
|
||||
listener.bind(("127.0.0.1", 0))
|
||||
listener.listen()
|
||||
real_port = listener.getsockname()[1]
|
||||
monkeypatch.setattr(dashboard_procs, "_iter_process_table", lambda: [
|
||||
(pid, "/usr/local/bin/hermes serve --host 127.0.0.1 --port 0 --ssh-isolated")])
|
||||
monkeypatch.setattr(process_identity, "ledger_entries", lambda: [
|
||||
{"pid": pid, "purpose": "serve", "host": "127.0.0.1", "port": real_port}])
|
||||
monkeypatch.setattr("gateway.status._pid_exists", lambda p: p == pid)
|
||||
|
||||
assert _find_stale_dashboard_pids() == [pid]
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
cmd_dashboard(_ns(status=True))
|
||||
assert exc.value.code == 0
|
||||
assert f"PID {pid} [serve]" in capsys.readouterr().out
|
||||
|
||||
def test_status_does_not_try_to_import_fastapi(self):
|
||||
"""`--status` must not require dashboard runtime deps — it's a
|
||||
process-table scan only. We prove this by making fastapi import
|
||||
|
||||
Reference in New Issue
Block a user