browser-use--browser-harness
acda8c72a0
* Simplify helper surface * Trim common module and restore dispatch key
140 行
3.9 KiB
Python
140 行
3.9 KiB
Python
import json
|
|
import os
|
|
import socket
|
|
import time
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
|
|
def _load_env():
|
|
p = Path(__file__).parent / ".env"
|
|
if not p.exists():
|
|
return
|
|
for line in p.read_text().splitlines():
|
|
line = line.strip()
|
|
if not line or line.startswith("#") or "=" not in line:
|
|
continue
|
|
k, v = line.split("=", 1)
|
|
os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
|
|
|
|
|
|
_load_env()
|
|
|
|
NAME = os.environ.get("BU_NAME", "default")
|
|
BU_API = "https://api.browser-use.com/api/v3"
|
|
|
|
|
|
def _paths(name):
|
|
n = name or NAME
|
|
return f"/tmp/bu-{n}.sock", f"/tmp/bu-{n}.pid"
|
|
|
|
|
|
def _log_tail(name):
|
|
p = f"/tmp/bu-{name or NAME}.log"
|
|
try:
|
|
return Path(p).read_text().strip().splitlines()[-1]
|
|
except (FileNotFoundError, IndexError):
|
|
return None
|
|
|
|
|
|
def daemon_alive(name=None):
|
|
try:
|
|
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
s.settimeout(1)
|
|
s.connect(_paths(name)[0])
|
|
s.close()
|
|
return True
|
|
except (FileNotFoundError, ConnectionRefusedError, socket.timeout):
|
|
return False
|
|
|
|
|
|
def ensure_daemon(wait=60.0, name=None, env=None):
|
|
"""Idempotent. `env` is merged into the child process env."""
|
|
if daemon_alive(name):
|
|
return
|
|
import subprocess
|
|
|
|
e = {**os.environ, **({"BU_NAME": name} if name else {}), **(env or {})}
|
|
p = subprocess.Popen(
|
|
["uv", "run", "daemon.py"],
|
|
cwd=os.path.dirname(os.path.abspath(__file__)),
|
|
env=e,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
start_new_session=True,
|
|
)
|
|
deadline = time.time() + wait
|
|
while time.time() < deadline:
|
|
if daemon_alive(name):
|
|
return
|
|
if p.poll() is not None:
|
|
break
|
|
time.sleep(0.2)
|
|
msg = _log_tail(name)
|
|
raise RuntimeError(msg or f"daemon {name or NAME} didn't come up -- check /tmp/bu-{name or NAME}.log")
|
|
|
|
|
|
def restart_daemon(name=None):
|
|
"""Best-effort daemon restart for setup/debug flows."""
|
|
import signal
|
|
|
|
sock, pid_path = _paths(name)
|
|
try:
|
|
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
s.settimeout(5)
|
|
s.connect(sock)
|
|
s.sendall(b'{"meta":"shutdown"}\n')
|
|
s.recv(1024)
|
|
s.close()
|
|
except Exception:
|
|
pass
|
|
try:
|
|
pid = int(open(pid_path).read())
|
|
except (FileNotFoundError, ValueError):
|
|
pid = None
|
|
if pid:
|
|
for _ in range(75):
|
|
try:
|
|
os.kill(pid, 0)
|
|
time.sleep(0.2)
|
|
except ProcessLookupError:
|
|
break
|
|
else:
|
|
try:
|
|
os.kill(pid, signal.SIGTERM)
|
|
except ProcessLookupError:
|
|
pass
|
|
for f in (sock, pid_path):
|
|
try:
|
|
os.unlink(f)
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
|
|
def _browser_use(path, method, body=None):
|
|
key = os.environ.get("BROWSER_USE_API_KEY")
|
|
if not key:
|
|
raise RuntimeError("BROWSER_USE_API_KEY missing -- see .env.example")
|
|
req = urllib.request.Request(
|
|
f"{BU_API}{path}",
|
|
method=method,
|
|
data=(json.dumps(body).encode() if body is not None else None),
|
|
headers={"X-Browser-Use-API-Key": key, "Content-Type": "application/json"},
|
|
)
|
|
return json.loads(urllib.request.urlopen(req, timeout=60).read() or b"{}")
|
|
|
|
|
|
def _cdp_ws_from_url(cdp_url):
|
|
return json.loads(urllib.request.urlopen(f"{cdp_url}/json/version", timeout=15).read())["webSocketDebuggerUrl"]
|
|
|
|
|
|
def start_remote_daemon(name="remote", **create_kwargs):
|
|
if daemon_alive(name):
|
|
raise RuntimeError(f"daemon {name!r} already alive -- restart_daemon({name!r}) first")
|
|
browser = _browser_use("/browsers", "POST", create_kwargs)
|
|
ensure_daemon(
|
|
name=name,
|
|
env={"BU_CDP_WS": _cdp_ws_from_url(browser["cdpUrl"]), "BU_BROWSER_ID": browser["id"]},
|
|
)
|
|
return browser
|