""" 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 SOCKET_DIR = Path("/tmp/.browser_cli") REGISTRY_PATH = SOCKET_DIR / "registry.json" DEFAULT_SOCKET = str(SOCKET_DIR / "default.sock") class BrowserNotConnected(Exception): """Raised when the native host socket is not available.""" def _active_sockets(reg: dict) -> dict: """Return only entries whose socket file exists on disk.""" return {k: v for k, v in reg.items() if Path(v).exists()} 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 str(SOCKET_DIR / f"{safe}.sock") # Auto-detect: error when multiple browser instances are active if REGISTRY_PATH.exists(): try: reg = json.loads(REGISTRY_PATH.read_text()) active = _active_sockets(reg) if len(active) > 1: aliases = list(active.keys()) examples = "\n".join(f" browser-cli --browser {a} ..." for a in aliases) raise BrowserNotConnected( f"Multiple browser instances are active: {', '.join(aliases)}\n" f"Use --browser to select one:\n{examples}" ) if active: return next(iter(active.values())) except BrowserNotConnected: raise 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(" 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(" 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