Files
wehub-resource-sync 41cb1c0170
OpenSSF Scorecard / scorecard (push) Failing after 0s
DCO / dco (push) Failing after 0s
CodeQL SAST / analyze (push) Failing after 1s
Deploy Pages / deploy (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:28:05 +08:00

115 lines
3.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# test_parent_watchdog.sh — regression guard for the parent-death watchdog.
# Distilled from #407 (fixes #406): when the process that launched the stdio
# MCP server dies, the orphaned server must exit on its own rather than linger
# forever blocked on stdin.
#
# Strategy: launch the binary under a wrapper "parent" process (stdin kept open
# via a FIFO so the server doesn't see EOF), record the child's PID, then kill
# the wrapper. The watchdog should notice the changed ppid and exit within a
# few seconds. Skipped on Windows-like shells (the watchdog is POSIX-only).
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BINARY="${ROOT}/build/c/codebase-memory-mcp"
case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*)
echo "skipping parent watchdog test on Windows"
exit 0
;;
esac
if [[ ! -x "${BINARY}" ]]; then
echo "missing binary: ${BINARY}" >&2
exit 2
fi
tmpdir="$(mktemp -d)"
wrapper_pid=""
cleanup() {
if [[ -s "${tmpdir}/child.pid" ]]; then
local child_pid
child_pid="$(cat "${tmpdir}/child.pid" 2>/dev/null || true)"
[[ -n "${child_pid}" ]] && kill "${child_pid}" 2>/dev/null || true
fi
[[ -n "${wrapper_pid}" ]] && kill "${wrapper_pid}" 2>/dev/null || true
rm -rf "${tmpdir}"
}
trap cleanup EXIT
# Wrapper "parent": opens the FIFO read-write so it stays open, launches the
# MCP server with that FIFO as stdin, records the child PID, then waits.
cat >"${tmpdir}/wrapper.sh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
exec 3<>"${FIFO}"
"${CBM_BINARY}" <&3 >/dev/null 2>"${TMPDIR_PATH}/child.err" &
echo "$!" >"${TMPDIR_PATH}/child.pid"
wait
SH
chmod +x "${tmpdir}/wrapper.sh"
mkfifo "${tmpdir}/stdin"
CBM_BINARY="${BINARY}" FIFO="${tmpdir}/stdin" TMPDIR_PATH="${tmpdir}" \
"${tmpdir}/wrapper.sh" &
wrapper_pid=$!
# Wait for the child PID file to appear.
for _ in {1..50}; do
[[ -s "${tmpdir}/child.pid" ]] && break
sleep 0.1
done
if [[ ! -s "${tmpdir}/child.pid" ]]; then
echo "child pid file was not written" >&2
[[ -s "${tmpdir}/child.err" ]] && cat "${tmpdir}/child.err" >&2
exit 3
fi
child_pid="$(cat "${tmpdir}/child.pid")"
if ! kill -0 "${child_pid}" 2>/dev/null; then
echo "child did not start" >&2
exit 3
fi
# Wait until the child has reached startup far enough to have installed the
# parent watchdog. The PID file is written immediately after fork; killing the
# wrapper before the child runs main() is an untestable early-reparent race where
# the child never observed the original parent PID.
for _ in {1..50}; do
if [[ -s "${tmpdir}/child.err" ]] && grep -q "mem.init" "${tmpdir}/child.err"; then
break
fi
sleep 0.1
done
if ! grep -q "mem.init" "${tmpdir}/child.err" 2>/dev/null; then
echo "child did not reach watchdog-ready startup point" >&2
[[ -s "${tmpdir}/child.err" ]] && cat "${tmpdir}/child.err" >&2
exit 3
fi
# Kill the wrapper parent: the orphaned child must now self-exit.
kill -9 "${wrapper_pid}"
wait "${wrapper_pid}" 2>/dev/null || true
deadline=$((SECONDS + 15))
while (( SECONDS < deadline )); do
if ! kill -0 "${child_pid}" 2>/dev/null; then
echo "ok: child ${child_pid} exited after parent death"
exit 0
fi
# A zombie no longer holds stdin or runs the MCP loop; kill -0 still reports
# it until launchd/test parent reaps it, so treat that as a successful exit.
child_state="$(ps -p "${child_pid}" -o stat= 2>/dev/null | tr -d '[:space:]' || true)"
if [[ "${child_state}" == Z* ]]; then
echo "ok: child ${child_pid} exited after parent death (zombie awaiting reap)"
exit 0
fi
sleep 0.2
done
echo "codebase-memory-mcp child ${child_pid} survived parent death" >&2
[[ -s "${tmpdir}/child.err" ]] && cat "${tmpdir}/child.err" >&2
exit 1