← back to Domain Sniper

launchd/sniper-watchdog.sh

54 lines

#!/bin/zsh
# sniper-watchdog.sh — minute-cadence /healthz probe for the dashboard.
# On HTTP 503 (or unreachable), bounces the dashboard process.
#
# Discovers the dashboard via either:
#   1. PM2  — `pm2 jlist | jq` for a process named "sniper-dashboard"
#   2. PID  — falls back to pgrep on `node dashboard.js`
#
# All logs go to ~/Projects/domain-sniper/logs/watchdog.log (append-only).
#
# OPT-IN — Steve must `launchctl load` the plist for this to run.
# Default repo state: present, not loaded.

set -u
PROJ="${HOME}/Projects/domain-sniper"
LOG="${PROJ}/logs/watchdog.log"
mkdir -p "${PROJ}/logs"

stamp() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
say()   { echo "$(stamp) $*" >> "$LOG"; }

# Probe /healthz; -m caps the request at 5s.
RESP=$(curl -sS -o /tmp/sniper-healthz.json -w "%{http_code}" -m 5 http://127.0.0.1:9895/healthz 2>/dev/null)
CODE="${RESP:-000}"

if [ "$CODE" = "200" ]; then
  # Quiet on healthy unless we want noise.
  exit 0
fi

say "UNHEALTHY  http=${CODE}  body=$(head -c 200 /tmp/sniper-healthz.json 2>/dev/null | tr -d '\n')"

# Try pm2 first
if command -v pm2 >/dev/null 2>&1; then
  if pm2 jlist 2>/dev/null | grep -q '"name":"sniper-dashboard"'; then
    say "ACTION pm2 restart sniper-dashboard"
    pm2 restart sniper-dashboard >> "$LOG" 2>&1
    exit 0
  fi
fi

# Fallback: pgrep + spawn
OLD_PIDS=$(pgrep -f "node dashboard.js" 2>/dev/null || true)
if [ -n "$OLD_PIDS" ]; then
  say "ACTION kill PIDs ${OLD_PIDS}"
  kill $OLD_PIDS 2>/dev/null
  sleep 2
fi
cd "$PROJ" || { say "FATAL cannot cd ${PROJ}"; exit 1; }
nohup node dashboard.js >> "${PROJ}/logs/dashboard.log" 2>&1 &
disown
say "ACTION respawned dashboard via nohup pid=$!"
exit 0