diff --git a/bin/amc b/bin/amc index bac124b..57d29ec 100755 --- a/bin/amc +++ b/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