Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/browser_harness/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,38 @@ def _chrome_running():
return False


def _detect_snap_browser():
"""On Linux, return the snap-confined browser path if a Chromium-based
browser is running from /snap/. Returns None on non-Linux, when no
matching process is found, or when the process check fails. Snap
confinement on Ubuntu/Lubuntu prevents binding to the CDP port (9222)
even with --no-sandbox, so the harness's auto-discovery fails silently
or with "Connection Refused"; surface this as a doctor warning so users
can install a non-snap browser before debugging the connection error.
"""
import platform, subprocess
if platform.system() != "Linux":
return None
try:
out = subprocess.check_output(
["ps", "-A", "-o", "args="], text=True, timeout=5,
)
except Exception:
return None
snap_path_markers = ("/snap/chromium/", "/snap/firefox/", "/snap/google-chrome/")
browser_basenames = ("chrome", "chromium", "msedge", "firefox")
for line in out.splitlines():
first = line.strip().split()
if not first:
continue
path = first[0]
if any(marker in path for marker in snap_path_markers):
basename = path.rsplit("/", 1)[-1].lower()
if any(b in basename for b in browser_basenames):
return path
return None


def _open_chrome_inspect():
"""Open chrome://inspect/#remote-debugging so the user can tick the checkbox."""
import platform, subprocess, webbrowser
Expand Down Expand Up @@ -695,6 +727,18 @@ def row(label, ok, detail=""):
else:
print(" latest release (could not reach github)")
row("chrome running", chrome, "" if chrome else "start chrome/edge")
snap_path = _detect_snap_browser()
if snap_path:
# Snap confinement runs the browser in a restricted mount namespace
# that blocks binding to CDP port 9222. The user will see the
# connection fail later; flagging it here saves the troubleshooting
# round-trip. Issue #191 has the long-form context.
print(
" [WARN] snap-confined browser detected — "
f"{snap_path}: install a non-snap chromium/chrome (e.g. "
"https://www.google.com/chrome/?platform=linux or `apt install "
"chromium-browser` from a non-snap PPA) for reliable CDP attach"
)
row("daemon alive", daemon, "" if daemon else "see install.md")
row("active browser connections", bool(connections), str(len(connections)))
for conn in connections:
Expand Down
107 changes: 107 additions & 0 deletions tests/unit/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ def test_run_doctor_prints_active_browser_connections_and_active_pages(monkeypat
monkeypatch.setattr(admin, "_version", lambda: "0.1.0")
monkeypatch.setattr(admin, "_install_mode", lambda: "git")
monkeypatch.setattr(admin, "_chrome_running", lambda: True)
monkeypatch.setattr(admin, "_detect_snap_browser", lambda: None)
monkeypatch.setattr(admin, "daemon_alive", lambda: True)
monkeypatch.setattr(admin, "browser_connections", lambda: [
{
Expand Down Expand Up @@ -537,3 +538,109 @@ def test_process_start_time_returns_none_for_invalid_pid():
)
# 2**31 - 1 is the largest pid_t; in practice no live process at that PID.
assert admin._process_start_time((1 << 31) - 1) is None


def test_detect_snap_browser_returns_none_on_non_linux(monkeypatch):
monkeypatch.setattr("platform.system", lambda: "Darwin")
assert admin._detect_snap_browser() is None


def test_detect_snap_browser_returns_none_when_ps_fails(monkeypatch):
monkeypatch.setattr("platform.system", lambda: "Linux")

def boom(*args, **kwargs):
raise OSError("ps not found")

monkeypatch.setattr("subprocess.check_output", boom)
assert admin._detect_snap_browser() is None


def test_detect_snap_browser_returns_none_when_no_browser_running(monkeypatch):
monkeypatch.setattr("platform.system", lambda: "Linux")
monkeypatch.setattr(
"subprocess.check_output",
lambda *args, **kwargs: "/usr/lib/systemd/systemd\n/usr/bin/dbus-daemon\n",
)
assert admin._detect_snap_browser() is None


def test_detect_snap_browser_flags_snap_chromium(monkeypatch):
monkeypatch.setattr("platform.system", lambda: "Linux")
monkeypatch.setattr(
"subprocess.check_output",
lambda *args, **kwargs: (
"/usr/lib/systemd/systemd\n"
"/snap/chromium/2876/usr/lib/chromium-browser/chromium --type=renderer\n"
"/usr/bin/dbus-daemon\n"
),
)
path = admin._detect_snap_browser()
assert path is not None
assert "/snap/chromium/" in path


def test_detect_snap_browser_flags_snap_firefox(monkeypatch):
monkeypatch.setattr("platform.system", lambda: "Linux")
monkeypatch.setattr(
"subprocess.check_output",
lambda *args, **kwargs: "/snap/firefox/4848/usr/lib/firefox/firefox\n",
)
path = admin._detect_snap_browser()
assert path is not None
assert "/snap/firefox/" in path


def test_detect_snap_browser_ignores_non_browser_snaps(monkeypatch):
monkeypatch.setattr("platform.system", lambda: "Linux")
monkeypatch.setattr(
"subprocess.check_output",
lambda *args, **kwargs: "/snap/code/198/usr/share/code/code\n",
)
assert admin._detect_snap_browser() is None


def test_detect_snap_browser_ignores_non_snap_chromium(monkeypatch):
monkeypatch.setattr("platform.system", lambda: "Linux")
monkeypatch.setattr(
"subprocess.check_output",
lambda *args, **kwargs: "/usr/bin/chromium --type=renderer\n",
)
assert admin._detect_snap_browser() is None


def test_run_doctor_emits_snap_warning_when_snap_browser_detected(monkeypatch, capsys):
monkeypatch.setattr(admin, "_version", lambda: "0.1.0")
monkeypatch.setattr(admin, "_install_mode", lambda: "git")
monkeypatch.setattr(admin, "_chrome_running", lambda: True)
monkeypatch.setattr(
admin, "_detect_snap_browser",
lambda: "/snap/chromium/2876/usr/lib/chromium-browser/chromium",
)
monkeypatch.setattr(admin, "daemon_alive", lambda: True)
monkeypatch.setattr(admin, "browser_connections", lambda: [])
monkeypatch.setattr(admin, "_latest_release_tag", lambda: "0.1.0")
monkeypatch.setattr("shutil.which", lambda _cmd: None)
monkeypatch.delenv("BROWSER_USE_API_KEY", raising=False)

admin.run_doctor()

out = capsys.readouterr().out
assert "[WARN] snap-confined browser detected" in out
assert "/snap/chromium/" in out


def test_run_doctor_skips_snap_warning_when_no_snap_browser(monkeypatch, capsys):
monkeypatch.setattr(admin, "_version", lambda: "0.1.0")
monkeypatch.setattr(admin, "_install_mode", lambda: "git")
monkeypatch.setattr(admin, "_chrome_running", lambda: True)
monkeypatch.setattr(admin, "_detect_snap_browser", lambda: None)
monkeypatch.setattr(admin, "daemon_alive", lambda: True)
monkeypatch.setattr(admin, "browser_connections", lambda: [])
monkeypatch.setattr(admin, "_latest_release_tag", lambda: "0.1.0")
monkeypatch.setattr("shutil.which", lambda _cmd: None)
monkeypatch.delenv("BROWSER_USE_API_KEY", raising=False)

admin.run_doctor()

out = capsys.readouterr().out
assert "snap-confined" not in out