Files
browser-cli/browser_cli/client/core.py
T
daniel156161 076914e5b7 refactor: reorganize client transport and extension internals
- Split client, native, remote, serve, markdown, and SDK internals into focused packages with direct imports.
- Move local and remote transport framing/protocol helpers behind clearer module boundaries.
- Break up the extension injected DOM logic into a separate content dispatch bundle and dedicated content modules.
- Add explicit client handling for passive remote discovery without noisy PQ warnings.
- Keep behavior covered with updated unit, integration, and extension tests.
2026-06-13 23:31:24 +02:00

201 lines
7.9 KiB
Python

"""
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 <alias> 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 <alias> 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(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(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 <alias> 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(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(profile)
return messages.decode_response(response)