mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
feat(foundation): add cross-platform subprocess spawn + exit classification
New src/foundation/subprocess.{h,c}: spawn a child, tail its output, and
classify how it ended — clean / exit-nonzero / crash / hang / killed — from
POSIX WIFSIGNALED/WTERMSIG and the Windows NTSTATUS exception exit codes
(0xC0000005 access violation, 0xC00000FD stack overflow). Adds an EINTR-safe
reap loop, a quiet-timeout that kills and reports a hung child (no new output
within the window), and partial-line-safe log tailing.
Generalized from the ad-hoc index spawn in src/ui/http_server.c so the
crash/hang supervisor can reuse one primitive across platforms. The exit-code
to outcome mapping lives in a pure cbm_proc_classify() so the Windows
crash-code path is unit-tested on every platform, not just an untested
Windows branch.
tests/test_subprocess.c: 13 tests — the classifier on all platforms plus real
POSIX spawn/reap of clean, non-zero, SIGSEGV-crash and hang children. Green
under ASan/UBSan.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
9cb3cabf76
commit
c7c94003cc
5 files changed
+575
-2
No files matched your search
+4
-2
@@ -113,7 +113,8 @@ FOUNDATION_SRCS = \
|
||||
src/foundation/diagnostics.c \
|
||||
src/foundation/profile.c \
|
||||
src/foundation/dump_verify.c \
|
||||
src/foundation/limits.c
|
||||
src/foundation/limits.c \
|
||||
src/foundation/subprocess.c
|
||||
|
||||
# Existing extraction C code (compiled from current location)
|
||||
EXTRACTION_SRCS = \
|
||||
@@ -310,7 +311,8 @@ TEST_FOUNDATION_SRCS = \
|
||||
tests/test_log.c \
|
||||
tests/test_str_util.c \
|
||||
tests/test_platform.c \
|
||||
tests/test_dump_verify.c
|
||||
tests/test_dump_verify.c \
|
||||
tests/test_subprocess.c
|
||||
|
||||
TEST_EXTRACTION_SRCS = \
|
||||
tests/test_extraction.c \
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
/*
|
||||
* subprocess.c — cross-platform spawn + supervise + classify.
|
||||
* See subprocess.h. The spawn/reap skeleton mirrors src/ui/http_server.c's
|
||||
* index subprocess; this generalizes it and adds crash/hang classification.
|
||||
*/
|
||||
#include "subprocess.h"
|
||||
|
||||
#include "compat.h" /* cbm_nanosleep */
|
||||
#include "platform.h" /* cbm_now_ms */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
/* NTSTATUS severity ERROR (top two bits set) covers the Windows crash exception
|
||||
* exit codes: 0xC0000005 (access violation), 0xC00000FD (stack overflow),
|
||||
* 0xC000001D (illegal instruction), 0xC0000094 (integer divide by zero), … */
|
||||
#define CBM_WIN_CRASH_CODE_MIN 0xC0000000u
|
||||
|
||||
#ifndef _WIN32
|
||||
static bool cbm_is_fault_signal(int sig) {
|
||||
switch (sig) {
|
||||
case SIGSEGV:
|
||||
case SIGBUS:
|
||||
case SIGILL:
|
||||
case SIGFPE:
|
||||
case SIGABRT:
|
||||
case SIGSYS:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
cbm_proc_outcome_t cbm_proc_classify(bool exited_normally, int exit_code, int term_signal,
|
||||
bool timed_out) {
|
||||
if (timed_out) {
|
||||
return CBM_PROC_HANG;
|
||||
}
|
||||
if (!exited_normally) {
|
||||
/* POSIX signal death. */
|
||||
#ifndef _WIN32
|
||||
if (cbm_is_fault_signal(term_signal)) {
|
||||
return CBM_PROC_CRASH;
|
||||
}
|
||||
#else
|
||||
(void)term_signal;
|
||||
#endif
|
||||
return CBM_PROC_KILLED;
|
||||
}
|
||||
/* Exited with a code. A Windows NTSTATUS exception code is a crash; on POSIX
|
||||
* exit codes are 0..255 so this branch never misfires there. */
|
||||
if ((unsigned)exit_code >= CBM_WIN_CRASH_CODE_MIN) {
|
||||
return CBM_PROC_CRASH;
|
||||
}
|
||||
return (exit_code == 0) ? CBM_PROC_CLEAN : CBM_PROC_EXIT_NONZERO;
|
||||
}
|
||||
|
||||
const char *cbm_proc_outcome_str(cbm_proc_outcome_t o) {
|
||||
switch (o) {
|
||||
case CBM_PROC_CLEAN:
|
||||
return "clean";
|
||||
case CBM_PROC_EXIT_NONZERO:
|
||||
return "exit_nonzero";
|
||||
case CBM_PROC_CRASH:
|
||||
return "crash";
|
||||
case CBM_PROC_HANG:
|
||||
return "hang";
|
||||
case CBM_PROC_KILLED:
|
||||
return "killed";
|
||||
case CBM_PROC_SPAWN_FAILED:
|
||||
default:
|
||||
return "spawn_failed";
|
||||
}
|
||||
}
|
||||
|
||||
/* Tail newly-appended complete lines from the child log, starting at *tail_pos.
|
||||
* A partial (non-newline-terminated) final line is left buffered: *tail_pos is
|
||||
* not advanced past it, so it is re-read once completed. Returns true if any
|
||||
* complete line was consumed (i.e. there was progress). */
|
||||
static bool cbm_tail_log(const char *log_file, long *tail_pos, cbm_proc_log_cb cb, void *ud) {
|
||||
if (!log_file) {
|
||||
return false;
|
||||
}
|
||||
FILE *lf = fopen(log_file, "r");
|
||||
if (!lf) {
|
||||
return false;
|
||||
}
|
||||
bool progressed = false;
|
||||
if (fseek(lf, *tail_pos, SEEK_SET) == 0) {
|
||||
char line[1024];
|
||||
for (;;) {
|
||||
long before = ftell(lf);
|
||||
if (!fgets(line, sizeof(line), lf)) {
|
||||
break;
|
||||
}
|
||||
size_t l = strlen(line);
|
||||
bool complete = (l > 0 && line[l - 1] == '\n');
|
||||
if (complete) {
|
||||
line[l - 1] = '\0';
|
||||
*tail_pos = ftell(lf);
|
||||
progressed = true;
|
||||
if (line[0] && cb) {
|
||||
cb(line, ud);
|
||||
}
|
||||
} else if (l == sizeof(line) - 1) {
|
||||
/* Oversized line filled the buffer without a newline — consume it
|
||||
* anyway (counts as progress) so we never stall on one long line. */
|
||||
*tail_pos = ftell(lf);
|
||||
progressed = true;
|
||||
if (cb) {
|
||||
cb(line, ud);
|
||||
}
|
||||
} else {
|
||||
/* Genuine partial final line — keep it buffered for next poll. */
|
||||
*tail_pos = before;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
fclose(lf);
|
||||
return progressed;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
static int cbm_run_win(const cbm_proc_opts_t *opts, cbm_proc_result_t *out) {
|
||||
const char *bin = opts->bin;
|
||||
const char *const default_argv[] = {bin, NULL};
|
||||
const char *const *argv = opts->argv ? opts->argv : default_argv;
|
||||
|
||||
/* Build a quoted command line from argv. */
|
||||
char cmdline[8192];
|
||||
size_t pos = 0;
|
||||
for (int i = 0; argv[i]; i++) {
|
||||
int n = snprintf(cmdline + pos, sizeof(cmdline) - pos, "%s\"%s\"", (i ? " " : ""), argv[i]);
|
||||
if (n < 0 || (size_t)n >= sizeof(cmdline) - pos) {
|
||||
out->outcome = CBM_PROC_SPAWN_FAILED;
|
||||
out->exit_code = -1;
|
||||
out->term_signal = 0;
|
||||
return -1;
|
||||
}
|
||||
pos += (size_t)n;
|
||||
}
|
||||
|
||||
HANDLE hlog = INVALID_HANDLE_VALUE;
|
||||
STARTUPINFOA si = {.cb = sizeof(si)};
|
||||
if (opts->log_file) {
|
||||
hlog = CreateFileA(opts->log_file, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS,
|
||||
FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
if (hlog != INVALID_HANDLE_VALUE) {
|
||||
si.dwFlags = STARTF_USESTDHANDLES;
|
||||
si.hStdError = hlog;
|
||||
si.hStdOutput = hlog;
|
||||
}
|
||||
}
|
||||
|
||||
PROCESS_INFORMATION pi = {0};
|
||||
BOOL ok = CreateProcessA(NULL, cmdline, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
|
||||
if (hlog != INVALID_HANDLE_VALUE) {
|
||||
CloseHandle(hlog);
|
||||
}
|
||||
if (!ok) {
|
||||
out->outcome = CBM_PROC_SPAWN_FAILED;
|
||||
out->exit_code = -1;
|
||||
out->term_signal = 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
long tail_pos = 0;
|
||||
uint64_t last_activity = cbm_now_ms();
|
||||
bool timed_out = false;
|
||||
for (;;) {
|
||||
DWORD w = WaitForSingleObject(pi.hProcess, 200);
|
||||
if (cbm_tail_log(opts->log_file, &tail_pos, opts->on_log_line, opts->log_ud)) {
|
||||
last_activity = cbm_now_ms();
|
||||
}
|
||||
if (w == WAIT_OBJECT_0) {
|
||||
break;
|
||||
}
|
||||
if (opts->quiet_timeout_ms > 0 &&
|
||||
(cbm_now_ms() - last_activity) >= (uint64_t)opts->quiet_timeout_ms) {
|
||||
TerminateProcess(pi.hProcess, 1);
|
||||
WaitForSingleObject(pi.hProcess, INFINITE);
|
||||
timed_out = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
DWORD code = 1;
|
||||
GetExitCodeProcess(pi.hProcess, &code);
|
||||
CloseHandle(pi.hProcess);
|
||||
CloseHandle(pi.hThread);
|
||||
if (opts->log_file && opts->delete_log_on_exit) {
|
||||
DeleteFileA(opts->log_file);
|
||||
}
|
||||
|
||||
out->exit_code = (int)code;
|
||||
out->term_signal = 0;
|
||||
out->outcome = cbm_proc_classify(true, (int)code, 0, timed_out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#else /* POSIX */
|
||||
|
||||
static int cbm_run_posix(const cbm_proc_opts_t *opts, cbm_proc_result_t *out) {
|
||||
const char *bin = opts->bin;
|
||||
const char *const default_argv[] = {bin, NULL};
|
||||
const char *const *argv = opts->argv ? opts->argv : default_argv;
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
out->outcome = CBM_PROC_SPAWN_FAILED;
|
||||
out->exit_code = -1;
|
||||
out->term_signal = 0;
|
||||
return -1;
|
||||
}
|
||||
if (pid == 0) {
|
||||
/* Child: redirect stdout+stderr to the log (or discard), then exec. */
|
||||
if (opts->log_file) {
|
||||
(void)!freopen(opts->log_file, "w", stderr);
|
||||
(void)!freopen(opts->log_file, "a", stdout);
|
||||
} else {
|
||||
(void)!freopen("/dev/null", "w", stderr);
|
||||
(void)!freopen("/dev/null", "w", stdout);
|
||||
}
|
||||
execv(bin, (char *const *)argv);
|
||||
_exit(127); /* exec failed */
|
||||
}
|
||||
|
||||
long tail_pos = 0;
|
||||
uint64_t last_activity = cbm_now_ms();
|
||||
bool timed_out = false;
|
||||
int wstatus = 0;
|
||||
for (;;) {
|
||||
pid_t wr;
|
||||
do {
|
||||
wr = waitpid(pid, &wstatus, WNOHANG);
|
||||
} while (wr < 0 && errno == EINTR);
|
||||
bool done = (wr == pid);
|
||||
|
||||
if (cbm_tail_log(opts->log_file, &tail_pos, opts->on_log_line, opts->log_ud)) {
|
||||
last_activity = cbm_now_ms();
|
||||
}
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
if (opts->quiet_timeout_ms > 0 &&
|
||||
(cbm_now_ms() - last_activity) >= (uint64_t)opts->quiet_timeout_ms) {
|
||||
kill(pid, SIGKILL);
|
||||
do {
|
||||
wr = waitpid(pid, &wstatus, 0);
|
||||
} while (wr < 0 && errno == EINTR);
|
||||
timed_out = true;
|
||||
break;
|
||||
}
|
||||
struct timespec ts = {0, 100000000L}; /* 100 ms poll */
|
||||
cbm_nanosleep(&ts, NULL);
|
||||
}
|
||||
|
||||
if (opts->log_file && opts->delete_log_on_exit) {
|
||||
(void)unlink(opts->log_file);
|
||||
}
|
||||
|
||||
if (WIFEXITED(wstatus)) {
|
||||
out->exit_code = WEXITSTATUS(wstatus);
|
||||
out->term_signal = 0;
|
||||
out->outcome = cbm_proc_classify(true, out->exit_code, 0, timed_out);
|
||||
} else if (WIFSIGNALED(wstatus)) {
|
||||
out->exit_code = -1;
|
||||
out->term_signal = WTERMSIG(wstatus);
|
||||
out->outcome = cbm_proc_classify(false, -1, out->term_signal, timed_out);
|
||||
} else {
|
||||
out->exit_code = -1;
|
||||
out->term_signal = 0;
|
||||
out->outcome = timed_out ? CBM_PROC_HANG : CBM_PROC_KILLED;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
int cbm_subprocess_run(const cbm_proc_opts_t *opts, cbm_proc_result_t *out) {
|
||||
cbm_proc_result_t local;
|
||||
if (!out) {
|
||||
out = &local;
|
||||
}
|
||||
out->outcome = CBM_PROC_SPAWN_FAILED;
|
||||
out->exit_code = -1;
|
||||
out->term_signal = 0;
|
||||
if (!opts || !opts->bin || !opts->bin[0]) {
|
||||
return -1;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
return cbm_run_win(opts, out);
|
||||
#else
|
||||
return cbm_run_posix(opts, out);
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* subprocess.h — spawn a child process, supervise it, and classify how it ended.
|
||||
*
|
||||
* Generalized from the crash-isolating index spawn in src/ui/http_server.c so the
|
||||
* crash/hang supervisor (Track C) can reuse one primitive across platforms.
|
||||
*
|
||||
* Beyond a plain spawn+wait it adds the two things a supervisor needs and the
|
||||
* ad-hoc harness lacked:
|
||||
* 1. Exit CLASSIFICATION — {clean, exit-nonzero, crash, hang, killed} — from
|
||||
* POSIX WIFSIGNALED/WTERMSIG and the Windows NTSTATUS exception exit codes
|
||||
* (0xC0000005 access-violation, 0xC00000FD stack-overflow, …).
|
||||
* 2. A quiet-timeout — kill + report HANG when the child makes no progress
|
||||
* (emits no new log line) for a configurable window. This catches external
|
||||
* tree-sitter scanners that infinite-loop (a hang, not a crash).
|
||||
*
|
||||
* The reap loop is EINTR-safe. Line tailing keeps a partial final line buffered
|
||||
* (an incomplete, un-newline-terminated line is not yet "progress" and is not
|
||||
* mis-read as a completed marker).
|
||||
*/
|
||||
#ifndef CBM_SUBPROCESS_H
|
||||
#define CBM_SUBPROCESS_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/* How a supervised child ended. */
|
||||
typedef enum {
|
||||
CBM_PROC_CLEAN = 0, /* exited with code 0 */
|
||||
CBM_PROC_EXIT_NONZERO, /* exited with a nonzero code (a graceful failure) */
|
||||
CBM_PROC_CRASH, /* died from a fault: POSIX SIGSEGV/BUS/ILL/FPE/ABRT/SYS,
|
||||
* or a Windows NTSTATUS exception exit code (>= 0xC0000000) */
|
||||
CBM_PROC_HANG, /* made no progress within the quiet-timeout; we killed it */
|
||||
CBM_PROC_KILLED, /* terminated by a non-fault signal we did not initiate */
|
||||
CBM_PROC_SPAWN_FAILED /* fork/exec/CreateProcess failed — no child ever ran */
|
||||
} cbm_proc_outcome_t;
|
||||
|
||||
typedef struct {
|
||||
cbm_proc_outcome_t outcome;
|
||||
int exit_code; /* WEXITSTATUS / GetExitCodeProcess; -1 when terminated by a POSIX signal */
|
||||
int term_signal; /* WTERMSIG on POSIX; 0 otherwise */
|
||||
} cbm_proc_result_t;
|
||||
|
||||
/* Called for each newly-completed (newline-terminated) log line while the child
|
||||
* runs. A completed line also resets the quiet-timeout (it is progress). */
|
||||
typedef void (*cbm_proc_log_cb)(const char *line, void *ud);
|
||||
|
||||
typedef struct {
|
||||
const char *bin; /* executable path; also argv[0] when argv is NULL */
|
||||
const char *const *argv; /* NULL-terminated argv; NULL => { bin, NULL } */
|
||||
const char *log_file; /* child stdout+stderr are redirected here and tailed;
|
||||
* NULL => discard child output, no tailing */
|
||||
cbm_proc_log_cb on_log_line; /* optional per-line callback */
|
||||
void *log_ud; /* user data for on_log_line */
|
||||
int quiet_timeout_ms; /* <= 0 => no timeout; else kill+HANG after this many
|
||||
* ms with no new completed log line */
|
||||
bool delete_log_on_exit; /* unlink log_file after reaping */
|
||||
} cbm_proc_opts_t;
|
||||
|
||||
/* Spawn opts->bin, supervise (tail + optional quiet-timeout), block until it ends,
|
||||
* and classify the result into *out. Returns 0 if a child was spawned and reaped
|
||||
* (out filled), or -1 if the spawn itself failed (out->outcome == CBM_PROC_SPAWN_FAILED). */
|
||||
int cbm_subprocess_run(const cbm_proc_opts_t *opts, cbm_proc_result_t *out);
|
||||
|
||||
/* Pure outcome classifier — exposed so the platform-specific exit-code mapping
|
||||
* (notably the Windows NTSTATUS crash codes) is unit-testable on every platform.
|
||||
* exited_normally: the child returned an exit code (POSIX WIFEXITED; always true
|
||||
* on Windows, which has no signals — crashes surface as codes).
|
||||
* exit_code: the exit / exception code (meaningful when exited_normally).
|
||||
* term_signal: POSIX terminating signal (meaningful when !exited_normally).
|
||||
* timed_out: we killed the child for exceeding the quiet-timeout. */
|
||||
cbm_proc_outcome_t cbm_proc_classify(bool exited_normally, int exit_code, int term_signal,
|
||||
bool timed_out);
|
||||
|
||||
/* Stable lowercase name for an outcome (for structured logs / skip reasons). */
|
||||
const char *cbm_proc_outcome_str(cbm_proc_outcome_t o);
|
||||
|
||||
#endif /* CBM_SUBPROCESS_H */
|
||||
@@ -43,6 +43,7 @@ extern void suite_str_intern(void);
|
||||
extern void suite_log(void);
|
||||
extern void suite_str_util(void);
|
||||
extern void suite_platform(void);
|
||||
extern void suite_subprocess(void);
|
||||
extern void suite_extraction(void);
|
||||
extern void suite_extraction_inheritance(void);
|
||||
extern void suite_extraction_imports(void);
|
||||
@@ -145,6 +146,7 @@ int main(int argc, char **argv) {
|
||||
RUN_SELECTED_SUITE(log);
|
||||
RUN_SELECTED_SUITE(str_util);
|
||||
RUN_SELECTED_SUITE(platform);
|
||||
RUN_SELECTED_SUITE(subprocess);
|
||||
RUN_SELECTED_SUITE(dump_verify);
|
||||
|
||||
/* Existing C code regression tests */
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* test_subprocess.c — foundation/subprocess: spawn + supervise + classify.
|
||||
*
|
||||
* Two layers:
|
||||
* 1. cbm_proc_classify() — pure, exercised on EVERY platform (so the Windows
|
||||
* NTSTATUS crash-code mapping is guarded on Linux/macOS CI too, not just an
|
||||
* untested Windows branch).
|
||||
* 2. cbm_subprocess_run() — real spawn/reap, exercised on POSIX via /bin/sh
|
||||
* (SKIP_PLATFORM on Windows, which lacks it).
|
||||
*/
|
||||
#include "test_framework.h"
|
||||
#include "../src/foundation/subprocess.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <signal.h>
|
||||
#endif
|
||||
|
||||
/* ── Layer 1: pure classifier (all platforms) ─────────────────────────────── */
|
||||
|
||||
TEST(subprocess_classify_clean) {
|
||||
ASSERT_EQ(cbm_proc_classify(true, 0, 0, false), CBM_PROC_CLEAN);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(subprocess_classify_exit_nonzero) {
|
||||
ASSERT_EQ(cbm_proc_classify(true, 3, 0, false), CBM_PROC_EXIT_NONZERO);
|
||||
ASSERT_EQ(cbm_proc_classify(true, 127, 0, false), CBM_PROC_EXIT_NONZERO);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Windows exception exit codes classify as CRASH — the key reason to unit-test
|
||||
* the classifier cross-platform. 0xC0000005 = access violation (SIGSEGV analog),
|
||||
* 0xC00000FD = stack overflow (the #668 reporter's 0xC00000FD). */
|
||||
TEST(subprocess_classify_windows_crash_codes) {
|
||||
ASSERT_EQ(cbm_proc_classify(true, (int)0xC0000005u, 0, false), CBM_PROC_CRASH);
|
||||
ASSERT_EQ(cbm_proc_classify(true, (int)0xC00000FDu, 0, false), CBM_PROC_CRASH);
|
||||
ASSERT_EQ(cbm_proc_classify(true, (int)0xC000001Du, 0, false), CBM_PROC_CRASH);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(subprocess_classify_posix_fault_signal_is_crash) {
|
||||
#ifndef _WIN32
|
||||
ASSERT_EQ(cbm_proc_classify(false, -1, SIGSEGV, false), CBM_PROC_CRASH);
|
||||
ASSERT_EQ(cbm_proc_classify(false, -1, SIGABRT, false), CBM_PROC_CRASH);
|
||||
ASSERT_EQ(cbm_proc_classify(false, -1, SIGBUS, false), CBM_PROC_CRASH);
|
||||
#endif
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(subprocess_classify_non_fault_signal_is_killed) {
|
||||
#ifndef _WIN32
|
||||
ASSERT_EQ(cbm_proc_classify(false, -1, SIGTERM, false), CBM_PROC_KILLED);
|
||||
ASSERT_EQ(cbm_proc_classify(false, -1, SIGKILL, false), CBM_PROC_KILLED);
|
||||
#endif
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* timed_out dominates every other signal — a killed-for-hang child is HANG,
|
||||
* not KILLED, even though we deliver SIGKILL/TerminateProcess to end it. */
|
||||
TEST(subprocess_classify_timeout_dominates) {
|
||||
ASSERT_EQ(cbm_proc_classify(false, -1, 9 /*SIGKILL*/, true), CBM_PROC_HANG);
|
||||
ASSERT_EQ(cbm_proc_classify(true, 0, 0, true), CBM_PROC_HANG);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(subprocess_outcome_str) {
|
||||
ASSERT_STR_EQ(cbm_proc_outcome_str(CBM_PROC_CLEAN), "clean");
|
||||
ASSERT_STR_EQ(cbm_proc_outcome_str(CBM_PROC_CRASH), "crash");
|
||||
ASSERT_STR_EQ(cbm_proc_outcome_str(CBM_PROC_HANG), "hang");
|
||||
ASSERT_STR_EQ(cbm_proc_outcome_str(CBM_PROC_EXIT_NONZERO), "exit_nonzero");
|
||||
ASSERT_STR_EQ(cbm_proc_outcome_str(CBM_PROC_KILLED), "killed");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Layer 2: real spawn/reap (POSIX) ─────────────────────────────────────── */
|
||||
|
||||
#ifndef _WIN32
|
||||
static cbm_proc_result_t run_sh(const char *script, int quiet_timeout_ms) {
|
||||
const char *argv[] = {"/bin/sh", "-c", script, NULL};
|
||||
cbm_proc_opts_t opts = {0};
|
||||
opts.bin = "/bin/sh";
|
||||
opts.argv = argv;
|
||||
opts.log_file = NULL;
|
||||
opts.quiet_timeout_ms = quiet_timeout_ms;
|
||||
cbm_proc_result_t r;
|
||||
cbm_subprocess_run(&opts, &r);
|
||||
return r;
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST(subprocess_run_clean) {
|
||||
#ifdef _WIN32
|
||||
SKIP_PLATFORM("POSIX /bin/sh spawn");
|
||||
#else
|
||||
cbm_proc_result_t r = run_sh("exit 0", 0);
|
||||
ASSERT_EQ(r.outcome, CBM_PROC_CLEAN);
|
||||
ASSERT_EQ(r.exit_code, 0);
|
||||
PASS();
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST(subprocess_run_exit_nonzero) {
|
||||
#ifdef _WIN32
|
||||
SKIP_PLATFORM("POSIX /bin/sh spawn");
|
||||
#else
|
||||
cbm_proc_result_t r = run_sh("exit 7", 0);
|
||||
ASSERT_EQ(r.outcome, CBM_PROC_EXIT_NONZERO);
|
||||
ASSERT_EQ(r.exit_code, 7);
|
||||
PASS();
|
||||
#endif
|
||||
}
|
||||
|
||||
/* A child that dies of SIGSEGV must classify as CRASH — NOT exit_nonzero and NOT
|
||||
* killed. This is the whole point of the primitive: distinguish a crash from a
|
||||
* clean failure so the supervisor can quarantine the culprit. */
|
||||
TEST(subprocess_run_crash_is_crash) {
|
||||
#ifdef _WIN32
|
||||
SKIP_PLATFORM("POSIX signal semantics");
|
||||
#else
|
||||
cbm_proc_result_t r = run_sh("kill -SEGV $$", 0);
|
||||
ASSERT_EQ(r.outcome, CBM_PROC_CRASH);
|
||||
ASSERT_EQ(r.term_signal, SIGSEGV);
|
||||
PASS();
|
||||
#endif
|
||||
}
|
||||
|
||||
/* A child that makes no progress within the quiet-timeout is killed and reported
|
||||
* as HANG — the sibling failure mode of a crash (external-scanner infinite loop). */
|
||||
TEST(subprocess_run_hang_is_hang) {
|
||||
#ifdef _WIN32
|
||||
SKIP_PLATFORM("POSIX /bin/sh spawn");
|
||||
#else
|
||||
cbm_proc_result_t r = run_sh("sleep 30", 300 /* ms quiet-timeout */);
|
||||
ASSERT_EQ(r.outcome, CBM_PROC_HANG);
|
||||
PASS();
|
||||
#endif
|
||||
}
|
||||
|
||||
/* A spawn of a non-existent binary fails cleanly (no child), not a crash. */
|
||||
TEST(subprocess_run_spawn_failure) {
|
||||
#ifdef _WIN32
|
||||
SKIP_PLATFORM("POSIX exec semantics");
|
||||
#else
|
||||
/* execv of a bogus path: fork succeeds, child _exit(127). We classify the
|
||||
* reaped 127 as exit_nonzero — spawn_failed is reserved for fork() failing. */
|
||||
const char *argv[] = {"/nonexistent/cbm-bogus-binary", NULL};
|
||||
cbm_proc_opts_t opts = {0};
|
||||
opts.bin = "/nonexistent/cbm-bogus-binary";
|
||||
opts.argv = argv;
|
||||
cbm_proc_result_t r;
|
||||
int rc = cbm_subprocess_run(&opts, &r);
|
||||
ASSERT_EQ(rc, 0); /* fork itself succeeded */
|
||||
ASSERT_EQ(r.outcome, CBM_PROC_EXIT_NONZERO);
|
||||
ASSERT_EQ(r.exit_code, 127);
|
||||
PASS();
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST(subprocess_run_null_bin_rejected) {
|
||||
cbm_proc_opts_t opts = {0};
|
||||
opts.bin = NULL;
|
||||
cbm_proc_result_t r;
|
||||
int rc = cbm_subprocess_run(&opts, &r);
|
||||
ASSERT_EQ(rc, -1);
|
||||
ASSERT_EQ(r.outcome, CBM_PROC_SPAWN_FAILED);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(subprocess) {
|
||||
RUN_TEST(subprocess_classify_clean);
|
||||
RUN_TEST(subprocess_classify_exit_nonzero);
|
||||
RUN_TEST(subprocess_classify_windows_crash_codes);
|
||||
RUN_TEST(subprocess_classify_posix_fault_signal_is_crash);
|
||||
RUN_TEST(subprocess_classify_non_fault_signal_is_killed);
|
||||
RUN_TEST(subprocess_classify_timeout_dominates);
|
||||
RUN_TEST(subprocess_outcome_str);
|
||||
RUN_TEST(subprocess_run_clean);
|
||||
RUN_TEST(subprocess_run_exit_nonzero);
|
||||
RUN_TEST(subprocess_run_crash_is_crash);
|
||||
RUN_TEST(subprocess_run_hang_is_hang);
|
||||
RUN_TEST(subprocess_run_spawn_failure);
|
||||
RUN_TEST(subprocess_run_null_bin_rejected);
|
||||
}
|
||||
Reference in new issue
Block a user