""" Local IPC client — sends commands to native host relay endpoint. Used by both CLI and public Python API. Profile selection order: 1. Explicit `profile` argument to send_command() 2. BROWSER_CLI_PROFILE environment variable 3. First entry in runtime registry 4. Otherwise, no browser can be resolved automatically """ import asyncio from typing import Any from browser_cli import local_transport from browser_cli.client import auth, messages, targets as target_discovery from browser_cli.client.targets import BrowserTarget from browser_cli.remote import registry as remote_registry from browser_cli.errors import BrowserNotConnected from browser_cli.endpoints import _remote_display_name from browser_cli.remote.transport import _send_remote, _send_remote_async def _remote_target_items(endpoint: str, items: list[dict] | None) -> list[BrowserTarget]: targets: list[BrowserTarget] = [] for item in items or []: profile = str(item.get("profile") or "default") display = str(item.get("displayName") or profile) targets.append( BrowserTarget( profile=profile, display_name=_remote_display_name(endpoint, profile, display), socket_path="", remote=endpoint, ) ) return targets def remote_browser_targets(endpoint: str, key=None, *, suppress_pq_warning: bool = False) -> list[BrowserTarget]: """Return browser targets advertised by a single remote endpoint.""" kwargs = {"suppress_pq_warning": True} if suppress_pq_warning else {} return _remote_target_items( endpoint, send_command("browser-cli.targets", remote=endpoint, key=key, **kwargs), ) def _remote_browser_targets(key=None, *, suppress_pq_warning: bool = False) -> list[BrowserTarget]: targets: list[BrowserTarget] = [] for endpoint in remote_registry.load_remotes(): try: targets.extend(remote_browser_targets(endpoint, key=key, suppress_pq_warning=suppress_pq_warning)) except (BrowserNotConnected, RuntimeError): continue return targets def remote_target_for_alias(alias: str | None) -> BrowserTarget | None: """Resolve a user-facing remote alias such as 'host:profile' to a target.""" if not alias: return None targets = _remote_browser_targets() for target in targets: endpoint_profile = f"{target.remote}:{target.profile}" if target.remote else None if alias in {target.display_name, endpoint_profile}: return target endpoint_matches = [] for target in targets: if not target.remote: continue remote_host, sep, _remote_port = target.remote.rpartition(":") if alias == target.remote or (sep and alias == remote_host): endpoint_matches.append(target) if len(endpoint_matches) == 1: return endpoint_matches[0] if len(endpoint_matches) > 1: aliases = [target.profile for target in endpoint_matches] endpoint = endpoint_matches[0].remote or alias examples = "\n".join( f" browser-cli --remote {endpoint} --browser {a} ..." for a in aliases ) display_aliases = [target.display_name for target in endpoint_matches] shorthand_examples = "\n".join( f" browser-cli --browser {a} ..." for a in display_aliases ) raise BrowserNotConnected( f"Multiple remote browser instances are active on {alias}: {', '.join(aliases)}\n" f"Use --browser with --remote to select one:\n{examples}\n" f"Or use the full remote browser alias:\n{shorthand_examples}" ) return None def active_browser_targets(*, include_remotes: bool = True, key=None, suppress_pq_warning: bool = False) -> list[BrowserTarget]: targets = target_discovery.active_local_browser_targets() if include_remotes: targets.extend(_remote_browser_targets(key=key, suppress_pq_warning=suppress_pq_warning)) return targets def _auto_route_remote(endpoint: str, key=None) -> str | None: targets = remote_browser_targets(endpoint, key=key) if len(targets) == 1: return targets[0].profile if len(targets) > 1: aliases = [target.profile for target in targets] examples = "\n".join(f" browser-cli --remote {endpoint} --browser {a} ..." for a in aliases) raise BrowserNotConnected( f"Multiple remote browser instances are active: {', '.join(aliases)}\n" f"Use --browser to select one:\n{examples}" ) return None def send_command( command: str, args: dict | None = None, profile: str | None = None, remote: str | None = None, key: "Path | None" = None, *, suppress_pq_warning: bool = False, ) -> Any: """Send a command to the browser and return the response data.""" requested_profile, remote_endpoint = messages.requested_target(profile, remote) if not remote_endpoint and requested_profile and not target_discovery.is_active_local_profile(requested_profile): if remote_alias_target := remote_target_for_alias(requested_profile): remote_endpoint = remote_alias_target.remote requested_profile = remote_alias_target.profile msg = messages.base_message(command, args) private_key = None if remote_endpoint: if suppress_pq_warning: msg["_suppress_pq_warning"] = True private_key = auth.add_remote_auth_fields(msg, command, requested_profile, remote_endpoint, key, _auto_route_remote) try: payload = messages.encode_payload(msg) response = ( _send_remote(remote_endpoint, msg, private_key) if remote_endpoint else local_transport.send_local_sync(requested_profile, payload, target_discovery.resolve_socket) ) except (FileNotFoundError, ConnectionRefusedError, OSError): raise messages.remote_connection_error(remote_endpoint) if remote_endpoint else messages.local_connection_error(requested_profile) return messages.decode_response(response) async def remote_browser_targets_async(endpoint: str, key=None) -> list[BrowserTarget]: """Async variant of :func:`remote_browser_targets`.""" return _remote_target_items( endpoint, await send_command_async("browser-cli.targets", remote=endpoint, key=key), ) async def _auto_route_remote_async(endpoint: str, key=None) -> str | None: targets = await remote_browser_targets_async(endpoint, key=key) if len(targets) == 1: return targets[0].profile if len(targets) > 1: aliases = [target.profile for target in targets] examples = "\n".join(f" browser-cli --remote {endpoint} --browser {a} ..." for a in aliases) raise BrowserNotConnected( f"Multiple remote browser instances are active: {', '.join(aliases)}\n" f"Use --browser to select one:\n{examples}" ) return None async def send_command_async( command: str, args: dict | None = None, profile: str | None = None, remote: str | None = None, key: "Path | None" = None, *, suppress_pq_warning: bool = False, ) -> Any: """Async variant of :func:`send_command` with native async socket/TCP paths.""" requested_profile, remote_endpoint = messages.requested_target(profile, remote) if not remote_endpoint and requested_profile and not await asyncio.to_thread(target_discovery.is_active_local_profile, requested_profile): if remote_alias_target := await asyncio.to_thread(remote_target_for_alias, requested_profile): remote_endpoint = remote_alias_target.remote requested_profile = remote_alias_target.profile msg = messages.base_message(command, args) private_key = None if remote_endpoint: if suppress_pq_warning: msg["_suppress_pq_warning"] = True private_key = await auth.add_remote_auth_fields_async(msg, command, requested_profile, remote_endpoint, key, _auto_route_remote_async) try: payload = messages.encode_payload(msg) response = ( await _send_remote_async(remote_endpoint, msg, private_key) if remote_endpoint else await local_transport.send_local_async(requested_profile, payload, target_discovery.resolve_socket) ) except (FileNotFoundError, ConnectionRefusedError, OSError): raise messages.remote_connection_error(remote_endpoint) if remote_endpoint else messages.local_connection_error(requested_profile) return messages.decode_response(response)