stop-server.test.sh 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. #!/usr/bin/env bash
  2. # Tests for stop-server.sh PID-ownership safety.
  3. #
  4. # A stale server.pid (e.g. after a reboot, when the kernel has recycled the PID)
  5. # can point at an unrelated, live process. stop-server.sh must verify the PID is
  6. # actually our brainstorm server before signalling it.
  7. set -u
  8. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
  9. STOP="$SCRIPT_DIR/../../skills/brainstorming/scripts/stop-server.sh"
  10. SERVER="$SCRIPT_DIR/../../skills/brainstorming/scripts/server.cjs"
  11. PASS=0; FAIL=0
  12. PIDS=()
  13. DIRS=()
  14. cleanup() {
  15. for pid in "${PIDS[@]}"; do
  16. kill -9 "$pid" 2>/dev/null || true
  17. wait "$pid" 2>/dev/null || true
  18. done
  19. for dir in "${DIRS[@]}"; do
  20. rm -rf "$dir"
  21. done
  22. }
  23. trap cleanup EXIT
  24. track_dir() { DIRS+=("$1"); }
  25. track_pid() { PIDS+=("$1"); }
  26. untrack_pid() {
  27. local remove="$1"
  28. local kept=()
  29. local pid
  30. for pid in "${PIDS[@]}"; do
  31. [[ "$pid" == "$remove" ]] || kept+=("$pid")
  32. done
  33. PIDS=("${kept[@]}")
  34. }
  35. new_server_id() {
  36. printf 'testid%026d\n' "$RANDOM"
  37. }
  38. ok() { echo " PASS: $1"; PASS=$((PASS + 1)); }
  39. bad() { echo " FAIL: $1"; echo " $2"; FAIL=$((FAIL + 1)); }
  40. # --- Test 1: an unrelated, reused PID must NOT be killed ---
  41. SESS="$(mktemp -d)"; track_dir "$SESS"; mkdir -p "$SESS/state"
  42. sleep 600 &
  43. UNRELATED=$!
  44. track_pid "$UNRELATED"
  45. disown "$UNRELATED" 2>/dev/null || true
  46. echo "$UNRELATED" > "$SESS/state/server.pid"
  47. OUT="$("$STOP" "$SESS")"
  48. if kill -0 "$UNRELATED" 2>/dev/null; then
  49. case "$OUT" in
  50. *stale_pid*) ok "unrelated reused PID is left alone (stale_pid)" ;;
  51. *) bad "unrelated PID survived but status was not stale_pid" "$OUT" ;;
  52. esac
  53. else
  54. bad "unrelated reused PID was KILLED" "$OUT"
  55. fi
  56. # --- Test 2: a real brainstorm server with matching instance id IS stopped ---
  57. SESS="$(mktemp -d)"; track_dir "$SESS"; mkdir -p "$SESS/content" "$SESS/state"
  58. SERVER_ID="$(new_server_id)"
  59. printf '%s\n' "$SERVER_ID" > "$SESS/state/server-instance-id"
  60. BRAINSTORM_DIR="$SESS" BRAINSTORM_PORT=3399 node "$SERVER" "--brainstorm-server-id=$SERVER_ID" > /dev/null 2>&1 &
  61. SRV=$!
  62. track_pid "$SRV"
  63. disown "$SRV" 2>/dev/null || true
  64. for _ in $(seq 1 40); do kill -0 "$SRV" 2>/dev/null && break; sleep 0.1; done
  65. sleep 0.4
  66. echo "$SRV" > "$SESS/state/server.pid"
  67. OUT="$("$STOP" "$SESS")"
  68. sleep 0.3
  69. if kill -0 "$SRV" 2>/dev/null; then
  70. bad "real brainstorm server still running after stop" "$OUT"
  71. else
  72. wait "$SRV" 2>/dev/null || true
  73. untrack_pid "$SRV"
  74. case "$OUT" in
  75. *stopped*) ok "real brainstorm server with matching instance id is stopped" ;;
  76. *) bad "server stopped but status was not 'stopped'" "$OUT" ;;
  77. esac
  78. fi
  79. # --- Test 2b: persistent sessions stop with explicit stopped metadata ---
  80. SESS="$(mktemp -d "$SCRIPT_DIR/.stop-persistent.XXXXXX")"; track_dir "$SESS"; mkdir -p "$SESS/content" "$SESS/state"
  81. SERVER_ID="$(new_server_id)"
  82. printf '%s\n' "$SERVER_ID" > "$SESS/state/server-instance-id"
  83. BRAINSTORM_DIR="$SESS" BRAINSTORM_PORT=0 node "$SERVER" "--brainstorm-server-id=$SERVER_ID" > /dev/null 2>&1 &
  84. SRV=$!
  85. track_pid "$SRV"
  86. disown "$SRV" 2>/dev/null || true
  87. for _ in $(seq 1 40); do
  88. [[ -f "$SESS/state/server-info" ]] && break
  89. sleep 0.1
  90. done
  91. echo "$SRV" > "$SESS/state/server.pid"
  92. OUT="$("$STOP" "$SESS")"
  93. sleep 0.3
  94. if kill -0 "$SRV" 2>/dev/null; then
  95. bad "persistent brainstorm server still running after stop" "$OUT"
  96. else
  97. wait "$SRV" 2>/dev/null || true
  98. untrack_pid "$SRV"
  99. if [[ -f "$SESS/state/server-info" ]]; then
  100. bad "persistent stop clears server-info" "server-info still exists after: $OUT"
  101. elif [[ ! -f "$SESS/state/server-stopped" ]]; then
  102. bad "persistent stop writes server-stopped" "server-stopped missing after: $OUT"
  103. elif grep -q '"reason":"stop-server.sh"' "$SESS/state/server-stopped"; then
  104. ok "persistent stop clears alive metadata and writes server-stopped"
  105. else
  106. bad "persistent stop writes stop reason" "$(cat "$SESS/state/server-stopped" 2>/dev/null || true)"
  107. fi
  108. fi
  109. # --- Test 3: no pid file ---
  110. SESS="$(mktemp -d)"; track_dir "$SESS"; mkdir -p "$SESS/state"
  111. OUT="$("$STOP" "$SESS")"
  112. case "$OUT" in
  113. *not_running*) ok "missing pid file reports not_running" ;;
  114. *) bad "missing pid file: unexpected status" "$OUT" ;;
  115. esac
  116. # --- Test 4: a node server.cjs impostor with missing instance id is spared ---
  117. SESS="$(mktemp -d)"; track_dir "$SESS"; mkdir -p "$SESS/state"
  118. ( exec -a "node server.cjs" sleep 600 ) &
  119. IMPOSTOR=$!
  120. track_pid "$IMPOSTOR"
  121. disown "$IMPOSTOR" 2>/dev/null || true
  122. echo "$IMPOSTOR" > "$SESS/state/server.pid"
  123. OUT="$("$STOP" "$SESS")"
  124. if kill -0 "$IMPOSTOR" 2>/dev/null; then
  125. case "$OUT" in
  126. *stale_pid*) ok "missing instance id leaves node server.cjs impostor alone" ;;
  127. *) bad "impostor survived but status was not stale_pid" "$OUT" ;;
  128. esac
  129. else
  130. bad "killed a node server.cjs impostor with missing instance id" "$OUT"
  131. fi
  132. # --- Test 5: a node server.cjs impostor with wrong instance id is spared ---
  133. SESS="$(mktemp -d)"; track_dir "$SESS"; mkdir -p "$SESS/state"
  134. EXPECTED_ID="$(new_server_id)"
  135. WRONG_ID="$(new_server_id)"
  136. printf '%s\n' "$EXPECTED_ID" > "$SESS/state/server-instance-id"
  137. ( exec -a "node server.cjs --brainstorm-server-id=$WRONG_ID" sleep 600 ) &
  138. IMPOSTOR=$!
  139. track_pid "$IMPOSTOR"
  140. disown "$IMPOSTOR" 2>/dev/null || true
  141. echo "$IMPOSTOR" > "$SESS/state/server.pid"
  142. OUT="$("$STOP" "$SESS")"
  143. if kill -0 "$IMPOSTOR" 2>/dev/null; then
  144. case "$OUT" in
  145. *stale_pid*) ok "wrong instance id leaves node server.cjs impostor alone" ;;
  146. *) bad "wrong-id impostor survived but status was not stale_pid" "$OUT" ;;
  147. esac
  148. else
  149. bad "killed a node server.cjs impostor with wrong instance id" "$OUT"
  150. fi
  151. # --- Test 6: malformed instance id is fail-closed ---
  152. SESS="$(mktemp -d)"; track_dir "$SESS"; mkdir -p "$SESS/state"
  153. printf '%s\n' 'bad id with spaces' > "$SESS/state/server-instance-id"
  154. ( exec -a "node server.cjs --brainstorm-server-id=bad-id-with-spaces" sleep 600 ) &
  155. IMPOSTOR=$!
  156. track_pid "$IMPOSTOR"
  157. disown "$IMPOSTOR" 2>/dev/null || true
  158. echo "$IMPOSTOR" > "$SESS/state/server.pid"
  159. OUT="$("$STOP" "$SESS")"
  160. if kill -0 "$IMPOSTOR" 2>/dev/null; then
  161. case "$OUT" in
  162. *stale_pid*) ok "malformed instance id is fail-closed" ;;
  163. *) bad "malformed-id impostor survived but status was not stale_pid" "$OUT" ;;
  164. esac
  165. else
  166. bad "killed process despite malformed instance id" "$OUT"
  167. fi
  168. echo "--- Results: $PASS passed, $FAIL failed ---"
  169. [ "$FAIL" -eq 0 ] || exit 1