Files
sovereign_browser/browser.sh
T

211 lines
6.9 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 any running instance
# ./browser.sh restart [ARGS...]— stop + build + start, forwarding ARGS
# ./browser.sh status — check if running + MCP responsive
# ./browser.sh log — tail the browser log
#
# 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"
PORT=17777
# 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: find the process LISTENing on the 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 :"$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
}
cmd_stop() {
local stopped_any=0
# Collect candidate PIDs from the PID file and from LISTENing sockets.
# We use -sTCP:LISTEN so we only match the server, never client
# connections (Roo Code / VS Code ext host connect to the MCP server
# and would be killed by a bare `lsof -ti :PORT`).
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 < <(lsof -ti :"$PORT" -sTCP:LISTEN 2>/dev/null || true)
# 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 $(lsof -ti :"$PORT" -sTCP:LISTEN 2>/dev/null || true); 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"
# Final check: is anything still LISTENing on the port?
if lsof -ti :"$PORT" -sTCP:LISTEN >/dev/null 2>&1; then
echo "[browser] Warning: process still listening on port $PORT."
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
# 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)
echo "[browser] Waiting for MCP server on port $PORT..."
for i in $(seq 1 100); do
if curl -s -o /dev/null -X POST "http://localhost:$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)."
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 pid
if pid=$(get_pid 2>/dev/null); then
echo "[browser] Running (PID $pid)."
# Check MCP
if curl -s -o /dev/null -X POST "http://localhost:$PORT/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
else
echo "[browser] Not running."
fi
}
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 ;;
*)
echo "Usage: $0 {start|stop|restart|status|log} [ARGS...]" >&2
echo " start/restart forward extra args to the binary." >&2
exit 1
;;
esac