mirror of
https://github.com/obra/superpowers.git
synced 2026-06-11 21:29:07 +08:00
When the companion idle-shuts-down and the agent restarts it, a fresh random port meant the user's open browser tab pointed at a dead URL. Persist the bound port per project and prefer it on the next start, so the restarted server comes up on the same port and the open tab's reconnect just works. - start-server.sh exports BRAINSTORM_PORT_FILE=<project>/.superpowers/brainstorm/ .last-port for project sessions (not /tmp). - server.cjs prefers an explicit BRAINSTORM_PORT, else the recorded port, else random; writes the actually-bound port back; and on EADDRINUSE (preferred port still in use) falls back to a random port once instead of crashing. lifecycle.test.js: restart reuses the recorded port; a taken preferred port falls back to a random one without crashing. Refs #1237
179 lines
5.6 KiB
Bash
Executable File
179 lines
5.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Start the brainstorm server and output connection info
|
|
# Usage: start-server.sh [--project-dir <path>] [--host <bind-host>] [--url-host <display-host>] [--foreground] [--background]
|
|
#
|
|
# Starts server on a random high port, outputs JSON with URL.
|
|
# Each session gets its own directory to avoid conflicts.
|
|
#
|
|
# Options:
|
|
# --project-dir <path> Store session files under <path>/.superpowers/brainstorm/
|
|
# instead of /tmp. Files persist after server stops.
|
|
# --host <bind-host> Host/interface to bind (default: 127.0.0.1).
|
|
# Use 0.0.0.0 in remote/containerized environments.
|
|
# --url-host <host> Hostname shown in returned URL JSON.
|
|
# --idle-timeout-minutes <n> Shut down after n minutes idle (default 240 = 4h).
|
|
# --foreground Run server in the current terminal (no backgrounding).
|
|
# --background Force background mode (overrides Codex auto-foreground).
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
|
# Parse arguments
|
|
PROJECT_DIR=""
|
|
FOREGROUND="false"
|
|
FORCE_BACKGROUND="false"
|
|
BIND_HOST="127.0.0.1"
|
|
URL_HOST=""
|
|
IDLE_TIMEOUT_MINUTES=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--project-dir)
|
|
PROJECT_DIR="$2"
|
|
shift 2
|
|
;;
|
|
--host)
|
|
BIND_HOST="$2"
|
|
shift 2
|
|
;;
|
|
--url-host)
|
|
URL_HOST="$2"
|
|
shift 2
|
|
;;
|
|
--idle-timeout-minutes)
|
|
IDLE_TIMEOUT_MINUTES="$2"
|
|
shift 2
|
|
;;
|
|
--foreground|--no-daemon)
|
|
FOREGROUND="true"
|
|
shift
|
|
;;
|
|
--background|--daemon)
|
|
FORCE_BACKGROUND="true"
|
|
shift
|
|
;;
|
|
*)
|
|
echo "{\"error\": \"Unknown argument: $1\"}"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$URL_HOST" ]]; then
|
|
if [[ "$BIND_HOST" == "127.0.0.1" || "$BIND_HOST" == "localhost" ]]; then
|
|
URL_HOST="localhost"
|
|
else
|
|
URL_HOST="$BIND_HOST"
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$IDLE_TIMEOUT_MINUTES" ]]; then
|
|
if ! [[ "$IDLE_TIMEOUT_MINUTES" =~ ^[0-9]+$ ]] || [[ "$IDLE_TIMEOUT_MINUTES" -lt 1 ]]; then
|
|
echo "{\"error\": \"--idle-timeout-minutes must be a positive integer\"}"
|
|
exit 1
|
|
fi
|
|
export BRAINSTORM_IDLE_TIMEOUT_MS=$(( IDLE_TIMEOUT_MINUTES * 60 * 1000 ))
|
|
fi
|
|
|
|
# Some environments reap detached/background processes. Auto-foreground when detected.
|
|
if [[ -n "${CODEX_CI:-}" && "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then
|
|
FOREGROUND="true"
|
|
fi
|
|
|
|
# Windows/Git Bash reaps nohup background processes. Auto-foreground when detected.
|
|
if [[ "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then
|
|
case "${OSTYPE:-}" in
|
|
msys*|cygwin*|mingw*) FOREGROUND="true" ;;
|
|
esac
|
|
if [[ -n "${MSYSTEM:-}" ]]; then
|
|
FOREGROUND="true"
|
|
fi
|
|
fi
|
|
|
|
# Generate unique session directory
|
|
SESSION_ID="$$-$(date +%s)"
|
|
|
|
if [[ -n "$PROJECT_DIR" ]]; then
|
|
SESSION_DIR="${PROJECT_DIR}/.superpowers/brainstorm/${SESSION_ID}"
|
|
# Persist the bound port per project so a restart reuses it and an already-open
|
|
# browser tab reconnects to the same URL.
|
|
export BRAINSTORM_PORT_FILE="${PROJECT_DIR}/.superpowers/brainstorm/.last-port"
|
|
else
|
|
SESSION_DIR="/tmp/brainstorm-${SESSION_ID}"
|
|
fi
|
|
|
|
STATE_DIR="${SESSION_DIR}/state"
|
|
PID_FILE="${STATE_DIR}/server.pid"
|
|
LOG_FILE="${STATE_DIR}/server.log"
|
|
|
|
# Create fresh session directory with content and state peers
|
|
mkdir -p "${SESSION_DIR}/content" "$STATE_DIR"
|
|
|
|
# Kill any existing server
|
|
if [[ -f "$PID_FILE" ]]; then
|
|
old_pid=$(cat "$PID_FILE")
|
|
kill "$old_pid" 2>/dev/null
|
|
rm -f "$PID_FILE"
|
|
fi
|
|
|
|
cd "$SCRIPT_DIR"
|
|
|
|
# Resolve the harness PID (grandparent of this script).
|
|
# $PPID is the ephemeral shell the harness spawned to run us — it dies
|
|
# when this script exits. The harness itself is $PPID's parent.
|
|
OWNER_PID="$(ps -o ppid= -p "$PPID" 2>/dev/null | tr -d ' ')"
|
|
if [[ -z "$OWNER_PID" || "$OWNER_PID" == "1" ]]; then
|
|
OWNER_PID="$PPID"
|
|
fi
|
|
|
|
# Windows/MSYS2: Node.js cannot see POSIX PIDs from the MSYS2 namespace.
|
|
# Passing a PID node cannot verify causes server to log owner-pid-invalid
|
|
# and self-terminate at the 60-second lifecycle check. Clear it so the
|
|
# watchdog is disabled and the idle timeout becomes the only shutdown trigger.
|
|
case "${OSTYPE:-}" in
|
|
msys*|cygwin*|mingw*) OWNER_PID="" ;;
|
|
esac
|
|
if [[ -n "${MSYSTEM:-}" ]]; then
|
|
OWNER_PID=""
|
|
fi
|
|
|
|
# Foreground mode for environments that reap detached/background processes.
|
|
if [[ "$FOREGROUND" == "true" ]]; then
|
|
env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs &
|
|
SERVER_PID=$!
|
|
echo "$SERVER_PID" > "$PID_FILE"
|
|
wait "$SERVER_PID"
|
|
exit $?
|
|
fi
|
|
|
|
# Start server, capturing output to log file
|
|
# Use nohup to survive shell exit; disown to remove from job table
|
|
nohup env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs > "$LOG_FILE" 2>&1 &
|
|
SERVER_PID=$!
|
|
disown "$SERVER_PID" 2>/dev/null
|
|
echo "$SERVER_PID" > "$PID_FILE"
|
|
|
|
# Wait for server-started message (check log file)
|
|
for i in {1..50}; do
|
|
if grep -q "server-started" "$LOG_FILE" 2>/dev/null; then
|
|
# Verify server is still alive after a short window (catches process reapers)
|
|
alive="true"
|
|
for _ in {1..20}; do
|
|
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
|
alive="false"
|
|
break
|
|
fi
|
|
sleep 0.1
|
|
done
|
|
if [[ "$alive" != "true" ]]; then
|
|
echo "{\"error\": \"Server started but was killed. Retry in a persistent terminal with: $SCRIPT_DIR/start-server.sh${PROJECT_DIR:+ --project-dir $PROJECT_DIR} --host $BIND_HOST --url-host $URL_HOST --foreground\"}"
|
|
exit 1
|
|
fi
|
|
grep "server-started" "$LOG_FILE" | head -1
|
|
exit 0
|
|
fi
|
|
sleep 0.1
|
|
done
|
|
|
|
# Timeout - server didn't start
|
|
echo '{"error": "Server failed to start within 5 seconds"}'
|
|
exit 1
|