fix(browser): retry next candidate when debug launch exits early

This commit is contained in:
LeonSGP43 2026-05-31 09:23:47 +08:00 committed by Teknium
parent c7103c637c
commit c74f093523
2 changed files with 67 additions and 2 deletions

View file

@ -7,6 +7,7 @@ import platform
import shlex
import shutil
import subprocess
import time
from hermes_constants import get_hermes_home
@ -196,6 +197,31 @@ def _detach_kwargs(system: str) -> dict:
return {"creationflags": flags} if flags else {}
def _wait_for_browser_debug_ready_or_exit(
proc: subprocess.Popen,
port: int,
timeout: float = 2.0,
interval: float = 0.1,
) -> str:
"""Classify a launched browser as ready, exited, or still starting.
We only need to wait long enough to catch the common failure mode where a
candidate binary exists but exits immediately before exposing the CDP port.
Slower browsers can still finish starting after this grace window.
"""
cdp_url = f"http://127.0.0.1:{port}"
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if is_browser_debug_ready(cdp_url, timeout=min(interval, 0.2)):
return "ready"
if proc.poll() is not None:
return "exited"
time.sleep(interval)
return "starting"
def try_launch_chrome_debug(port: int = DEFAULT_BROWSER_CDP_PORT, system: str | None = None) -> bool:
system = system or platform.system()
candidates = get_chrome_debug_candidates(system)
@ -205,13 +231,15 @@ def try_launch_chrome_debug(port: int = DEFAULT_BROWSER_CDP_PORT, system: str |
os.makedirs(chrome_debug_data_dir(), exist_ok=True)
for candidate in candidates:
try:
subprocess.Popen(
proc = subprocess.Popen(
[candidate, *_chrome_debug_args(port)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
**_detach_kwargs(system),
)
return True
state = _wait_for_browser_debug_ready_or_exit(proc, port)
if state != "exited":
return True
except Exception:
continue
return False

View file

@ -9,6 +9,7 @@ from unittest.mock import patch
from cli import HermesCLI
from hermes_cli.browser_connect import (
_wait_for_browser_debug_ready_or_exit,
get_chrome_debug_candidates,
is_browser_debug_ready,
manual_chrome_debug_command,
@ -65,6 +66,7 @@ class TestChromeDebugLaunch:
with patch("hermes_cli.browser_connect.shutil.which", side_effect=lambda name: r"C:\Chrome\chrome.exe" if name == "chrome.exe" else None), \
patch("hermes_cli.browser_connect.os.path.isfile", side_effect=lambda path: path == r"C:\Chrome\chrome.exe"), \
patch("hermes_cli.browser_connect._wait_for_browser_debug_ready_or_exit", return_value="ready"), \
patch("subprocess.Popen", side_effect=fake_popen):
assert HermesCLI._try_launch_chrome_debug(9333, "Windows") is True
@ -94,6 +96,7 @@ class TestChromeDebugLaunch:
with patch("hermes_cli.browser_connect.shutil.which", return_value=None), \
patch("hermes_cli.browser_connect.os.path.isfile", side_effect=lambda path: path == installed), \
patch("hermes_cli.browser_connect._wait_for_browser_debug_ready_or_exit", return_value="ready"), \
patch("subprocess.Popen", side_effect=fake_popen):
assert HermesCLI._try_launch_chrome_debug(9222, "Windows") is True
@ -199,6 +202,40 @@ class TestChromeDebugLaunch:
assert attempts == [brave, chrome]
def test_wait_for_browser_debug_ready_or_exit_detects_early_exit(self, monkeypatch):
class _Proc:
def __init__(self):
self.calls = 0
def poll(self):
self.calls += 1
return 1 if self.calls >= 2 else None
monkeypatch.setattr("hermes_cli.browser_connect.time.sleep", lambda _seconds: None)
with patch("hermes_cli.browser_connect.is_browser_debug_ready", return_value=False):
state = _wait_for_browser_debug_ready_or_exit(_Proc(), 9222, timeout=0.3, interval=0.01)
assert state == "exited"
def test_launch_tries_next_browser_when_first_candidate_exits_before_debug_ready(self):
brave = "/usr/bin/brave-browser"
chrome = "/usr/bin/google-chrome"
attempts = []
class _Proc:
pass
def fake_popen(cmd, **kwargs):
attempts.append(cmd[0])
return _Proc()
with patch("hermes_cli.browser_connect.get_chrome_debug_candidates", return_value=[brave, chrome]), \
patch("hermes_cli.browser_connect._wait_for_browser_debug_ready_or_exit", side_effect=["exited", "ready"]), \
patch("subprocess.Popen", side_effect=fake_popen):
assert HermesCLI._try_launch_chrome_debug(9222, "Linux") is True
assert attempts == [brave, chrome]
def test_manual_command_uses_wsl_windows_chrome_when_available(self):
chrome = "/mnt/c/Program Files/Google/Chrome/Application/chrome.exe"