stop-server.sh 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. #!/usr/bin/env bash
  2. # Stop the brainstorm server and clean up
  3. # Usage: stop-server.sh <session_dir>
  4. #
  5. # Kills the server process. Only deletes session directory if it's
  6. # under /tmp (ephemeral). Persistent directories (.superpowers/) are
  7. # kept so mockups can be reviewed later.
  8. SESSION_DIR="$1"
  9. if [[ -z "$SESSION_DIR" ]]; then
  10. echo '{"error": "Usage: stop-server.sh <session_dir>"}'
  11. exit 1
  12. fi
  13. STATE_DIR="${SESSION_DIR}/state"
  14. PID_FILE="${STATE_DIR}/server.pid"
  15. # Confirm a PID is actually our brainstorm server (node running server.cjs),
  16. # not a reused/unrelated process whose PID was recycled into a stale pid file.
  17. is_brainstorm_server() {
  18. kill -0 "$1" 2>/dev/null || return 1
  19. case "$(ps -p "$1" -o command= 2>/dev/null)" in
  20. *node*server.cjs*) ;;
  21. *) return 1 ;;
  22. esac
  23. # Stronger check: if we recorded the bound port and lsof is available, require
  24. # the PID to be the process actually LISTENING on this session's port. This
  25. # rules out an unrelated `node ... server.cjs` (another project, an editor task
  26. # runner, a different session) that happened to recycle the stale PID.
  27. local info="${STATE_DIR}/server-info"
  28. if [[ -f "$info" ]] && command -v lsof >/dev/null 2>&1; then
  29. local port
  30. port=$(sed -n 's/.*"port":\([0-9][0-9]*\).*/\1/p' "$info" | head -1)
  31. if [[ -n "$port" ]]; then
  32. [[ "$(lsof -nP -iTCP:"$port" -sTCP:LISTEN -t 2>/dev/null | head -1)" == "$1" ]] || return 1
  33. fi
  34. fi
  35. return 0
  36. }
  37. if [[ -f "$PID_FILE" ]]; then
  38. pid=$(cat "$PID_FILE")
  39. # Refuse to signal a PID we can't prove is our server. A stale pid file may
  40. # point at an unrelated process after a reboot/PID wraparound.
  41. if ! is_brainstorm_server "$pid"; then
  42. rm -f "$PID_FILE"
  43. echo '{"status": "stale_pid"}'
  44. exit 0
  45. fi
  46. # Try to stop gracefully, fallback to force if still alive
  47. kill "$pid" 2>/dev/null || true
  48. # Wait for graceful shutdown (up to ~2s)
  49. for i in {1..20}; do
  50. if ! kill -0 "$pid" 2>/dev/null; then
  51. break
  52. fi
  53. sleep 0.1
  54. done
  55. # If still running, escalate to SIGKILL
  56. if kill -0 "$pid" 2>/dev/null; then
  57. kill -9 "$pid" 2>/dev/null || true
  58. # Give SIGKILL a moment to take effect
  59. sleep 0.1
  60. fi
  61. if kill -0 "$pid" 2>/dev/null; then
  62. echo '{"status": "failed", "error": "process still running"}'
  63. exit 1
  64. fi
  65. rm -f "$PID_FILE" "${STATE_DIR}/server.log"
  66. # Only delete ephemeral /tmp directories
  67. if [[ "$SESSION_DIR" == /tmp/* ]]; then
  68. rm -rf "$SESSION_DIR"
  69. fi
  70. echo '{"status": "stopped"}'
  71. else
  72. echo '{"status": "not_running"}'
  73. fi