367 lines
12 KiB
Bash
Executable File
367 lines
12 KiB
Bash
Executable File
#!/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_<pid>.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/<pid>/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
|