Files
browser-cli/browser_cli/client.py
T

101 lines
3.2 KiB
Python

"""
Unix socket client — sends commands to the native host relay socket.
Used by both the CLI and the public Python API.
Profile selection order:
1. Explicit `profile` argument to send_command()
2. BROWSER_CLI_PROFILE environment variable
3. First entry in /tmp/browser-cli-registry.json
4. Fallback: /tmp/browser-cli-default.sock
"""
import json
import os
import socket
import struct
import uuid
from pathlib import Path
from typing import Any
REGISTRY_PATH = Path("/tmp/browser-cli-registry.json")
DEFAULT_SOCKET = "/tmp/browser-cli-default.sock"
class BrowserNotConnected(Exception):
"""Raised when the native host socket is not available."""
def _resolve_socket(profile: str | None = None) -> str:
"""Return the socket path for the given profile (or auto-detect)."""
target = profile or os.environ.get("BROWSER_CLI_PROFILE")
if target:
if REGISTRY_PATH.exists():
try:
reg = json.loads(REGISTRY_PATH.read_text())
if target in reg:
return reg[target]
except Exception:
pass
safe = target.replace(" ", "_").replace("/", "_")
return f"/tmp/browser-cli-{safe}.sock"
# Auto-detect: use first registered entry
if REGISTRY_PATH.exists():
try:
reg = json.loads(REGISTRY_PATH.read_text())
if reg:
return next(iter(reg.values()))
except Exception:
pass
return DEFAULT_SOCKET
def send_command(command: str, args: dict | None = None, profile: str | None = None) -> Any:
"""Send a command to the browser and return the response data."""
sock_path = _resolve_socket(profile)
msg = {
"id": str(uuid.uuid4()),
"command": command,
"args": args or {},
}
payload = json.dumps(msg).encode("utf-8")
framed = struct.pack("<I", len(payload)) + payload
try:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
sock.connect(sock_path)
sock.sendall(framed)
response = _recv_all(sock)
except (FileNotFoundError, ConnectionRefusedError, OSError):
profile_hint = f" (profile: {profile})" if profile else ""
raise BrowserNotConnected(
f"Cannot connect to browser{profile_hint}.\n"
"Make sure:\n"
" 1. The browser-cli extension is installed and enabled\n"
" 2. The native host is registered: uv run browser-cli install chrome\n"
" 3. Your browser is running\n"
" Tip: use BROWSER_CLI_PROFILE=<name> to select a specific profile"
)
result = json.loads(response)
if not result.get("success", True):
raise RuntimeError(result.get("error", "unknown error from browser"))
return result.get("data")
def _recv_all(sock: socket.socket) -> bytes:
raw_len = _recv_exact(sock, 4)
msg_len = struct.unpack("<I", raw_len)[0]
return _recv_exact(sock, msg_len)
def _recv_exact(sock: socket.socket, n: int) -> bytes:
buf = b""
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk:
raise ConnectionError("Socket closed before full message received")
buf += chunk
return buf