Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions scripts/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,8 @@ scripts/clean.sh
# Step 2 + 3: Build and run tests (with arch prefix on macOS)
$ARCH_PREFIX make -j"$NPROC" -f Makefile.cbm test $MAKE_ARGS

# Step 4: Build the stdio MCP binary and verify it exits when its parent dies.
$ARCH_PREFIX make -j"$NPROC" -f Makefile.cbm cbm $MAKE_ARGS
bash tests/test_parent_watchdog.sh

echo "=== All tests passed ==="
71 changes: 68 additions & 3 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@ enum {
MAIN_FLAG_OFF = 5, /* strlen("--ui=") */
MAIN_PORT_OFF = 7, /* strlen("--port=") */
MAIN_MAX_PORT = 65536,
PARENT_WATCHDOG_STACK_SIZE = 64 * CBM_SZ_1K,
};
#define MAIN_RAM_FRACTION 0.5

#define SLEN(s) (sizeof(s) - 1)
#include "foundation/log.h"
#include "foundation/diagnostics.h"
#include "foundation/platform.h"
#include "foundation/compat.h"
#include "foundation/compat_thread.h"
#include "foundation/mem.h"
#include "foundation/profile.h"
Expand All @@ -59,9 +61,10 @@ static cbm_mcp_server_t *g_server = NULL;
static cbm_http_server_t *g_http_server = NULL;
static atomic_int g_shutdown = 0;

static void signal_handler(int sig) {
(void)sig;
atomic_store(&g_shutdown, 1);
static void request_shutdown(void) {
if (atomic_exchange(&g_shutdown, 1)) {
return;
}

/* Cancel any in-progress pipeline (async-signal-safe: only does atomic_store) */
if (g_server) {
Expand All @@ -83,6 +86,37 @@ static void signal_handler(int sig) {
(void)fclose(stdin);
}

static void signal_handler(int sig) {
(void)sig;
request_shutdown();
}

/* ── Parent process watchdog ───────────────────────────────────── */

#ifndef _WIN32
static void *parent_watchdog_thread(void *arg) {
pid_t initial_ppid = *(pid_t *)arg;
const unsigned int interval_us = 500000;

while (!atomic_load(&g_shutdown)) {
cbm_usleep(interval_us);

if (atomic_load(&g_shutdown)) {
break;
}

pid_t current_ppid = getppid();
if (initial_ppid > 1 && current_ppid != initial_ppid) {
cbm_log_warn("parent.exited", "reason", "ppid_changed");
request_shutdown();
exit(0);
}
}

return NULL;
}
#endif

/* ── Watcher background thread ──────────────────────────────────── */

static void *watcher_thread(void *arg) {
Expand Down Expand Up @@ -345,6 +379,24 @@ int main(int argc, char **argv) {
return subcmd;
}

#ifndef _WIN32
pid_t initial_ppid = getppid();
if (initial_ppid <= 1) {
return 0;
}

cbm_thread_t parent_watchdog_tid;
bool parent_watchdog_started = false;
int parent_watchdog_rc = cbm_thread_create(&parent_watchdog_tid, PARENT_WATCHDOG_STACK_SIZE,
parent_watchdog_thread, &initial_ppid);
if (parent_watchdog_rc == 0) {
parent_watchdog_started = true;
} else {
cbm_log_int(CBM_LOG_WARN, "parent.watchdog.unavailable", "rc", parent_watchdog_rc);
return SKIP_ONE;
}
#endif

/* Default: MCP server on stdio */
cbm_mem_init(MAIN_RAM_FRACTION); /* 50% of RAM — safe now because mimalloc tracks ALL
* memory (C + C++ allocations) via global override.
Expand Down Expand Up @@ -391,6 +443,12 @@ int main(int argc, char **argv) {
if (!g_server) {
cbm_log_error("server.err", "msg", "failed to create server");
cbm_config_close(runtime_config);
#ifndef _WIN32
atomic_store(&g_shutdown, 1);
if (parent_watchdog_started) {
cbm_thread_join(&parent_watchdog_tid);
}
#endif
return SKIP_ONE;
}

Expand Down Expand Up @@ -430,10 +488,17 @@ int main(int argc, char **argv) {

/* Run MCP event loop (blocks until EOF or signal) */
int rc = cbm_mcp_server_run(g_server, stdin, stdout);
atomic_store(&g_shutdown, 1);

/* Shutdown */
cbm_log_info("server.shutdown");

#ifndef _WIN32
if (parent_watchdog_started) {
cbm_thread_join(&parent_watchdog_tid);
}
#endif

if (http_started) {
cbm_http_server_stop(g_http_server);
cbm_thread_join(&http_tid);
Expand Down
82 changes: 82 additions & 0 deletions tests/test_parent_watchdog.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
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)"
cleanup() {
if [[ -f "${tmpdir}/child.pid" ]]; then
child_pid="$(cat "${tmpdir}/child.pid" 2>/dev/null || true)"
if [[ -n "${child_pid}" ]]; then
kill "${child_pid}" 2>/dev/null || true
fi
fi
if [[ -n "${wrapper_pid:-}" ]]; then
kill "${wrapper_pid}" 2>/dev/null || true
fi
rm -rf "${tmpdir}"
}
trap cleanup EXIT

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=$!

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
if [[ -s "${tmpdir}/child.err" ]]; then
cat "${tmpdir}/child.err" >&2
fi
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

kill -9 "${wrapper_pid}"
wait "${wrapper_pid}" 2>/dev/null || true

deadline=$((SECONDS + 6))
while (( SECONDS < deadline )); do
if ! kill -0 "${child_pid}" 2>/dev/null; then
exit 0
fi
sleep 0.2
done

echo "codebase-memory-mcp child ${child_pid} survived parent death" >&2
if [[ -s "${tmpdir}/child.err" ]]; then
cat "${tmpdir}/child.err" >&2
fi
exit 1