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
|
#!/usr/bin/env bash
|
||||||
# AMC — Agent Mission Control launcher
|
# AMC — Agent Mission Control launcher
|
||||||
# Usage: amc [start|stop|status]
|
# Usage: amc [start|stop|status|logs]
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Runtime data
|
# Runtime data
|
||||||
DATA_DIR="$HOME/.local/share/amc"
|
DATA_DIR="$HOME/.local/share/amc"
|
||||||
PID_FILE="$DATA_DIR/server.pid"
|
PID_FILE="$DATA_DIR/server.pid"
|
||||||
|
LOG_FILE="$DATA_DIR/server.log"
|
||||||
PORT=7400
|
PORT=7400
|
||||||
|
LAUNCH_LABEL="com.tayloreernisse.amc-server"
|
||||||
|
|
||||||
# Find server relative to this script (handles symlinks)
|
# Find server relative to this script (handles symlinks)
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
|
||||||
@@ -15,23 +17,142 @@ SERVER="$SCRIPT_DIR/amc-server"
|
|||||||
|
|
||||||
cmd="${1:-start}"
|
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() {
|
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
|
case "$cmd" in
|
||||||
start)
|
start)
|
||||||
if is_running; then
|
if is_running; then
|
||||||
echo "AMC already running (pid $(cat "$PID_FILE"))"
|
if healthcheck_ok; then
|
||||||
else
|
echo "AMC already running and healthy (pid $(cat "$PID_FILE"))"
|
||||||
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"))"
|
|
||||||
else
|
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
|
||||||
fi
|
fi
|
||||||
# Open browser (platform-specific)
|
# Open browser (platform-specific)
|
||||||
@@ -42,26 +163,58 @@ case "$cmd" in
|
|||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
stop)
|
stop)
|
||||||
if is_running; then
|
if stopped_pid="$(stop_running_instance)"; then
|
||||||
pid="$(cat "$PID_FILE")"
|
echo "AMC stopped (pid $stopped_pid)"
|
||||||
kill "$pid" 2>/dev/null || true
|
|
||||||
rm -f "$PID_FILE"
|
|
||||||
echo "AMC stopped (pid $pid)"
|
|
||||||
else
|
else
|
||||||
echo "AMC not running"
|
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
|
fi
|
||||||
;;
|
;;
|
||||||
status)
|
status)
|
||||||
if is_running; then
|
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
|
else
|
||||||
echo "AMC not running"
|
echo "AMC not running"
|
||||||
rm -f "$PID_FILE"
|
rm -f "$PID_FILE"
|
||||||
fi
|
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
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
Reference in New Issue
Block a user