mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-26 06:43:15 +08:00
A main commit can now publish its test-impact bundle — the commit's frozen graph, its per-test coverage map and receipt.json binding both — and a PR selection given the bundle of its merge base, verified by the caller, builds its graph incrementally from the bundle's graph and narrows suites by the admitted map. Without a verified bundle nothing changes: the graph is built from scratch and every reached suite runs whole. Producer and consumer: - `codebase-memory-mcp test-impact publish|select` (in process, no daemon): publish builds the bundle of the checked-out commit, from the previous bundle's graph when that bundle verifies; select is the selection with optional --artifact/--artifact-commit/--artifact-verified. The MCP surface is unchanged. - receipt.json binds the exact imported graph (graph digest), the graph's content and topology, the map's three files and a compatibility digest (platform label + the bytes of test_impact.coverage.compatibility_paths). - A bundle is used only when the caller verified its source, it is the merge base's own, and its imported graph is the one the receipt names; the map is admitted by the existing receipt policy (source, ancestry, content, age, compatibility, metadata). Refusals become rejection reasons in the answer's receipt. - C_eff: a changed function the map knows contributes its own id; one it does not know (new, platform-only, a type or macro) contributes its nearest mapped callers on the static inbound closure; deleted names contribute every mapped function of that name. Graph: - the frozen build accepts a base candidate and repairs it through the incremental closure route; any doubt declines to the full build. Pinned to one worker like the full frozen build. - incremental closure repair skipped the interprocedural complexity pass: re-parsed functions lost transitive_loop_depth/recursive and callers kept stale values. One traversal now serves the in-memory pass and a new store-level recompute that runs before the importance rescore. - test-impact's frozen builds use a fixed display Git context: the per-run pin path and the commit ids made two builds of one tree differ and made every commit a semantic-input change, so repair always declined. - graph content and topology digests (store_content_digest.c): the exact graph digest binds one database (ids, db_uid, generation), never equal across builds; the content digest keys rows by what they say, the topology digest by what selection reads. Measured on this repository (frozen publish, macOS arm64): a two-file C change repairs in 18-20 s instead of 75-80 s with the topology of the full build; a docs-only change repairs with identical content; selections with and without the bundle are identical on all seven replayed pairs (two repaired incrementally, five declined to the full build). Recorded, not fixed: closure repair still differs from a full build in edge PROPERTIES (argument quoting and key order, CONFIGURES args) and in the corpus-wide SIMILAR_TO/SEMANTICALLY_RELATED edges; selection reads neither, so the topology digest is the incremental-equals-full check and the receipt carries both. Tooling: scripts/test-impact/coverage-merge.py builds a push's incremental coverage map (re-run suites verbatim, the rest carried with ids remapped onto the new image's function table), with its contract as test.sh Step 0z7. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1428 lines
57 KiB
C
1428 lines
57 KiB
C
/*
|
|
* bootstrap.c — Mandatory per-account daemon startup policy.
|
|
*/
|
|
#include "daemon/bootstrap.h"
|
|
|
|
#include "daemon/ipc.h"
|
|
#include "daemon/service.h"
|
|
#include "foundation/compat.h"
|
|
#include "foundation/compat_fs.h"
|
|
#include "foundation/log.h"
|
|
#include "foundation/platform.h"
|
|
|
|
#include <errno.h>
|
|
#include <limits.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#ifdef _WIN32
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
#include "foundation/subprocess.h"
|
|
#include "foundation/win_utf8.h"
|
|
#include <windows.h>
|
|
#else
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <signal.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
#ifdef __APPLE__
|
|
#include <pthread.h>
|
|
#include <spawn.h>
|
|
extern char **environ;
|
|
#endif
|
|
#endif
|
|
|
|
enum {
|
|
BOOTSTRAP_RETRY_NS = 1000000,
|
|
/* #1828: how often a waiting client re-reads the start-failure log
|
|
* (the wait loop itself ticks every millisecond). */
|
|
BOOTSTRAP_START_FAILURE_CHECK_MS = 50,
|
|
/* A record this many seconds OLDER than the client's own spawn instant is
|
|
* still accepted: a sibling client's attempt that just failed is the same
|
|
* evidence, and second-granularity clocks need the slack anyway. */
|
|
BOOTSTRAP_START_FAILURE_SKEW_S = 2,
|
|
BOOTSTRAP_START_FAILURE_LOG_CAP = 65536,
|
|
BOOTSTRAP_COORDINATION_CLEANUP_MS = 500,
|
|
BOOTSTRAP_PATH_CAP = 4096,
|
|
};
|
|
|
|
static bool bootstrap_arg_is(const char *arg, const char *expected) {
|
|
return arg && expected && strcmp(arg, expected) == 0;
|
|
}
|
|
|
|
static int bootstrap_find_arg(int argc, char *const argv[], const char *expected) {
|
|
for (int i = 1; argv && i < argc; i++) {
|
|
if (bootstrap_arg_is(argv[i], expected)) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static bool bootstrap_has_help_after(int argc, char *const argv[], int start) {
|
|
for (int i = start; argv && i < argc; i++) {
|
|
if (bootstrap_arg_is(argv[i], "--help") || bootstrap_arg_is(argv[i], "-h")) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool bootstrap_worker_fingerprint_valid(const char *fingerprint) {
|
|
if (!fingerprint || strlen(fingerprint) != 64U) {
|
|
return false;
|
|
}
|
|
for (size_t i = 0; i < 64U; i++) {
|
|
char ch = fingerprint[i];
|
|
if (!((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f'))) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool bootstrap_worker_budget_valid(const char *text) {
|
|
if (!text || !text[0]) {
|
|
return false;
|
|
}
|
|
size_t value = 0;
|
|
for (const unsigned char *cursor = (const unsigned char *)text; *cursor; cursor++) {
|
|
if (*cursor < '0' || *cursor > '9') {
|
|
return false;
|
|
}
|
|
size_t digit = (size_t)(*cursor - '0');
|
|
if (value > (SIZE_MAX - digit) / 10U) {
|
|
return false;
|
|
}
|
|
value = value * 10U + digit;
|
|
}
|
|
return value > 0;
|
|
}
|
|
|
|
/* Keep the bootstrap role boundary exact and fail closed before any client or
|
|
* worker state is initialized. index_supervisor owns the matching builder and
|
|
* performs the captured-build comparison after this syntax-only classification. */
|
|
static bool bootstrap_worker_argv_exact(int argc, char *const argv[]) {
|
|
if (argc < 9 || !argv || !bootstrap_arg_is(argv[1], "cli") ||
|
|
!bootstrap_arg_is(argv[2], "--index-worker") ||
|
|
!bootstrap_arg_is(argv[3], "--index-worker-build") ||
|
|
!bootstrap_worker_fingerprint_valid(argv[4]) ||
|
|
!bootstrap_arg_is(argv[5], "index_repository") || !argv[6] || !argv[6][0] ||
|
|
!bootstrap_arg_is(argv[7], "--response-out") || !argv[8] || !argv[8][0]) {
|
|
return false;
|
|
}
|
|
int next = 9;
|
|
if (next < argc && bootstrap_arg_is(argv[next], "--index-worker-memory-budget-bytes")) {
|
|
if (next + 1 >= argc || !bootstrap_worker_budget_valid(argv[next + 1])) {
|
|
return false;
|
|
}
|
|
next += 2;
|
|
}
|
|
if (next < argc && bootstrap_arg_is(argv[next], "--index-worker-single-thread")) {
|
|
next++;
|
|
}
|
|
if (next < argc && bootstrap_arg_is(argv[next], "--index-worker-marker")) {
|
|
if (next + 1 >= argc || !argv[next + 1] || !argv[next + 1][0]) {
|
|
return false;
|
|
}
|
|
next += 2;
|
|
}
|
|
if (next < argc && bootstrap_arg_is(argv[next], "--index-worker-quarantine")) {
|
|
if (next + 1 >= argc || !argv[next + 1] || !argv[next + 1][0]) {
|
|
return false;
|
|
}
|
|
next += 2;
|
|
}
|
|
return next == argc;
|
|
}
|
|
|
|
cbm_daemon_process_role_t cbm_daemon_process_role(int argc, char *const argv[]) {
|
|
if (argc <= 0 || !argv || !argv[0] || argv[0][0] == '\0') {
|
|
return CBM_DAEMON_PROCESS_INVALID;
|
|
}
|
|
|
|
int daemon_arg = bootstrap_find_arg(argc, argv, CBM_DAEMON_INTERNAL_ARG);
|
|
if (daemon_arg >= 0) {
|
|
/* Byte-exact daemon-role grammar, deliberately unforgiving: the bare
|
|
* internal marker, or the marker followed by exactly the permanent
|
|
* flag. Every other shape — reordered, repeated, or extended — is
|
|
* INVALID, so argv smuggling cannot reach the daemon role. */
|
|
if (argc == 2 && daemon_arg == 1) {
|
|
return CBM_DAEMON_PROCESS_DAEMON;
|
|
}
|
|
if (argc == 3 && daemon_arg == 1 && bootstrap_arg_is(argv[2], CBM_DAEMON_PERMANENT_ARG)) {
|
|
return CBM_DAEMON_PROCESS_DAEMON;
|
|
}
|
|
return CBM_DAEMON_PROCESS_INVALID;
|
|
}
|
|
|
|
int worker_arg = bootstrap_find_arg(argc, argv, "--index-worker");
|
|
if (worker_arg >= 0) {
|
|
return bootstrap_worker_argv_exact(argc, argv) ? CBM_DAEMON_PROCESS_WORKER
|
|
: CBM_DAEMON_PROCESS_INVALID;
|
|
}
|
|
|
|
static const char *const stateless_commands[] = {
|
|
"install",
|
|
"uninstall",
|
|
"update",
|
|
/* allow-root writes one line of user-level config and reads nothing from a
|
|
* project, so it needs no daemon. Listed here rather than routed through
|
|
* the daemon so enrolling a root cannot depend on daemon state. */
|
|
"allow-root",
|
|
/* test-impact runs its own frozen index of a pinned tree in process
|
|
* (CI: publish the team artifact, select tests); it never reads or
|
|
* writes the daemon's projects, so it needs no daemon. */
|
|
"test-impact",
|
|
};
|
|
/* Stop at the first top-level mode token. Tool names, flag values, and JSON
|
|
* following `cli` are opaque user input: a search query named "install"
|
|
* or containing "--version" must never bypass the mandatory daemon. */
|
|
for (int arg = 1; arg < argc; arg++) {
|
|
if (bootstrap_arg_is(argv[arg], "cli")) {
|
|
if (bootstrap_has_help_after(argc, argv, arg + 1)) {
|
|
return CBM_DAEMON_PROCESS_STATELESS;
|
|
}
|
|
return CBM_DAEMON_PROCESS_LOCAL_CLI;
|
|
}
|
|
if (bootstrap_arg_is(argv[arg], "hook-augment")) {
|
|
return CBM_DAEMON_PROCESS_HOOK_CLIENT;
|
|
}
|
|
if (bootstrap_arg_is(argv[arg], "config")) {
|
|
return bootstrap_has_help_after(argc, argv, arg + 1) ? CBM_DAEMON_PROCESS_STATELESS
|
|
: CBM_DAEMON_PROCESS_LOCAL_CLI;
|
|
}
|
|
/* Placed after the `cli` check on purpose: `cbm cli search "daemon
|
|
* start"` is opaque tool input and must stay LOCAL_CLI. */
|
|
if (bootstrap_arg_is(argv[arg], "daemon")) {
|
|
return bootstrap_has_help_after(argc, argv, arg + 1) ? CBM_DAEMON_PROCESS_STATELESS
|
|
: CBM_DAEMON_PROCESS_DAEMON_CTL;
|
|
}
|
|
if (bootstrap_arg_is(argv[arg], "--version") || bootstrap_arg_is(argv[arg], "--help") ||
|
|
bootstrap_arg_is(argv[arg], "-h")) {
|
|
return CBM_DAEMON_PROCESS_STATELESS;
|
|
}
|
|
for (size_t command = 0;
|
|
command < sizeof(stateless_commands) / sizeof(stateless_commands[0]); command++) {
|
|
if (bootstrap_arg_is(argv[arg], stateless_commands[command])) {
|
|
return CBM_DAEMON_PROCESS_STATELESS;
|
|
}
|
|
}
|
|
}
|
|
return CBM_DAEMON_PROCESS_MCP_CLIENT;
|
|
}
|
|
|
|
bool cbm_daemon_process_role_requires_client(cbm_daemon_process_role_t role) {
|
|
return role == CBM_DAEMON_PROCESS_MCP_CLIENT || role == CBM_DAEMON_PROCESS_HOOK_CLIENT;
|
|
}
|
|
|
|
/* #1574/#1621: the rendezvous directory is created under %LOCALAPPDATA%
|
|
* (Windows) or /tmp — /private/tmp on macOS — and that ancestry is not always
|
|
* acceptable to the private-directory walk. A profile that carries a
|
|
* mutation-granting ACE for an untrusted identity (an AppContainer capability
|
|
* SID, for instance) fails it, and the binary then cannot start at all: every
|
|
* command needs this endpoint, `config list` included, so the operator cannot
|
|
* even reconfigure their way out. The only relocation hook was
|
|
* CBM_TEST_DAEMON_RUNTIME_PARENT, compiled out unless CBM_ENABLE_TEST_SEAMS is
|
|
* defined, so a test build started while the shipped build did not. CBM_CACHE_DIR
|
|
* does not help either — it moves the cache, never the rendezvous.
|
|
*
|
|
* CBM_RUNTIME_DIR does NOT relax the check. The directory it names goes through
|
|
* exactly the same validation as the default; the operator only chooses an
|
|
* ancestry that passes, and a value that fails is refused rather than ignored.
|
|
* cbm_safe_getenv never truncates: a value too long for the buffer is reported
|
|
* as absent, so no half a path can ever become a runtime parent. */
|
|
static const char *bootstrap_runtime_parent_override(char *buffer, size_t capacity) {
|
|
const char *value = cbm_safe_getenv("CBM_RUNTIME_DIR", buffer, capacity, NULL);
|
|
return value && value[0] != '\0' ? value : NULL;
|
|
}
|
|
|
|
/* An explicit parent keeps precedence: it carries the compile-time test seam
|
|
* and the lifecycle guards' isolated namespace. NULL means the account-wide
|
|
* default rendezvous. */
|
|
static const char *bootstrap_runtime_parent_resolve(const char *runtime_parent, char *buffer,
|
|
size_t capacity) {
|
|
return runtime_parent ? runtime_parent : bootstrap_runtime_parent_override(buffer, capacity);
|
|
}
|
|
|
|
#ifdef CBM_ENABLE_TEST_SEAMS
|
|
/* Armed once by a test runner before any suite starts its threads. */
|
|
static bool g_bootstrap_default_runtime_forbidden = false;
|
|
|
|
void cbm_daemon_bootstrap_forbid_default_runtime_for_test(bool forbid) {
|
|
g_bootstrap_default_runtime_forbidden = forbid;
|
|
}
|
|
|
|
bool cbm_daemon_bootstrap_default_runtime_refused_for_test(const char *runtime_parent) {
|
|
char override_parent[BOOTSTRAP_PATH_CAP];
|
|
return g_bootstrap_default_runtime_forbidden &&
|
|
!bootstrap_runtime_parent_resolve(runtime_parent, override_parent,
|
|
sizeof(override_parent));
|
|
}
|
|
#endif
|
|
|
|
cbm_daemon_ipc_endpoint_t *cbm_daemon_bootstrap_endpoint_new(const char *runtime_parent) {
|
|
char key[CBM_DAEMON_KEY_SIZE];
|
|
if (!cbm_daemon_rendezvous_key(key)) {
|
|
return NULL;
|
|
}
|
|
/* The override is resolved HERE, the one function every product endpoint
|
|
* goes through (daemon, MCP client, local CLI, index worker, activation),
|
|
* so no call site can silently keep the default. */
|
|
char override_parent[BOOTSTRAP_PATH_CAP];
|
|
const char *parent =
|
|
bootstrap_runtime_parent_resolve(runtime_parent, override_parent, sizeof(override_parent));
|
|
#ifdef CBM_ENABLE_TEST_SEAMS
|
|
if (!parent && g_bootstrap_default_runtime_forbidden) {
|
|
(void)fprintf(stderr, "codebase-memory-mcp: test run refused the default daemon runtime "
|
|
"directory (no explicit parent and CBM_RUNTIME_DIR unset): the "
|
|
"developer's live daemon listens there\n");
|
|
return NULL;
|
|
}
|
|
#endif
|
|
return cbm_daemon_ipc_endpoint_new(key, parent);
|
|
}
|
|
|
|
bool cbm_daemon_bootstrap_launch_spec_init(const char *executable_path,
|
|
cbm_daemon_bootstrap_launch_spec_t *spec_out) {
|
|
if (!executable_path || executable_path[0] == '\0' || !spec_out) {
|
|
return false;
|
|
}
|
|
memset(spec_out, 0, sizeof(*spec_out));
|
|
spec_out->executable_path = executable_path;
|
|
spec_out->argv[0] = executable_path;
|
|
spec_out->argv[1] = CBM_DAEMON_INTERNAL_ARG;
|
|
spec_out->argc = 2U;
|
|
spec_out->detached = true;
|
|
spec_out->inherit_standard_handles = false;
|
|
spec_out->use_shell = false;
|
|
return true;
|
|
}
|
|
|
|
bool cbm_daemon_bootstrap_launch_spec_init_permanent(const char *executable_path,
|
|
cbm_daemon_bootstrap_launch_spec_t *spec_out) {
|
|
if (!cbm_daemon_bootstrap_launch_spec_init(executable_path, spec_out)) {
|
|
return false;
|
|
}
|
|
spec_out->argv[2] = CBM_DAEMON_PERMANENT_ARG;
|
|
spec_out->argc = 3U;
|
|
return true;
|
|
}
|
|
|
|
static uint64_t bootstrap_deadline_after(uint32_t timeout_ms) {
|
|
uint64_t now = cbm_now_ms();
|
|
return UINT64_MAX - now < timeout_ms ? UINT64_MAX : now + (uint64_t)timeout_ms;
|
|
}
|
|
|
|
static _Noreturn void bootstrap_cleanup_fail_stop(const char *component) {
|
|
(void)fprintf(stderr,
|
|
"codebase-memory-mcp: coordination cleanup failed (%s); "
|
|
"terminating so the OS releases retained claims\n",
|
|
component ? component : "unknown");
|
|
(void)fflush(stderr);
|
|
#ifdef _WIN32
|
|
(void)TerminateProcess(GetCurrentProcess(), EXIT_FAILURE);
|
|
abort();
|
|
#else
|
|
_exit(EXIT_FAILURE);
|
|
#endif
|
|
}
|
|
|
|
static const char BOOTSTRAP_START_FAILURE_LOG_NAME[] = "cbm-daemon.start-failures.log";
|
|
|
|
bool cbm_daemon_bootstrap_log_directory(char *out, size_t capacity) {
|
|
if (!out || capacity == 0) {
|
|
return false;
|
|
}
|
|
out[0] = '\0';
|
|
const char *cache = cbm_resolve_cache_dir();
|
|
if (!cache || !cache[0]) {
|
|
return false;
|
|
}
|
|
int written = snprintf(out, capacity, "%s/logs", cache);
|
|
if (written <= 0 || (size_t)written >= capacity) {
|
|
out[0] = '\0';
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* One record field: the line format is tab-separated, so a tab or newline in
|
|
* a value (a path, in theory) becomes '?' rather than a parser ambiguity. */
|
|
static void bootstrap_record_field(char *out, size_t capacity, const char *value) {
|
|
size_t used = 0;
|
|
for (const char *cursor = value ? value : ""; *cursor && used + 1 < capacity; cursor++) {
|
|
unsigned char ch = (unsigned char)*cursor;
|
|
out[used++] = ch == '\t' || ch == '\n' || ch == '\r' ? '?' : (char)ch;
|
|
}
|
|
out[used] = '\0';
|
|
}
|
|
|
|
static uint64_t bootstrap_current_pid(void) {
|
|
#ifdef _WIN32
|
|
return (uint64_t)GetCurrentProcessId();
|
|
#else
|
|
return (uint64_t)getpid();
|
|
#endif
|
|
}
|
|
|
|
bool cbm_daemon_bootstrap_start_failure_record(const char *log_directory,
|
|
const cbm_daemon_ipc_endpoint_t *endpoint,
|
|
const char *component) {
|
|
if (!log_directory || !log_directory[0] || !endpoint || !component || !component[0]) {
|
|
return false;
|
|
}
|
|
const char *runtime_dir = cbm_daemon_ipc_endpoint_runtime_dir(endpoint);
|
|
if (!runtime_dir || !runtime_dir[0]) {
|
|
return false;
|
|
}
|
|
cbm_daemon_ipc_listen_failure_t detail;
|
|
if (!cbm_daemon_ipc_listen_failure_detail(&detail)) {
|
|
memset(&detail, 0, sizeof(detail));
|
|
}
|
|
char safe_runtime_dir[BOOTSTRAP_PATH_CAP];
|
|
char safe_component[CBM_DAEMON_BOOTSTRAP_COMPONENT_CAP];
|
|
char safe_stage[CBM_DAEMON_IPC_LISTEN_FAILURE_STAGE_CAP];
|
|
char safe_path[CBM_DAEMON_IPC_LISTEN_FAILURE_PATH_CAP];
|
|
bootstrap_record_field(safe_runtime_dir, sizeof(safe_runtime_dir), runtime_dir);
|
|
bootstrap_record_field(safe_component, sizeof(safe_component), component);
|
|
bootstrap_record_field(safe_stage, sizeof(safe_stage), detail.stage);
|
|
bootstrap_record_field(safe_path, sizeof(safe_path), detail.path);
|
|
FILE *stream = cbm_daemon_ipc_private_log_open(log_directory, BOOTSTRAP_START_FAILURE_LOG_NAME,
|
|
BOOTSTRAP_START_FAILURE_LOG_CAP);
|
|
if (!stream) {
|
|
return false;
|
|
}
|
|
int written =
|
|
fprintf(stream, "v1\t%llu\t%llu\t%s\t%s\t%s\t%d\t%s\n", (unsigned long long)time(NULL),
|
|
(unsigned long long)bootstrap_current_pid(), safe_runtime_dir, safe_component,
|
|
safe_stage, detail.errno_value, safe_path);
|
|
bool ok = written > 0 && fflush(stream) == 0;
|
|
if (fclose(stream) != 0) {
|
|
ok = false;
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
/* Parse one "v1\tts\tpid\truntime_dir\tcomponent\tstage\terrno\tpath" line
|
|
* (line is NUL-terminated, newline already stripped, and is modified). */
|
|
static bool bootstrap_start_failure_parse(char *line, char **runtime_dir_out,
|
|
cbm_daemon_bootstrap_start_failure_t *out) {
|
|
char *fields[8];
|
|
size_t count = 0;
|
|
char *cursor = line;
|
|
while (count < 8) {
|
|
fields[count++] = cursor;
|
|
char *tab = strchr(cursor, '\t');
|
|
if (!tab) {
|
|
break;
|
|
}
|
|
*tab = '\0';
|
|
cursor = tab + 1;
|
|
}
|
|
if (count != 8 || strcmp(fields[0], "v1") != 0) {
|
|
return false;
|
|
}
|
|
memset(out, 0, sizeof(*out));
|
|
out->recorded_at_s = strtoull(fields[1], NULL, 10);
|
|
out->pid = strtoull(fields[2], NULL, 10);
|
|
*runtime_dir_out = fields[3];
|
|
(void)snprintf(out->component, sizeof(out->component), "%s", fields[4]);
|
|
(void)snprintf(out->stage, sizeof(out->stage), "%s", fields[5]);
|
|
out->errno_value = (int)strtol(fields[6], NULL, 10);
|
|
(void)snprintf(out->path, sizeof(out->path), "%s", fields[7]);
|
|
return true;
|
|
}
|
|
|
|
static FILE *bootstrap_start_failure_open(const char *log_directory, int *status_out) {
|
|
char path[BOOTSTRAP_PATH_CAP];
|
|
int written =
|
|
snprintf(path, sizeof(path), "%s/%s", log_directory, BOOTSTRAP_START_FAILURE_LOG_NAME);
|
|
if (written <= 0 || written >= (int)sizeof(path)) {
|
|
*status_out = -1;
|
|
return NULL;
|
|
}
|
|
#ifndef _WIN32
|
|
struct stat status;
|
|
if (lstat(path, &status) != 0) {
|
|
*status_out = errno == ENOENT ? 0 : -1;
|
|
return NULL;
|
|
}
|
|
if (!S_ISREG(status.st_mode) || status.st_uid != geteuid() || status.st_nlink != 1) {
|
|
*status_out = -1;
|
|
return NULL;
|
|
}
|
|
#endif
|
|
/* The directory is the trust boundary (owner-only, no symlinked
|
|
* ancestry); the same validation guards cbm-daemon.log itself. */
|
|
if (!cbm_daemon_ipc_private_directory_secure(log_directory)) {
|
|
*status_out = -1;
|
|
return NULL;
|
|
}
|
|
FILE *file = cbm_fopen(path, "rb");
|
|
if (!file) {
|
|
*status_out = errno == ENOENT ? 0 : -1;
|
|
return NULL;
|
|
}
|
|
*status_out = 1;
|
|
return file;
|
|
}
|
|
|
|
int cbm_daemon_bootstrap_start_failure_read(const char *log_directory,
|
|
const cbm_daemon_ipc_endpoint_t *endpoint,
|
|
uint64_t not_before_s,
|
|
cbm_daemon_bootstrap_start_failure_t *out_failure) {
|
|
if (!out_failure) {
|
|
return -1;
|
|
}
|
|
memset(out_failure, 0, sizeof(*out_failure));
|
|
if (!log_directory || !log_directory[0] || !endpoint) {
|
|
return -1;
|
|
}
|
|
const char *runtime_dir = cbm_daemon_ipc_endpoint_runtime_dir(endpoint);
|
|
if (!runtime_dir || !runtime_dir[0]) {
|
|
return -1;
|
|
}
|
|
int status = 0;
|
|
FILE *file = bootstrap_start_failure_open(log_directory, &status);
|
|
if (!file) {
|
|
return status;
|
|
}
|
|
char *buffer = malloc(BOOTSTRAP_START_FAILURE_LOG_CAP + 1U);
|
|
if (!buffer) {
|
|
(void)fclose(file);
|
|
return -1;
|
|
}
|
|
size_t used = fread(buffer, 1, BOOTSTRAP_START_FAILURE_LOG_CAP, file);
|
|
bool read_ok = !ferror(file);
|
|
(void)fclose(file);
|
|
buffer[used] = '\0';
|
|
int found = 0;
|
|
if (read_ok) {
|
|
char *line = buffer;
|
|
while (line && *line) {
|
|
char *end = strchr(line, '\n');
|
|
if (!end) {
|
|
break; /* a partial trailing line is a record still being written */
|
|
}
|
|
*end = '\0';
|
|
char *record_runtime_dir = NULL;
|
|
cbm_daemon_bootstrap_start_failure_t candidate;
|
|
if (bootstrap_start_failure_parse(line, &record_runtime_dir, &candidate) &&
|
|
strcmp(record_runtime_dir, runtime_dir) == 0 &&
|
|
candidate.recorded_at_s >= not_before_s) {
|
|
*out_failure = candidate; /* later lines are newer */
|
|
found = 1;
|
|
}
|
|
line = end + 1;
|
|
}
|
|
} else {
|
|
found = -1;
|
|
}
|
|
free(buffer);
|
|
return found;
|
|
}
|
|
|
|
void cbm_daemon_bootstrap_start_failure_format(const cbm_daemon_bootstrap_start_failure_t *failure,
|
|
const char *log_directory, char *out,
|
|
size_t capacity) {
|
|
if (!out || capacity == 0) {
|
|
return;
|
|
}
|
|
if (!failure) {
|
|
out[0] = '\0';
|
|
return;
|
|
}
|
|
const char *what = failure->stage[0] ? failure->stage
|
|
: failure->component[0] ? failure->component
|
|
: "startup";
|
|
char where[BOOTSTRAP_PATH_CAP];
|
|
if (log_directory && log_directory[0]) {
|
|
(void)snprintf(where, sizeof(where), "see %s/cbm-daemon.log", log_directory);
|
|
} else {
|
|
(void)snprintf(where, sizeof(where), "see the daemon log");
|
|
}
|
|
if (failure->errno_value != 0 && failure->path[0]) {
|
|
(void)snprintf(out, capacity,
|
|
"CBM daemon failed to start: %s failed with %s (%s) at %s; %s", what,
|
|
cbm_errno_name(failure->errno_value), strerror(failure->errno_value),
|
|
failure->path, where);
|
|
} else if (failure->errno_value != 0) {
|
|
(void)snprintf(out, capacity, "CBM daemon failed to start: %s failed with %s (%s); %s",
|
|
what, cbm_errno_name(failure->errno_value), strerror(failure->errno_value),
|
|
where);
|
|
} else if (failure->path[0]) {
|
|
(void)snprintf(out, capacity, "CBM daemon failed to start: %s failed at %s; %s", what,
|
|
failure->path, where);
|
|
} else {
|
|
(void)snprintf(out, capacity, "CBM daemon failed to start: %s failed; %s", what, where);
|
|
}
|
|
}
|
|
|
|
/* After this attempt spawned a daemon: did that daemon (or a sibling attempt
|
|
* moments earlier) record a start failure? A found record ends the wait with
|
|
* its cause -- respawning a deterministically failing daemon until the
|
|
* deadline only produced "active or starting" (#1828). */
|
|
static bool bootstrap_start_failure_detected(const cbm_daemon_bootstrap_config_t *config,
|
|
const cbm_daemon_bootstrap_ops_t *ops,
|
|
uint64_t spawn_wall_s, bool force,
|
|
uint64_t *next_check_ms,
|
|
cbm_daemon_bootstrap_result_t *result_out) {
|
|
if (spawn_wall_s == 0 || !ops->start_failure_probe) {
|
|
return false;
|
|
}
|
|
uint64_t now_ms = cbm_now_ms();
|
|
if (!force && now_ms < *next_check_ms) {
|
|
return false;
|
|
}
|
|
*next_check_ms = now_ms + BOOTSTRAP_START_FAILURE_CHECK_MS;
|
|
uint64_t not_before_s = spawn_wall_s > BOOTSTRAP_START_FAILURE_SKEW_S
|
|
? spawn_wall_s - BOOTSTRAP_START_FAILURE_SKEW_S
|
|
: 0;
|
|
cbm_daemon_bootstrap_start_failure_t failure;
|
|
if (ops->start_failure_probe(ops->context, config->endpoint, not_before_s, &failure) != 1) {
|
|
return false;
|
|
}
|
|
char logs[BOOTSTRAP_PATH_CAP];
|
|
if (!cbm_daemon_bootstrap_log_directory(logs, sizeof(logs))) {
|
|
logs[0] = '\0';
|
|
}
|
|
cbm_daemon_bootstrap_start_failure_format(&failure, logs, result_out->message,
|
|
sizeof(result_out->message));
|
|
return true;
|
|
}
|
|
|
|
static void bootstrap_pause(uint64_t deadline) {
|
|
uint64_t now = cbm_now_ms();
|
|
if (now >= deadline) {
|
|
return;
|
|
}
|
|
uint64_t remaining_ms = deadline - now;
|
|
struct timespec pause = {
|
|
.tv_sec = 0,
|
|
.tv_nsec = remaining_ms > 1 ? BOOTSTRAP_RETRY_NS : (long)(remaining_ms * 1000000ULL),
|
|
};
|
|
(void)cbm_nanosleep(&pause, NULL);
|
|
}
|
|
|
|
static void bootstrap_startup_lock_release_complete(const cbm_daemon_bootstrap_ops_t *ops,
|
|
cbm_daemon_bootstrap_lock_t *lock_io) {
|
|
uint64_t deadline = bootstrap_deadline_after(BOOTSTRAP_COORDINATION_CLEANUP_MS);
|
|
while (ops && ops->startup_lock_release && lock_io && *lock_io) {
|
|
(void)ops->startup_lock_release(ops->context, lock_io);
|
|
if (!*lock_io) {
|
|
return;
|
|
}
|
|
if (cbm_now_ms() >= deadline) {
|
|
bootstrap_cleanup_fail_stop("startup_lock_cleanup");
|
|
}
|
|
cbm_usleep(1000);
|
|
}
|
|
}
|
|
|
|
static void bootstrap_result_reset(cbm_daemon_bootstrap_result_t *result,
|
|
cbm_daemon_process_role_t role) {
|
|
memset(result, 0, sizeof(*result));
|
|
result->status = cbm_daemon_process_role_requires_client(role) ? CBM_DAEMON_BOOTSTRAP_FAILED
|
|
: CBM_DAEMON_BOOTSTRAP_BYPASSED;
|
|
}
|
|
|
|
static cbm_daemon_bootstrap_status_t bootstrap_finish_probe(
|
|
cbm_daemon_bootstrap_probe_status_t probe, cbm_daemon_runtime_client_t *client,
|
|
const cbm_daemon_runtime_connect_result_t *connect_result,
|
|
const cbm_daemon_bootstrap_ops_t *ops, cbm_daemon_bootstrap_result_t *result) {
|
|
if (connect_result) {
|
|
result->connect_result = *connect_result;
|
|
}
|
|
result->client = client;
|
|
if (probe == CBM_DAEMON_BOOTSTRAP_PROBE_CONNECTED && client) {
|
|
result->status = CBM_DAEMON_BOOTSTRAP_CONNECTED;
|
|
return result->status;
|
|
}
|
|
result->client = NULL;
|
|
if (probe == CBM_DAEMON_BOOTSTRAP_PROBE_CONFLICT) {
|
|
result->status = CBM_DAEMON_BOOTSTRAP_CONFLICT;
|
|
(void)snprintf(result->message, sizeof(result->message), "%s",
|
|
connect_result && connect_result->message[0]
|
|
? connect_result->message
|
|
: "CBM could not start because a conflicting CBM process is active; "
|
|
"close all CBM sessions and commands, then retry. If a permanent "
|
|
"daemon from another build is running, `codebase-memory-mcp daemon "
|
|
"stop` retires it");
|
|
if (ops->visible_diagnostic) {
|
|
ops->visible_diagnostic(ops->context, result->message);
|
|
}
|
|
return result->status;
|
|
}
|
|
return CBM_DAEMON_BOOTSTRAP_FAILED;
|
|
}
|
|
|
|
static cbm_daemon_bootstrap_probe_status_t bootstrap_probe(
|
|
const cbm_daemon_bootstrap_config_t *config, const cbm_daemon_bootstrap_ops_t *ops,
|
|
cbm_daemon_runtime_client_t **client_out, cbm_daemon_runtime_connect_result_t *connect_result,
|
|
uint64_t *muted_holder_pid_io) {
|
|
memset(connect_result, 0, sizeof(*connect_result));
|
|
*client_out = NULL;
|
|
cbm_daemon_bootstrap_probe_status_t status =
|
|
ops->probe(ops->context, config->endpoint, config->identity, config->connect_timeout_ms,
|
|
client_out, connect_result);
|
|
/* Sticky across probes: a later fast-path probe never attempts a connect
|
|
* and reports no holder, but a mute holder seen once must survive into
|
|
* the final timeout diagnostic. */
|
|
if (muted_holder_pid_io && connect_result->muted_endpoint_holder_pid != 0) {
|
|
*muted_holder_pid_io = connect_result->muted_endpoint_holder_pid;
|
|
}
|
|
return status;
|
|
}
|
|
|
|
static bool bootstrap_probe_is_finishable(cbm_daemon_bootstrap_probe_status_t probe) {
|
|
return probe == CBM_DAEMON_BOOTSTRAP_PROBE_CONNECTED ||
|
|
probe == CBM_DAEMON_BOOTSTRAP_PROBE_CONFLICT;
|
|
}
|
|
|
|
static bool bootstrap_probe_is_waitable(cbm_daemon_bootstrap_probe_status_t probe) {
|
|
return probe == CBM_DAEMON_BOOTSTRAP_PROBE_UNAVAILABLE ||
|
|
probe == CBM_DAEMON_BOOTSTRAP_PROBE_RESERVED ||
|
|
probe == CBM_DAEMON_BOOTSTRAP_PROBE_TERMINAL;
|
|
}
|
|
|
|
static bool bootstrap_config_valid(const cbm_daemon_bootstrap_config_t *config,
|
|
const cbm_daemon_bootstrap_ops_t *ops) {
|
|
return config && ops && config->endpoint && config->identity && config->executable_path &&
|
|
config->executable_path[0] && config->connect_timeout_ms > 0 &&
|
|
config->startup_timeout_ms > 0 && ops->cohort_acquire && ops->cohort_release &&
|
|
ops->probe && ops->startup_lock_try_acquire && ops->startup_lock_prepare_handoff &&
|
|
ops->startup_lock_release && ops->spawn_daemon;
|
|
}
|
|
|
|
cbm_daemon_bootstrap_status_t cbm_daemon_bootstrap_execute_with_ops(
|
|
const cbm_daemon_bootstrap_config_t *config, const cbm_daemon_bootstrap_ops_t *ops,
|
|
cbm_daemon_bootstrap_result_t *result_out) {
|
|
if (!result_out) {
|
|
return CBM_DAEMON_BOOTSTRAP_FAILED;
|
|
}
|
|
cbm_daemon_process_role_t role = config ? config->role : CBM_DAEMON_PROCESS_INVALID;
|
|
bootstrap_result_reset(result_out, role);
|
|
if (!cbm_daemon_process_role_requires_client(role)) {
|
|
return role == CBM_DAEMON_PROCESS_INVALID ? CBM_DAEMON_BOOTSTRAP_FAILED
|
|
: CBM_DAEMON_BOOTSTRAP_BYPASSED;
|
|
}
|
|
if (!bootstrap_config_valid(config, ops)) {
|
|
return CBM_DAEMON_BOOTSTRAP_FAILED;
|
|
}
|
|
|
|
uint64_t deadline = bootstrap_deadline_after(config->startup_timeout_ms);
|
|
cbm_daemon_bootstrap_cohort_t cohort = NULL;
|
|
cbm_daemon_conflict_t cohort_conflict;
|
|
cbm_version_cohort_status_t cohort_status = ops->cohort_acquire(
|
|
ops->context, config->endpoint, config->identity, deadline, &cohort, &cohort_conflict);
|
|
if (cohort_status != CBM_VERSION_COHORT_OK) {
|
|
result_out->status = cohort_status == CBM_VERSION_COHORT_CONFLICT
|
|
? CBM_DAEMON_BOOTSTRAP_CONFLICT
|
|
: CBM_DAEMON_BOOTSTRAP_FAILED;
|
|
bool formatted = cohort_status == CBM_VERSION_COHORT_CONFLICT &&
|
|
cbm_daemon_conflict_format(&cohort_conflict, result_out->message,
|
|
sizeof(result_out->message));
|
|
if (!formatted) {
|
|
const char *reason = cohort_status == CBM_VERSION_COHORT_BUSY
|
|
? "another CBM activation is in progress"
|
|
: "exact-build admission could not be verified";
|
|
(void)snprintf(result_out->message, sizeof(result_out->message),
|
|
"CBM daemon could not start: %s", reason);
|
|
}
|
|
if (ops->visible_diagnostic) {
|
|
ops->visible_diagnostic(ops->context, result_out->message);
|
|
}
|
|
if (cohort) {
|
|
ops->cohort_release(ops->context, cohort);
|
|
}
|
|
return result_out->status;
|
|
}
|
|
|
|
cbm_daemon_runtime_client_t *client = NULL;
|
|
cbm_daemon_runtime_connect_result_t connect_result;
|
|
uint64_t muted_holder_pid = 0;
|
|
cbm_daemon_bootstrap_probe_status_t probe =
|
|
bootstrap_probe(config, ops, &client, &connect_result, &muted_holder_pid);
|
|
if (bootstrap_probe_is_finishable(probe)) {
|
|
cbm_daemon_bootstrap_status_t status =
|
|
bootstrap_finish_probe(probe, client, &connect_result, ops, result_out);
|
|
ops->cohort_release(ops->context, cohort);
|
|
return status;
|
|
}
|
|
if (!bootstrap_probe_is_waitable(probe)) {
|
|
probe = CBM_DAEMON_BOOTSTRAP_PROBE_ERROR;
|
|
}
|
|
|
|
cbm_daemon_bootstrap_lock_t startup_lock = NULL;
|
|
bool lock_acquired = false;
|
|
bool generation_observed = probe == CBM_DAEMON_BOOTSTRAP_PROBE_RESERVED ||
|
|
probe == CBM_DAEMON_BOOTSTRAP_PROBE_TERMINAL;
|
|
uint64_t spawn_wall_s = 0;
|
|
uint64_t next_failure_check_ms = 0;
|
|
bool start_failed = false;
|
|
while (cbm_now_ms() < deadline) {
|
|
if (bootstrap_start_failure_detected(config, ops, spawn_wall_s, false,
|
|
&next_failure_check_ms, result_out)) {
|
|
start_failed = true;
|
|
break;
|
|
}
|
|
if (!bootstrap_probe_is_waitable(probe)) {
|
|
break;
|
|
}
|
|
if (probe == CBM_DAEMON_BOOTSTRAP_PROBE_RESERVED ||
|
|
probe == CBM_DAEMON_BOOTSTRAP_PROBE_TERMINAL) {
|
|
/* A live or stopping generation owns the transition for now. Its
|
|
* disappearance is not sticky: after observing true absence, the
|
|
* same bootstrap attempt may serialize and become the next first
|
|
* client. The startup lock and the re-probe below prevent two
|
|
* replacements from being launched. */
|
|
generation_observed = true;
|
|
bootstrap_pause(deadline);
|
|
probe = bootstrap_probe(config, ops, &client, &connect_result, &muted_holder_pid);
|
|
continue;
|
|
}
|
|
|
|
int lock_status =
|
|
ops->startup_lock_try_acquire(ops->context, config->endpoint, &startup_lock);
|
|
if (lock_status < 0) {
|
|
probe = CBM_DAEMON_BOOTSTRAP_PROBE_ERROR;
|
|
break;
|
|
}
|
|
if (lock_status == 0) {
|
|
bootstrap_pause(deadline);
|
|
probe = bootstrap_probe(config, ops, &client, &connect_result, &muted_holder_pid);
|
|
continue;
|
|
}
|
|
if (lock_status != 1 || !startup_lock) {
|
|
probe = CBM_DAEMON_BOOTSTRAP_PROBE_ERROR;
|
|
break;
|
|
}
|
|
|
|
lock_acquired = true;
|
|
probe = bootstrap_probe(config, ops, &client, &connect_result, &muted_holder_pid);
|
|
if (probe == CBM_DAEMON_BOOTSTRAP_PROBE_RESERVED ||
|
|
probe == CBM_DAEMON_BOOTSTRAP_PROBE_TERMINAL) {
|
|
generation_observed = true;
|
|
bootstrap_startup_lock_release_complete(ops, &startup_lock);
|
|
lock_acquired = false;
|
|
continue;
|
|
}
|
|
if (bootstrap_probe_is_finishable(probe) || probe == CBM_DAEMON_BOOTSTRAP_PROBE_ERROR) {
|
|
break;
|
|
}
|
|
if (probe != CBM_DAEMON_BOOTSTRAP_PROBE_UNAVAILABLE) {
|
|
probe = CBM_DAEMON_BOOTSTRAP_PROBE_ERROR;
|
|
break;
|
|
}
|
|
|
|
/* Never relaunch a daemon whose predecessor already recorded why it
|
|
* could not start; the record is written before that daemon lets go
|
|
* of its lifetime reservation, so this unconditional read sees it. */
|
|
if (bootstrap_start_failure_detected(config, ops, spawn_wall_s, true,
|
|
&next_failure_check_ms, result_out)) {
|
|
start_failed = true;
|
|
break;
|
|
}
|
|
cbm_daemon_bootstrap_launch_spec_t spec;
|
|
bool spec_ready =
|
|
config->spawn_permanent
|
|
? cbm_daemon_bootstrap_launch_spec_init_permanent(config->executable_path, &spec)
|
|
: cbm_daemon_bootstrap_launch_spec_init(config->executable_path, &spec);
|
|
spawn_wall_s = (uint64_t)time(NULL);
|
|
if (!spec_ready || !ops->startup_lock_prepare_handoff(ops->context, startup_lock) ||
|
|
!ops->spawn_daemon(ops->context, &spec)) {
|
|
probe = CBM_DAEMON_BOOTSTRAP_PROBE_ERROR;
|
|
break;
|
|
}
|
|
result_out->daemon_spawned = true;
|
|
|
|
/* Keep startup ownership only until the child becomes observable.
|
|
* Windows participant teardown reacquires the startup transition;
|
|
* retaining it while probing an observable generation can deadlock the
|
|
* bootstrap against a daemon that is trying to cleanly stand down. */
|
|
do {
|
|
bootstrap_pause(deadline);
|
|
probe = bootstrap_probe(config, ops, &client, &connect_result, &muted_holder_pid);
|
|
if (probe == CBM_DAEMON_BOOTSTRAP_PROBE_RESERVED ||
|
|
probe == CBM_DAEMON_BOOTSTRAP_PROBE_TERMINAL) {
|
|
generation_observed = true;
|
|
bootstrap_startup_lock_release_complete(ops, &startup_lock);
|
|
lock_acquired = false;
|
|
break;
|
|
}
|
|
if (!bootstrap_probe_is_waitable(probe)) {
|
|
break;
|
|
}
|
|
if (bootstrap_start_failure_detected(config, ops, spawn_wall_s, false,
|
|
&next_failure_check_ms, result_out)) {
|
|
start_failed = true;
|
|
break;
|
|
}
|
|
} while (cbm_now_ms() < deadline);
|
|
if (start_failed) {
|
|
break;
|
|
}
|
|
if (!lock_acquired) {
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (lock_acquired) {
|
|
bootstrap_startup_lock_release_complete(ops, &startup_lock);
|
|
}
|
|
if (bootstrap_probe_is_finishable(probe)) {
|
|
cbm_daemon_bootstrap_status_t status =
|
|
bootstrap_finish_probe(probe, client, &connect_result, ops, result_out);
|
|
ops->cohort_release(ops->context, cohort);
|
|
return status;
|
|
}
|
|
|
|
result_out->status = CBM_DAEMON_BOOTSTRAP_FAILED;
|
|
if (start_failed) {
|
|
/* result_out->message already names the recorded cause. */
|
|
} else if (muted_holder_pid != 0) {
|
|
/* The one diagnostic the 2026-08-29 zombie recovery had to assemble by
|
|
* hand from process, pipe, and log correlation: name the pid that
|
|
* holds the endpoint without answering, and say what to do with it. */
|
|
(void)snprintf(result_out->message, sizeof(result_out->message),
|
|
"CBM daemon endpoint is held by pid %llu but that process answered no "
|
|
"rendezvous within %u ms; the daemon runtime is likely dead — stop that "
|
|
"process, then retry",
|
|
(unsigned long long)muted_holder_pid, config->startup_timeout_ms);
|
|
} else if (generation_observed) {
|
|
(void)snprintf(result_out->message, sizeof(result_out->message),
|
|
"CBM daemon is active or starting but could not accept this client "
|
|
"within %u ms",
|
|
config->startup_timeout_ms);
|
|
} else {
|
|
(void)snprintf(result_out->message, sizeof(result_out->message),
|
|
"CBM daemon could not start within %u ms", config->startup_timeout_ms);
|
|
}
|
|
if (ops->visible_diagnostic) {
|
|
ops->visible_diagnostic(ops->context, result_out->message);
|
|
}
|
|
ops->cohort_release(ops->context, cohort);
|
|
return result_out->status;
|
|
}
|
|
|
|
cbm_daemon_bootstrap_probe_status_t cbm_daemon_bootstrap_classify_failed_connect(
|
|
const cbm_daemon_runtime_connect_result_t *connect_result, int lifetime_status) {
|
|
if (!connect_result) {
|
|
return CBM_DAEMON_BOOTSTRAP_PROBE_ERROR;
|
|
}
|
|
if (connect_result->status == CBM_DAEMON_RUNTIME_CONNECT_CONFLICT) {
|
|
return CBM_DAEMON_BOOTSTRAP_PROBE_CONFLICT;
|
|
}
|
|
if (connect_result->status == CBM_DAEMON_RUNTIME_CONNECT_REJECTED) {
|
|
if (strstr(connect_result->message, "stopping") ||
|
|
strstr(connect_result->message, "shutting down")) {
|
|
return CBM_DAEMON_BOOTSTRAP_PROBE_TERMINAL;
|
|
}
|
|
/* Capacity, admission, and other protocol-level rejections prove an
|
|
* existing generation answered. Never reinterpret them as absence. */
|
|
return CBM_DAEMON_BOOTSTRAP_PROBE_RESERVED;
|
|
}
|
|
if (connect_result->status == CBM_DAEMON_RUNTIME_CONNECT_ERROR &&
|
|
connect_result->muted_endpoint_holder_pid != 0) {
|
|
/* The transport connected to a live process that answered nothing.
|
|
* That endpoint is OWNED, whatever the advisory locks read right now
|
|
* (a wedged generation can hold pipes while its lock files churn).
|
|
* Classifying this as absence made the 2026-08-29 zombie invisible:
|
|
* the starter spawned doomed competitors for 30 s and then reported a
|
|
* bare timeout with no holder named. */
|
|
return CBM_DAEMON_BOOTSTRAP_PROBE_RESERVED;
|
|
}
|
|
if (lifetime_status == 1) {
|
|
return CBM_DAEMON_BOOTSTRAP_PROBE_RESERVED;
|
|
}
|
|
if (lifetime_status != 0) {
|
|
return CBM_DAEMON_BOOTSTRAP_PROBE_ERROR;
|
|
}
|
|
return connect_result->status == CBM_DAEMON_RUNTIME_CONNECT_ERROR
|
|
? CBM_DAEMON_BOOTSTRAP_PROBE_UNAVAILABLE
|
|
: CBM_DAEMON_BOOTSTRAP_PROBE_ERROR;
|
|
}
|
|
|
|
typedef struct bootstrap_production_cohort {
|
|
cbm_version_cohort_manager_t *manager;
|
|
cbm_version_cohort_lease_t *lease;
|
|
} bootstrap_production_cohort_t;
|
|
|
|
typedef struct {
|
|
bootstrap_production_cohort_t *cohort;
|
|
#ifdef _WIN32
|
|
DWORD spawn_error;
|
|
#elif defined(__APPLE__)
|
|
int spawn_error;
|
|
#endif
|
|
} bootstrap_production_context_t;
|
|
|
|
static cbm_daemon_bootstrap_probe_status_t bootstrap_production_probe(
|
|
void *context, const cbm_daemon_ipc_endpoint_t *endpoint,
|
|
const cbm_daemon_build_identity_t *identity, uint32_t timeout_ms,
|
|
cbm_daemon_runtime_client_t **client_out, cbm_daemon_runtime_connect_result_t *result_out) {
|
|
bootstrap_production_context_t *production = context;
|
|
if (!production || !production->cohort || !production->cohort->manager) {
|
|
return CBM_DAEMON_BOOTSTRAP_PROBE_ERROR;
|
|
}
|
|
cbm_version_cohort_daemon_presence_t claim =
|
|
cbm_version_cohort_daemon_claim_presence(production->cohort->manager);
|
|
if (claim == CBM_VERSION_COHORT_DAEMON_ABSENT) {
|
|
int lifetime = cbm_daemon_ipc_lifetime_reservation_probe(endpoint);
|
|
if (lifetime == 0) {
|
|
/* Do not spend the per-connect timeout polling a generation that
|
|
* both independent ownership signals prove absent. The startup
|
|
* lock and its mandatory re-probe serialize a concurrent launch. */
|
|
return CBM_DAEMON_BOOTSTRAP_PROBE_UNAVAILABLE;
|
|
}
|
|
if (lifetime != 1) {
|
|
return CBM_DAEMON_BOOTSTRAP_PROBE_ERROR;
|
|
}
|
|
} else if (claim != CBM_VERSION_COHORT_DAEMON_COORDINATED) {
|
|
return CBM_DAEMON_BOOTSTRAP_PROBE_ERROR;
|
|
}
|
|
|
|
*client_out = cbm_daemon_runtime_client_connect(endpoint, identity, timeout_ms, result_out);
|
|
if (*client_out) {
|
|
return CBM_DAEMON_BOOTSTRAP_PROBE_CONNECTED;
|
|
}
|
|
|
|
/* Ownership may turn over while the connection attempt is in flight.
|
|
* Re-observe both signals so disappearance is not sticky and a live or
|
|
* cleaning-up generation is never mistaken for absence. */
|
|
claim = cbm_version_cohort_daemon_claim_presence(production->cohort->manager);
|
|
if (claim == CBM_VERSION_COHORT_DAEMON_COORDINATED) {
|
|
return cbm_daemon_bootstrap_classify_failed_connect(result_out, 1);
|
|
}
|
|
if (claim != CBM_VERSION_COHORT_DAEMON_ABSENT) {
|
|
return CBM_DAEMON_BOOTSTRAP_PROBE_ERROR;
|
|
}
|
|
int lifetime_status = cbm_daemon_ipc_lifetime_reservation_probe(endpoint);
|
|
return cbm_daemon_bootstrap_classify_failed_connect(result_out, lifetime_status);
|
|
}
|
|
|
|
static cbm_version_cohort_status_t bootstrap_production_cohort_acquire(
|
|
void *context, const cbm_daemon_ipc_endpoint_t *endpoint,
|
|
const cbm_daemon_build_identity_t *identity, uint64_t deadline_ms,
|
|
cbm_daemon_bootstrap_cohort_t *cohort_out, cbm_daemon_conflict_t *conflict_out) {
|
|
*cohort_out = NULL;
|
|
bootstrap_production_cohort_t *cohort = calloc(1, sizeof(*cohort));
|
|
if (cohort) {
|
|
cohort->manager = cbm_version_cohort_manager_new(endpoint);
|
|
}
|
|
if (!cohort || !cohort->manager) {
|
|
free(cohort);
|
|
return CBM_VERSION_COHORT_IO;
|
|
}
|
|
cbm_version_cohort_status_t status = cbm_version_cohort_acquire(
|
|
cohort->manager, identity, deadline_ms, &cohort->lease, conflict_out);
|
|
if (status == CBM_VERSION_COHORT_CONFLICT) {
|
|
(void)cbm_version_cohort_log_conflict(conflict_out);
|
|
}
|
|
if (status == CBM_VERSION_COHORT_OK || cohort->lease) {
|
|
*cohort_out = cohort;
|
|
if (context) {
|
|
bootstrap_production_context_t *production = context;
|
|
production->cohort = cohort;
|
|
}
|
|
return status;
|
|
}
|
|
uint64_t cleanup_deadline = bootstrap_deadline_after(BOOTSTRAP_COORDINATION_CLEANUP_MS);
|
|
cbm_private_file_lock_status_t cleanup = CBM_PRIVATE_FILE_LOCK_OK;
|
|
while (cohort->manager) {
|
|
cleanup = cbm_version_cohort_manager_free(&cohort->manager);
|
|
if (!cohort->manager) {
|
|
break;
|
|
}
|
|
if (cbm_now_ms() >= cleanup_deadline) {
|
|
bootstrap_cleanup_fail_stop("cohort_manager_cleanup");
|
|
}
|
|
cbm_usleep(1000);
|
|
}
|
|
free(cohort);
|
|
return cleanup == CBM_PRIVATE_FILE_LOCK_OK ? status : CBM_VERSION_COHORT_IO;
|
|
}
|
|
|
|
static void bootstrap_production_cohort_release(void *context,
|
|
cbm_daemon_bootstrap_cohort_t opaque) {
|
|
bootstrap_production_context_t *production = context;
|
|
bootstrap_production_cohort_t *cohort = opaque;
|
|
if (!cohort) {
|
|
return;
|
|
}
|
|
uint64_t cleanup_deadline = bootstrap_deadline_after(BOOTSTRAP_COORDINATION_CLEANUP_MS);
|
|
while (cohort->lease) {
|
|
(void)cbm_version_cohort_lease_release(&cohort->lease);
|
|
if (!cohort->lease) {
|
|
break;
|
|
}
|
|
if (cbm_now_ms() >= cleanup_deadline) {
|
|
bootstrap_cleanup_fail_stop("cohort_lease_cleanup");
|
|
}
|
|
cbm_usleep(1000);
|
|
}
|
|
cleanup_deadline = bootstrap_deadline_after(BOOTSTRAP_COORDINATION_CLEANUP_MS);
|
|
while (cohort->manager) {
|
|
(void)cbm_version_cohort_manager_free(&cohort->manager);
|
|
if (!cohort->manager) {
|
|
break;
|
|
}
|
|
if (cbm_now_ms() >= cleanup_deadline) {
|
|
bootstrap_cleanup_fail_stop("cohort_manager_cleanup");
|
|
}
|
|
cbm_usleep(1000);
|
|
}
|
|
if (production && production->cohort == cohort) {
|
|
production->cohort = NULL;
|
|
}
|
|
free(cohort);
|
|
}
|
|
|
|
static int bootstrap_production_lock(void *context, const cbm_daemon_ipc_endpoint_t *endpoint,
|
|
cbm_daemon_bootstrap_lock_t *lock_out) {
|
|
(void)context;
|
|
cbm_daemon_ipc_startup_lock_t *lock = NULL;
|
|
int status = cbm_daemon_ipc_startup_lock_try_acquire(endpoint, &lock);
|
|
*lock_out = lock;
|
|
return status;
|
|
}
|
|
|
|
static bool bootstrap_production_unlock(void *context, cbm_daemon_bootstrap_lock_t *lock_io) {
|
|
(void)context;
|
|
if (!lock_io) {
|
|
return false;
|
|
}
|
|
cbm_daemon_ipc_startup_lock_t *lock = *lock_io;
|
|
bool released = cbm_daemon_ipc_startup_lock_release(&lock);
|
|
*lock_io = lock;
|
|
return released;
|
|
}
|
|
|
|
static int bootstrap_production_start_failure_probe(
|
|
void *context, const cbm_daemon_ipc_endpoint_t *endpoint, uint64_t not_before_s,
|
|
cbm_daemon_bootstrap_start_failure_t *out_failure) {
|
|
(void)context;
|
|
char logs[BOOTSTRAP_PATH_CAP];
|
|
if (!cbm_daemon_bootstrap_log_directory(logs, sizeof(logs))) {
|
|
return -1;
|
|
}
|
|
return cbm_daemon_bootstrap_start_failure_read(logs, endpoint, not_before_s, out_failure);
|
|
}
|
|
|
|
static bool bootstrap_production_handoff(void *context, cbm_daemon_bootstrap_lock_t lock) {
|
|
(void)context;
|
|
return cbm_daemon_ipc_startup_lock_prepare_handoff((cbm_daemon_ipc_startup_lock_t *)lock);
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
static bool bootstrap_production_spawn(void *context,
|
|
const cbm_daemon_bootstrap_launch_spec_t *spec) {
|
|
bootstrap_production_context_t *production = context;
|
|
if (production) {
|
|
production->spawn_error = ERROR_SUCCESS;
|
|
}
|
|
if (!spec || !spec->detached || spec->inherit_standard_handles || spec->use_shell) {
|
|
if (production) {
|
|
production->spawn_error = ERROR_INVALID_PARAMETER;
|
|
}
|
|
return false;
|
|
}
|
|
char command_line[BOOTSTRAP_PATH_CAP * 2];
|
|
if (!cbm_build_win_cmdline(command_line, sizeof(command_line), spec->argv)) {
|
|
if (production) {
|
|
production->spawn_error = ERROR_INVALID_PARAMETER;
|
|
}
|
|
return false;
|
|
}
|
|
wchar_t *application = cbm_utf8_to_wide(spec->executable_path);
|
|
wchar_t *command = cbm_utf8_to_wide(command_line);
|
|
if (!application || !command) {
|
|
if (production) {
|
|
production->spawn_error = ERROR_NOT_ENOUGH_MEMORY;
|
|
}
|
|
free(application);
|
|
free(command);
|
|
return false;
|
|
}
|
|
STARTUPINFOW startup;
|
|
PROCESS_INFORMATION child;
|
|
ZeroMemory(&startup, sizeof(startup));
|
|
ZeroMemory(&child, sizeof(child));
|
|
startup.cb = sizeof(startup);
|
|
DWORD flags = DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW;
|
|
/* A managed frontend payload is intentionally contained in the permanent
|
|
* launcher's kill-on-close job. The account daemon outlives that one
|
|
* frontend and therefore uses breakaway when the containing job explicitly
|
|
* permits it. Do not request breakaway from an unrelated restrictive job:
|
|
* CreateProcess would fail and regress portable/package-manager payloads. */
|
|
BOOL in_job = FALSE;
|
|
JOBOBJECT_EXTENDED_LIMIT_INFORMATION job_limits;
|
|
memset(&job_limits, 0, sizeof(job_limits));
|
|
if (IsProcessInJob(GetCurrentProcess(), NULL, &in_job) && in_job &&
|
|
QueryInformationJobObject(NULL, JobObjectExtendedLimitInformation, &job_limits,
|
|
sizeof(job_limits), NULL) &&
|
|
(job_limits.BasicLimitInformation.LimitFlags &
|
|
(JOB_OBJECT_LIMIT_BREAKAWAY_OK | JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK)) != 0) {
|
|
flags |= CREATE_BREAKAWAY_FROM_JOB;
|
|
}
|
|
BOOL created = CreateProcessW(application, command, NULL, NULL, FALSE, flags, NULL, NULL,
|
|
&startup, &child);
|
|
DWORD spawn_error = created ? ERROR_SUCCESS : GetLastError();
|
|
free(application);
|
|
free(command);
|
|
if (production) {
|
|
production->spawn_error = spawn_error;
|
|
}
|
|
if (!created) {
|
|
return false;
|
|
}
|
|
(void)CloseHandle(child.hThread);
|
|
(void)CloseHandle(child.hProcess);
|
|
return true;
|
|
}
|
|
#elif defined(__APPLE__)
|
|
static bool bootstrap_darwin_spawn_state_init(posix_spawn_file_actions_t *actions,
|
|
posix_spawnattr_t *attributes) {
|
|
if (posix_spawn_file_actions_init(actions) != 0) {
|
|
return false;
|
|
}
|
|
bool actions_ready =
|
|
posix_spawn_file_actions_addopen(actions, STDIN_FILENO, "/dev/null", O_RDWR, 0) == 0 &&
|
|
posix_spawn_file_actions_addopen(actions, STDOUT_FILENO, "/dev/null", O_RDWR, 0) == 0 &&
|
|
posix_spawn_file_actions_addopen(actions, STDERR_FILENO, "/dev/null", O_RDWR, 0) == 0;
|
|
if (!actions_ready) {
|
|
(void)posix_spawn_file_actions_destroy(actions);
|
|
return false;
|
|
}
|
|
if (posix_spawnattr_init(attributes) != 0) {
|
|
(void)posix_spawn_file_actions_destroy(actions);
|
|
return false;
|
|
}
|
|
sigset_t empty;
|
|
(void)sigemptyset(&empty);
|
|
short flags = POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSID | POSIX_SPAWN_CLOEXEC_DEFAULT;
|
|
if (posix_spawnattr_setsigmask(attributes, &empty) != 0 ||
|
|
posix_spawnattr_setflags(attributes, flags) != 0) {
|
|
(void)posix_spawnattr_destroy(attributes);
|
|
(void)posix_spawn_file_actions_destroy(actions);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
typedef struct {
|
|
pid_t pid;
|
|
} bootstrap_darwin_reaper_t;
|
|
|
|
static void *bootstrap_darwin_reap(void *opaque) {
|
|
bootstrap_darwin_reaper_t *reaper = opaque;
|
|
int status = 0;
|
|
pid_t waited;
|
|
do {
|
|
waited = waitpid(reaper->pid, &status, 0);
|
|
} while (waited < 0 && errno == EINTR);
|
|
free(reaper);
|
|
return NULL;
|
|
}
|
|
|
|
static bool bootstrap_darwin_reaper_start(pid_t daemon) {
|
|
bootstrap_darwin_reaper_t *reaper = malloc(sizeof(*reaper));
|
|
if (!reaper) {
|
|
return false;
|
|
}
|
|
reaper->pid = daemon;
|
|
pthread_attr_t attributes;
|
|
bool attributes_ready = pthread_attr_init(&attributes) == 0;
|
|
bool detached =
|
|
attributes_ready && pthread_attr_setdetachstate(&attributes, PTHREAD_CREATE_DETACHED) == 0;
|
|
pthread_t thread;
|
|
int thread_status =
|
|
detached ? pthread_create(&thread, &attributes, bootstrap_darwin_reap, reaper) : -1;
|
|
if (attributes_ready) {
|
|
(void)pthread_attr_destroy(&attributes);
|
|
}
|
|
if (thread_status != 0) {
|
|
free(reaper);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool bootstrap_production_spawn(void *context,
|
|
const cbm_daemon_bootstrap_launch_spec_t *spec) {
|
|
bootstrap_production_context_t *production = context;
|
|
if (production) {
|
|
production->spawn_error = 0;
|
|
}
|
|
if (!spec || !spec->detached || spec->inherit_standard_handles || spec->use_shell) {
|
|
return false;
|
|
}
|
|
posix_spawn_file_actions_t actions;
|
|
posix_spawnattr_t attributes;
|
|
if (!bootstrap_darwin_spawn_state_init(&actions, &attributes)) {
|
|
return false;
|
|
}
|
|
/* Calling library-heavy process creation between fork() and exec is not a
|
|
* safe boundary once another thread may own libc state. Darwin's spawn
|
|
* primitive can create the detached session and close inherited FDs in
|
|
* one operation, so launch it from the original process and retain only a
|
|
* tiny detached waiter to prevent a crashed daemon from becoming a
|
|
* long-lived zombie owned by the first frontend. */
|
|
pid_t daemon = 0;
|
|
int spawn_status = posix_spawn(&daemon, spec->executable_path, &actions, &attributes,
|
|
(char *const *)spec->argv, environ);
|
|
(void)posix_spawnattr_destroy(&attributes);
|
|
(void)posix_spawn_file_actions_destroy(&actions);
|
|
if (production) {
|
|
production->spawn_error = spawn_status;
|
|
}
|
|
if (spawn_status != 0) {
|
|
return false;
|
|
}
|
|
if (!bootstrap_darwin_reaper_start(daemon)) {
|
|
(void)kill(daemon, SIGKILL);
|
|
int status = 0;
|
|
while (waitpid(daemon, &status, 0) < 0 && errno == EINTR) {}
|
|
if (production) {
|
|
production->spawn_error = EAGAIN;
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
#else
|
|
static void bootstrap_child_close_fds(void) {
|
|
long open_max = sysconf(_SC_OPEN_MAX);
|
|
if (open_max < 0 || open_max > 1048576L) {
|
|
open_max = 65536L;
|
|
}
|
|
for (int fd = 3; fd < open_max; fd++) {
|
|
(void)close(fd);
|
|
}
|
|
}
|
|
|
|
static void bootstrap_daemon_grandchild(const cbm_daemon_bootstrap_launch_spec_t *spec) {
|
|
(void)umask(077);
|
|
sigset_t empty;
|
|
(void)sigemptyset(&empty);
|
|
(void)sigprocmask(SIG_SETMASK, &empty, NULL);
|
|
int null_fd = open("/dev/null", O_RDWR);
|
|
if (null_fd < 0 || dup2(null_fd, STDIN_FILENO) < 0 || dup2(null_fd, STDOUT_FILENO) < 0 ||
|
|
dup2(null_fd, STDERR_FILENO) < 0) {
|
|
_exit(127);
|
|
}
|
|
if (null_fd > STDERR_FILENO) {
|
|
(void)close(null_fd);
|
|
}
|
|
bootstrap_child_close_fds();
|
|
execv(spec->executable_path, (char *const *)spec->argv);
|
|
_exit(127);
|
|
}
|
|
|
|
static bool bootstrap_production_spawn(void *context,
|
|
const cbm_daemon_bootstrap_launch_spec_t *spec) {
|
|
(void)context;
|
|
if (!spec || !spec->detached || spec->inherit_standard_handles || spec->use_shell) {
|
|
return false;
|
|
}
|
|
pid_t first = fork();
|
|
if (first < 0) {
|
|
return false;
|
|
}
|
|
if (first == 0) {
|
|
if (setsid() < 0) {
|
|
_exit(127);
|
|
}
|
|
pid_t daemon = fork();
|
|
if (daemon < 0) {
|
|
_exit(127);
|
|
}
|
|
if (daemon > 0) {
|
|
_exit(0);
|
|
}
|
|
bootstrap_daemon_grandchild(spec);
|
|
}
|
|
|
|
int status = 0;
|
|
pid_t waited;
|
|
do {
|
|
waited = waitpid(first, &status, 0);
|
|
} while (waited < 0 && errno == EINTR);
|
|
return waited == first && WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
|
}
|
|
#endif
|
|
|
|
static void bootstrap_production_diagnostic(void *context, const char *message) {
|
|
bootstrap_production_context_t *production = context;
|
|
#ifdef _WIN32
|
|
if (production && production->spawn_error != ERROR_SUCCESS) {
|
|
(void)fprintf(stderr, "codebase-memory-mcp: %s (daemon launch error %lu)\n",
|
|
message ? message : "daemon startup failed",
|
|
(unsigned long)production->spawn_error);
|
|
(void)fflush(stderr);
|
|
return;
|
|
}
|
|
#elif defined(__APPLE__)
|
|
if (production && production->spawn_error != 0) {
|
|
(void)fprintf(stderr, "codebase-memory-mcp: %s (daemon launch: %s)\n",
|
|
message ? message : "daemon startup failed",
|
|
strerror(production->spawn_error));
|
|
(void)fflush(stderr);
|
|
return;
|
|
}
|
|
#else
|
|
(void)production;
|
|
#endif
|
|
(void)fprintf(stderr, "codebase-memory-mcp: %s\n", message ? message : "daemon startup failed");
|
|
(void)fflush(stderr);
|
|
}
|
|
|
|
#ifdef CBM_ENABLE_TEST_SEAMS
|
|
static cbm_daemon_bootstrap_spawn_fn g_bootstrap_spawn_override_for_test;
|
|
static void *g_bootstrap_spawn_override_context_for_test;
|
|
|
|
void cbm_daemon_bootstrap_spawn_override_set_for_test(cbm_daemon_bootstrap_spawn_fn spawn,
|
|
void *context) {
|
|
g_bootstrap_spawn_override_context_for_test = context;
|
|
g_bootstrap_spawn_override_for_test = spawn;
|
|
}
|
|
|
|
static bool bootstrap_seam_spawn(void *context, const cbm_daemon_bootstrap_launch_spec_t *spec) {
|
|
(void)context;
|
|
return g_bootstrap_spawn_override_for_test(g_bootstrap_spawn_override_context_for_test, spec);
|
|
}
|
|
#endif
|
|
|
|
cbm_daemon_bootstrap_status_t cbm_daemon_bootstrap_execute(
|
|
const cbm_daemon_bootstrap_config_t *config, cbm_daemon_bootstrap_result_t *result_out) {
|
|
bootstrap_production_context_t context = {0};
|
|
cbm_daemon_bootstrap_ops_t ops = {
|
|
.context = &context,
|
|
.cohort_acquire = bootstrap_production_cohort_acquire,
|
|
.cohort_release = bootstrap_production_cohort_release,
|
|
.probe = bootstrap_production_probe,
|
|
.startup_lock_try_acquire = bootstrap_production_lock,
|
|
.startup_lock_prepare_handoff = bootstrap_production_handoff,
|
|
.startup_lock_release = bootstrap_production_unlock,
|
|
.spawn_daemon = bootstrap_production_spawn,
|
|
.visible_diagnostic = bootstrap_production_diagnostic,
|
|
.start_failure_probe = bootstrap_production_start_failure_probe,
|
|
};
|
|
#ifdef CBM_ENABLE_TEST_SEAMS
|
|
if (g_bootstrap_spawn_override_for_test) {
|
|
ops.spawn_daemon = bootstrap_seam_spawn;
|
|
}
|
|
#endif
|
|
return cbm_daemon_bootstrap_execute_with_ops(config, &ops, result_out);
|
|
}
|