mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
libgit2 is licensed GPLv2-with-linking-exception, and this project is
deliberately GPL-free (see scripts/license-policy.json). libgit2 was only
an OPTIONAL, faster git-history code path guarded by HAVE_LIBGIT2 and
auto-detected via pkg-config; it always shipped with a popen("git log ...")
fallback that release binaries already used (they were built without
libgit2). Making that popen fallback the SOLE git-history implementation
means there is NO change to shipped behavior.
Changes:
- pass_githistory.c: collapse the HAVE_LIBGIT2 #ifdef so only the popen
parse_git_log remains; drop the now-unused <git2.h>/<time.h> includes.
- cbm.c / cbm.h / main.c / subprocess.c / index_supervisor.h: remove the
libgit2 mimalloc allocator bind and its >=1.7.0 version guards; keep the
tree-sitter + sqlite3 binds; correct the comments.
- Makefile.cbm: remove the optional-libgit2 pkg-config detection block,
the REQUIRE_LIBGIT2 error gate, and all LIBGIT2 CFLAGS/LIBS/FLAGS.
- .github/workflows/_test.yml: drop the REQUIRE_LIBGIT2 test-matrix leg
plus its libgit2-dev/pkg-config apt install (lower CI cost, no new
gating, no trigger change).
- flake.nix: drop libgit2/pkg-config from the dev shell (now-dead dep).
HAVE_LIBGIT2 no longer exists anywhere in the tree.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
316 lines
9.6 KiB
C
316 lines
9.6 KiB
C
/*
|
|
* 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 <fcntl.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) {
|
|
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.
|
|
* Use open()+dup2() (async-signal-safe, no malloc) rather than freopen():
|
|
* the parent may be multithreaded (the MCP server holds worker/watcher/http
|
|
* threads plus mimalloc/sqlite global state), and a fork() copies
|
|
* only the calling thread — a malloc between fork and exec could deadlock on
|
|
* a lock another thread held at fork time. open/dup2/execv touch no heap. */
|
|
const char *bin = opts->bin;
|
|
const char *const default_argv[] = {bin, NULL};
|
|
const char *const *argv = opts->argv ? opts->argv : default_argv;
|
|
const char *target = opts->log_file ? opts->log_file : "/dev/null";
|
|
int fd = open(target, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
|
if (fd >= 0) {
|
|
(void)dup2(fd, STDOUT_FILENO);
|
|
(void)dup2(fd, STDERR_FILENO);
|
|
if (fd > STDERR_FILENO) {
|
|
(void)close(fd);
|
|
}
|
|
}
|
|
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
|
|
}
|