From b063452bea30e0c8b7f1b94131ba2f0b048d2372 Mon Sep 17 00:00:00 2001 From: voidborne-d Date: Tue, 5 May 2026 08:48:06 +0800 Subject: [PATCH] fix(admin): spawn chrome.exe directly on Windows for chrome:// inspect URL (#290) webbrowser.open("chrome://inspect/...") on Windows routes through ShellExecute, which has no registered handler for the chrome:// scheme. Instead of opening the page in Chrome, Windows shows a "Get an app to open this" dialog and the setup flow stalls waiting for the user to click Allow on a tab that never opened. Match the existing macOS branch's intent (talk to Chrome directly) by walking the standard Chrome install locations on Windows and spawning chrome.exe with the URL as a command-line argument. Fall through to the pre-existing webbrowser.open path when no Chrome binary is found, so Linux behaviour is unchanged. Tests cover both the chrome.exe-found path and the no-chrome-found fallback under a Windows platform mock. --- src/browser_harness/admin.py | 23 ++++++++++++++++++- tests/unit/test_admin.py | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/browser_harness/admin.py b/src/browser_harness/admin.py index 83109c41..09031fad 100644 --- a/src/browser_harness/admin.py +++ b/src/browser_harness/admin.py @@ -517,7 +517,8 @@ def _open_chrome_inspect(): """Open chrome://inspect/#remote-debugging so the user can tick the checkbox.""" import platform, subprocess, webbrowser url = "chrome://inspect/#remote-debugging" - if platform.system() == "Darwin": + system = platform.system() + if system == "Darwin": try: subprocess.run([ "osascript", @@ -527,6 +528,26 @@ def _open_chrome_inspect(): return except Exception: pass + elif system == "Windows": + # webbrowser.open() routes chrome:// through ShellExecute, which has no + # registered handler for the scheme -> Windows shows a "Get an app to + # open this" dialog instead of opening Chrome. Hand the URL to chrome.exe + # directly. (#290) + for env_var, suffix in ( + ("PROGRAMFILES", "Google/Chrome/Application/chrome.exe"), + ("PROGRAMFILES(X86)", "Google/Chrome/Application/chrome.exe"), + ("LOCALAPPDATA", "Google/Chrome/Application/chrome.exe"), + ): + base = os.environ.get(env_var) + if not base: + continue + candidate = Path(base) / suffix + if candidate.exists(): + try: + subprocess.Popen([str(candidate), url]) + return + except Exception: + pass try: webbrowser.open(url, new=2) except Exception: diff --git a/tests/unit/test_admin.py b/tests/unit/test_admin.py index 70be8afa..d08eed6f 100644 --- a/tests/unit/test_admin.py +++ b/tests/unit/test_admin.py @@ -1,3 +1,7 @@ +import platform +import subprocess +import webbrowser + import pytest from browser_harness import admin @@ -48,6 +52,46 @@ def test_stale_websocket_does_not_open_chrome_inspect(): assert not admin._needs_chrome_remote_debugging_prompt(msg) +def test_open_chrome_inspect_invokes_chrome_exe_directly_on_windows(tmp_path, monkeypatch): + """On Windows, webbrowser.open() can't handle chrome:// URLs (#290). + + Without a registered protocol handler, ShellExecute pops a "Get an app to + open this" dialog instead of opening Chrome -- so the function must spawn + chrome.exe with the URL as a command-line argument instead. + """ + chrome_exe = tmp_path / "Google" / "Chrome" / "Application" / "chrome.exe" + chrome_exe.parent.mkdir(parents=True) + chrome_exe.touch() + + monkeypatch.setattr(platform, "system", lambda: "Windows") + monkeypatch.setenv("PROGRAMFILES", str(tmp_path)) + monkeypatch.delenv("PROGRAMFILES(X86)", raising=False) + monkeypatch.delenv("LOCALAPPDATA", raising=False) + + spawned, opened = [], [] + monkeypatch.setattr(subprocess, "Popen", lambda argv, **kw: spawned.append(argv) or object()) + monkeypatch.setattr(webbrowser, "open", lambda url, **kw: opened.append(url) or True) + + admin._open_chrome_inspect() + + assert spawned == [[str(chrome_exe), "chrome://inspect/#remote-debugging"]] + assert opened == [], "webbrowser.open fallback must not run when chrome.exe was spawned" + + +def test_open_chrome_inspect_falls_back_to_webbrowser_when_no_chrome_exe_on_windows(monkeypatch): + monkeypatch.setattr(platform, "system", lambda: "Windows") + monkeypatch.delenv("PROGRAMFILES", raising=False) + monkeypatch.delenv("PROGRAMFILES(X86)", raising=False) + monkeypatch.delenv("LOCALAPPDATA", raising=False) + + opened = [] + monkeypatch.setattr(webbrowser, "open", lambda url, **kw: opened.append(url) or True) + + admin._open_chrome_inspect() + + assert opened == ["chrome://inspect/#remote-debugging"] + + def test_daemon_endpoint_names_discovers_valid_socket_names(tmp_path, monkeypatch): monkeypatch.setattr(admin.ipc, "IS_WINDOWS", False) monkeypatch.setattr(admin.ipc, "BH_TMP_DIR", None) # shared-tmpdir mode