mirror of
https://github.com/Such-Software/adaptive-multi-protocol-browser.git
synced 2026-10-17 14:31:46 +02:00
187 lines
4.7 KiB
Python
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
|