Files
adaptive-multi-protocol-bro…/ampbrowser/transport_manager.py
T
2026-07-07 19:44:24 -04:00

187 lines
4.7 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import os
import shutil
import socket
import subprocess
import time
from urllib.parse import urlparse
from .adapters import TransportStatus, adapter_for
from .config import AppConfig
@dataclass(frozen=True)
class ManagedTransportResult:
transport: str
status: str
endpoint: str
owned: bool
pid: int
state_dir: str
command: tuple[str, ...]
message: str
@property
def ready(self) -> bool:
return self.status in {"ready", "started"}
def ensure_transport_ready(
transport: str,
*,
config: AppConfig,
root: Path,
status: TransportStatus | None = None,
wait_seconds: float = 20.0,
) -> ManagedTransportResult:
adapter = adapter_for(transport)
if adapter is None:
return ManagedTransportResult(transport, "unsupported", "-", False, 0, "-", (), "transport adapter not found")
status = status or adapter.inspect()
if status.adoptable:
return ManagedTransportResult(
transport,
"ready",
status.endpoint,
False,
0,
_managed_state_dir(root, config, transport),
(),
f"adopted existing {transport} transport",
)
if transport != "tor":
return ManagedTransportResult(
transport,
"unsupported",
status.endpoint,
False,
0,
_managed_state_dir(root, config, transport),
(),
f"managed start for {transport} is not implemented yet",
)
return _start_tor(config=config, root=root, endpoint=status.endpoint, wait_seconds=wait_seconds)
def _start_tor(
*,
config: AppConfig,
root: Path,
endpoint: str,
wait_seconds: float,
) -> ManagedTransportResult:
binary = _transport_binary(config, "tor")
state_dir = _managed_state_dir(root, config, "tor")
if not binary:
return ManagedTransportResult(
"tor",
"missing-provider",
endpoint,
False,
0,
state_dir,
(),
"Tor provider not found; set AMPB_TOR_BIN or transports.tor.binary_path",
)
state_path = Path(state_dir)
data_dir = state_path / "data"
log_path = state_path / "tor.log"
data_dir.mkdir(parents=True, exist_ok=True)
host, port = _endpoint_host_port(endpoint)
command = (
binary,
"--SocksPort",
f"{host}:{port}",
"--DataDirectory",
str(data_dir),
"--ClientOnly",
"1",
"--Log",
f"notice file {log_path}",
)
try:
process = subprocess.Popen(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # noqa: S603
except OSError as exc:
return ManagedTransportResult(
"tor",
"start-failed",
endpoint,
False,
0,
state_dir,
command,
f"managed Tor could not start: {exc}",
)
if _wait_for_endpoint(endpoint, timeout_seconds=wait_seconds):
return ManagedTransportResult(
"tor",
"started",
endpoint,
True,
process.pid,
state_dir,
command,
"started managed Tor transport",
)
process.terminate()
return ManagedTransportResult(
"tor",
"start-timeout",
endpoint,
True,
process.pid,
state_dir,
command,
f"managed Tor did not become ready at {endpoint}",
)
def _transport_binary(config: AppConfig, transport: str) -> str:
env_name = f"AMPB_{transport.upper()}_BIN"
env_path = os.environ.get(env_name)
if env_path:
return env_path
config_path = config.transport_binary(transport)
if config_path:
return config_path
if transport == "tor":
return shutil.which("tor") or ""
return ""
def _managed_state_dir(root: Path, config: AppConfig, transport: str) -> str:
return str(root / config.state_dir / "transports" / transport)
def _wait_for_endpoint(endpoint: str, *, timeout_seconds: float) -> bool:
deadline = time.monotonic() + timeout_seconds
while time.monotonic() <= deadline:
host, port = _endpoint_host_port(endpoint)
if _can_connect(host, port):
return True
time.sleep(0.2)
return False
def _can_connect(host: str, port: int) -> bool:
try:
with socket.create_connection((host, port), timeout=0.2):
return True
except OSError:
return False
def _endpoint_host_port(endpoint: str) -> tuple[str, int]:
parsed = urlparse(endpoint)
return parsed.hostname or "127.0.0.1", parsed.port or 9050