feat(launcher): improve bin/amc with launchctl, health checks, and PID recovery
Enhance the launcher script with robust process management: Process Lifecycle: - Use macOS launchctl to spawn server process, avoiding parent-shell cleanup kills that terminate AMC when Claude Code's shell exits - Fall back to setsid+nohup on non-macOS systems - Graceful shutdown with 5-second wait loop before force-kill Health Checking: - Replace simple PID file check with actual HTTP health check - curl /api/state to verify server is truly responsive, not just alive - Report "healthy" vs "running but not responding" states PID Recovery: - Recover orphan PID file from port listener (lsof -tiTCP:7400) - Validate listener is actually amc-server (not another process on 7400) - Auto-refresh PID file when process exists but file is stale Logging: - New LOG_FILE at ~/.local/share/amc/server.log - show_recent_logs() helper for diagnostics - Consistent log routing through launchctl submit Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
191
bin/amc
191
bin/amc
@@ -1,13 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# AMC — Agent Mission Control launcher
|
||||
# Usage: amc [start|stop|status]
|
||||
# Usage: amc [start|stop|status|logs]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Runtime data
|
||||
DATA_DIR="$HOME/.local/share/amc"
|
||||
PID_FILE="$DATA_DIR/server.pid"
|
||||
LOG_FILE="$DATA_DIR/server.log"
|
||||
PORT=7400
|
||||
LAUNCH_LABEL="com.tayloreernisse.amc-server"
|
||||
|
||||
# Find server relative to this script (handles symlinks)
|
||||
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
|
||||
@@ -15,23 +17,142 @@ SERVER="$SCRIPT_DIR/amc-server"
|
||||
|
||||
cmd="${1:-start}"
|
||||
|
||||
port_listener_pid() {
|
||||
lsof -tiTCP:"$PORT" -sTCP:LISTEN 2>/dev/null | head -n 1 || true
|
||||
}
|
||||
|
||||
is_amc_server_pid() {
|
||||
local pid="${1:-}"
|
||||
[ -n "$pid" ] || return 1
|
||||
local cmdline
|
||||
cmdline="$(ps -p "$pid" -o command= 2>/dev/null || true)"
|
||||
[[ "$cmdline" == *"amc-server"* ]]
|
||||
}
|
||||
|
||||
refresh_pid_file_from_port() {
|
||||
local pid
|
||||
pid="$(port_listener_pid)"
|
||||
if is_amc_server_pid "$pid"; then
|
||||
echo "$pid" > "$PID_FILE"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
is_running() {
|
||||
[ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null
|
||||
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
refresh_pid_file_from_port
|
||||
}
|
||||
|
||||
healthcheck_ok() {
|
||||
# Require real HTTP readiness, not just a live PID.
|
||||
curl -fsS "http://127.0.0.1:$PORT/api/state" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
stop_running_instance() {
|
||||
local pid=""
|
||||
if is_running; then
|
||||
pid="$(cat "$PID_FILE")"
|
||||
kill "$pid" 2>/dev/null || true
|
||||
for _ in {1..50}; do
|
||||
if ! kill -0 "$pid" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
rm -f "$PID_FILE"
|
||||
echo "$pid"
|
||||
return 0
|
||||
fi
|
||||
rm -f "$PID_FILE"
|
||||
return 1
|
||||
}
|
||||
|
||||
show_recent_logs() {
|
||||
local lines="${1:-120}"
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
echo "----- Last ${lines} lines of $LOG_FILE -----"
|
||||
tail -n "$lines" "$LOG_FILE"
|
||||
echo "----- End logs -----"
|
||||
else
|
||||
echo "No log file found at $LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
launchctl_start() {
|
||||
# macOS-specific: hand process ownership to launchd to avoid parent-shell cleanup kills.
|
||||
launchctl remove "$LAUNCH_LABEL" >/dev/null 2>&1 || true
|
||||
launchctl submit -l "$LAUNCH_LABEL" -o "$LOG_FILE" -e "$LOG_FILE" -- "$SERVER"
|
||||
}
|
||||
|
||||
fallback_start() {
|
||||
if command -v setsid >/dev/null 2>&1; then
|
||||
# Detach from current session/process group so parent shell cleanup doesn't kill AMC.
|
||||
nohup setsid "$SERVER" >> "$LOG_FILE" 2>&1 < /dev/null &
|
||||
else
|
||||
nohup "$SERVER" >> "$LOG_FILE" 2>&1 < /dev/null &
|
||||
fi
|
||||
echo "$!"
|
||||
}
|
||||
|
||||
case "$cmd" in
|
||||
start)
|
||||
if is_running; then
|
||||
echo "AMC already running (pid $(cat "$PID_FILE"))"
|
||||
else
|
||||
mkdir -p "$DATA_DIR/sessions" "$DATA_DIR/events"
|
||||
nohup "$SERVER" > "$DATA_DIR/server.log" 2>&1 &
|
||||
# Wait briefly for server to start and write PID
|
||||
sleep 0.3
|
||||
if is_running; then
|
||||
echo "AMC started (pid $(cat "$PID_FILE"))"
|
||||
if healthcheck_ok; then
|
||||
echo "AMC already running and healthy (pid $(cat "$PID_FILE"))"
|
||||
else
|
||||
echo "AMC started (pid $!)"
|
||||
stale_pid="$(cat "$PID_FILE")"
|
||||
echo "AMC process is running but unhealthy (pid $stale_pid); restarting..."
|
||||
stop_running_instance >/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! is_running; then
|
||||
existing_pid="$(port_listener_pid)"
|
||||
if [ -n "$existing_pid" ] && ! is_amc_server_pid "$existing_pid"; then
|
||||
existing_cmd="$(ps -p "$existing_pid" -o command= 2>/dev/null || true)"
|
||||
echo "Port $PORT is already in use by pid $existing_pid ($existing_cmd)."
|
||||
echo "Stop that process or choose a different AMC port."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$DATA_DIR/sessions" "$DATA_DIR/events"
|
||||
{
|
||||
echo "=== $(date -u +"%Y-%m-%dT%H:%M:%SZ") AMC start ==="
|
||||
} >> "$LOG_FILE"
|
||||
|
||||
launched_pid=""
|
||||
if [ "$(uname -s)" = "Darwin" ] && command -v launchctl >/dev/null 2>&1; then
|
||||
if launchctl_start; then
|
||||
launched_pid="launchctl:$LAUNCH_LABEL"
|
||||
else
|
||||
echo "launchctl start failed; falling back to nohup" >> "$LOG_FILE"
|
||||
launched_pid="$(fallback_start)"
|
||||
fi
|
||||
else
|
||||
launched_pid="$(fallback_start)"
|
||||
fi
|
||||
|
||||
# Wait up to ~5s for server process + HTTP health
|
||||
for _ in {1..50}; do
|
||||
if is_running && healthcheck_ok; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
if is_running && healthcheck_ok; then
|
||||
echo "AMC started (pid $(cat "$PID_FILE"), log $LOG_FILE)"
|
||||
else
|
||||
echo "AMC failed to start (launch pid $launched_pid)."
|
||||
if is_running && ! healthcheck_ok; then
|
||||
bad_pid="$(cat "$PID_FILE")"
|
||||
echo "AMC pid $bad_pid is up but failed healthcheck (/api/state)."
|
||||
fi
|
||||
show_recent_logs 160
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
# Open browser (platform-specific)
|
||||
@@ -42,26 +163,58 @@ case "$cmd" in
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
if is_running; then
|
||||
pid="$(cat "$PID_FILE")"
|
||||
kill "$pid" 2>/dev/null || true
|
||||
rm -f "$PID_FILE"
|
||||
echo "AMC stopped (pid $pid)"
|
||||
if stopped_pid="$(stop_running_instance)"; then
|
||||
echo "AMC stopped (pid $stopped_pid)"
|
||||
else
|
||||
echo "AMC not running"
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
if [ "$(uname -s)" = "Darwin" ] && command -v launchctl >/dev/null 2>&1; then
|
||||
launchctl remove "$LAUNCH_LABEL" >/dev/null 2>&1 || true
|
||||
fi
|
||||
;;
|
||||
status)
|
||||
if is_running; then
|
||||
echo "AMC running (pid $(cat "$PID_FILE"), port $PORT)"
|
||||
if healthcheck_ok; then
|
||||
echo "AMC running (pid $(cat "$PID_FILE"), port $PORT, healthy, log $LOG_FILE)"
|
||||
else
|
||||
echo "AMC running but unhealthy (pid $(cat "$PID_FILE"), port $PORT, log $LOG_FILE)"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "AMC not running"
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
;;
|
||||
logs)
|
||||
# Usage:
|
||||
# amc logs
|
||||
# amc logs 200
|
||||
# amc logs -f
|
||||
# amc logs 200 -f
|
||||
lines=120
|
||||
follow=false
|
||||
if [ "${2:-}" = "-f" ]; then
|
||||
follow=true
|
||||
elif [ -n "${2:-}" ]; then
|
||||
lines="$2"
|
||||
fi
|
||||
if [ "${3:-}" = "-f" ]; then
|
||||
follow=true
|
||||
fi
|
||||
|
||||
if [ ! -f "$LOG_FILE" ]; then
|
||||
echo "No logs yet at $LOG_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$follow" = true ]; then
|
||||
tail -n "$lines" -f "$LOG_FILE"
|
||||
else
|
||||
tail -n "$lines" "$LOG_FILE"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Usage: amc [start|stop|status]"
|
||||
echo "Usage: amc [start|stop|status|logs]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user