From 69ff114ee2ceffda9ea25dc40d6f43476bd9c843 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Tue, 28 Apr 2026 22:18:41 -0500 Subject: [PATCH] fix(browser): avoid bogus Chrome launch fallback Detect an actual Chrome/Chromium executable before printing a manual CDP launch command, including common WSL-mounted Windows browser paths, so /browser connect does not suggest google-chrome when it is unavailable. --- cli.py | 8 +++-- hermes_cli/browser_connect.py | 44 ++++++++++++++++----------- tests/cli/test_cli_browser_connect.py | 24 +++++++++++++++ tests/test_tui_gateway_server.py | 7 +++-- tui_gateway/server.py | 10 +++++- 5 files changed, 69 insertions(+), 24 deletions(-) diff --git a/cli.py b/cli.py index 09b31f614ec..a017b97e6ce 100644 --- a/cli.py +++ b/cli.py @@ -6630,8 +6630,12 @@ class HermesCLI: else: print(" ⚠ Could not auto-launch Chrome") sys_name = _plat.system() - print(f" Launch Chrome manually:") - print(f" {manual_chrome_debug_command(_port, sys_name)}") + chrome_cmd = manual_chrome_debug_command(_port, sys_name) + if chrome_cmd: + print(f" Launch Chrome manually:") + print(f" {chrome_cmd}") + else: + print(" No Chrome/Chromium executable found in this environment") else: print(f" ⚠ Port {_port} is not reachable at {cdp_url}") diff --git a/hermes_cli/browser_connect.py b/hermes_cli/browser_connect.py index f28a20a17b7..cd0d6889cab 100644 --- a/hermes_cli/browser_connect.py +++ b/hermes_cli/browser_connect.py @@ -4,6 +4,7 @@ from __future__ import annotations import os import platform +import shlex import shutil import subprocess @@ -64,6 +65,14 @@ def get_chrome_debug_candidates(system: str) -> list[str]: "google-chrome", "google-chrome-stable", "chromium-browser", "chromium", "brave-browser", "microsoft-edge", ) + for base in ("/mnt/c/Program Files", "/mnt/c/Program Files (x86)"): + for parts in ( + ("Google", "Chrome", "Application", "chrome.exe"), + ("Chromium", "Application", "chrome.exe"), + ("BraveSoftware", "Brave-Browser", "Application", "brave.exe"), + ("Microsoft", "Edge", "Application", "msedge.exe"), + ): + add(os.path.join(base, *parts)) return candidates @@ -72,27 +81,29 @@ def chrome_debug_data_dir() -> str: return str(get_hermes_home() / "chrome-debug") -def manual_chrome_debug_command(port: int = DEFAULT_BROWSER_CDP_PORT, system: str | None = None) -> str: +def _chrome_debug_args(port: int) -> list[str]: + return [ + f"--remote-debugging-port={port}", + f"--user-data-dir={chrome_debug_data_dir()}", + "--no-first-run", + "--no-default-browser-check", + ] + + +def manual_chrome_debug_command(port: int = DEFAULT_BROWSER_CDP_PORT, system: str | None = None) -> str | None: system = system or platform.system() - data_dir = chrome_debug_data_dir() + candidates = get_chrome_debug_candidates(system) + if candidates: + return " ".join(shlex.quote(part) for part in [candidates[0], *_chrome_debug_args(port)]) + if system == "Darwin": return ( 'open -a "Google Chrome" --args' f" --remote-debugging-port={port}" - f' --user-data-dir="{data_dir}"' + f' --user-data-dir="{chrome_debug_data_dir()}"' " --no-first-run --no-default-browser-check" ) - if system == "Windows": - return ( - f"chrome.exe --remote-debugging-port={port}" - f' --user-data-dir="{data_dir}"' - " --no-first-run --no-default-browser-check" - ) - return ( - f"google-chrome --remote-debugging-port={port}" - f' --user-data-dir="{data_dir}"' - " --no-first-run --no-default-browser-check" - ) + return None def try_launch_chrome_debug(port: int = DEFAULT_BROWSER_CDP_PORT, system: str | None = None) -> bool: @@ -105,10 +116,7 @@ def try_launch_chrome_debug(port: int = DEFAULT_BROWSER_CDP_PORT, system: str | subprocess.Popen( [ candidates[0], - f"--remote-debugging-port={port}", - f"--user-data-dir={chrome_debug_data_dir()}", - "--no-first-run", - "--no-default-browser-check", + *_chrome_debug_args(port), ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, diff --git a/tests/cli/test_cli_browser_connect.py b/tests/cli/test_cli_browser_connect.py index 69503d087a7..9805297b38f 100644 --- a/tests/cli/test_cli_browser_connect.py +++ b/tests/cli/test_cli_browser_connect.py @@ -4,6 +4,7 @@ import os from unittest.mock import patch from cli import HermesCLI +from hermes_cli.browser_connect import manual_chrome_debug_command def _assert_chrome_debug_cmd(cmd, expected_chrome, expected_port): @@ -55,3 +56,26 @@ class TestChromeDebugLaunch: assert HermesCLI._try_launch_chrome_debug(9222, "Windows") is True _assert_chrome_debug_cmd(captured["cmd"], installed, 9222) + + def test_manual_command_uses_detected_linux_browser(self): + with patch("hermes_cli.browser_connect.shutil.which", side_effect=lambda name: "/usr/bin/chromium" if name == "chromium" else None), \ + patch("hermes_cli.browser_connect.os.path.isfile", side_effect=lambda path: path == "/usr/bin/chromium"): + command = manual_chrome_debug_command(9222, "Linux") + + assert command is not None + assert command.startswith("/usr/bin/chromium --remote-debugging-port=9222") + + def test_manual_command_uses_wsl_windows_chrome_when_available(self): + chrome = "/mnt/c/Program Files/Google/Chrome/Application/chrome.exe" + + with patch("hermes_cli.browser_connect.shutil.which", return_value=None), \ + patch("hermes_cli.browser_connect.os.path.isfile", side_effect=lambda path: path == chrome): + command = manual_chrome_debug_command(9222, "Linux") + + assert command is not None + assert command.startswith(f"'{chrome}' --remote-debugging-port=9222") + + def test_manual_command_returns_none_when_linux_browser_missing(self): + with patch("hermes_cli.browser_connect.shutil.which", return_value=None), \ + patch("hermes_cli.browser_connect.os.path.isfile", return_value=False): + assert manual_chrome_debug_command(9222, "Linux") is None diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index 4a8cf1adb51..c5a56660282 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -2904,14 +2904,15 @@ def test_browser_manage_connect_default_local_reports_launch_hint(monkeypatch): ) with patch.dict(sys.modules, {"tools.browser_tool": fake}): _stub_urlopen(monkeypatch, ok=False) - with patch("hermes_cli.browser_connect.try_launch_chrome_debug", return_value=False): + with patch("hermes_cli.browser_connect.try_launch_chrome_debug", return_value=False), \ + patch("hermes_cli.browser_connect.get_chrome_debug_candidates", return_value=[]): resp = server.handle_request( {"id": "1", "method": "browser.manage", "params": {"action": "connect"}} ) assert resp["error"]["code"] == 5031 - assert "Start Chrome with remote debugging" in resp["error"]["message"] - assert "google-chrome --remote-debugging-port=9222" in resp["error"]["message"] + assert "No Chrome/Chromium executable was found" in resp["error"]["message"] + assert "--remote-debugging-port=9222" in resp["error"]["message"] assert "BROWSER_CDP_URL" not in os.environ diff --git a/tui_gateway/server.py b/tui_gateway/server.py index d426bba62a9..798fdeb55c3 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -4762,10 +4762,18 @@ def _is_default_local_cdp(parsed) -> bool: def _browser_connect_error(url: str, port: int) -> str: from hermes_cli.browser_connect import manual_chrome_debug_command + command = manual_chrome_debug_command(port) + if not command: + return ( + f"Chrome is not reachable at {url}. " + "No Chrome/Chromium executable was found in this environment; " + f"install one or start Chrome with --remote-debugging-port={port}, then retry /browser connect." + ) + return ( f"Chrome is not reachable at {url}. " "Start Chrome with remote debugging, then retry /browser connect:\n" - f"{manual_chrome_debug_command(port)}" + f"{command}" )