#!/bin/bash # # browser.sh — safe start/stop/restart for sovereign_browser # # Agents (Roo Code, etc.) should ALWAYS use this script to manage the browser # process. It fully detaches the GUI from the terminal so the agent's command # runner doesn't hang waiting for output that never ends. # # Usage: # ./browser.sh start [ARGS...] — build + launch (detached), forwarding ARGS # ./browser.sh stop — kill ALL running instances # ./browser.sh restart [ARGS...]— stop + build + start, forwarding ARGS # ./browser.sh status — list all running instances + MCP status # ./browser.sh log — tail the browser log # ./browser.sh mcp-url — print MCP endpoint URL(s) for running instance(s) # # Multiple instances: if the default MCP port (17777) is already in use by # another instance, the browser automatically falls back to an OS-assigned # port and records it in /tmp/sovereign_browser_.port. browser.sh # reads these discovery files to find/stop/status every instance. # # Extra args after start/restart are forwarded to the binary, e.g.: # ./browser.sh start --login-method generate --url https://example.com # ./browser.sh restart --no-agent --url https://a.com --url https://b.com # # The browser's stdout/stderr go to /tmp/sovereign_browser.log # The PID is stored in /tmp/sovereign_browser.pid set -euo pipefail BIN="./sovereign_browser" LOG="/tmp/sovereign_browser.log" PID_FILE="/tmp/sovereign_browser.pid" DEFAULT_PORT=17777 DISCOVERY_GLOB="/tmp/sovereign_browser_*.port" # Read the port for a given PID from its discovery file. Falls back to # DEFAULT_PORT (17777) if no discovery file exists (e.g. older binary, or # the first instance that successfully bound the default port and wrote a # discovery file with 17777). Echoes the port to stdout. port_for_pid() { local pid="${1:-}" local file="/tmp/sovereign_browser_${pid}.port" if [[ -f "$file" ]]; then local p p=$(awk '{print $2}' "$file" 2>/dev/null | head -n1) if [[ -n "$p" ]]; then echo "$p" return 0 fi fi echo "$DEFAULT_PORT" } # Echo the port for the currently-running instance (per get_pid), or # DEFAULT_PORT if not running. current_port() { local pid if pid=$(get_pid 2>/dev/null); then port_for_pid "$pid" else echo "$DEFAULT_PORT" fi } # Check if a PID is actually the sovereign_browser process. # This safety guard prevents us from accidentally killing unrelated # processes (e.g. Roo Code / VS Code extension host, which connects # to the MCP server as a client and would otherwise be caught by # a naive `lsof -ti :PORT` lookup). is_browser_pid() { local pid="$1" [[ -z "$pid" ]] && return 1 kill -0 "$pid" 2>/dev/null || return 1 # /proc//comm contains the executable name (truncated to 15 chars). # For sovereign_browser it's "sovereign_brows" (15-char limit). local comm comm=$(ps -p "$pid" -o comm= 2>/dev/null | tr -d '[:space:]') [[ "$comm" == "sovereign_browser" || "$comm" == "sovereign_brows" ]] } get_pid() { # Primary: use the PID file (written by cmd_start). if [[ -f "$PID_FILE" ]]; then local pid pid=$(cat "$PID_FILE" 2>/dev/null | head -n1 || echo "") if [[ -n "$pid" ]] && is_browser_pid "$pid"; then echo "$pid" return 0 fi fi # Fallback 1: scan discovery files for a live sovereign_browser PID. local f for f in $DISCOVERY_GLOB; do [[ -f "$f" ]] || continue local dpid dpid=$(awk '{print $1}' "$f" 2>/dev/null | head -n1) if [[ -n "$dpid" ]] && is_browser_pid "$dpid"; then echo "$dpid" return 0 fi done # Fallback 2: find the process LISTENing on the default port. # -sTCP:LISTEN matches only the listening socket, never client # connections (e.g. Roo Code's MCP client). This is critical — # without it, `lsof -ti :PORT` would also return Roo Code's PID # and we'd kill the agent driving the browser. local port_pid port_pid=$(lsof -ti :"$DEFAULT_PORT" -sTCP:LISTEN 2>/dev/null | head -n1 || echo "") if [[ -n "$port_pid" ]] && is_browser_pid "$port_pid"; then echo "$port_pid" return 0 fi return 1 } # Echo all live sovereign_browser PIDs (one per line), discovered via the # discovery files and the default-port listen socket. Used by cmd_stop to # kill every running instance. all_pids() { local pids=() local f for f in $DISCOVERY_GLOB; do [[ -f "$f" ]] || continue local dpid dpid=$(awk '{print $1}' "$f" 2>/dev/null | head -n1) if [[ -n "$dpid" ]] && is_browser_pid "$dpid"; then pids+=("$dpid") fi done local pp for pp in $(lsof -ti :"$DEFAULT_PORT" -sTCP:LISTEN 2>/dev/null || true); do if is_browser_pid "$pp"; then pids+=("$pp") fi done # Deduplicate. printf '%s\n' "${pids[@]}" 2>/dev/null | sort -u | grep -v '^$' || true } cmd_stop() { local stopped_any=0 # Collect ALL running sovereign_browser PIDs — from the PID file, # discovery files, and the default-port listen socket. This kills # every instance when multiple are running on different ports. local candidates=() if [[ -f "$PID_FILE" ]]; then while IFS= read -r pid; do [[ -n "$pid" ]] && candidates+=("$pid") done < "$PID_FILE" 2>/dev/null fi while IFS= read -r pid; do [[ -n "$pid" ]] && candidates+=("$pid") done < <(all_pids) # Deduplicate and kill only verified sovereign_browser PIDs. for pid in $(printf '%s\n' "${candidates[@]}" 2>/dev/null | sort -u | grep -v '^$'); do if is_browser_pid "$pid"; then echo "[browser] Stopping PID $pid..." kill "$pid" 2>/dev/null || true stopped_any=1 fi done # Wait for graceful shutdown, then force-kill survivors (verified only). if [[ "$stopped_any" -eq 1 ]]; then sleep 1 for pid in $(all_pids); do if is_browser_pid "$pid"; then echo "[browser] Force killing PID $pid..." kill -9 "$pid" 2>/dev/null || true fi done sleep 1 fi rm -f "$PID_FILE" # Clean up any leftover discovery files (the browser removes its own on # clean shutdown, but force-killed instances leave them behind). local f for f in $DISCOVERY_GLOB; do [[ -f "$f" ]] || continue local dpid dpid=$(awk '{print $1}' "$f" 2>/dev/null | head -n1) if [[ -z "$dpid" ]] || ! kill -0 "$dpid" 2>/dev/null; then rm -f "$f" fi done # Final check: is anything still running? if [[ -n "$(all_pids)" ]]; then echo "[browser] Warning: some instances still running." else echo "[browser] Stopped." fi return 0 } cmd_start() { local extra_args=("$@") # Don't start if already running if get_pid >/dev/null 2>&1; then echo "[browser] Already running (PID $(get_pid)). Use 'restart' to reload." return 0 fi # ── Rendering environment detection ────────────────────────────── # WebKitGTK's compositor re-composites every frame. On systems with # GPU acceleration this is cheap (hardware-accelerated). But in # software-rendering environments (Qubes OS, headless VMs, no GPU, # llvmpipe/softpipe Mesa), the compositor pegs the CPU at ~100% on # content-heavy pages. In those cases we disable compositing mode and # the DMA-BUF renderer so WebKit uses the lighter non-composited path. # # On a normal desktop Linux with a real GPU, we leave these unset so # WebKit can use hardware-accelerated compositing. local software_rendering=0 # Check 1: LIBGL_ALWAYS_SOFTWARE is set (Qubes OS sets this unconditionally) if [[ "${LIBGL_ALWAYS_SOFTWARE:-}" == "1" ]]; then software_rendering=1 fi # Check 2: No DRM render device → no GPU available if [[ ! -e /dev/dri/renderD128 ]]; then software_rendering=1 fi # Check 3: EGL/GL renderer is a software rasterizer if command -v eglinfo >/dev/null 2>&1; then if eglinfo 2>/dev/null | grep -qiE 'llvmpipe|softpipe|swrast'; then software_rendering=1 fi elif command -v glxinfo >/dev/null 2>&1; then if glxinfo 2>/dev/null | grep -qiE 'llvmpipe|softpipe|swrast'; then software_rendering=1 fi fi if [[ "$software_rendering" -eq 1 ]]; then echo "[browser] Software rendering detected — disabling WebKit compositor." export WEBKIT_DISABLE_COMPOSITING_MODE=1 export WEBKIT_DISABLE_DMABUF_RENDERER=1 fi # Build first echo "[browser] Building..." if ! make -s 2>/dev/null; then echo "[browser] Build failed!" >&2 return 1 fi echo "[browser] Build OK." # Launch fully detached: # setsid — new session, detached from controlling terminal # nohup — immune to SIGHUP # < /dev/null — detach stdin (GUI processes sometimes block on this) # > $LOG 2>&1 — redirect all output to log file # & disown — background + remove from job table echo "[browser] Launching (detached)..." if [[ ${#extra_args[@]} -gt 0 ]]; then echo "[browser] Forwarding args: ${extra_args[*]}" fi setsid nohup "$BIN" "${extra_args[@]}" < /dev/null > "$LOG" 2>&1 & local pid=$! disown 2>/dev/null || true echo "$pid" > "$PID_FILE" # Wait for the MCP server to be ready (up to 10 seconds). The actual # port may differ from DEFAULT_PORT if it was in use (the browser falls # back to an OS-assigned port and writes it to a discovery file), so # read the port from the discovery file each iteration. echo "[browser] Waiting for MCP server (PID $pid)..." local mcp_port="" for i in $(seq 1 100); do mcp_port=$(port_for_pid "$pid") if curl -s -o /dev/null -X POST "http://localhost:$mcp_port/mcp" \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"ping"}' 2>/dev/null; then echo "[browser] MCP server is ready (PID $pid, port $mcp_port)." return 0 fi sleep 0.1 done echo "[browser] Warning: MCP server didn't respond within 10s." >&2 echo "[browser] Check log: $LOG" >&2 return 1 } cmd_restart() { cmd_stop sleep 1 cmd_start "$@" } cmd_status() { local pids pids=$(all_pids) if [[ -z "$pids" ]]; then echo "[browser] Not running." return 0 fi local count count=$(echo "$pids" | wc -l) local pid for pid in $pids; do local p p=$(port_for_pid "$pid") echo "[browser] Running (PID $pid, port $p)." if curl -s -o /dev/null -X POST "http://localhost:$p/mcp" \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"ping"}' 2>/dev/null; then echo "[browser] MCP server: responsive" else echo "[browser] MCP server: NOT responding" fi done if [[ "$count" -gt 1 ]]; then echo "[browser] $count instances running." fi } # Print the MCP endpoint URL for the running instance (or all instances # if multiple are running). Useful for external clients that need to # discover the port programmatically: # URL=$(./browser.sh mcp-url) cmd_mcp_url() { local pids pids=$(all_pids) if [[ -z "$pids" ]]; then echo "[browser] Not running." >&2 return 1 fi local pid for pid in $pids; do local p p=$(port_for_pid "$pid") echo "http://localhost:$p/mcp" done } cmd_log() { if [[ -f "$LOG" ]]; then tail -50 "$LOG" else echo "[browser] No log file at $LOG" fi } case "${1:-}" in start) shift; cmd_start "$@" ;; stop) cmd_stop ;; restart) shift; cmd_restart "$@" ;; status) cmd_status ;; log) cmd_log ;; mcp-url) cmd_mcp_url ;; *) echo "Usage: $0 {start|stop|restart|status|log|mcp-url} [ARGS...]" >&2 echo " start/restart forward extra args to the binary." >&2 echo " mcp-url prints the MCP endpoint URL(s) for running instance(s)." >&2 exit 1 ;; esac