From 1b4e7b2de4a4f257e8399a618b9af84860e256b7 Mon Sep 17 00:00:00 2001 From: liuhao1024 Date: Thu, 11 Jun 2026 14:15:27 +0800 Subject: [PATCH] fix(cli): detect dashboard PIDs when --profile precedes the subcommand (#44035) The stale-dashboard PID scanner used fixed substrings like 'hermes_cli.main dashboard' to find running dashboard processes. When --profile (or -p) sits between the entry point and the subcommand (e.g. 'python -m hermes_cli.main --profile prod dashboard'), none of the patterns matched and the process was missed. Add a _cmd_is_dashboard_subcommand() helper that checks for a known Hermes entry point plus 'dashboard' as a standalone whitespace-delimited token, covering the flag-in-between case while still rejecting unrelated processes that merely mention 'dashboard' in arguments. --- hermes_cli/main.py | 30 ++++++++++++-- .../hermes_cli/test_update_stale_dashboard.py | 40 +++++++++++++++++++ 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 5344105595890..78c45e49bc3df 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -5314,6 +5314,25 @@ def cmd_gui(args: argparse.Namespace): sys.exit(launch_result.returncode) +def _cmd_is_dashboard_subcommand(cmd: str) -> bool: + """Return True if *cmd* looks like a ``hermes dashboard`` invocation where + flags (e.g. ``--profile``) sit between the entry point and the subcommand. + + The existing fixed-substring patterns (``"hermes dashboard"``, etc.) + require the entry point and ``dashboard`` to be adjacent. This helper + covers the gap: it accepts *any* flags in between, as long as + ``dashboard`` appears as a standalone whitespace-delimited token. + """ + _ENTRY_POINTS = ("hermes_cli.main", "hermes_cli/main.py", "hermes") + if not any(ep in cmd for ep in _ENTRY_POINTS): + return False + # ``dashboard`` must appear as a standalone token — surrounded by + # whitespace or at a string boundary. This avoids matching the word + # inside quoted arguments (where a trailing quote follows) or as part + # of a hyphenated name (e.g. grafana-dashboard-server). + return " dashboard " in cmd or cmd.endswith(" dashboard") + + def _find_stale_dashboard_pids( *, exclude_pids: set[int] | None = None, @@ -5343,7 +5362,9 @@ def _find_stale_dashboard_pids( Returns an empty list on any scan error (missing ps/wmic, timeout, etc.). """ - patterns = [ + # Exact-substring patterns for the common case (no flags between entry + # point and subcommand). + _EXACT_DASHBOARD = [ "hermes dashboard", "hermes_cli.main dashboard", "hermes_cli/main.py dashboard", @@ -5378,8 +5399,9 @@ def _find_stale_dashboard_pids( elif line.startswith("ProcessId="): pid_str = line[len("ProcessId=") :] if ( - any(p in current_cmd for p in patterns) - and int(pid_str) != self_pid + int(pid_str) != self_pid + and (any(p in current_cmd for p in _EXACT_DASHBOARD) + or _cmd_is_dashboard_subcommand(current_cmd)) ): try: dashboard_pids.append(int(pid_str)) @@ -5411,7 +5433,7 @@ def _find_stale_dashboard_pids( except ValueError: continue command = parts[1] - if any(p in command for p in patterns) and pid != self_pid: + if pid != self_pid and (any(p in command for p in _EXACT_DASHBOARD) or _cmd_is_dashboard_subcommand(command)): dashboard_pids.append(pid) except (FileNotFoundError, subprocess.TimeoutExpired, OSError): return [] diff --git a/tests/hermes_cli/test_update_stale_dashboard.py b/tests/hermes_cli/test_update_stale_dashboard.py index 4134bacc9e87c..ad1794aee9385 100644 --- a/tests/hermes_cli/test_update_stale_dashboard.py +++ b/tests/hermes_cli/test_update_stale_dashboard.py @@ -157,6 +157,46 @@ def test_unrelated_process_containing_word_dashboard_not_matched(self): pids = _find_stale_dashboard_pids() assert pids == [12345] + def test_profile_flag_before_subcommand(self): + """Flags between entry point and subcommand must not break detection. + + Regression test for #44035: ``--profile $PROFILE`` between + ``hermes_cli.main`` and ``dashboard`` caused the stale-PID scan + to miss the process. + """ + with patch("subprocess.run") as mock_run: + mock_run.return_value = MagicMock( + returncode=0, + stdout="\n".join([ + _ps_line( + 12345, + "$HOME/.hermes/hermes-agent/venv/bin/python " + "-m hermes_cli.main " + "--profile prod dashboard " + "--host 0.0.0.0 --port 9119 --no-open --skip-build", + ), + _ps_line( + 22222, + "hermes --profile prod dashboard --port 9120", + ), + ]) + "\n", + stderr="", + ) + pids = _find_stale_dashboard_pids() + assert 12345 in pids + assert 22222 in pids + + def test_short_profile_flag_before_subcommand(self): + """``-p`` (short form of ``--profile``) must also be handled.""" + with patch("subprocess.run") as mock_run: + mock_run.return_value = MagicMock( + returncode=0, + stdout=_ps_line(12345, "hermes -p dev dashboard --port 9119") + "\n", + stderr="", + ) + pids = _find_stale_dashboard_pids() + assert 12345 in pids + def test_grep_lines_ignored(self): with patch("subprocess.run") as mock_run: mock_run.return_value = MagicMock(