mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-25 06:33:13 +08:00
3537 lines
148 KiB
C
3537 lines
148 KiB
C
/*
|
|
* application.c — Daemon-owned CBM application sessions and thin-client wire.
|
|
*/
|
|
#include "daemon/application.h"
|
|
#include "daemon/application_internal.h"
|
|
|
|
#include "cli/cli.h"
|
|
#include "foundation/compat.h"
|
|
#include "foundation/compat_fs.h"
|
|
#include "foundation/compat_thread.h"
|
|
#include "foundation/log.h"
|
|
#include "foundation/mem.h"
|
|
#include "foundation/platform.h"
|
|
#include "foundation/secure_random.h"
|
|
#include "foundation/sha256.h"
|
|
#include "foundation/subprocess.h"
|
|
#include "foundation/workspace.h"
|
|
#include "mcp/index_supervisor.h"
|
|
#include "mcp/mcp.h"
|
|
#include "mcp/mcp_internal.h"
|
|
#include "pipeline/pipeline.h"
|
|
#include "ui/config.h"
|
|
#include "watcher/watcher.h"
|
|
|
|
#include <yyjson/yyjson.h>
|
|
|
|
#include <ctype.h>
|
|
#include <limits.h>
|
|
#include <stdint.h>
|
|
#include <stdatomic.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
|
|
#ifdef _WIN32
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
#include <io.h>
|
|
#include <windows.h>
|
|
#define application_close _close
|
|
#else
|
|
#include <unistd.h>
|
|
#define application_close close
|
|
#endif
|
|
|
|
enum {
|
|
APPLICATION_CONTEXT_HEADER_SIZE = 19,
|
|
APPLICATION_TOOL_HEADER_SIZE = 5,
|
|
APPLICATION_UI_CONFIG_REQUEST_SIZE = 7,
|
|
APPLICATION_UI_READINESS_REQUEST_SIZE = 1 + CBM_SHA256_DIGEST_LEN,
|
|
APPLICATION_PATH_CAP = 4096,
|
|
APPLICATION_JOB_THREAD_STACK = 256 * 1024,
|
|
APPLICATION_JOB_POLL_US = 10000,
|
|
APPLICATION_COORDINATION_CLEANUP_MS = 500,
|
|
APPLICATION_DEFAULT_PHYSICAL_JOB_LIMIT = 4,
|
|
APPLICATION_DEFAULT_MAX_RESTARTS = 100,
|
|
APPLICATION_MARKER_MAX_BYTES = 64 * 1024 * 1024,
|
|
APPLICATION_MAX_SUSPECTS = 65536,
|
|
APPLICATION_UPDATE_POLL_US = 10000,
|
|
APPLICATION_BACKGROUND_REAP_MS = 10000,
|
|
APPLICATION_UPDATE_VERSION_CAP = 128,
|
|
APPLICATION_UPDATE_NOTICE_CAP = 1024,
|
|
};
|
|
|
|
/* There is deliberately NO production update-check provider. The daemon used to
|
|
* spawn `curl` against the GitHub releases API on the first eligible session of
|
|
* every run, purely to print "a newer version exists". That put a release URL
|
|
* and an outbound request into every shipped binary, and made a developer tool
|
|
* phone home from every agent session, to deliver something the install scripts
|
|
* and package managers already report.
|
|
*
|
|
* The SEAM below survives: `update_ops` remains injectable, and the notice,
|
|
* ownership, cancellation and generation-replay logic is still exercised by the
|
|
* fakes in tests/test_daemon_application.c. With no provider installed the whole
|
|
* machinery simply never starts a generation (see
|
|
* application_update_subscribe_locked), so a build that ships no provider makes
|
|
* no network request by default -- the property, not just the absence of a call.
|
|
*/
|
|
|
|
typedef struct cbm_daemon_application_watch cbm_daemon_application_watch_t;
|
|
typedef struct cbm_daemon_application_session cbm_daemon_application_session_t;
|
|
typedef struct cbm_daemon_application_job cbm_daemon_application_job_t;
|
|
typedef struct cbm_daemon_application_mutation cbm_daemon_application_mutation_t;
|
|
typedef struct cbm_daemon_application_watch_job_subscription
|
|
cbm_daemon_application_watch_job_subscription_t;
|
|
|
|
typedef enum {
|
|
APPLICATION_JOB_SUBSCRIBE_OK = 0,
|
|
APPLICATION_JOB_SUBSCRIBE_OPTIONS_CONFLICT,
|
|
APPLICATION_JOB_SUBSCRIBE_BUSY,
|
|
APPLICATION_JOB_SUBSCRIBE_CANCELLING,
|
|
APPLICATION_JOB_SUBSCRIBE_UNAVAILABLE,
|
|
APPLICATION_JOB_SUBSCRIBE_ALLOCATION_FAILED,
|
|
} application_job_subscribe_status_t;
|
|
|
|
struct cbm_daemon_application_watch {
|
|
char *project;
|
|
char *root;
|
|
size_t subscribers;
|
|
cbm_daemon_application_watch_t *next;
|
|
};
|
|
|
|
struct cbm_daemon_application_session {
|
|
cbm_daemon_application_t *application;
|
|
cbm_mcp_server_t *mcp;
|
|
cbm_daemon_client_id_t client_id;
|
|
uint64_t authenticated_process_id;
|
|
bool context_set;
|
|
cbm_mcp_tool_profile_t tool_profile;
|
|
char *hook_event;
|
|
char *hook_dialect;
|
|
bool session_cancelled;
|
|
bool request_active;
|
|
cbm_daemon_runtime_application_token_t active_request_token;
|
|
cbm_daemon_runtime_application_token_t request_cancel_token;
|
|
cbm_daemon_application_watch_t *watch;
|
|
cbm_daemon_application_job_t *active_job;
|
|
bool active_job_subscribed;
|
|
cbm_daemon_application_job_t *auto_index_job;
|
|
bool auto_index_subscribed;
|
|
bool auto_index_evaluated;
|
|
bool auto_index_retry_pending;
|
|
bool background_eligible;
|
|
bool update_owner;
|
|
bool update_notice_delivered;
|
|
bool pending_background_initialize;
|
|
bool pending_update_notice;
|
|
cbm_daemon_application_session_t *next;
|
|
};
|
|
|
|
struct cbm_daemon_application_job {
|
|
cbm_daemon_application_t *application;
|
|
char *project_key;
|
|
char *root_path;
|
|
char *args_json;
|
|
char *response;
|
|
cbm_daemon_application_worker_t worker;
|
|
cbm_thread_t thread;
|
|
size_t subscribers;
|
|
size_t watcher_waiters;
|
|
bool thread_started;
|
|
bool thread_done;
|
|
bool terminal;
|
|
bool successful;
|
|
bool cancelled;
|
|
bool cancel_requested;
|
|
bool supervision_failed;
|
|
cbm_daemon_application_job_t *next;
|
|
};
|
|
|
|
/* A watcher-triggered physical job is owned by the exact live sessions that
|
|
* currently subscribe to its project/root watch. The callback waiting for the
|
|
* job is only a storage waiter; it is deliberately not an ownership
|
|
* subscription, so the worker is cancelled when the last matching session
|
|
* disconnects even while unrelated daemon sessions remain alive. */
|
|
struct cbm_daemon_application_watch_job_subscription {
|
|
cbm_daemon_application_session_t *session;
|
|
cbm_daemon_application_job_t *job;
|
|
cbm_daemon_application_watch_job_subscription_t *next;
|
|
};
|
|
|
|
struct cbm_daemon_application_mutation {
|
|
char *project_key;
|
|
cbm_project_lock_lease_t *project_lock_lease;
|
|
bool releasing;
|
|
cbm_daemon_application_mutation_t *next;
|
|
};
|
|
|
|
struct cbm_daemon_application {
|
|
cbm_mutex_t mutex;
|
|
struct cbm_watcher *watcher;
|
|
struct cbm_config *config;
|
|
cbm_daemon_application_session_t *sessions;
|
|
cbm_daemon_application_watch_t *watches;
|
|
cbm_daemon_application_job_t *jobs;
|
|
cbm_daemon_application_watch_job_subscription_t *watch_job_subscriptions;
|
|
cbm_daemon_application_mutation_t *mutations;
|
|
cbm_daemon_application_worker_ops_t worker_ops;
|
|
cbm_daemon_application_update_ops_t update_ops;
|
|
cbm_project_lock_manager_t *project_locks;
|
|
size_t physical_job_limit;
|
|
size_t worker_memory_budget_bytes;
|
|
size_t active_mutations;
|
|
size_t update_owners;
|
|
cbm_daemon_application_update_worker_t update_worker;
|
|
cbm_thread_t update_thread;
|
|
char update_notice[APPLICATION_UPDATE_NOTICE_CAP];
|
|
bool update_generation_started;
|
|
bool update_cancel_requested;
|
|
bool update_thread_started;
|
|
bool update_thread_done;
|
|
bool update_thread_joining;
|
|
bool stopping;
|
|
/* See cbm_daemon_application_set_permanent. */
|
|
bool permanent;
|
|
uint8_t ui_readiness_secret[CBM_SHA256_DIGEST_LEN];
|
|
bool ui_readiness_secret_set;
|
|
};
|
|
|
|
static void application_job_unsubscribe_locked(cbm_daemon_application_job_t *job);
|
|
static void application_watch_job_unsubscribe_session_locked(
|
|
cbm_daemon_application_session_t *session);
|
|
static bool application_watch_job_subscribe_late_session_locked(
|
|
cbm_daemon_application_session_t *session, cbm_daemon_application_watch_t *watch);
|
|
static bool application_unique_recovery_file(char out[APPLICATION_PATH_CAP], const char *kind);
|
|
static bool application_update_reap(cbm_daemon_application_t *application, bool wait,
|
|
uint32_t timeout_ms);
|
|
static void *application_job_thread(void *opaque);
|
|
static char *application_auto_index_args(const char *root_path);
|
|
static cbm_daemon_application_job_t *application_job_subscribe_locked(
|
|
cbm_daemon_application_t *application, const char *project_key, const char *root_path,
|
|
const char *args_json, application_job_subscribe_status_t *status_out);
|
|
|
|
static atomic_bool g_application_fail_next_job_thread_start_for_test = ATOMIC_VAR_INIT(false);
|
|
|
|
void cbm_daemon_application_fail_next_job_thread_start_for_test(void) {
|
|
atomic_store_explicit(&g_application_fail_next_job_thread_start_for_test, true,
|
|
memory_order_release);
|
|
}
|
|
|
|
static atomic_bool g_application_hold_job_before_start_for_test = ATOMIC_VAR_INIT(false);
|
|
|
|
/* Counts completed background-initialize passes (the request-handler tail
|
|
* that evaluates auto-index admission). The client can observe its
|
|
* initialize RESPONSE before this tail finishes, so a test asserting the
|
|
* tail's NEGATIVE outcome ("no job was admitted") needs this positive
|
|
* completion signal — a fixed sleep is a lottery in both directions. */
|
|
static atomic_int g_application_background_initializes_for_test = ATOMIC_VAR_INIT(0);
|
|
|
|
int cbm_daemon_application_background_initializes_for_test(void) {
|
|
return atomic_load_explicit(&g_application_background_initializes_for_test,
|
|
memory_order_acquire);
|
|
}
|
|
|
|
/* Counts iterations of the explicit-index busy-queue wait loop. A test that
|
|
* needs "the request is parked behind the physical job limit" waits for a
|
|
* DELTA here — the positive signal that queueing (not an error return)
|
|
* happened — before releasing the slot. */
|
|
static atomic_int g_application_busy_queue_waits_for_test = ATOMIC_VAR_INIT(0);
|
|
|
|
int cbm_daemon_application_busy_queue_waits_for_test(void) {
|
|
return atomic_load_explicit(&g_application_busy_queue_waits_for_test, memory_order_acquire);
|
|
}
|
|
|
|
void cbm_daemon_application_hold_job_before_start_for_test(bool hold) {
|
|
atomic_store_explicit(&g_application_hold_job_before_start_for_test, hold,
|
|
memory_order_release);
|
|
}
|
|
|
|
static int application_job_thread_create(cbm_thread_t *thread, void *context) {
|
|
if (atomic_exchange_explicit(&g_application_fail_next_job_thread_start_for_test, false,
|
|
memory_order_acq_rel)) {
|
|
return -1;
|
|
}
|
|
return cbm_thread_create(thread, APPLICATION_JOB_THREAD_STACK, application_job_thread, context);
|
|
}
|
|
|
|
static bool application_request_cancelled_locked(const cbm_daemon_application_session_t *session) {
|
|
return session &&
|
|
(session->session_cancelled ||
|
|
(session->request_active &&
|
|
session->active_request_token != CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID &&
|
|
session->request_cancel_token == session->active_request_token));
|
|
}
|
|
|
|
static uint64_t application_deadline_after(uint32_t timeout_ms) {
|
|
uint64_t now = cbm_now_ms();
|
|
return now > UINT64_MAX - timeout_ms ? UINT64_MAX : now + timeout_ms;
|
|
}
|
|
|
|
static _Noreturn void application_cleanup_force_terminate(const char *component) {
|
|
/* In production this module is daemon-owned and the host log sink flushes
|
|
* every record synchronously. Continuing would either lose the only retry
|
|
* handle or falsely make a project mutation appear released. */
|
|
cbm_log_error("daemon.forced_shutdown", "component", component);
|
|
(void)fflush(stdout);
|
|
(void)fflush(stderr);
|
|
#ifdef _WIN32
|
|
(void)TerminateProcess(GetCurrentProcess(), EXIT_FAILURE);
|
|
abort();
|
|
#else
|
|
_exit(EXIT_FAILURE);
|
|
#endif
|
|
}
|
|
|
|
static void application_project_lock_release_fully(cbm_project_lock_lease_t **lease) {
|
|
uint64_t deadline = application_deadline_after(APPLICATION_COORDINATION_CLEANUP_MS);
|
|
while (lease && *lease) {
|
|
(void)cbm_project_lock_lease_release(lease);
|
|
if (!*lease) {
|
|
return;
|
|
}
|
|
if (cbm_now_ms() >= deadline) {
|
|
application_cleanup_force_terminate("project_lock_cleanup");
|
|
}
|
|
cbm_usleep(1000);
|
|
}
|
|
}
|
|
|
|
static int application_worker_start_default(void *context, const char *args_json,
|
|
size_t memory_budget_bytes, const char *marker_file,
|
|
const char *quarantine_file,
|
|
cbm_daemon_application_worker_t *worker_out) {
|
|
(void)context;
|
|
cbm_index_worker_handle_t *worker = NULL;
|
|
int result = cbm_index_worker_start(args_json, memory_budget_bytes, false, marker_file,
|
|
quarantine_file, &worker);
|
|
*worker_out = worker;
|
|
return result;
|
|
}
|
|
|
|
static cbm_index_worker_poll_t application_worker_poll_default(
|
|
void *context, cbm_daemon_application_worker_t worker,
|
|
const cbm_index_worker_result_t **result_out) {
|
|
(void)context;
|
|
return cbm_index_worker_poll((cbm_index_worker_handle_t *)worker, result_out);
|
|
}
|
|
|
|
static bool application_worker_cancel_default(void *context,
|
|
cbm_daemon_application_worker_t worker) {
|
|
(void)context;
|
|
return cbm_index_worker_request_cancel((cbm_index_worker_handle_t *)worker);
|
|
}
|
|
|
|
static const char *application_worker_log_path_default(void *context,
|
|
cbm_daemon_application_worker_t worker) {
|
|
(void)context;
|
|
return cbm_index_worker_log_path((cbm_index_worker_handle_t *)worker);
|
|
}
|
|
|
|
static void application_worker_destroy_default(void *context,
|
|
cbm_daemon_application_worker_t worker) {
|
|
(void)context;
|
|
cbm_index_worker_destroy((cbm_index_worker_handle_t *)worker);
|
|
}
|
|
|
|
static uint32_t application_get_u32(const uint8_t *bytes) {
|
|
return ((uint32_t)bytes[0] << 24) | ((uint32_t)bytes[1] << 16) | ((uint32_t)bytes[2] << 8) |
|
|
(uint32_t)bytes[3];
|
|
}
|
|
|
|
static void application_put_u32(uint8_t *bytes, uint32_t value) {
|
|
bytes[0] = (uint8_t)(value >> 24);
|
|
bytes[1] = (uint8_t)(value >> 16);
|
|
bytes[2] = (uint8_t)(value >> 8);
|
|
bytes[3] = (uint8_t)value;
|
|
}
|
|
|
|
static char *application_text_copy(const uint8_t *bytes, uint32_t length) {
|
|
if (!bytes || length == 0 || memchr(bytes, '\0', length) != NULL) {
|
|
return NULL;
|
|
}
|
|
char *copy = malloc((size_t)length + 1U);
|
|
if (copy) {
|
|
memcpy(copy, bytes, length);
|
|
copy[length] = '\0';
|
|
}
|
|
return copy;
|
|
}
|
|
|
|
static bool application_regular_db_exists(const char *project) {
|
|
const char *cache = cbm_resolve_cache_dir();
|
|
if (!cache || !project || !project[0]) {
|
|
return false;
|
|
}
|
|
char path[APPLICATION_PATH_CAP];
|
|
int written = snprintf(path, sizeof(path), "%s/%s.db", cache, project);
|
|
if (written <= 0 || (size_t)written >= sizeof(path)) {
|
|
return false;
|
|
}
|
|
struct stat status;
|
|
return stat(path, &status) == 0 && S_ISREG(status.st_mode);
|
|
}
|
|
|
|
static cbm_daemon_application_watch_t *application_find_watch_locked(
|
|
cbm_daemon_application_t *application, const char *project) {
|
|
for (cbm_daemon_application_watch_t *watch = application->watches; watch; watch = watch->next) {
|
|
if (strcmp(watch->project, project) == 0) {
|
|
return watch;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void application_remove_watch_entry_locked(cbm_daemon_application_t *application,
|
|
cbm_daemon_application_watch_t *watch,
|
|
bool unregister_physical_watch) {
|
|
cbm_daemon_application_watch_t **cursor = &application->watches;
|
|
while (*cursor && *cursor != watch) {
|
|
cursor = &(*cursor)->next;
|
|
}
|
|
if (*cursor != watch) {
|
|
return;
|
|
}
|
|
*cursor = watch->next;
|
|
for (cbm_daemon_application_session_t *session = application->sessions; session;
|
|
session = session->next) {
|
|
if (session->watch == watch) {
|
|
application_watch_job_unsubscribe_session_locked(session);
|
|
session->watch = NULL;
|
|
}
|
|
}
|
|
if (unregister_physical_watch && application->watcher) {
|
|
cbm_watcher_unwatch(application->watcher, watch->project);
|
|
}
|
|
free(watch->project);
|
|
free(watch->root);
|
|
free(watch);
|
|
}
|
|
|
|
static void application_remove_watch_locked(cbm_daemon_application_t *application,
|
|
cbm_daemon_application_watch_t *watch) {
|
|
application_remove_watch_entry_locked(application, watch, true);
|
|
}
|
|
|
|
static void application_release_session_watch_locked(cbm_daemon_application_session_t *session) {
|
|
cbm_daemon_application_watch_t *watch = session->watch;
|
|
if (!watch) {
|
|
return;
|
|
}
|
|
application_watch_job_unsubscribe_session_locked(session);
|
|
session->watch = NULL;
|
|
if (watch->subscribers > 0) {
|
|
watch->subscribers--;
|
|
}
|
|
if (watch->subscribers == 0) {
|
|
application_remove_watch_locked(session->application, watch);
|
|
}
|
|
}
|
|
|
|
static bool application_session_workspace_allowed(const cbm_daemon_application_session_t *session,
|
|
const char *operation) {
|
|
const char *root = session ? cbm_mcp_server_session_root(session->mcp) : NULL;
|
|
char boundary_error[CBM_SZ_1K];
|
|
bool allowed =
|
|
root && root[0] &&
|
|
cbm_workspace_root_allowed(root, cbm_workspace_home_dir(), cbm_workspace_cache_dir(),
|
|
cbm_mcp_server_allowed_root(session->mcp), boundary_error,
|
|
sizeof(boundary_error));
|
|
if (!allowed) {
|
|
cbm_log_warn("daemon.workspace.skipped", "operation", operation, "detail",
|
|
root && root[0] ? boundary_error : "session root is unavailable");
|
|
}
|
|
return allowed;
|
|
}
|
|
|
|
/* Caller holds application->mutex. */
|
|
static void application_refresh_watch_locked(cbm_daemon_application_session_t *session) {
|
|
cbm_daemon_application_t *application = session->application;
|
|
if (!application->watcher || !session->context_set ||
|
|
session->tool_profile != CBM_MCP_TOOL_PROFILE_ALL || session->hook_event ||
|
|
session->hook_dialect || session->auto_index_subscribed) {
|
|
return;
|
|
}
|
|
const char *project = cbm_mcp_server_session_project(session->mcp);
|
|
const char *root = cbm_mcp_server_session_root(session->mcp);
|
|
if (!project || !project[0] || !root || !root[0]) {
|
|
return;
|
|
}
|
|
if (!application_session_workspace_allowed(session, "watch")) {
|
|
application_release_session_watch_locked(session);
|
|
return;
|
|
}
|
|
bool enabled = !application->config ||
|
|
cbm_config_get_bool(application->config, CBM_CONFIG_AUTO_WATCH, true);
|
|
bool db_exists = application_regular_db_exists(project);
|
|
|
|
/* Disconnect cancellation is the logical ownership boundary. An
|
|
* in-flight request may reach this refresh after cancel returned but
|
|
* before runtime can join it and call session_close; it must never
|
|
* recreate that session's watch. */
|
|
if (application->stopping || application_request_cancelled_locked(session)) {
|
|
return;
|
|
}
|
|
cbm_daemon_application_watch_t *watch = application_find_watch_locked(application, project);
|
|
if (!enabled || !db_exists) {
|
|
/* A delete/config transition is global for this project. Remove the
|
|
* physical watch and clear every logical subscriber in one step. */
|
|
if (watch) {
|
|
application_remove_watch_locked(application, watch);
|
|
}
|
|
return;
|
|
}
|
|
if (session->watch) {
|
|
return;
|
|
}
|
|
if (watch) {
|
|
if (strcmp(watch->root, root) == 0) {
|
|
if (!application_watch_job_subscribe_late_session_locked(session, watch)) {
|
|
cbm_log_warn("daemon.watch.late_owner_allocation_failed", "project", project,
|
|
"action", "retry");
|
|
return;
|
|
}
|
|
watch->subscribers++;
|
|
session->watch = watch;
|
|
} else {
|
|
cbm_log_warn("daemon.watch.project_collision", "project", project, "existing_root",
|
|
watch->root, "requested_root", root);
|
|
}
|
|
return;
|
|
}
|
|
|
|
watch = calloc(1, sizeof(*watch));
|
|
if (watch) {
|
|
watch->project = strdup(project);
|
|
watch->root = strdup(root);
|
|
}
|
|
if (!watch || !watch->project || !watch->root) {
|
|
if (watch) {
|
|
free(watch->project);
|
|
free(watch->root);
|
|
free(watch);
|
|
}
|
|
return;
|
|
}
|
|
/* Physical registration is the commit point. Never publish a logical
|
|
* subscription that the shared watcher failed to install. */
|
|
if (!cbm_watcher_watch(application->watcher, project, root)) {
|
|
cbm_log_warn("daemon.watch.registration_failed", "project", project, "action", "retry");
|
|
free(watch->project);
|
|
free(watch->root);
|
|
free(watch);
|
|
return;
|
|
}
|
|
watch->subscribers = 1;
|
|
watch->next = application->watches;
|
|
application->watches = watch;
|
|
session->watch = watch;
|
|
}
|
|
|
|
static void application_refresh_watch(cbm_daemon_application_session_t *session) {
|
|
if (!session || !session->application) {
|
|
return;
|
|
}
|
|
cbm_mutex_lock(&session->application->mutex);
|
|
application_refresh_watch_locked(session);
|
|
cbm_mutex_unlock(&session->application->mutex);
|
|
}
|
|
|
|
static void application_job_free(cbm_daemon_application_job_t *job) {
|
|
if (!job) {
|
|
return;
|
|
}
|
|
free(job->project_key);
|
|
free(job->root_path);
|
|
free(job->args_json);
|
|
free(job->response);
|
|
free(job);
|
|
}
|
|
|
|
/* Reap completed job threads only after every logical demand subscription and
|
|
* watcher callback storage waiter has released the job. Exactly one caller
|
|
* removes a job under the mutex. */
|
|
static void application_jobs_reap_completed(cbm_daemon_application_t *application) {
|
|
for (;;) {
|
|
cbm_daemon_application_job_t *reap = NULL;
|
|
cbm_mutex_lock(&application->mutex);
|
|
cbm_daemon_application_job_t **cursor = &application->jobs;
|
|
while (*cursor) {
|
|
if ((*cursor)->thread_done && (*cursor)->subscribers == 0 &&
|
|
(*cursor)->watcher_waiters == 0) {
|
|
reap = *cursor;
|
|
*cursor = reap->next;
|
|
reap->next = NULL;
|
|
break;
|
|
}
|
|
cursor = &(*cursor)->next;
|
|
}
|
|
cbm_mutex_unlock(&application->mutex);
|
|
if (!reap) {
|
|
return;
|
|
}
|
|
if (reap->thread_started) {
|
|
(void)cbm_thread_join(&reap->thread);
|
|
}
|
|
application_job_free(reap);
|
|
}
|
|
}
|
|
|
|
typedef enum {
|
|
APPLICATION_ATTEMPT_TERMINAL = 0,
|
|
APPLICATION_ATTEMPT_START_FAILED,
|
|
APPLICATION_ATTEMPT_CANCELLED,
|
|
} application_attempt_status_t;
|
|
|
|
typedef struct {
|
|
cbm_index_worker_result_t result;
|
|
bool has_result;
|
|
char log_path[APPLICATION_PATH_CAP];
|
|
} application_attempt_t;
|
|
|
|
static atomic_flag g_application_tmp_lock = ATOMIC_FLAG_INIT;
|
|
|
|
static void application_tmp_lock(void) {
|
|
while (atomic_flag_test_and_set_explicit(&g_application_tmp_lock, memory_order_acquire)) {
|
|
cbm_usleep(1000);
|
|
}
|
|
}
|
|
|
|
static void application_tmp_unlock(void) {
|
|
atomic_flag_clear_explicit(&g_application_tmp_lock, memory_order_release);
|
|
}
|
|
|
|
static bool application_cache_dir(char out[APPLICATION_PATH_CAP]) {
|
|
char configured[APPLICATION_PATH_CAP] = {0};
|
|
if (cbm_safe_getenv("CBM_CACHE_DIR", configured, sizeof(configured), NULL) && configured[0]) {
|
|
int written = snprintf(out, APPLICATION_PATH_CAP, "%s", configured);
|
|
if (written <= 0 || written >= APPLICATION_PATH_CAP) {
|
|
return false;
|
|
}
|
|
cbm_normalize_path_sep(out);
|
|
return true;
|
|
}
|
|
char home[APPLICATION_PATH_CAP] = {0};
|
|
if (!cbm_safe_getenv("HOME", home, sizeof(home), NULL) || !home[0]) {
|
|
(void)cbm_safe_getenv("USERPROFILE", home, sizeof(home), NULL);
|
|
}
|
|
if (!home[0]) {
|
|
return false;
|
|
}
|
|
int written = snprintf(out, APPLICATION_PATH_CAP, "%s/.cache/codebase-memory-mcp", home);
|
|
if (written <= 0 || written >= APPLICATION_PATH_CAP) {
|
|
return false;
|
|
}
|
|
cbm_normalize_path_sep(out);
|
|
return true;
|
|
}
|
|
|
|
static bool application_unique_recovery_file(char out[APPLICATION_PATH_CAP], const char *kind) {
|
|
char cache[APPLICATION_PATH_CAP] = {0};
|
|
char directory[APPLICATION_PATH_CAP] = {0};
|
|
int written;
|
|
if (application_cache_dir(cache)) {
|
|
written = snprintf(directory, sizeof(directory), "%s/logs", cache);
|
|
if (written <= 0 || written >= (int)sizeof(directory) || !cbm_mkdir_p(directory, 0700)) {
|
|
return false;
|
|
}
|
|
} else {
|
|
written = snprintf(directory, sizeof(directory), "%s", cbm_tmpdir());
|
|
if (written <= 0 || written >= (int)sizeof(directory)) {
|
|
return false;
|
|
}
|
|
}
|
|
written = snprintf(out, APPLICATION_PATH_CAP, "%s/.daemon-index-%s-XXXXXX", directory, kind);
|
|
if (written <= 0 || written >= APPLICATION_PATH_CAP) {
|
|
out[0] = '\0';
|
|
return false;
|
|
}
|
|
application_tmp_lock();
|
|
int descriptor = cbm_mkstemp(out);
|
|
application_tmp_unlock();
|
|
if (descriptor < 0) {
|
|
out[0] = '\0';
|
|
return false;
|
|
}
|
|
(void)application_close(descriptor);
|
|
return true;
|
|
}
|
|
|
|
static bool application_recovery_files_create(char marker_path[APPLICATION_PATH_CAP],
|
|
char quarantine_path[APPLICATION_PATH_CAP]) {
|
|
if (!application_unique_recovery_file(marker_path, "marker")) {
|
|
return false;
|
|
}
|
|
if (!application_unique_recovery_file(quarantine_path, "quarantine")) {
|
|
(void)cbm_unlink(marker_path);
|
|
marker_path[0] = '\0';
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void application_recovery_files_remove(const char *marker_path,
|
|
const char *quarantine_path) {
|
|
if (marker_path && marker_path[0]) {
|
|
(void)cbm_unlink(marker_path);
|
|
}
|
|
if (quarantine_path && quarantine_path[0]) {
|
|
(void)cbm_unlink(quarantine_path);
|
|
}
|
|
}
|
|
|
|
static bool application_truncate_file(const char *path) {
|
|
FILE *file = cbm_fopen(path, "wb");
|
|
return file && fclose(file) == 0;
|
|
}
|
|
|
|
static bool application_job_cancel_requested(cbm_daemon_application_job_t *job);
|
|
|
|
static char **application_read_suspects(cbm_daemon_application_job_t *job, const char *path,
|
|
int *count_out, bool *cancelled_out) {
|
|
*count_out = 0;
|
|
*cancelled_out = false;
|
|
int64_t marker_size = cbm_file_size(path);
|
|
if (marker_size < 0 || marker_size > APPLICATION_MARKER_MAX_BYTES) {
|
|
return NULL;
|
|
}
|
|
FILE *file = cbm_fopen(path, "rb");
|
|
if (!file) {
|
|
return NULL;
|
|
}
|
|
char **open_paths = NULL;
|
|
int open_count = 0;
|
|
int open_capacity = 0;
|
|
bool allocation_failed = false;
|
|
size_t lines_read = 0;
|
|
char line[APPLICATION_PATH_CAP];
|
|
while (fgets(line, sizeof(line), file)) {
|
|
if ((lines_read++ & 255U) == 0U && application_job_cancel_requested(job)) {
|
|
*cancelled_out = true;
|
|
allocation_failed = true;
|
|
break;
|
|
}
|
|
size_t length = strlen(line);
|
|
if (length == 0 || line[length - 1] != '\n') {
|
|
break;
|
|
}
|
|
line[--length] = '\0';
|
|
if (length > 0 && line[length - 1] == '\r') {
|
|
line[--length] = '\0';
|
|
}
|
|
if (length < 3 || (line[0] != 'S' && line[0] != 'D') || line[1] != ' ') {
|
|
continue;
|
|
}
|
|
const char *relative = line + 2;
|
|
if (line[0] == 'S') {
|
|
if (open_count >= APPLICATION_MAX_SUSPECTS) {
|
|
allocation_failed = true;
|
|
break;
|
|
}
|
|
bool already_open = false;
|
|
for (int i = 0; i < open_count && !already_open; i++) {
|
|
already_open = strcmp(open_paths[i], relative) == 0;
|
|
}
|
|
if (already_open) {
|
|
continue;
|
|
}
|
|
if (open_count == open_capacity) {
|
|
int next_capacity = open_capacity ? open_capacity * 2 : 16;
|
|
char **next = realloc(open_paths, (size_t)next_capacity * sizeof(*next));
|
|
if (!next) {
|
|
allocation_failed = true;
|
|
break;
|
|
}
|
|
open_paths = next;
|
|
open_capacity = next_capacity;
|
|
}
|
|
char *copy = cbm_strdup(relative);
|
|
if (!copy) {
|
|
allocation_failed = true;
|
|
break;
|
|
}
|
|
open_paths[open_count++] = copy;
|
|
} else {
|
|
for (int i = 0; i < open_count; i++) {
|
|
if (strcmp(open_paths[i], relative) == 0) {
|
|
free(open_paths[i]);
|
|
memmove(&open_paths[i], &open_paths[i + 1],
|
|
(size_t)(open_count - i - 1) * sizeof(*open_paths));
|
|
open_count--;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
(void)fclose(file);
|
|
if (allocation_failed) {
|
|
for (int i = 0; i < open_count; i++) {
|
|
free(open_paths[i]);
|
|
}
|
|
free(open_paths);
|
|
return NULL;
|
|
}
|
|
if (open_count == 0) {
|
|
free(open_paths);
|
|
return NULL;
|
|
}
|
|
*count_out = open_count;
|
|
return open_paths;
|
|
}
|
|
|
|
static void application_free_suspects(char **suspects, int count) {
|
|
if (!suspects) {
|
|
return;
|
|
}
|
|
for (int i = 0; i < count; i++) {
|
|
free(suspects[i]);
|
|
}
|
|
free(suspects);
|
|
}
|
|
|
|
static bool application_suspect_contains(char **suspects, int count, const char *relative) {
|
|
for (int i = 0; i < count; i++) {
|
|
if (strcmp(suspects[i], relative) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool application_append_quarantine(const char *path, const char *relative,
|
|
const char *phase) {
|
|
if (!relative || !relative[0] || strpbrk(relative, "\r\n\t")) {
|
|
return false;
|
|
}
|
|
FILE *file = cbm_fopen(path, "ab");
|
|
if (!file) {
|
|
return false;
|
|
}
|
|
bool written = fprintf(file, "%s\t%s\n", relative, phase) >= 0;
|
|
return fclose(file) == 0 && written;
|
|
}
|
|
|
|
static int application_max_restarts(void) {
|
|
char value[32] = {0};
|
|
if (!cbm_safe_getenv("CBM_INDEX_MAX_RESTARTS", value, sizeof(value), NULL) || !value[0]) {
|
|
return APPLICATION_DEFAULT_MAX_RESTARTS;
|
|
}
|
|
char *end = NULL;
|
|
long parsed = strtol(value, &end, 10);
|
|
return end && *end == '\0' && parsed > 0 && parsed <= INT_MAX
|
|
? (int)parsed
|
|
: APPLICATION_DEFAULT_MAX_RESTARTS;
|
|
}
|
|
|
|
static void application_attempt_init(application_attempt_t *attempt) {
|
|
memset(attempt, 0, sizeof(*attempt));
|
|
attempt->result.outcome = CBM_PROC_SPAWN_FAILED;
|
|
attempt->result.exit_code = -1;
|
|
}
|
|
|
|
static void application_attempt_free(application_attempt_t *attempt) {
|
|
free(attempt->result.response);
|
|
attempt->result.response = NULL;
|
|
}
|
|
|
|
static bool application_job_cancel_requested(cbm_daemon_application_job_t *job) {
|
|
cbm_daemon_application_t *application = job->application;
|
|
cbm_mutex_lock(&application->mutex);
|
|
bool cancelled = job->cancel_requested || application->stopping;
|
|
cbm_mutex_unlock(&application->mutex);
|
|
return cancelled;
|
|
}
|
|
|
|
static bool application_project_keys_conflict(const char *left, const char *right) {
|
|
return left && right &&
|
|
(strcmp(left, right) == 0 || strcmp(left, "*") == 0 || strcmp(right, "*") == 0);
|
|
}
|
|
|
|
static bool application_mutation_conflicts_locked(cbm_daemon_application_t *application,
|
|
const char *project_key) {
|
|
for (cbm_daemon_application_mutation_t *mutation = application->mutations; mutation;
|
|
mutation = mutation->next) {
|
|
if (application_project_keys_conflict(mutation->project_key, project_key)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool application_job_reserves_project_locked(cbm_daemon_application_t *application,
|
|
const char *project_key) {
|
|
for (cbm_daemon_application_job_t *job = application->jobs; job; job = job->next) {
|
|
if (!job->terminal && application_project_keys_conflict(job->project_key, project_key)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool application_mutation_begin_internal(cbm_daemon_application_t *application,
|
|
cbm_daemon_application_session_t *session,
|
|
const char *project_key, bool wait) {
|
|
if (!application || !project_key || !project_key[0]) {
|
|
return false;
|
|
}
|
|
cbm_daemon_application_mutation_t *reserved = NULL;
|
|
for (;;) {
|
|
cbm_mutex_lock(&application->mutex);
|
|
bool cancelled =
|
|
application->stopping || (session && application_request_cancelled_locked(session));
|
|
bool busy = application_mutation_conflicts_locked(application, project_key) ||
|
|
application_job_reserves_project_locked(application, project_key);
|
|
if (!cancelled && !busy) {
|
|
cbm_daemon_application_mutation_t *mutation = calloc(1, sizeof(*mutation));
|
|
if (mutation) {
|
|
mutation->project_key = cbm_strdup(project_key);
|
|
}
|
|
if (!mutation || !mutation->project_key) {
|
|
if (mutation) {
|
|
free(mutation->project_key);
|
|
free(mutation);
|
|
}
|
|
cbm_mutex_unlock(&application->mutex);
|
|
return false;
|
|
}
|
|
mutation->next = application->mutations;
|
|
application->mutations = mutation;
|
|
application->active_mutations++;
|
|
cbm_mutex_unlock(&application->mutex);
|
|
reserved = mutation;
|
|
break;
|
|
}
|
|
cbm_mutex_unlock(&application->mutex);
|
|
if (cancelled || !wait) {
|
|
return false;
|
|
}
|
|
cbm_usleep(APPLICATION_JOB_POLL_US);
|
|
}
|
|
|
|
if (!application->project_locks) {
|
|
return true;
|
|
}
|
|
for (;;) {
|
|
uint64_t now = cbm_now_ms();
|
|
uint64_t deadline = now > UINT64_MAX - 100U ? UINT64_MAX : now + 100U;
|
|
cbm_project_lock_lease_t *lease = NULL;
|
|
cbm_private_file_lock_status_t status =
|
|
wait ? cbm_project_lock_acquire(application->project_locks, project_key, deadline, NULL,
|
|
&lease)
|
|
: cbm_project_lock_try_acquire(application->project_locks, project_key, &lease);
|
|
if (status == CBM_PRIVATE_FILE_LOCK_OK && lease) {
|
|
cbm_mutex_lock(&application->mutex);
|
|
bool cancelled =
|
|
application->stopping || (session && application_request_cancelled_locked(session));
|
|
cbm_mutex_unlock(&application->mutex);
|
|
if (cancelled) {
|
|
application_project_lock_release_fully(&lease);
|
|
status = CBM_PRIVATE_FILE_LOCK_BUSY;
|
|
} else {
|
|
reserved->project_lock_lease = lease;
|
|
return true;
|
|
}
|
|
}
|
|
application_project_lock_release_fully(&lease);
|
|
|
|
cbm_mutex_lock(&application->mutex);
|
|
bool cancelled =
|
|
application->stopping || (session && application_request_cancelled_locked(session));
|
|
if (status != CBM_PRIVATE_FILE_LOCK_BUSY || cancelled || !wait) {
|
|
cbm_daemon_application_mutation_t **cursor = &application->mutations;
|
|
while (*cursor && *cursor != reserved) {
|
|
cursor = &(*cursor)->next;
|
|
}
|
|
if (*cursor == reserved) {
|
|
*cursor = reserved->next;
|
|
if (application->active_mutations > 0) {
|
|
application->active_mutations--;
|
|
}
|
|
}
|
|
cbm_mutex_unlock(&application->mutex);
|
|
free(reserved->project_key);
|
|
free(reserved);
|
|
if (status != CBM_PRIVATE_FILE_LOCK_BUSY) {
|
|
cbm_log_error("daemon.project_lock_failed", "project", project_key, "action",
|
|
"refuse_mutation");
|
|
}
|
|
return false;
|
|
}
|
|
cbm_mutex_unlock(&application->mutex);
|
|
}
|
|
}
|
|
|
|
bool cbm_daemon_application_project_mutation_try_begin(cbm_daemon_application_t *application,
|
|
const char *project) {
|
|
return application_mutation_begin_internal(application, NULL, project, false);
|
|
}
|
|
|
|
void cbm_daemon_application_project_mutation_end(cbm_daemon_application_t *application,
|
|
const char *project) {
|
|
if (!application || !project || !project[0]) {
|
|
return;
|
|
}
|
|
cbm_mutex_lock(&application->mutex);
|
|
cbm_daemon_application_mutation_t *mutation = application->mutations;
|
|
while (mutation && strcmp(mutation->project_key, project) != 0) {
|
|
mutation = mutation->next;
|
|
}
|
|
if (!mutation || mutation->releasing) {
|
|
cbm_mutex_unlock(&application->mutex);
|
|
return;
|
|
}
|
|
mutation->releasing = true;
|
|
cbm_mutex_unlock(&application->mutex);
|
|
|
|
/* Keep the logical reservation visible until the native lease is gone.
|
|
* Otherwise another in-daemon operation can observe an apparently free
|
|
* project and enter the OS-lock wait during the release handoff. */
|
|
application_project_lock_release_fully(&mutation->project_lock_lease);
|
|
|
|
cbm_mutex_lock(&application->mutex);
|
|
cbm_daemon_application_mutation_t **cursor = &application->mutations;
|
|
while (*cursor && *cursor != mutation) {
|
|
cursor = &(*cursor)->next;
|
|
}
|
|
if (*cursor == mutation) {
|
|
*cursor = mutation->next;
|
|
if (application->active_mutations > 0) {
|
|
application->active_mutations--;
|
|
}
|
|
}
|
|
cbm_mutex_unlock(&application->mutex);
|
|
free(mutation->project_key);
|
|
free(mutation);
|
|
}
|
|
|
|
static bool application_watcher_mutation_begin(void *context, const char *project) {
|
|
return cbm_daemon_application_project_mutation_try_begin(context, project);
|
|
}
|
|
|
|
static void application_watcher_mutation_end(void *context, const char *project) {
|
|
cbm_daemon_application_project_mutation_end(context, project);
|
|
}
|
|
|
|
static void application_watcher_project_pruned(void *context, const char *project) {
|
|
cbm_daemon_application_t *application = context;
|
|
if (!application || !project) {
|
|
return;
|
|
}
|
|
cbm_mutex_lock(&application->mutex);
|
|
cbm_daemon_application_watch_t *watch = application_find_watch_locked(application, project);
|
|
if (watch) {
|
|
/* The watcher already removed its physical entry. Invalidate every
|
|
* logical subscriber so a later successful index can re-register it. */
|
|
application_remove_watch_entry_locked(application, watch, false);
|
|
}
|
|
cbm_mutex_unlock(&application->mutex);
|
|
}
|
|
|
|
static bool application_session_mutation_begin(void *context, const char *project) {
|
|
cbm_daemon_application_session_t *session = context;
|
|
return session &&
|
|
application_mutation_begin_internal(session->application, session, project, true);
|
|
}
|
|
|
|
static bool application_session_mutation_try_begin(void *context, const char *project) {
|
|
cbm_daemon_application_session_t *session = context;
|
|
return session &&
|
|
application_mutation_begin_internal(session->application, session, project, false);
|
|
}
|
|
|
|
static void application_session_mutation_end(void *context, const char *project) {
|
|
cbm_daemon_application_session_t *session = context;
|
|
if (session) {
|
|
cbm_daemon_application_project_mutation_end(session->application, project);
|
|
}
|
|
}
|
|
|
|
static bool application_job_wait_for_mutations(cbm_daemon_application_job_t *job) {
|
|
cbm_daemon_application_t *application = job->application;
|
|
for (;;) {
|
|
cbm_mutex_lock(&application->mutex);
|
|
bool cancelled = job->cancel_requested || application->stopping;
|
|
bool busy = application_mutation_conflicts_locked(application, job->project_key);
|
|
cbm_mutex_unlock(&application->mutex);
|
|
if (cancelled) {
|
|
return false;
|
|
}
|
|
if (!busy) {
|
|
return true;
|
|
}
|
|
cbm_usleep(APPLICATION_JOB_POLL_US);
|
|
}
|
|
}
|
|
|
|
static void application_cancelled_result(cbm_index_worker_result_t *result) {
|
|
memset(result, 0, sizeof(*result));
|
|
result->outcome = CBM_PROC_KILLED;
|
|
result->exit_code = -1;
|
|
result->cancellation_requested = true;
|
|
result->tree_quiesced = true;
|
|
}
|
|
|
|
static application_attempt_status_t application_job_run_attempt(cbm_daemon_application_job_t *job,
|
|
const char *marker_path,
|
|
const char *quarantine_path,
|
|
application_attempt_t *attempt) {
|
|
application_attempt_init(attempt);
|
|
cbm_daemon_application_t *application = job->application;
|
|
if (application_job_cancel_requested(job)) {
|
|
return APPLICATION_ATTEMPT_CANCELLED;
|
|
}
|
|
|
|
cbm_daemon_application_worker_t worker = NULL;
|
|
application_tmp_lock();
|
|
int start_result = application->worker_ops.start(
|
|
application->worker_ops.context, job->args_json, application->worker_memory_budget_bytes,
|
|
marker_path, quarantine_path, &worker);
|
|
application_tmp_unlock();
|
|
if (start_result != 0 || !worker) {
|
|
return application_job_cancel_requested(job) ? APPLICATION_ATTEMPT_CANCELLED
|
|
: APPLICATION_ATTEMPT_START_FAILED;
|
|
}
|
|
|
|
cbm_mutex_lock(&application->mutex);
|
|
job->worker = worker;
|
|
bool cancel_now = job->cancel_requested || application->stopping;
|
|
cbm_mutex_unlock(&application->mutex);
|
|
if (cancel_now) {
|
|
/* The worker thread owns this handle until destroy below. Invoke the
|
|
* external supervisor without the application mutex held. */
|
|
(void)application->worker_ops.cancel(application->worker_ops.context, worker);
|
|
}
|
|
|
|
const cbm_index_worker_result_t *borrowed = NULL;
|
|
for (;;) {
|
|
cbm_index_worker_poll_t state =
|
|
application->worker_ops.poll(application->worker_ops.context, worker, &borrowed);
|
|
if (state == CBM_INDEX_WORKER_POLL_TERMINAL) {
|
|
break;
|
|
}
|
|
cbm_mutex_lock(&application->mutex);
|
|
bool cancel_pending =
|
|
(job->cancel_requested || application->stopping) && job->worker == worker;
|
|
cbm_mutex_unlock(&application->mutex);
|
|
if (cancel_pending || state == CBM_INDEX_WORKER_POLL_ERROR) {
|
|
(void)application->worker_ops.cancel(application->worker_ops.context, worker);
|
|
}
|
|
cbm_usleep(APPLICATION_JOB_POLL_US);
|
|
}
|
|
|
|
if (borrowed) {
|
|
attempt->result = *borrowed;
|
|
attempt->result.response = borrowed->response ? cbm_strdup(borrowed->response) : NULL;
|
|
attempt->has_result = true;
|
|
}
|
|
const char *worker_log =
|
|
application->worker_ops.log_path(application->worker_ops.context, worker);
|
|
if (worker_log) {
|
|
(void)snprintf(attempt->log_path, sizeof(attempt->log_path), "%s", worker_log);
|
|
}
|
|
|
|
cbm_mutex_lock(&application->mutex);
|
|
if (job->worker == worker) {
|
|
job->worker = NULL;
|
|
}
|
|
bool cancelled = job->cancel_requested || application->stopping;
|
|
cbm_mutex_unlock(&application->mutex);
|
|
application->worker_ops.destroy(application->worker_ops.context, worker);
|
|
|
|
if (cancelled) {
|
|
if (!attempt->has_result) {
|
|
application_cancelled_result(&attempt->result);
|
|
attempt->has_result = true;
|
|
} else {
|
|
attempt->result.cancellation_requested = true;
|
|
}
|
|
}
|
|
return APPLICATION_ATTEMPT_TERMINAL;
|
|
}
|
|
|
|
static char *application_job_failure_response(const cbm_index_worker_result_t *result,
|
|
const char *log_path) {
|
|
char message[1024];
|
|
if (result && result->cancellation_requested) {
|
|
(void)snprintf(message, sizeof(message),
|
|
"index operation cancelled after its final owning session disconnected");
|
|
} else if (result && (result->supervision_failed || !result->tree_quiesced)) {
|
|
(void)snprintf(message, sizeof(message),
|
|
"index worker containment failed (%s); inspect log: %s",
|
|
cbm_proc_outcome_str(result->outcome), log_path ? log_path : "unavailable");
|
|
} else if (result) {
|
|
(void)snprintf(message, sizeof(message),
|
|
"index worker ended with %s (exit=%d, signal=%d); inspect log: %s",
|
|
cbm_proc_outcome_str(result->outcome), result->exit_code,
|
|
result->term_signal, log_path ? log_path : "unavailable");
|
|
} else {
|
|
(void)snprintf(message, sizeof(message), "index worker could not be started");
|
|
}
|
|
return cbm_mcp_text_result(message, true);
|
|
}
|
|
|
|
static cbm_mcp_supervised_result_disposition_t application_attempt_disposition(
|
|
cbm_daemon_application_job_t *job, application_attempt_t *attempt) {
|
|
if (application_job_cancel_requested(job)) {
|
|
if (!attempt->has_result) {
|
|
application_cancelled_result(&attempt->result);
|
|
attempt->has_result = true;
|
|
} else {
|
|
attempt->result.cancellation_requested = true;
|
|
}
|
|
}
|
|
return cbm_mcp_supervised_result_disposition(0, attempt->has_result ? &attempt->result : NULL);
|
|
}
|
|
|
|
static void application_record_attempt(const application_attempt_t *attempt,
|
|
cbm_index_worker_result_t *last_result,
|
|
bool *have_last_result,
|
|
char last_log[APPLICATION_PATH_CAP]) {
|
|
*last_result = attempt->result;
|
|
last_result->response = NULL;
|
|
*have_last_result = attempt->has_result;
|
|
last_log[0] = '\0';
|
|
if (attempt->log_path[0]) {
|
|
(void)snprintf(last_log, APPLICATION_PATH_CAP, "%s", attempt->log_path);
|
|
}
|
|
}
|
|
|
|
static void application_record_cancelled(cbm_index_worker_result_t *last_result,
|
|
bool *have_last_result,
|
|
char last_log[APPLICATION_PATH_CAP]) {
|
|
application_cancelled_result(last_result);
|
|
*have_last_result = true;
|
|
last_log[0] = '\0';
|
|
}
|
|
|
|
static bool application_result_is_attributable_failure(
|
|
const application_attempt_t *attempt, cbm_mcp_supervised_result_disposition_t disposition) {
|
|
return disposition == CBM_MCP_SUPERVISED_RESULT_CONTAINED_FAILURE && attempt->has_result &&
|
|
(attempt->result.outcome == CBM_PROC_CRASH || attempt->result.outcome == CBM_PROC_HANG);
|
|
}
|
|
|
|
typedef enum {
|
|
APPLICATION_ATTEMPT_DECISION_STOP = 0,
|
|
APPLICATION_ATTEMPT_DECISION_SUCCESS,
|
|
APPLICATION_ATTEMPT_DECISION_RECOVERABLE,
|
|
} application_attempt_decision_t;
|
|
|
|
typedef struct {
|
|
char *response;
|
|
bool successful;
|
|
bool unsafe_terminal;
|
|
bool supervision_failed;
|
|
cbm_index_worker_result_t last_result;
|
|
bool have_last_result;
|
|
char last_log[APPLICATION_PATH_CAP];
|
|
} application_job_execution_t;
|
|
|
|
static void application_job_execution_init(application_job_execution_t *execution) {
|
|
memset(execution, 0, sizeof(*execution));
|
|
application_cancelled_result(&execution->last_result);
|
|
}
|
|
|
|
static void application_job_execution_cancel(application_job_execution_t *execution) {
|
|
application_record_cancelled(&execution->last_result, &execution->have_last_result,
|
|
execution->last_log);
|
|
execution->unsafe_terminal = true;
|
|
}
|
|
|
|
static application_attempt_decision_t application_consume_attempt(
|
|
cbm_daemon_application_job_t *job, application_attempt_t *attempt,
|
|
application_job_execution_t *execution, cbm_proc_outcome_t *failure_outcome) {
|
|
cbm_mcp_supervised_result_disposition_t disposition =
|
|
application_attempt_disposition(job, attempt);
|
|
application_record_attempt(attempt, &execution->last_result, &execution->have_last_result,
|
|
execution->last_log);
|
|
if (disposition == CBM_MCP_SUPERVISED_RESULT_SUCCESS) {
|
|
execution->response = attempt->result.response;
|
|
attempt->result.response = NULL;
|
|
execution->successful = execution->response != NULL;
|
|
application_attempt_free(attempt);
|
|
return APPLICATION_ATTEMPT_DECISION_SUCCESS;
|
|
}
|
|
if (disposition == CBM_MCP_SUPERVISED_RESULT_UNSAFE_TERMINAL) {
|
|
execution->unsafe_terminal = true;
|
|
execution->supervision_failed =
|
|
attempt->has_result && !attempt->result.cancellation_requested &&
|
|
(attempt->result.supervision_failed || !attempt->result.tree_quiesced);
|
|
application_attempt_free(attempt);
|
|
return APPLICATION_ATTEMPT_DECISION_STOP;
|
|
}
|
|
if (application_result_is_attributable_failure(attempt, disposition)) {
|
|
*failure_outcome = attempt->result.outcome;
|
|
application_attempt_free(attempt);
|
|
return APPLICATION_ATTEMPT_DECISION_RECOVERABLE;
|
|
}
|
|
application_attempt_free(attempt);
|
|
return APPLICATION_ATTEMPT_DECISION_STOP;
|
|
}
|
|
|
|
static bool application_recovery_record_suspects(
|
|
cbm_daemon_application_job_t *job, const char *marker_path, const char *quarantine_path,
|
|
cbm_proc_outcome_t outcome, int recovery_index, char ***previous_suspects, int *previous_count,
|
|
int *quarantined, application_job_execution_t *execution) {
|
|
int suspect_count = 0;
|
|
bool cancelled = false;
|
|
char **suspects = application_read_suspects(job, marker_path, &suspect_count, &cancelled);
|
|
if (cancelled || application_job_cancel_requested(job)) {
|
|
application_free_suspects(suspects, suspect_count);
|
|
application_job_execution_cancel(execution);
|
|
return false;
|
|
}
|
|
if (!suspects || suspect_count == 0) {
|
|
application_free_suspects(suspects, suspect_count);
|
|
cbm_log_warn("daemon.index.recovery_unattributable", "action", "stop");
|
|
return false;
|
|
}
|
|
if (*previous_suspects) {
|
|
const char *pick = NULL;
|
|
for (int i = 0; i < suspect_count && !pick; i++) {
|
|
if (application_suspect_contains(*previous_suspects, *previous_count, suspects[i])) {
|
|
pick = suspects[i];
|
|
}
|
|
}
|
|
if (!pick) {
|
|
application_free_suspects(suspects, suspect_count);
|
|
cbm_log_warn("daemon.index.recovery_unattributable", "action", "stop");
|
|
return false;
|
|
}
|
|
const char *phase = outcome == CBM_PROC_HANG ? "hang" : "crash";
|
|
if (!application_append_quarantine(quarantine_path, pick, phase)) {
|
|
cbm_log_warn("daemon.index.quarantine_write_fail", "path", pick);
|
|
application_free_suspects(suspects, suspect_count);
|
|
return false;
|
|
}
|
|
(*quarantined)++;
|
|
char attempt_text[32];
|
|
(void)snprintf(attempt_text, sizeof(attempt_text), "%d", recovery_index + 1);
|
|
cbm_log_warn("daemon.index.file_quarantined", "path", pick, "outcome", phase, "attempt",
|
|
attempt_text);
|
|
}
|
|
application_free_suspects(*previous_suspects, *previous_count);
|
|
*previous_suspects = suspects;
|
|
*previous_count = suspect_count;
|
|
return true;
|
|
}
|
|
|
|
static void application_job_try_partial(cbm_daemon_application_job_t *job,
|
|
const char *quarantine_path, int quarantined,
|
|
application_job_execution_t *execution) {
|
|
if (application_job_cancel_requested(job)) {
|
|
application_job_execution_cancel(execution);
|
|
return;
|
|
}
|
|
application_attempt_t attempt;
|
|
application_attempt_status_t status =
|
|
application_job_run_attempt(job, NULL, quarantine_path, &attempt);
|
|
if (status == APPLICATION_ATTEMPT_CANCELLED) {
|
|
application_job_execution_cancel(execution);
|
|
return;
|
|
}
|
|
if (status != APPLICATION_ATTEMPT_TERMINAL) {
|
|
return;
|
|
}
|
|
cbm_proc_outcome_t failure_outcome = CBM_PROC_SPAWN_FAILED;
|
|
application_attempt_decision_t decision =
|
|
application_consume_attempt(job, &attempt, execution, &failure_outcome);
|
|
if (decision == APPLICATION_ATTEMPT_DECISION_SUCCESS) {
|
|
char quarantined_text[32];
|
|
(void)snprintf(quarantined_text, sizeof(quarantined_text), "%d", quarantined);
|
|
cbm_log_warn("daemon.index.recovery_partial", "quarantined", quarantined_text);
|
|
}
|
|
}
|
|
|
|
static void application_job_recover(cbm_daemon_application_job_t *job,
|
|
application_job_execution_t *execution) {
|
|
if (application_job_cancel_requested(job)) {
|
|
application_job_execution_cancel(execution);
|
|
return;
|
|
}
|
|
char marker_path[APPLICATION_PATH_CAP] = {0};
|
|
char quarantine_path[APPLICATION_PATH_CAP] = {0};
|
|
if (!application_recovery_files_create(marker_path, quarantine_path)) {
|
|
return;
|
|
}
|
|
|
|
int quarantined = 0;
|
|
char **previous_suspects = NULL;
|
|
int previous_count = 0;
|
|
int recovery_cap = application_max_restarts();
|
|
for (int recovery_index = 0; recovery_index < recovery_cap; recovery_index++) {
|
|
if (application_job_cancel_requested(job)) {
|
|
application_job_execution_cancel(execution);
|
|
break;
|
|
}
|
|
if (!application_truncate_file(marker_path)) {
|
|
break;
|
|
}
|
|
application_attempt_t attempt;
|
|
application_attempt_status_t status =
|
|
application_job_run_attempt(job, marker_path, quarantine_path, &attempt);
|
|
if (status == APPLICATION_ATTEMPT_CANCELLED) {
|
|
application_job_execution_cancel(execution);
|
|
break;
|
|
}
|
|
if (status != APPLICATION_ATTEMPT_TERMINAL) {
|
|
break;
|
|
}
|
|
cbm_proc_outcome_t failure_outcome = CBM_PROC_SPAWN_FAILED;
|
|
application_attempt_decision_t decision =
|
|
application_consume_attempt(job, &attempt, execution, &failure_outcome);
|
|
if (decision != APPLICATION_ATTEMPT_DECISION_RECOVERABLE ||
|
|
!application_recovery_record_suspects(
|
|
job, marker_path, quarantine_path, failure_outcome, recovery_index,
|
|
&previous_suspects, &previous_count, &quarantined, execution)) {
|
|
break;
|
|
}
|
|
}
|
|
application_free_suspects(previous_suspects, previous_count);
|
|
if (!execution->response && !execution->unsafe_terminal && quarantined > 0) {
|
|
application_job_try_partial(job, quarantine_path, quarantined, execution);
|
|
}
|
|
application_recovery_files_remove(marker_path, quarantine_path);
|
|
}
|
|
|
|
/* A capacity/cancellation conflict is resolved by the terminal publication
|
|
* that freed the project or global slot. Admit waiting session auto-index work
|
|
* at that same boundary so an otherwise-idle MCP session does not need to send
|
|
* another request merely to make background progress. Caller holds mutex. */
|
|
static void application_auto_index_retry_pending_locked(cbm_daemon_application_t *application) {
|
|
if (application->stopping) {
|
|
return;
|
|
}
|
|
for (cbm_daemon_application_session_t *session = application->sessions; session;
|
|
session = session->next) {
|
|
if (!session->auto_index_retry_pending || session->auto_index_subscribed ||
|
|
session->session_cancelled || !session->context_set) {
|
|
continue;
|
|
}
|
|
const char *project = cbm_mcp_server_session_project(session->mcp);
|
|
const char *root_path = cbm_mcp_server_session_root(session->mcp);
|
|
if (!project || !project[0] || !root_path || !root_path[0]) {
|
|
session->auto_index_retry_pending = false;
|
|
continue;
|
|
}
|
|
if (!application_session_workspace_allowed(session, "auto_index_retry")) {
|
|
session->auto_index_retry_pending = false;
|
|
application_refresh_watch_locked(session);
|
|
continue;
|
|
}
|
|
if (application_regular_db_exists(project)) {
|
|
session->auto_index_retry_pending = false;
|
|
application_refresh_watch_locked(session);
|
|
continue;
|
|
}
|
|
char *args = application_auto_index_args(root_path);
|
|
if (!args) {
|
|
continue;
|
|
}
|
|
application_job_subscribe_status_t subscribe_status = APPLICATION_JOB_SUBSCRIBE_UNAVAILABLE;
|
|
cbm_daemon_application_job_t *retry = application_job_subscribe_locked(
|
|
application, project, root_path, args, &subscribe_status);
|
|
free(args);
|
|
if (retry) {
|
|
session->auto_index_job = retry;
|
|
session->auto_index_subscribed = true;
|
|
session->auto_index_retry_pending = false;
|
|
cbm_log_info("daemon.autoindex.admission_retried", "project", project);
|
|
continue;
|
|
}
|
|
if (subscribe_status == APPLICATION_JOB_SUBSCRIBE_BUSY) {
|
|
/* No further distinct job can fit until another terminal publish. */
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void application_job_publish(cbm_daemon_application_job_t *job,
|
|
application_job_execution_t *execution) {
|
|
if (!execution->response) {
|
|
execution->response = application_job_failure_response(
|
|
execution->have_last_result ? &execution->last_result : NULL,
|
|
execution->last_log[0] ? execution->last_log : NULL);
|
|
}
|
|
cbm_daemon_application_t *application = job->application;
|
|
cbm_mutex_lock(&application->mutex);
|
|
job->response = execution->response;
|
|
job->successful = execution->successful;
|
|
job->cancelled = execution->have_last_result && execution->last_result.cancellation_requested;
|
|
job->supervision_failed = execution->supervision_failed ||
|
|
(execution->unsafe_terminal && execution->have_last_result &&
|
|
!execution->last_result.cancellation_requested);
|
|
job->terminal = true;
|
|
job->thread_done = true;
|
|
for (cbm_daemon_application_session_t *session = application->sessions; session;
|
|
session = session->next) {
|
|
if (session->auto_index_job != job || !session->auto_index_subscribed) {
|
|
continue;
|
|
}
|
|
session->auto_index_job = NULL;
|
|
session->auto_index_subscribed = false;
|
|
application_job_unsubscribe_locked(job);
|
|
if (job->successful) {
|
|
application_refresh_watch_locked(session);
|
|
}
|
|
}
|
|
application_auto_index_retry_pending_locked(application);
|
|
cbm_mutex_unlock(&application->mutex);
|
|
}
|
|
|
|
static void *application_job_thread(void *opaque) {
|
|
cbm_daemon_application_job_t *job = opaque;
|
|
application_job_execution_t execution;
|
|
application_job_execution_init(&execution);
|
|
/* Deterministic-interleaving seam: a held gate parks this thread before
|
|
* its first pre-start cancel check, so tests can force the
|
|
* cancel-wins-before-worker-start ordering that otherwise needs a
|
|
* descheduled thread on a loaded runner (CI is never a lottery: the
|
|
* interleaving must be reproducible by construction). */
|
|
while (
|
|
atomic_load_explicit(&g_application_hold_job_before_start_for_test, memory_order_acquire)) {
|
|
cbm_usleep(1000);
|
|
}
|
|
|
|
/* The linked non-terminal job is the daemon-internal reservation: it
|
|
* coalesces identical requests and blocks same-project daemon mutations.
|
|
* The physical worker is the sole owner of the cross-process project lock,
|
|
* so the daemon must not pre-acquire that same native lease here. */
|
|
if (!application_job_wait_for_mutations(job)) {
|
|
application_job_execution_cancel(&execution);
|
|
} else {
|
|
application_attempt_t attempt;
|
|
application_attempt_status_t status =
|
|
application_job_run_attempt(job, NULL, NULL, &attempt);
|
|
if (status == APPLICATION_ATTEMPT_CANCELLED) {
|
|
application_job_execution_cancel(&execution);
|
|
} else if (status == APPLICATION_ATTEMPT_TERMINAL) {
|
|
cbm_proc_outcome_t failure_outcome = CBM_PROC_SPAWN_FAILED;
|
|
application_attempt_decision_t decision =
|
|
application_consume_attempt(job, &attempt, &execution, &failure_outcome);
|
|
if (decision == APPLICATION_ATTEMPT_DECISION_RECOVERABLE) {
|
|
application_job_recover(job, &execution);
|
|
}
|
|
}
|
|
}
|
|
application_job_publish(job, &execution);
|
|
return NULL;
|
|
}
|
|
|
|
static cbm_daemon_application_job_t *application_find_job_locked(
|
|
cbm_daemon_application_t *application, const char *project_key) {
|
|
for (cbm_daemon_application_job_t *job = application->jobs; job; job = job->next) {
|
|
if (strcmp(job->project_key, project_key) == 0) {
|
|
return job;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Terminal jobs may remain linked while their original waiters copy the
|
|
* published response. They are immutable history, not coalescing targets for
|
|
* a later request of the same project. */
|
|
static cbm_daemon_application_job_t *application_find_active_job_locked(
|
|
cbm_daemon_application_t *application, const char *project_key) {
|
|
for (cbm_daemon_application_job_t *job = application->jobs; job; job = job->next) {
|
|
if (!job->terminal && strcmp(job->project_key, project_key) == 0) {
|
|
return job;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static char *application_index_project_key(const char *root_path, const char *args_json) {
|
|
char *override = cbm_mcp_get_string_arg(args_json, "name");
|
|
char *key = cbm_project_name_from_path(override && override[0] ? override : root_path);
|
|
free(override);
|
|
return key;
|
|
}
|
|
|
|
static size_t application_active_job_count_locked(cbm_daemon_application_t *application) {
|
|
size_t count = 0;
|
|
for (cbm_daemon_application_job_t *job = application->jobs; job; job = job->next) {
|
|
if (!job->terminal) {
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
/* Compare the effective index request, not its JSON spelling. yyjson's deep
|
|
* equality treats object member order as insignificant, while the small
|
|
* normalization below removes values that the index handler interprets as
|
|
* its defaults. Arrays remain order-sensitive and all non-default options are
|
|
* preserved. */
|
|
static bool application_index_args_normalize_defaults(yyjson_mut_val *root) {
|
|
if (!root || !yyjson_mut_is_obj(root)) {
|
|
return false;
|
|
}
|
|
yyjson_mut_val *mode = yyjson_mut_obj_get(root, "mode");
|
|
if (mode && (!yyjson_mut_is_str(mode) || yyjson_mut_equals_str(mode, "full"))) {
|
|
(void)yyjson_mut_obj_remove_key(root, "mode");
|
|
}
|
|
yyjson_mut_val *persistence = yyjson_mut_obj_get(root, "persistence");
|
|
if (persistence && (!yyjson_mut_is_bool(persistence) || !yyjson_mut_get_bool(persistence))) {
|
|
(void)yyjson_mut_obj_remove_key(root, "persistence");
|
|
}
|
|
yyjson_mut_val *name = yyjson_mut_obj_get(root, "name");
|
|
if (name && (!yyjson_mut_is_str(name) || yyjson_mut_get_len(name) == 0)) {
|
|
(void)yyjson_mut_obj_remove_key(root, "name");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool application_index_args_equal(const char *left, const char *right) {
|
|
if (!left || !right) {
|
|
return false;
|
|
}
|
|
yyjson_doc *left_source = yyjson_read(left, strlen(left), 0);
|
|
yyjson_doc *right_source = yyjson_read(right, strlen(right), 0);
|
|
yyjson_mut_doc *left_copy = left_source ? yyjson_doc_mut_copy(left_source, NULL) : NULL;
|
|
yyjson_mut_doc *right_copy = right_source ? yyjson_doc_mut_copy(right_source, NULL) : NULL;
|
|
yyjson_mut_val *left_root = left_copy ? yyjson_mut_doc_get_root(left_copy) : NULL;
|
|
yyjson_mut_val *right_root = right_copy ? yyjson_mut_doc_get_root(right_copy) : NULL;
|
|
bool equal = application_index_args_normalize_defaults(left_root) &&
|
|
application_index_args_normalize_defaults(right_root) &&
|
|
yyjson_mut_equals(left_root, right_root);
|
|
yyjson_mut_doc_free(left_copy);
|
|
yyjson_mut_doc_free(right_copy);
|
|
yyjson_doc_free(left_source);
|
|
yyjson_doc_free(right_source);
|
|
return equal;
|
|
}
|
|
|
|
/* Caller holds application->mutex. Keeping watcher ownership validation and
|
|
* this admission in the same critical section closes the unwatch race. */
|
|
static cbm_daemon_application_job_t *application_job_subscribe_locked(
|
|
cbm_daemon_application_t *application, const char *project_key, const char *root_path,
|
|
const char *args_json, application_job_subscribe_status_t *status_out) {
|
|
*status_out = APPLICATION_JOB_SUBSCRIBE_UNAVAILABLE;
|
|
if (application->stopping) {
|
|
return NULL;
|
|
}
|
|
cbm_daemon_application_job_t *job =
|
|
application_find_active_job_locked(application, project_key);
|
|
if (job) {
|
|
if (job->cancel_requested) {
|
|
*status_out = APPLICATION_JOB_SUBSCRIBE_CANCELLING;
|
|
return NULL;
|
|
}
|
|
if (!application_index_args_equal(job->args_json, args_json)) {
|
|
*status_out = APPLICATION_JOB_SUBSCRIBE_OPTIONS_CONFLICT;
|
|
return NULL;
|
|
}
|
|
job->subscribers++;
|
|
*status_out = APPLICATION_JOB_SUBSCRIBE_OK;
|
|
return job;
|
|
}
|
|
|
|
if (application_active_job_count_locked(application) >= application->physical_job_limit) {
|
|
char limit[32];
|
|
(void)snprintf(limit, sizeof(limit), "%zu", application->physical_job_limit);
|
|
cbm_log_warn("daemon.index.admission_busy", "limit", limit, "project", project_key);
|
|
*status_out = APPLICATION_JOB_SUBSCRIBE_BUSY;
|
|
return NULL;
|
|
}
|
|
|
|
job = calloc(1, sizeof(*job));
|
|
if (job) {
|
|
job->project_key = strdup(project_key);
|
|
job->root_path = strdup(root_path);
|
|
job->args_json = strdup(args_json);
|
|
}
|
|
if (!job || !job->project_key || !job->root_path || !job->args_json) {
|
|
application_job_free(job);
|
|
*status_out = APPLICATION_JOB_SUBSCRIBE_ALLOCATION_FAILED;
|
|
return NULL;
|
|
}
|
|
job->application = application;
|
|
job->subscribers = 1;
|
|
job->next = application->jobs;
|
|
application->jobs = job;
|
|
if (application_job_thread_create(&job->thread, job) == 0) {
|
|
job->thread_started = true;
|
|
} else {
|
|
/* The job was linked only so a concurrently started thread could
|
|
* observe its reservation. No thread exists on this path, so roll the
|
|
* reservation back synchronously and let background callers retry. */
|
|
application->jobs = job->next;
|
|
job->next = NULL;
|
|
application_job_free(job);
|
|
*status_out = APPLICATION_JOB_SUBSCRIBE_UNAVAILABLE;
|
|
cbm_log_warn("daemon.index.thread_start_failed", "action", "retry");
|
|
return NULL;
|
|
}
|
|
*status_out = APPLICATION_JOB_SUBSCRIBE_OK;
|
|
return job;
|
|
}
|
|
|
|
static cbm_daemon_application_job_t *application_job_subscribe(
|
|
cbm_daemon_application_t *application, const char *project_key, const char *root_path,
|
|
const char *args_json, application_job_subscribe_status_t *status_out) {
|
|
application_jobs_reap_completed(application);
|
|
cbm_mutex_lock(&application->mutex);
|
|
cbm_daemon_application_job_t *job = application_job_subscribe_locked(
|
|
application, project_key, root_path, args_json, status_out);
|
|
cbm_mutex_unlock(&application->mutex);
|
|
return job;
|
|
}
|
|
|
|
static void application_job_unsubscribe_locked(cbm_daemon_application_job_t *job) {
|
|
if (!job || job->subscribers == 0) {
|
|
return;
|
|
}
|
|
job->subscribers--;
|
|
if (job->subscribers == 0 && !job->terminal) {
|
|
job->cancel_requested = true;
|
|
}
|
|
}
|
|
|
|
/* Transfer a final session's subscription to the disconnect cleanup path so
|
|
* the job cannot be reaped before that path has observed terminal containment.
|
|
* Non-final subscriptions detach immediately and leave the shared worker live. */
|
|
static cbm_daemon_application_job_t *application_auto_index_release_locked(
|
|
cbm_daemon_application_session_t *session) {
|
|
if (!session || !session->auto_index_job || !session->auto_index_subscribed) {
|
|
return NULL;
|
|
}
|
|
cbm_daemon_application_job_t *job = session->auto_index_job;
|
|
session->auto_index_job = NULL;
|
|
session->auto_index_subscribed = false;
|
|
if (!job->terminal && job->subscribers == 1) {
|
|
job->cancel_requested = true;
|
|
return job;
|
|
}
|
|
application_job_unsubscribe_locked(job);
|
|
return NULL;
|
|
}
|
|
|
|
static void application_auto_index_cancel_join(cbm_daemon_application_t *application,
|
|
cbm_daemon_application_job_t *job) {
|
|
if (!application || !job) {
|
|
return;
|
|
}
|
|
uint64_t deadline = application_deadline_after(APPLICATION_BACKGROUND_REAP_MS);
|
|
for (;;) {
|
|
cbm_mutex_lock(&application->mutex);
|
|
bool terminal = job->terminal && job->thread_done;
|
|
if (terminal) {
|
|
application_job_unsubscribe_locked(job);
|
|
}
|
|
cbm_mutex_unlock(&application->mutex);
|
|
if (terminal) {
|
|
application_jobs_reap_completed(application);
|
|
return;
|
|
}
|
|
if (cbm_now_ms() >= deadline) {
|
|
application_cleanup_force_terminate("auto_index_cleanup");
|
|
}
|
|
cbm_usleep(APPLICATION_JOB_POLL_US);
|
|
}
|
|
}
|
|
|
|
static void application_update_cancel_locked(cbm_daemon_application_t *application) {
|
|
if (!application->update_generation_started || application->update_thread_done) {
|
|
return;
|
|
}
|
|
application->update_cancel_requested = true;
|
|
if (application->update_worker) {
|
|
(void)application->update_ops.cancel(application->update_ops.context,
|
|
application->update_worker);
|
|
}
|
|
}
|
|
|
|
static bool application_update_owner_release_locked(cbm_daemon_application_session_t *session) {
|
|
if (!session || !session->update_owner) {
|
|
return false;
|
|
}
|
|
cbm_daemon_application_t *application = session->application;
|
|
session->update_owner = false;
|
|
if (application->update_owners > 0) {
|
|
application->update_owners--;
|
|
}
|
|
if (application->update_owners == 0) {
|
|
application_update_cancel_locked(application);
|
|
return application->update_thread_started;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool application_update_version_valid(const char *version) {
|
|
if (!version || !version[0] || strlen(version) >= APPLICATION_UPDATE_VERSION_CAP) {
|
|
return false;
|
|
}
|
|
for (const unsigned char *cursor = (const unsigned char *)version; *cursor; cursor++) {
|
|
if (!(isalnum(*cursor) || *cursor == '.' || *cursor == '-' || *cursor == '_' ||
|
|
*cursor == '+')) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void application_update_publish_terminal_locked(cbm_daemon_application_t *application,
|
|
const char *latest_version,
|
|
bool completed_generation) {
|
|
if (!application->update_cancel_requested && application_update_version_valid(latest_version) &&
|
|
cbm_compare_versions(latest_version, cbm_cli_get_version()) > 0) {
|
|
(void)snprintf(application->update_notice, sizeof(application->update_notice),
|
|
"Update available: %s -> %s -- run: codebase-memory-mcp update | "
|
|
"Enjoying codebase-memory-mcp? Please leave a star: "
|
|
"https://github.com/DeusData/codebase-memory-mcp",
|
|
cbm_cli_get_version(), latest_version);
|
|
cbm_log_info("update.available", "current", cbm_cli_get_version(), "latest",
|
|
latest_version);
|
|
}
|
|
for (cbm_daemon_application_session_t *session = application->sessions; session;
|
|
session = session->next) {
|
|
session->update_owner = false;
|
|
}
|
|
application->update_owners = 0;
|
|
application->update_worker = NULL;
|
|
/* A clean/poll-terminal generation is immutable daemon history and is
|
|
* replayed to late sessions. Cancellation and worker-start failure did
|
|
* not perform a check, so they release the generation slot for retry once
|
|
* this thread has been joined. */
|
|
application->update_generation_started = completed_generation;
|
|
application->update_thread_done = true;
|
|
application_auto_index_retry_pending_locked(application);
|
|
}
|
|
|
|
static void *application_update_thread(void *opaque) {
|
|
cbm_daemon_application_t *application = opaque;
|
|
cbm_daemon_application_update_worker_t worker = NULL;
|
|
if (application->update_ops.start(application->update_ops.context, &worker) != 0 || !worker) {
|
|
cbm_mutex_lock(&application->mutex);
|
|
application_update_publish_terminal_locked(application, NULL, false);
|
|
cbm_mutex_unlock(&application->mutex);
|
|
return NULL;
|
|
}
|
|
|
|
cbm_mutex_lock(&application->mutex);
|
|
application->update_worker = worker;
|
|
if (application->update_cancel_requested || application->stopping ||
|
|
application->update_owners == 0) {
|
|
application_update_cancel_locked(application);
|
|
}
|
|
cbm_mutex_unlock(&application->mutex);
|
|
|
|
const char *latest_version = NULL;
|
|
for (;;) {
|
|
cbm_daemon_application_update_poll_t status =
|
|
application->update_ops.poll(application->update_ops.context, worker, &latest_version);
|
|
if (status != CBM_DAEMON_APPLICATION_UPDATE_POLL_RUNNING) {
|
|
if (status == CBM_DAEMON_APPLICATION_UPDATE_POLL_ERROR) {
|
|
latest_version = NULL;
|
|
}
|
|
break;
|
|
}
|
|
cbm_mutex_lock(&application->mutex);
|
|
if (application->update_cancel_requested || application->stopping ||
|
|
application->update_owners == 0) {
|
|
application_update_cancel_locked(application);
|
|
}
|
|
cbm_mutex_unlock(&application->mutex);
|
|
cbm_usleep(APPLICATION_UPDATE_POLL_US);
|
|
}
|
|
|
|
char version[APPLICATION_UPDATE_VERSION_CAP] = {0};
|
|
if (latest_version && strlen(latest_version) < sizeof(version)) {
|
|
(void)snprintf(version, sizeof(version), "%s", latest_version);
|
|
}
|
|
cbm_mutex_lock(&application->mutex);
|
|
bool completed_generation = !application->update_cancel_requested && !application->stopping &&
|
|
application->update_owners > 0;
|
|
application_update_publish_terminal_locked(application, version[0] ? version : NULL,
|
|
completed_generation);
|
|
cbm_mutex_unlock(&application->mutex);
|
|
application->update_ops.destroy(application->update_ops.context, worker);
|
|
return NULL;
|
|
}
|
|
|
|
static void application_update_subscribe_locked(cbm_daemon_application_session_t *session) {
|
|
cbm_daemon_application_t *application = session->application;
|
|
/* No provider, no generation. This is what makes "the daemon performs no
|
|
* network request by default" a structural property rather than a promise:
|
|
* with update_ops empty nothing is ever started, so no session can observe
|
|
* a generation, become its owner, or wait on it. */
|
|
if (!application->update_ops.start) {
|
|
return;
|
|
}
|
|
if (application->update_generation_started) {
|
|
if (application->update_thread_started && !application->update_thread_done &&
|
|
!application->update_cancel_requested && !session->update_owner) {
|
|
session->update_owner = true;
|
|
application->update_owners++;
|
|
}
|
|
return;
|
|
}
|
|
/* A retryable generation may already be terminal but not yet joined. The
|
|
* owning thread handle cannot be overwritten; the next request retries
|
|
* after application_update_reap() clears it. */
|
|
if (application->update_thread_started) {
|
|
return;
|
|
}
|
|
application->update_generation_started = true;
|
|
application->update_thread_done = false;
|
|
application->update_cancel_requested = false;
|
|
session->update_owner = true;
|
|
application->update_owners = 1;
|
|
if (cbm_thread_create(&application->update_thread, APPLICATION_JOB_THREAD_STACK,
|
|
application_update_thread, application) == 0) {
|
|
application->update_thread_started = true;
|
|
return;
|
|
}
|
|
session->update_owner = false;
|
|
application->update_owners = 0;
|
|
application->update_generation_started = false;
|
|
application->update_thread_done = false;
|
|
cbm_log_warn("daemon.update.thread_start_failed", "action", "retry");
|
|
}
|
|
|
|
static bool application_update_reap(cbm_daemon_application_t *application, bool wait,
|
|
uint32_t timeout_ms) {
|
|
if (!application) {
|
|
return false;
|
|
}
|
|
uint64_t deadline = application_deadline_after(timeout_ms);
|
|
for (;;) {
|
|
bool join = false;
|
|
cbm_mutex_lock(&application->mutex);
|
|
if (!application->update_thread_started) {
|
|
cbm_mutex_unlock(&application->mutex);
|
|
return true;
|
|
}
|
|
if (application->update_thread_done && !application->update_thread_joining) {
|
|
application->update_thread_joining = true;
|
|
join = true;
|
|
}
|
|
cbm_mutex_unlock(&application->mutex);
|
|
if (join) {
|
|
bool joined = cbm_thread_join(&application->update_thread) == 0;
|
|
cbm_mutex_lock(&application->mutex);
|
|
if (joined) {
|
|
application->update_thread_started = false;
|
|
}
|
|
application->update_thread_joining = false;
|
|
cbm_mutex_unlock(&application->mutex);
|
|
return joined;
|
|
}
|
|
if (!wait || cbm_now_ms() >= deadline) {
|
|
return false;
|
|
}
|
|
cbm_usleep(APPLICATION_UPDATE_POLL_US);
|
|
}
|
|
}
|
|
|
|
static char *application_auto_index_args(const char *root_path) {
|
|
yyjson_mut_doc *document = yyjson_mut_doc_new(NULL);
|
|
yyjson_mut_val *root = document ? yyjson_mut_obj(document) : NULL;
|
|
if (!document || !root) {
|
|
yyjson_mut_doc_free(document);
|
|
return NULL;
|
|
}
|
|
yyjson_mut_doc_set_root(document, root);
|
|
char *args = yyjson_mut_obj_add_strcpy(document, root, "repo_path", root_path)
|
|
? yyjson_mut_write(document, 0, NULL)
|
|
: NULL;
|
|
yyjson_mut_doc_free(document);
|
|
return args;
|
|
}
|
|
|
|
static void application_background_initialize_impl(cbm_daemon_application_session_t *session) {
|
|
if (!session || !session->application || !session->context_set ||
|
|
session->tool_profile != CBM_MCP_TOOL_PROFILE_ALL || session->hook_event ||
|
|
session->hook_dialect) {
|
|
return;
|
|
}
|
|
cbm_daemon_application_t *application = session->application;
|
|
const char *project = cbm_mcp_server_session_project(session->mcp);
|
|
const char *root_path = cbm_mcp_server_session_root(session->mcp);
|
|
if (!project || !project[0] || !root_path || !root_path[0]) {
|
|
return;
|
|
}
|
|
/* Join a terminal retryable update generation before reusing its single
|
|
* thread slot. This is non-blocking unless the thread already published
|
|
* terminal state. */
|
|
(void)application_update_reap(application, false, 0);
|
|
bool db_exists = application_regular_db_exists(project);
|
|
bool auto_index = application->config &&
|
|
cbm_config_get_bool(application->config, CBM_CONFIG_AUTO_INDEX, false);
|
|
int auto_index_limit =
|
|
application->config ? cbm_config_get_int(application->config, CBM_CONFIG_AUTO_INDEX_LIMIT,
|
|
CBM_MCP_DEFAULT_AUTO_INDEX_LIMIT)
|
|
: CBM_MCP_DEFAULT_AUTO_INDEX_LIMIT;
|
|
int tracked_files = -1;
|
|
bool auto_index_candidate = auto_index && !db_exists;
|
|
if (auto_index_candidate &&
|
|
!application_session_workspace_allowed(session, "auto_index_discovery")) {
|
|
auto_index_candidate = false;
|
|
}
|
|
bool within_auto_index_limit =
|
|
!auto_index_candidate ||
|
|
cbm_mcp_auto_index_within_file_limit(root_path, auto_index_limit, &tracked_files);
|
|
if (auto_index_candidate && !within_auto_index_limit) {
|
|
char files[32];
|
|
(void)snprintf(files, sizeof(files), "%d", tracked_files);
|
|
cbm_log_warn("daemon.autoindex.skipped", "project", project, "reason",
|
|
tracked_files >= 0 ? "too_many_files" : "unsafe_or_unavailable_path", "files",
|
|
files);
|
|
}
|
|
bool args_required = auto_index_candidate && within_auto_index_limit;
|
|
char *args = args_required ? application_auto_index_args(root_path) : NULL;
|
|
application_jobs_reap_completed(application);
|
|
cbm_mutex_lock(&application->mutex);
|
|
if (application->stopping || application_request_cancelled_locked(session)) {
|
|
cbm_mutex_unlock(&application->mutex);
|
|
free(args);
|
|
return;
|
|
}
|
|
session->background_eligible = true;
|
|
application_update_subscribe_locked(session);
|
|
bool attempt_auto_index = !session->auto_index_subscribed &&
|
|
(!session->auto_index_evaluated || session->auto_index_retry_pending);
|
|
if (attempt_auto_index) {
|
|
session->auto_index_evaluated = true;
|
|
session->auto_index_retry_pending = false;
|
|
}
|
|
if (attempt_auto_index && args) {
|
|
application_job_subscribe_status_t subscribe_status = APPLICATION_JOB_SUBSCRIBE_UNAVAILABLE;
|
|
cbm_daemon_application_job_t *job = application_job_subscribe_locked(
|
|
application, project, root_path, args, &subscribe_status);
|
|
if (job) {
|
|
session->auto_index_job = job;
|
|
session->auto_index_subscribed = true;
|
|
} else {
|
|
session->auto_index_retry_pending =
|
|
subscribe_status == APPLICATION_JOB_SUBSCRIBE_BUSY ||
|
|
subscribe_status == APPLICATION_JOB_SUBSCRIBE_CANCELLING ||
|
|
subscribe_status == APPLICATION_JOB_SUBSCRIBE_OPTIONS_CONFLICT ||
|
|
subscribe_status == APPLICATION_JOB_SUBSCRIBE_UNAVAILABLE ||
|
|
subscribe_status == APPLICATION_JOB_SUBSCRIBE_ALLOCATION_FAILED;
|
|
cbm_log_warn("daemon.autoindex.admission_failed", "project", project, "action",
|
|
session->auto_index_retry_pending ? "retry" : "skip");
|
|
}
|
|
} else if (attempt_auto_index && args_required && !args) {
|
|
/* JSON allocation is a transient resource failure, not a permanent
|
|
* policy decision. Retry at the next coordinator opportunity. */
|
|
session->auto_index_retry_pending = true;
|
|
}
|
|
cbm_mutex_unlock(&application->mutex);
|
|
free(args);
|
|
application_refresh_watch(session);
|
|
}
|
|
|
|
static void application_background_initialize(cbm_daemon_application_session_t *session) {
|
|
application_background_initialize_impl(session);
|
|
atomic_fetch_add_explicit(&g_application_background_initializes_for_test, 1,
|
|
memory_order_release);
|
|
}
|
|
|
|
static bool application_jsonrpc_success(const char *response) {
|
|
yyjson_doc *document = response ? yyjson_read(response, strlen(response), 0) : NULL;
|
|
yyjson_val *root = document ? yyjson_doc_get_root(document) : NULL;
|
|
bool success = yyjson_is_obj(root) && yyjson_obj_get(root, "result") != NULL &&
|
|
yyjson_obj_get(root, "error") == NULL;
|
|
yyjson_doc_free(document);
|
|
return success;
|
|
}
|
|
|
|
static void application_update_notice_inject(cbm_daemon_application_session_t *session,
|
|
char **response_io) {
|
|
cbm_daemon_application_t *application = session->application;
|
|
cbm_mutex_lock(&application->mutex);
|
|
if (session->background_eligible && !session->update_notice_delivered &&
|
|
!session->pending_update_notice && application->update_thread_done &&
|
|
application->update_notice[0] &&
|
|
cbm_mcp_jsonrpc_response_prepend_notice(response_io, application->update_notice)) {
|
|
session->pending_update_notice = true;
|
|
}
|
|
cbm_mutex_unlock(&application->mutex);
|
|
}
|
|
|
|
static bool application_watch_job_subscription_exists_locked(
|
|
cbm_daemon_application_t *application, cbm_daemon_application_session_t *session,
|
|
cbm_daemon_application_job_t *job) {
|
|
for (cbm_daemon_application_watch_job_subscription_t *subscription =
|
|
application->watch_job_subscriptions;
|
|
subscription; subscription = subscription->next) {
|
|
if (subscription->session == session && subscription->job == job) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* Attach a newly registered logical watch to a watcher callback that was
|
|
* already admitted for the same project/root. Without this step, closing the
|
|
* pre-existing owners can cancel the physical worker while this late session
|
|
* still expects the shared watch to remain live. Caller holds the mutex. */
|
|
static bool application_watch_job_subscribe_late_session_locked(
|
|
cbm_daemon_application_session_t *session, cbm_daemon_application_watch_t *watch) {
|
|
if (!session || !watch || !session->application) {
|
|
return false;
|
|
}
|
|
cbm_daemon_application_t *application = session->application;
|
|
cbm_daemon_application_job_t *job =
|
|
application_find_active_job_locked(application, watch->project);
|
|
if (!job || job->watcher_waiters == 0 || strcmp(job->root_path, watch->root) != 0 ||
|
|
application_watch_job_subscription_exists_locked(application, session, job)) {
|
|
return true;
|
|
}
|
|
cbm_daemon_application_watch_job_subscription_t *subscription =
|
|
calloc(1, sizeof(*subscription));
|
|
if (!subscription) {
|
|
return false;
|
|
}
|
|
subscription->session = session;
|
|
subscription->job = job;
|
|
subscription->next = application->watch_job_subscriptions;
|
|
application->watch_job_subscriptions = subscription;
|
|
job->subscribers++;
|
|
return true;
|
|
}
|
|
|
|
/* Caller holds application->mutex. Allocate the complete change before
|
|
* publishing any node so an allocation failure never leaves only a subset of
|
|
* the exact live watch owners subscribed. */
|
|
static bool application_watch_job_subscribe_sessions_locked(cbm_daemon_application_t *application,
|
|
cbm_daemon_application_watch_t *watch,
|
|
cbm_daemon_application_job_t *job,
|
|
size_t *matched_out) {
|
|
*matched_out = 0;
|
|
if (!watch || !job || strcmp(watch->project, job->project_key) != 0 ||
|
|
strcmp(watch->root, job->root_path) != 0) {
|
|
return true;
|
|
}
|
|
|
|
cbm_daemon_application_watch_job_subscription_t *pending = NULL;
|
|
for (cbm_daemon_application_session_t *session = application->sessions; session;
|
|
session = session->next) {
|
|
if (session->session_cancelled || !session->context_set || session->watch != watch) {
|
|
continue;
|
|
}
|
|
(*matched_out)++;
|
|
if (application_watch_job_subscription_exists_locked(application, session, job)) {
|
|
continue;
|
|
}
|
|
cbm_daemon_application_watch_job_subscription_t *subscription =
|
|
calloc(1, sizeof(*subscription));
|
|
if (!subscription) {
|
|
while (pending) {
|
|
cbm_daemon_application_watch_job_subscription_t *next = pending->next;
|
|
free(pending);
|
|
pending = next;
|
|
}
|
|
return false;
|
|
}
|
|
subscription->session = session;
|
|
subscription->job = job;
|
|
subscription->next = pending;
|
|
pending = subscription;
|
|
}
|
|
|
|
while (pending) {
|
|
cbm_daemon_application_watch_job_subscription_t *subscription = pending;
|
|
pending = pending->next;
|
|
subscription->next = application->watch_job_subscriptions;
|
|
application->watch_job_subscriptions = subscription;
|
|
job->subscribers++;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void application_watch_job_unsubscribe_session_locked(
|
|
cbm_daemon_application_session_t *session) {
|
|
if (!session || !session->application) {
|
|
return;
|
|
}
|
|
cbm_daemon_application_watch_job_subscription_t **cursor =
|
|
&session->application->watch_job_subscriptions;
|
|
while (*cursor) {
|
|
cbm_daemon_application_watch_job_subscription_t *subscription = *cursor;
|
|
if (subscription->session != session) {
|
|
cursor = &subscription->next;
|
|
continue;
|
|
}
|
|
*cursor = subscription->next;
|
|
application_job_unsubscribe_locked(subscription->job);
|
|
free(subscription);
|
|
}
|
|
}
|
|
|
|
static void application_watch_job_unsubscribe_job_locked(cbm_daemon_application_t *application,
|
|
cbm_daemon_application_job_t *job) {
|
|
cbm_daemon_application_watch_job_subscription_t **cursor =
|
|
&application->watch_job_subscriptions;
|
|
while (*cursor) {
|
|
cbm_daemon_application_watch_job_subscription_t *subscription = *cursor;
|
|
if (subscription->job != job) {
|
|
cursor = &subscription->next;
|
|
continue;
|
|
}
|
|
*cursor = subscription->next;
|
|
application_job_unsubscribe_locked(job);
|
|
free(subscription);
|
|
}
|
|
}
|
|
|
|
static char *application_job_wait_for_session(cbm_daemon_application_session_t *session,
|
|
cbm_daemon_application_job_t *job) {
|
|
cbm_daemon_application_t *application = session->application;
|
|
for (;;) {
|
|
cbm_mutex_lock(&application->mutex);
|
|
if (session->active_job != job || !session->active_job_subscribed) {
|
|
cbm_mutex_unlock(&application->mutex);
|
|
return cbm_mcp_text_result("index operation cancelled for this session", true);
|
|
}
|
|
if (job->terminal) {
|
|
char *response = job->response ? strdup(job->response) : NULL;
|
|
session->active_job = NULL;
|
|
session->active_job_subscribed = false;
|
|
application_job_unsubscribe_locked(job);
|
|
cbm_mutex_unlock(&application->mutex);
|
|
application_jobs_reap_completed(application);
|
|
return response ? response
|
|
: cbm_mcp_text_result("index coordinator lost its result", true);
|
|
}
|
|
cbm_mutex_unlock(&application->mutex);
|
|
cbm_usleep(APPLICATION_JOB_POLL_US);
|
|
}
|
|
}
|
|
|
|
static char *application_index_execute(void *context, const char *root_path,
|
|
const char *args_json) {
|
|
cbm_daemon_application_session_t *session = context;
|
|
if (!session || !root_path || !args_json) {
|
|
return NULL;
|
|
}
|
|
char *project_key = application_index_project_key(root_path, args_json);
|
|
if (!project_key) {
|
|
return cbm_mcp_text_result("failed to derive index project identity", true);
|
|
}
|
|
application_job_subscribe_status_t subscribe_status = APPLICATION_JOB_SUBSCRIBE_UNAVAILABLE;
|
|
cbm_daemon_application_job_t *job = NULL;
|
|
for (;;) {
|
|
job = application_job_subscribe(session->application, project_key, root_path, args_json,
|
|
&subscribe_status);
|
|
if (job || (subscribe_status != APPLICATION_JOB_SUBSCRIBE_BUSY &&
|
|
subscribe_status != APPLICATION_JOB_SUBSCRIBE_CANCELLING)) {
|
|
break;
|
|
}
|
|
/* Physical job limit reached (or a same-project cancel still
|
|
* draining): QUEUE instead of surfacing a raw busy error. The
|
|
* request thread blocks for the whole index anyway, so waiting for
|
|
* a slot is the same contract — and the wait stays cancellable and
|
|
* shutdown-aware (a stopping coordinator answers UNAVAILABLE, which
|
|
* exits this loop with the error below). */
|
|
atomic_fetch_add_explicit(&g_application_busy_queue_waits_for_test, 1,
|
|
memory_order_release);
|
|
cbm_mutex_lock(&session->application->mutex);
|
|
bool queued_cancelled = application_request_cancelled_locked(session);
|
|
cbm_mutex_unlock(&session->application->mutex);
|
|
if (queued_cancelled) {
|
|
free(project_key);
|
|
return cbm_mcp_text_result("index operation cancelled for this session", true);
|
|
}
|
|
cbm_usleep(APPLICATION_JOB_POLL_US);
|
|
}
|
|
free(project_key);
|
|
if (!job) {
|
|
const char *message = "daemon index coordinator is stopping or unavailable";
|
|
if (subscribe_status == APPLICATION_JOB_SUBSCRIBE_OPTIONS_CONFLICT) {
|
|
message = "another index operation for this project is active with different options";
|
|
} else if (subscribe_status == APPLICATION_JOB_SUBSCRIBE_ALLOCATION_FAILED) {
|
|
message = "daemon index coordinator could not allocate an index job";
|
|
}
|
|
return cbm_mcp_text_result(message, true);
|
|
}
|
|
cbm_mutex_lock(&session->application->mutex);
|
|
if (application_request_cancelled_locked(session)) {
|
|
application_job_unsubscribe_locked(job);
|
|
cbm_mutex_unlock(&session->application->mutex);
|
|
return cbm_mcp_text_result("index operation cancelled for this session", true);
|
|
}
|
|
if (session->active_job) {
|
|
application_job_unsubscribe_locked(job);
|
|
cbm_mutex_unlock(&session->application->mutex);
|
|
return cbm_mcp_text_result("this session already has an active index operation", true);
|
|
}
|
|
session->active_job = job;
|
|
session->active_job_subscribed = true;
|
|
cbm_mutex_unlock(&session->application->mutex);
|
|
return application_job_wait_for_session(session, job);
|
|
}
|
|
|
|
static cbm_daemon_runtime_application_session_t *application_session_open(
|
|
void *context, cbm_daemon_client_id_t client_id, uint64_t authenticated_process_id) {
|
|
cbm_daemon_application_t *application = context;
|
|
if (!application || client_id == CBM_DAEMON_CLIENT_ID_INVALID ||
|
|
authenticated_process_id == 0) {
|
|
return NULL;
|
|
}
|
|
cbm_daemon_application_session_t *session = calloc(1, sizeof(*session));
|
|
if (!session) {
|
|
return NULL;
|
|
}
|
|
session->mcp = cbm_mcp_server_new(NULL);
|
|
if (!session->mcp || !cbm_mcp_server_release_pristine_memory_store(session->mcp)) {
|
|
cbm_mcp_server_free(session->mcp);
|
|
free(session);
|
|
return NULL;
|
|
}
|
|
cbm_mcp_server_set_background_tasks(session->mcp, false);
|
|
cbm_mcp_server_set_config(session->mcp, application->config);
|
|
cbm_mcp_server_set_index_executor(session->mcp, application_index_execute, session);
|
|
cbm_mcp_server_set_project_mutation_guard(session->mcp, application_session_mutation_begin,
|
|
application_session_mutation_end, session);
|
|
cbm_mcp_server_set_project_mutation_try_guard(session->mcp,
|
|
application_session_mutation_try_begin);
|
|
session->tool_profile = CBM_MCP_TOOL_PROFILE_ALL;
|
|
session->application = application;
|
|
session->client_id = client_id;
|
|
session->authenticated_process_id = authenticated_process_id;
|
|
|
|
cbm_mutex_lock(&application->mutex);
|
|
if (application->stopping) {
|
|
cbm_mutex_unlock(&application->mutex);
|
|
cbm_mcp_server_free(session->mcp);
|
|
free(session);
|
|
return NULL;
|
|
}
|
|
session->next = application->sessions;
|
|
application->sessions = session;
|
|
cbm_mutex_unlock(&application->mutex);
|
|
return (cbm_daemon_runtime_application_session_t *)session;
|
|
}
|
|
|
|
static cbm_daemon_runtime_application_status_t application_set_context(
|
|
cbm_daemon_application_session_t *session, const uint8_t *request, uint32_t request_length) {
|
|
if (session->context_set || request_length < APPLICATION_CONTEXT_HEADER_SIZE) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
uint32_t root_length = application_get_u32(request + 1);
|
|
bool allowed_present = request[5] == 1;
|
|
uint32_t allowed_length = application_get_u32(request + 6);
|
|
uint8_t profile_value = request[10];
|
|
uint32_t event_length = application_get_u32(request + 11);
|
|
uint32_t dialect_length = application_get_u32(request + 15);
|
|
uint64_t expected = (uint64_t)APPLICATION_CONTEXT_HEADER_SIZE + root_length + allowed_length +
|
|
event_length + dialect_length;
|
|
if (request[5] > 1 || root_length == 0 || expected != request_length ||
|
|
(!allowed_present && allowed_length != 0) ||
|
|
profile_value > (uint8_t)CBM_MCP_TOOL_PROFILE_SCOUT ||
|
|
(profile_value != (uint8_t)CBM_MCP_TOOL_PROFILE_ALL &&
|
|
(event_length != 0 || dialect_length != 0))) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
cbm_mcp_tool_profile_t tool_profile = (cbm_mcp_tool_profile_t)profile_value;
|
|
const uint8_t *payload = request + APPLICATION_CONTEXT_HEADER_SIZE;
|
|
char *root = application_text_copy(request + APPLICATION_CONTEXT_HEADER_SIZE, root_length);
|
|
char *allowed =
|
|
allowed_present ? application_text_copy(payload + root_length, allowed_length) : NULL;
|
|
char *hook_event =
|
|
event_length ? application_text_copy(payload + root_length + allowed_length, event_length)
|
|
: NULL;
|
|
char *hook_dialect =
|
|
dialect_length ? application_text_copy(
|
|
payload + root_length + allowed_length + event_length, dialect_length)
|
|
: NULL;
|
|
if (!root || (allowed_present && !allowed) || (event_length && !hook_event) ||
|
|
(dialect_length && !hook_dialect) ||
|
|
!cbm_hook_augment_invocation_supported(hook_event, hook_dialect)) {
|
|
free(root);
|
|
free(allowed);
|
|
free(hook_event);
|
|
free(hook_dialect);
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
char canonical_root[APPLICATION_PATH_CAP] = {0};
|
|
char canonical_allowed[APPLICATION_PATH_CAP] = {0};
|
|
bool canonical = cbm_canonical_path(root, canonical_root, sizeof(canonical_root));
|
|
if (canonical && allowed_present) {
|
|
canonical = cbm_canonical_path(allowed, canonical_allowed, sizeof(canonical_allowed));
|
|
}
|
|
struct stat root_status;
|
|
canonical =
|
|
canonical && stat(canonical_root, &root_status) == 0 && S_ISDIR(root_status.st_mode);
|
|
bool set =
|
|
canonical && cbm_mcp_server_set_session_context(session->mcp, canonical_root,
|
|
allowed_present ? canonical_allowed : NULL);
|
|
free(root);
|
|
free(allowed);
|
|
if (!set) {
|
|
free(hook_event);
|
|
free(hook_dialect);
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
cbm_mcp_server_set_tool_profile(session->mcp, tool_profile);
|
|
session->tool_profile = tool_profile;
|
|
session->hook_event = hook_event;
|
|
session->hook_dialect = hook_dialect;
|
|
session->context_set = true;
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_OK;
|
|
}
|
|
|
|
/* Build the JSON-RPC error that replaces a reply too large to frame.
|
|
*
|
|
* Says the two numbers a caller needs to act — what the reply measured and what
|
|
* fits — because the alternative the reporter lived through was a silent exit
|
|
* with nothing to go on. The advice is concrete for the same reason: "narrow the
|
|
* projection or add LIMIT" is actionable, "internal error" is not.
|
|
*
|
|
* `request` may be NULL when the message did not parse; the response then omits
|
|
* the id, which is what JSON-RPC requires for an unidentifiable request. */
|
|
static char *application_oversized_response_error(const cbm_jsonrpc_request_t *request,
|
|
size_t response_length) {
|
|
char message[CBM_SZ_256];
|
|
(void)snprintf(message, sizeof(message),
|
|
"response too large: %zu bytes exceeds the %u byte transport limit; "
|
|
"narrow the projection, add LIMIT, or paginate",
|
|
response_length, (unsigned)CBM_DAEMON_RUNTIME_APPLICATION_PAYLOAD_MAX);
|
|
|
|
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
|
|
if (!doc) {
|
|
return NULL;
|
|
}
|
|
yyjson_mut_val *err = yyjson_mut_obj(doc);
|
|
yyjson_mut_doc_set_root(doc, err);
|
|
/* -32603 (internal error) is the closest standard code: the request was
|
|
* well-formed and the failure is server-side. */
|
|
yyjson_mut_obj_add_int(doc, err, "code", -32603);
|
|
yyjson_mut_obj_add_str(doc, err, "message", message);
|
|
char *error_json = yyjson_mut_write(doc, 0, NULL);
|
|
yyjson_mut_doc_free(doc);
|
|
if (!error_json) {
|
|
return NULL;
|
|
}
|
|
|
|
cbm_jsonrpc_response_t response = {
|
|
.id = request && request->has_id ? request->id : 0,
|
|
.id_str = request ? request->id_str : NULL,
|
|
.error_json = error_json,
|
|
.error_code = -32603,
|
|
};
|
|
char *encoded = cbm_jsonrpc_format_response(&response);
|
|
free(error_json);
|
|
return encoded;
|
|
}
|
|
|
|
/* Decide whether `response` can be framed, substituting the error when it
|
|
* cannot. Owns `response` either way and returns the reply to send, or NULL
|
|
* only on allocation failure. This is ONE function on purpose: the bug was the
|
|
* DECISION (an oversized reply travelling on as if it were fine), so the
|
|
* decision and the substitution have to be testable together — a test that
|
|
* only builds the error passes even with the size check removed. */
|
|
static char *application_framable_response(char *response, const cbm_jsonrpc_request_t *request) {
|
|
if (!response || strlen(response) <= CBM_DAEMON_RUNTIME_APPLICATION_PAYLOAD_MAX) {
|
|
return response;
|
|
}
|
|
char *replacement = application_oversized_response_error(request, strlen(response));
|
|
free(response);
|
|
return replacement;
|
|
}
|
|
|
|
char *cbm_daemon_application_framable_response_for_test(char *response,
|
|
const cbm_jsonrpc_request_t *request) {
|
|
return application_framable_response(response, request);
|
|
}
|
|
|
|
static cbm_daemon_runtime_application_status_t application_mcp_request(
|
|
cbm_daemon_application_session_t *session, const uint8_t *request, uint32_t request_length,
|
|
uint8_t **response_out, uint32_t *response_length_out) {
|
|
if (!session->context_set || request_length <= 1) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
char *message = application_text_copy(request + 1, request_length - 1);
|
|
if (!message) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
cbm_jsonrpc_request_t parsed = {0};
|
|
bool parsed_ok = cbm_jsonrpc_parse(message, &parsed) == 0;
|
|
bool initialize_request =
|
|
parsed_ok && parsed.has_id && parsed.method && strcmp(parsed.method, "initialize") == 0;
|
|
bool tool_request =
|
|
parsed_ok && parsed.has_id && parsed.method && strcmp(parsed.method, "tools/call") == 0;
|
|
char *response = cbm_mcp_server_handle(session->mcp, message);
|
|
free(message);
|
|
if (initialize_request && application_jsonrpc_success(response)) {
|
|
cbm_mutex_lock(&session->application->mutex);
|
|
session->pending_background_initialize = true;
|
|
cbm_mutex_unlock(&session->application->mutex);
|
|
} else if (tool_request && response) {
|
|
application_update_notice_inject(session, &response);
|
|
}
|
|
if (response) {
|
|
size_t response_length = strlen(response);
|
|
if (response_length > UINT32_MAX) {
|
|
if (parsed_ok) {
|
|
cbm_jsonrpc_request_free(&parsed);
|
|
}
|
|
free(response);
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR;
|
|
}
|
|
/* A reply larger than one frame has to become a JSON-RPC error HERE,
|
|
* while the request id is still in hand. Handing it to the transport
|
|
* instead is indistinguishable from a dead socket at the frontend
|
|
* worker, which responds by _Exit()ing the process — right for a broken
|
|
* transport, a fatal over-reaction to a large answer. Under an MCP host
|
|
* that does not respawn the server, that took every tool on it out for
|
|
* the rest of the session, leaving exit=1 and an empty stderr to debug
|
|
* from (#1375). */
|
|
response = application_framable_response(response, parsed_ok ? &parsed : NULL);
|
|
if (!response) {
|
|
if (parsed_ok) {
|
|
cbm_jsonrpc_request_free(&parsed);
|
|
}
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR;
|
|
}
|
|
response_length = strlen(response);
|
|
*response_out = (uint8_t *)response;
|
|
*response_length_out = (uint32_t)response_length;
|
|
}
|
|
if (parsed_ok) {
|
|
cbm_jsonrpc_request_free(&parsed);
|
|
}
|
|
application_refresh_watch(session);
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_OK;
|
|
}
|
|
|
|
static cbm_daemon_runtime_application_status_t application_tool_request(
|
|
cbm_daemon_application_session_t *session, const uint8_t *request, uint32_t request_length,
|
|
uint8_t **response_out, uint32_t *response_length_out) {
|
|
if (!session->context_set || request_length <= APPLICATION_TOOL_HEADER_SIZE) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
uint32_t tool_length = application_get_u32(request + 1);
|
|
if (tool_length == 0 || tool_length >= request_length - APPLICATION_TOOL_HEADER_SIZE) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
char *tool = application_text_copy(request + APPLICATION_TOOL_HEADER_SIZE, tool_length);
|
|
uint32_t args_length = request_length - APPLICATION_TOOL_HEADER_SIZE - tool_length;
|
|
char *args =
|
|
application_text_copy(request + APPLICATION_TOOL_HEADER_SIZE + tool_length, args_length);
|
|
if (!tool || !args) {
|
|
free(tool);
|
|
free(args);
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
char *response = cbm_mcp_handle_tool(session->mcp, tool, args);
|
|
free(tool);
|
|
free(args);
|
|
if (!response) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR;
|
|
}
|
|
size_t response_length = strlen(response);
|
|
if (response_length > UINT32_MAX) {
|
|
free(response);
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR;
|
|
}
|
|
*response_out = (uint8_t *)response;
|
|
*response_length_out = (uint32_t)response_length;
|
|
application_refresh_watch(session);
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_OK;
|
|
}
|
|
|
|
static cbm_daemon_runtime_application_status_t application_set_ui_config(
|
|
cbm_daemon_application_t *application, cbm_daemon_application_session_t *session,
|
|
const uint8_t *request, uint32_t request_length) {
|
|
const uint8_t valid_mask =
|
|
CBM_DAEMON_APPLICATION_UI_CONFIG_ENABLED | CBM_DAEMON_APPLICATION_UI_CONFIG_PORT;
|
|
if (!session || !session->context_set || session->tool_profile != CBM_MCP_TOOL_PROFILE_ALL ||
|
|
request_length != APPLICATION_UI_CONFIG_REQUEST_SIZE) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
uint8_t update_mask = request[1];
|
|
uint8_t enabled = request[2];
|
|
uint32_t port = application_get_u32(request + 3);
|
|
bool enabled_present = (update_mask & CBM_DAEMON_APPLICATION_UI_CONFIG_ENABLED) != 0;
|
|
bool port_present = (update_mask & CBM_DAEMON_APPLICATION_UI_CONFIG_PORT) != 0;
|
|
if (update_mask == 0 || (update_mask & (uint8_t)~valid_mask) != 0 ||
|
|
(enabled_present ? enabled > 1U : enabled != 0U) ||
|
|
(port_present ? port == 0 || request[3] != 0 || request[4] != 0 : port != 0U)) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
|
|
cbm_mutex_lock(&application->mutex);
|
|
if (application->stopping) {
|
|
cbm_mutex_unlock(&application->mutex);
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_UNAVAILABLE;
|
|
}
|
|
cbm_ui_config_t config;
|
|
cbm_ui_config_load(&config);
|
|
if (enabled_present) {
|
|
config.ui_enabled = enabled != 0;
|
|
}
|
|
if (port_present) {
|
|
config.ui_port = (int)port;
|
|
}
|
|
bool saved = cbm_ui_config_save(&config);
|
|
cbm_mutex_unlock(&application->mutex);
|
|
return saved ? CBM_DAEMON_RUNTIME_APPLICATION_OK : CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR;
|
|
}
|
|
|
|
static cbm_daemon_runtime_application_status_t application_ui_readiness_proof(
|
|
cbm_daemon_application_t *application, const uint8_t *request, uint32_t request_length,
|
|
uint8_t **response_out, uint32_t *response_length_out) {
|
|
if (!application->ui_readiness_secret_set ||
|
|
request_length != APPLICATION_UI_READINESS_REQUEST_SIZE) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
uint8_t *proof = malloc(CBM_SHA256_DIGEST_LEN);
|
|
if (!proof) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR;
|
|
}
|
|
cbm_hmac_sha256(application->ui_readiness_secret, sizeof(application->ui_readiness_secret),
|
|
request + 1, CBM_SHA256_DIGEST_LEN, proof);
|
|
*response_out = proof;
|
|
*response_length_out = CBM_SHA256_DIGEST_LEN;
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_OK;
|
|
}
|
|
|
|
static cbm_daemon_runtime_application_status_t application_request_dispatch(
|
|
cbm_daemon_application_t *application, cbm_daemon_application_session_t *session,
|
|
const uint8_t *request, uint32_t request_length, uint8_t **response_out,
|
|
uint32_t *response_length_out) {
|
|
switch ((cbm_daemon_application_request_kind_t)request[0]) {
|
|
case CBM_DAEMON_APPLICATION_REQUEST_SET_CONTEXT:
|
|
return application_set_context(session, request, request_length);
|
|
case CBM_DAEMON_APPLICATION_REQUEST_MCP:
|
|
return application_mcp_request(session, request, request_length, response_out,
|
|
response_length_out);
|
|
case CBM_DAEMON_APPLICATION_REQUEST_TOOL:
|
|
return application_tool_request(session, request, request_length, response_out,
|
|
response_length_out);
|
|
case CBM_DAEMON_APPLICATION_REQUEST_SET_UI_CONFIG:
|
|
return application_set_ui_config(application, session, request, request_length);
|
|
case CBM_DAEMON_APPLICATION_REQUEST_UI_READINESS_PROOF:
|
|
return application_ui_readiness_proof(application, request, request_length, response_out,
|
|
response_length_out);
|
|
case CBM_DAEMON_APPLICATION_REQUEST_HOOK_AUGMENT: {
|
|
if (!session->context_set || request_length <= 1) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
char *input = application_text_copy(request + 1, request_length - 1);
|
|
if (!input) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
char *response = cbm_hook_augment_process_for(session->mcp, input, session->hook_event,
|
|
session->hook_dialect);
|
|
free(input);
|
|
if (response) {
|
|
size_t response_length = strlen(response);
|
|
if (response_length > UINT32_MAX) {
|
|
free(response);
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR;
|
|
}
|
|
*response_out = (uint8_t *)response;
|
|
*response_length_out = (uint32_t)response_length;
|
|
}
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_OK;
|
|
}
|
|
default:
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
}
|
|
|
|
static cbm_daemon_runtime_application_status_t application_request(
|
|
void *context, cbm_daemon_runtime_application_session_t *opaque_session,
|
|
cbm_daemon_runtime_application_token_t request_token, const uint8_t *request,
|
|
uint32_t request_length, uint8_t **response_out, uint32_t *response_length_out) {
|
|
cbm_daemon_application_t *application = context;
|
|
cbm_daemon_application_session_t *session = (cbm_daemon_application_session_t *)opaque_session;
|
|
if (response_out) {
|
|
*response_out = NULL;
|
|
}
|
|
if (response_length_out) {
|
|
*response_length_out = 0;
|
|
}
|
|
if (!application || !session || session->application != application ||
|
|
request_token == CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID || !request ||
|
|
request_length == 0 || !response_out || !response_length_out) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
|
|
cbm_mutex_lock(&application->mutex);
|
|
if (application->stopping) {
|
|
cbm_mutex_unlock(&application->mutex);
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_UNAVAILABLE;
|
|
}
|
|
if (session->session_cancelled) {
|
|
cbm_mutex_unlock(&application->mutex);
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
if (session->request_active) {
|
|
cbm_mutex_unlock(&application->mutex);
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR;
|
|
}
|
|
if (session->request_cancel_token != request_token) {
|
|
session->request_cancel_token = CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID;
|
|
}
|
|
session->request_active = true;
|
|
session->active_request_token = request_token;
|
|
bool mcp_scope_started = cbm_mcp_server_request_scope_begin(session->mcp);
|
|
if (!mcp_scope_started) {
|
|
session->request_active = false;
|
|
session->active_request_token = CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID;
|
|
cbm_mutex_unlock(&application->mutex);
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR;
|
|
}
|
|
bool cancelled_before_entry = session->request_cancel_token == request_token;
|
|
cbm_mutex_unlock(&application->mutex);
|
|
|
|
cbm_daemon_runtime_application_status_t status =
|
|
cancelled_before_entry
|
|
? CBM_DAEMON_RUNTIME_APPLICATION_CANCELLED
|
|
: application_request_dispatch(application, session, request, request_length,
|
|
response_out, response_length_out);
|
|
|
|
/* This mutex boundary is the cancellation/completion linearization point.
|
|
* A matching cancel published before it wins; a later cancel is stale and
|
|
* cannot affect the next unique request token. */
|
|
cbm_mutex_lock(&application->mutex);
|
|
bool cancelled = session->session_cancelled || session->request_cancel_token == request_token;
|
|
cbm_mcp_server_request_scope_end(session->mcp);
|
|
session->request_active = false;
|
|
session->active_request_token = CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID;
|
|
if (session->request_cancel_token == request_token) {
|
|
session->request_cancel_token = CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID;
|
|
}
|
|
bool activate_background =
|
|
!cancelled && status == CBM_DAEMON_RUNTIME_APPLICATION_OK &&
|
|
(session->pending_background_initialize ||
|
|
(session->background_eligible &&
|
|
(session->auto_index_retry_pending ||
|
|
(!application->update_generation_started && !session->update_owner))));
|
|
if (!cancelled && status == CBM_DAEMON_RUNTIME_APPLICATION_OK &&
|
|
session->pending_update_notice) {
|
|
session->update_notice_delivered = true;
|
|
}
|
|
session->pending_background_initialize = false;
|
|
session->pending_update_notice = false;
|
|
cbm_mutex_unlock(&application->mutex);
|
|
if (cancelled) {
|
|
free(*response_out);
|
|
*response_out = NULL;
|
|
*response_length_out = 0;
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_CANCELLED;
|
|
}
|
|
if (activate_background) {
|
|
application_background_initialize(session);
|
|
}
|
|
return status;
|
|
}
|
|
|
|
static void application_cancel_jobs_locked(cbm_daemon_application_t *application) {
|
|
for (cbm_daemon_application_job_t *job = application->jobs; job; job = job->next) {
|
|
if (!job->terminal) {
|
|
job->cancel_requested = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void application_request_cancel(void *context,
|
|
cbm_daemon_runtime_application_session_t *opaque_session,
|
|
cbm_daemon_runtime_application_token_t request_token) {
|
|
cbm_daemon_application_t *application = context;
|
|
cbm_daemon_application_session_t *session = (cbm_daemon_application_session_t *)opaque_session;
|
|
if (!application || !session || session->application != application ||
|
|
request_token == CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID) {
|
|
return;
|
|
}
|
|
cbm_mutex_lock(&application->mutex);
|
|
if (!session->session_cancelled &&
|
|
(!session->request_active || session->active_request_token == request_token)) {
|
|
session->request_cancel_token = request_token;
|
|
bool active_match = session->request_active;
|
|
if (active_match && session->active_job && session->active_job_subscribed) {
|
|
cbm_daemon_application_job_t *job = session->active_job;
|
|
session->active_job = NULL;
|
|
session->active_job_subscribed = false;
|
|
application_job_unsubscribe_locked(job);
|
|
}
|
|
if (active_match) {
|
|
/* Keep the MCP atomic cancel inside the same completion mutex.
|
|
* Otherwise this callback could pause after unlocking and set the
|
|
* flag on a later request that already reused this session. */
|
|
(void)cbm_mcp_server_cancel_active(session->mcp);
|
|
}
|
|
}
|
|
cbm_mutex_unlock(&application->mutex);
|
|
}
|
|
|
|
static void application_session_cancel(void *context,
|
|
cbm_daemon_runtime_application_session_t *opaque_session) {
|
|
cbm_daemon_application_t *application = context;
|
|
cbm_daemon_application_session_t *session = (cbm_daemon_application_session_t *)opaque_session;
|
|
if (application && session && session->application == application) {
|
|
bool reap_update = false;
|
|
cbm_daemon_application_job_t *join_auto_index = NULL;
|
|
cbm_mutex_lock(&application->mutex);
|
|
/* Runtime may need to keep the session allocation alive until its
|
|
* request callback joins. Cancellation, not the later close, is the
|
|
* ownership boundary for watches and session-scoped index work. */
|
|
bool newly_cancelled = !session->session_cancelled;
|
|
session->session_cancelled = true;
|
|
if (session->request_active) {
|
|
session->request_cancel_token = session->active_request_token;
|
|
}
|
|
if (session->active_job && session->active_job_subscribed) {
|
|
cbm_daemon_application_job_t *job = session->active_job;
|
|
session->active_job = NULL;
|
|
session->active_job_subscribed = false;
|
|
application_job_unsubscribe_locked(job);
|
|
}
|
|
join_auto_index = application_auto_index_release_locked(session);
|
|
reap_update = application_update_owner_release_locked(session);
|
|
reap_update =
|
|
reap_update || (session->background_eligible && application->update_thread_started &&
|
|
application->update_owners == 0);
|
|
application_release_session_watch_locked(session);
|
|
bool final_live_session = newly_cancelled;
|
|
for (cbm_daemon_application_session_t *other = application->sessions;
|
|
final_live_session && other; other = other->next) {
|
|
if (other != session && !other->session_cancelled) {
|
|
final_live_session = false;
|
|
}
|
|
}
|
|
if (final_live_session && !application->permanent) {
|
|
/* The daemon runtime cannot close this allocation until an
|
|
* in-flight callback joins. Stop new admission now and cancel
|
|
* watcher/UI jobs that are not owned by session->active_job.
|
|
* A PERMANENT generation deliberately outlives its sessions and
|
|
* keeps admitting new ones; only the stop/drain paths latch it. */
|
|
application->stopping = true;
|
|
application_cancel_jobs_locked(application);
|
|
application_update_cancel_locked(application);
|
|
reap_update = reap_update || application->update_thread_started;
|
|
}
|
|
cbm_mutex_unlock(&application->mutex);
|
|
(void)cbm_mcp_server_cancel_active(session->mcp);
|
|
application_auto_index_cancel_join(application, join_auto_index);
|
|
application_jobs_reap_completed(application);
|
|
if (reap_update) {
|
|
if (!application_update_reap(application, true, APPLICATION_BACKGROUND_REAP_MS)) {
|
|
application_cleanup_force_terminate("update_cleanup");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void application_session_close(void *context,
|
|
cbm_daemon_runtime_application_session_t *opaque_session) {
|
|
cbm_daemon_application_t *application = context;
|
|
cbm_daemon_application_session_t *session = (cbm_daemon_application_session_t *)opaque_session;
|
|
if (!application || !session || session->application != application) {
|
|
return;
|
|
}
|
|
bool reap_update = false;
|
|
cbm_daemon_application_job_t *join_auto_index = NULL;
|
|
cbm_mutex_lock(&application->mutex);
|
|
cbm_daemon_application_session_t **cursor = &application->sessions;
|
|
while (*cursor && *cursor != session) {
|
|
cursor = &(*cursor)->next;
|
|
}
|
|
if (*cursor == session) {
|
|
*cursor = session->next;
|
|
}
|
|
if (session->active_job && session->active_job_subscribed) {
|
|
application_job_unsubscribe_locked(session->active_job);
|
|
session->active_job = NULL;
|
|
session->active_job_subscribed = false;
|
|
}
|
|
join_auto_index = application_auto_index_release_locked(session);
|
|
reap_update = application_update_owner_release_locked(session);
|
|
reap_update =
|
|
reap_update || (session->background_eligible && application->update_thread_started &&
|
|
application->update_owners == 0);
|
|
application_release_session_watch_locked(session);
|
|
cbm_mutex_unlock(&application->mutex);
|
|
application_auto_index_cancel_join(application, join_auto_index);
|
|
application_jobs_reap_completed(application);
|
|
if (reap_update) {
|
|
if (!application_update_reap(application, true, APPLICATION_BACKGROUND_REAP_MS)) {
|
|
application_cleanup_force_terminate("update_cleanup");
|
|
}
|
|
}
|
|
cbm_mcp_server_free(session->mcp);
|
|
free(session->hook_event);
|
|
free(session->hook_dialect);
|
|
free(session);
|
|
}
|
|
|
|
void cbm_daemon_application_set_permanent(cbm_daemon_application_t *application, bool permanent) {
|
|
if (!application) {
|
|
return;
|
|
}
|
|
cbm_mutex_lock(&application->mutex);
|
|
application->permanent = permanent;
|
|
cbm_mutex_unlock(&application->mutex);
|
|
}
|
|
|
|
cbm_daemon_application_t *cbm_daemon_application_new(
|
|
const cbm_daemon_application_config_t *config) {
|
|
cbm_daemon_application_t *application = calloc(1, sizeof(*application));
|
|
if (!application) {
|
|
return NULL;
|
|
}
|
|
cbm_mutex_init(&application->mutex);
|
|
application->physical_job_limit = APPLICATION_DEFAULT_PHYSICAL_JOB_LIMIT;
|
|
size_t aggregate_memory_budget_bytes = cbm_mem_budget();
|
|
if (config) {
|
|
if ((config->ui_readiness_secret != NULL || config->ui_readiness_secret_length != 0) &&
|
|
(!config->ui_readiness_secret ||
|
|
config->ui_readiness_secret_length != sizeof(application->ui_readiness_secret))) {
|
|
cbm_mutex_destroy(&application->mutex);
|
|
cbm_secure_zero(application->ui_readiness_secret,
|
|
sizeof(application->ui_readiness_secret));
|
|
free(application);
|
|
return NULL;
|
|
}
|
|
application->watcher = config->watcher;
|
|
application->config = config->config;
|
|
application->project_locks = config->project_locks;
|
|
if (config->physical_job_limit > 0) {
|
|
application->physical_job_limit = config->physical_job_limit;
|
|
}
|
|
if (config->aggregate_memory_budget_bytes > 0) {
|
|
aggregate_memory_budget_bytes = config->aggregate_memory_budget_bytes;
|
|
}
|
|
if (config->worker_ops) {
|
|
application->worker_ops = *config->worker_ops;
|
|
}
|
|
if (config->update_ops) {
|
|
application->update_ops = *config->update_ops;
|
|
}
|
|
if (config->ui_readiness_secret &&
|
|
config->ui_readiness_secret_length == sizeof(application->ui_readiness_secret)) {
|
|
memcpy(application->ui_readiness_secret, config->ui_readiness_secret,
|
|
sizeof(application->ui_readiness_secret));
|
|
application->ui_readiness_secret_set = true;
|
|
}
|
|
}
|
|
/* Equal fixed slices keep admission deterministic: starting fewer jobs does
|
|
* not let an early worker claim memory reserved for later concurrent jobs.
|
|
* The absurd sub-byte-per-slot case is made safe by reducing effective
|
|
* capacity before division; normal daemon budgets are many orders larger. */
|
|
if (aggregate_memory_budget_bytes > 0 &&
|
|
application->physical_job_limit > aggregate_memory_budget_bytes) {
|
|
application->physical_job_limit = aggregate_memory_budget_bytes;
|
|
}
|
|
if (aggregate_memory_budget_bytes > 0 && application->physical_job_limit > 0) {
|
|
application->worker_memory_budget_bytes =
|
|
aggregate_memory_budget_bytes / application->physical_job_limit;
|
|
}
|
|
if (!application->worker_ops.start) {
|
|
application->worker_ops = (cbm_daemon_application_worker_ops_t){
|
|
.context = NULL,
|
|
.start = application_worker_start_default,
|
|
.poll = application_worker_poll_default,
|
|
.cancel = application_worker_cancel_default,
|
|
.log_path = application_worker_log_path_default,
|
|
.destroy = application_worker_destroy_default,
|
|
};
|
|
}
|
|
if (!application->worker_ops.poll || !application->worker_ops.cancel ||
|
|
!application->worker_ops.log_path || !application->worker_ops.destroy) {
|
|
cbm_mutex_destroy(&application->mutex);
|
|
cbm_secure_zero(application->ui_readiness_secret, sizeof(application->ui_readiness_secret));
|
|
free(application);
|
|
return NULL;
|
|
}
|
|
/* No provider means no update checking, which is the production default now
|
|
* that the curl-based GitHub check is gone. An INCOMPLETE provider is still
|
|
* a programming error: a caller that supplies `start` must supply the whole
|
|
* quartet, or the thread would start work it cannot poll, cancel or free. */
|
|
if (application->update_ops.start &&
|
|
(!application->update_ops.poll || !application->update_ops.cancel ||
|
|
!application->update_ops.destroy)) {
|
|
cbm_mutex_destroy(&application->mutex);
|
|
cbm_secure_zero(application->ui_readiness_secret, sizeof(application->ui_readiness_secret));
|
|
free(application);
|
|
return NULL;
|
|
}
|
|
if (application->watcher) {
|
|
cbm_watcher_set_project_mutation_guard(
|
|
application->watcher, application_watcher_mutation_begin,
|
|
application_watcher_mutation_end, application_watcher_project_pruned, application);
|
|
}
|
|
return application;
|
|
}
|
|
|
|
bool cbm_daemon_application_shutdown(cbm_daemon_application_t *application, uint32_t timeout_ms) {
|
|
if (!application) {
|
|
return false;
|
|
}
|
|
uint64_t deadline = application_deadline_after(timeout_ms);
|
|
cbm_mutex_lock(&application->mutex);
|
|
application->stopping = true;
|
|
for (cbm_daemon_application_session_t *session = application->sessions; session;
|
|
session = session->next) {
|
|
(void)cbm_mcp_server_cancel_active(session->mcp);
|
|
if (session->auto_index_job && session->auto_index_subscribed) {
|
|
application_job_unsubscribe_locked(session->auto_index_job);
|
|
session->auto_index_job = NULL;
|
|
session->auto_index_subscribed = false;
|
|
}
|
|
(void)application_update_owner_release_locked(session);
|
|
}
|
|
application_cancel_jobs_locked(application);
|
|
application_update_cancel_locked(application);
|
|
cbm_mutex_unlock(&application->mutex);
|
|
for (;;) {
|
|
bool all_done = true;
|
|
cbm_mutex_lock(&application->mutex);
|
|
if (application->active_mutations != 0) {
|
|
all_done = false;
|
|
}
|
|
if (application->update_thread_started && !application->update_thread_done) {
|
|
all_done = false;
|
|
}
|
|
for (cbm_daemon_application_session_t *session = application->sessions; all_done && session;
|
|
session = session->next) {
|
|
if (session->request_active) {
|
|
all_done = false;
|
|
}
|
|
}
|
|
for (cbm_daemon_application_job_t *job = application->jobs; job; job = job->next) {
|
|
if (!job->thread_done || job->subscribers != 0 || job->watcher_waiters != 0) {
|
|
all_done = false;
|
|
break;
|
|
}
|
|
}
|
|
cbm_mutex_unlock(&application->mutex);
|
|
if (all_done) {
|
|
application_jobs_reap_completed(application);
|
|
uint64_t now = cbm_now_ms();
|
|
uint32_t remaining =
|
|
now >= deadline
|
|
? 0
|
|
: (uint32_t)((deadline - now) > UINT32_MAX ? UINT32_MAX : deadline - now);
|
|
return application_update_reap(application, true, remaining);
|
|
}
|
|
if (cbm_now_ms() >= deadline) {
|
|
return false;
|
|
}
|
|
cbm_usleep(APPLICATION_JOB_POLL_US);
|
|
}
|
|
}
|
|
|
|
bool cbm_daemon_application_free_with_timeout(cbm_daemon_application_t *application,
|
|
uint32_t timeout_ms) {
|
|
if (!application) {
|
|
return true;
|
|
}
|
|
if (!cbm_daemon_application_shutdown(application, timeout_ms)) {
|
|
/* Never detach/free live job threads. The caller must retain the
|
|
* application and retry shutdown after the containment failure is
|
|
* resolved. */
|
|
cbm_log_error("daemon.application.free_busy", "action", "retain");
|
|
return false;
|
|
}
|
|
if (application->watcher) {
|
|
/* Waits for any in-flight prune callback before application storage is
|
|
* detached, preventing a borrowed callback context from becoming UAF. */
|
|
cbm_watcher_set_project_mutation_guard(application->watcher, NULL, NULL, NULL, NULL);
|
|
}
|
|
cbm_mutex_lock(&application->mutex);
|
|
cbm_daemon_application_session_t *sessions = application->sessions;
|
|
application->sessions = NULL;
|
|
cbm_daemon_application_watch_t *watches = application->watches;
|
|
application->watches = NULL;
|
|
cbm_daemon_application_job_t *jobs = application->jobs;
|
|
application->jobs = NULL;
|
|
cbm_daemon_application_watch_job_subscription_t *watch_job_subscriptions =
|
|
application->watch_job_subscriptions;
|
|
application->watch_job_subscriptions = NULL;
|
|
cbm_daemon_application_mutation_t *mutations = application->mutations;
|
|
application->mutations = NULL;
|
|
cbm_mutex_unlock(&application->mutex);
|
|
while (sessions) {
|
|
cbm_daemon_application_session_t *next = sessions->next;
|
|
cbm_mcp_server_free(sessions->mcp);
|
|
free(sessions);
|
|
sessions = next;
|
|
}
|
|
while (watches) {
|
|
cbm_daemon_application_watch_t *next = watches->next;
|
|
if (application->watcher) {
|
|
cbm_watcher_unwatch(application->watcher, watches->project);
|
|
}
|
|
free(watches->project);
|
|
free(watches->root);
|
|
free(watches);
|
|
watches = next;
|
|
}
|
|
while (watch_job_subscriptions) {
|
|
cbm_daemon_application_watch_job_subscription_t *next = watch_job_subscriptions->next;
|
|
free(watch_job_subscriptions);
|
|
watch_job_subscriptions = next;
|
|
}
|
|
while (jobs) {
|
|
cbm_daemon_application_job_t *next = jobs->next;
|
|
if (jobs->thread_started) {
|
|
(void)cbm_thread_join(&jobs->thread);
|
|
}
|
|
application_job_free(jobs);
|
|
jobs = next;
|
|
}
|
|
while (mutations) {
|
|
cbm_daemon_application_mutation_t *next = mutations->next;
|
|
free(mutations->project_key);
|
|
free(mutations);
|
|
mutations = next;
|
|
}
|
|
cbm_mutex_destroy(&application->mutex);
|
|
cbm_secure_zero(application->ui_readiness_secret, sizeof(application->ui_readiness_secret));
|
|
free(application);
|
|
return true;
|
|
}
|
|
|
|
bool cbm_daemon_application_free(cbm_daemon_application_t *application) {
|
|
return cbm_daemon_application_free_with_timeout(application, 3000);
|
|
}
|
|
|
|
cbm_daemon_runtime_application_callbacks_t cbm_daemon_application_runtime_callbacks(
|
|
cbm_daemon_application_t *application) {
|
|
cbm_daemon_runtime_application_callbacks_t callbacks = {
|
|
.context = application,
|
|
.session_open = application_session_open,
|
|
.request = application_request,
|
|
.request_cancel = application_request_cancel,
|
|
.session_cancel = application_session_cancel,
|
|
.session_close = application_session_close,
|
|
};
|
|
if (!application) {
|
|
memset(&callbacks, 0, sizeof(callbacks));
|
|
}
|
|
return callbacks;
|
|
}
|
|
|
|
static cbm_daemon_runtime_application_status_t application_client_exchange_tagged(
|
|
cbm_daemon_runtime_client_t *client, cbm_daemon_runtime_application_token_t request_token,
|
|
uint8_t *request, uint32_t request_length, uint8_t **response_out,
|
|
uint32_t *response_length_out, uint32_t timeout_ms) {
|
|
uint8_t *response = NULL;
|
|
uint32_t response_length = 0;
|
|
if (response_out) {
|
|
*response_out = NULL;
|
|
}
|
|
if (response_length_out) {
|
|
*response_length_out = 0;
|
|
}
|
|
cbm_daemon_runtime_application_status_t status =
|
|
request_token == CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID
|
|
? cbm_daemon_runtime_client_application_request(client, request, request_length,
|
|
&response, &response_length, timeout_ms)
|
|
: cbm_daemon_runtime_client_application_request_tagged(client, request_token, request,
|
|
request_length, &response,
|
|
&response_length, timeout_ms);
|
|
free(request);
|
|
if (status != CBM_DAEMON_RUNTIME_APPLICATION_OK) {
|
|
free(response);
|
|
return status;
|
|
}
|
|
if (response) {
|
|
uint8_t *terminated = realloc(response, (size_t)response_length + 1U);
|
|
if (!terminated) {
|
|
free(response);
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR;
|
|
}
|
|
terminated[response_length] = '\0';
|
|
response = terminated;
|
|
}
|
|
if (response_out) {
|
|
*response_out = response;
|
|
} else {
|
|
free(response);
|
|
}
|
|
if (response_length_out) {
|
|
*response_length_out = response_length;
|
|
}
|
|
return status;
|
|
}
|
|
|
|
static cbm_daemon_runtime_application_status_t application_client_exchange(
|
|
cbm_daemon_runtime_client_t *client, uint8_t *request, uint32_t request_length,
|
|
uint8_t **response_out, uint32_t *response_length_out, uint32_t timeout_ms) {
|
|
return application_client_exchange_tagged(client, CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID,
|
|
request, request_length, response_out,
|
|
response_length_out, timeout_ms);
|
|
}
|
|
|
|
cbm_daemon_runtime_application_status_t cbm_daemon_application_client_set_context(
|
|
cbm_daemon_runtime_client_t *client, const char *session_root, const char *allowed_root,
|
|
cbm_mcp_tool_profile_t tool_profile, const char *hook_event, const char *hook_dialect,
|
|
uint32_t timeout_ms) {
|
|
if (!client || !session_root || !session_root[0]) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
size_t root_length = strlen(session_root);
|
|
size_t allowed_length = allowed_root ? strlen(allowed_root) : 0;
|
|
size_t event_length = hook_event ? strlen(hook_event) : 0;
|
|
size_t dialect_length = hook_dialect ? strlen(hook_dialect) : 0;
|
|
uint64_t total = (uint64_t)APPLICATION_CONTEXT_HEADER_SIZE + root_length + allowed_length +
|
|
event_length + dialect_length;
|
|
if (tool_profile < CBM_MCP_TOOL_PROFILE_ALL || tool_profile > CBM_MCP_TOOL_PROFILE_SCOUT ||
|
|
(tool_profile != CBM_MCP_TOOL_PROFILE_ALL && (event_length != 0 || dialect_length != 0)) ||
|
|
!cbm_hook_augment_invocation_supported(hook_event, hook_dialect) ||
|
|
root_length > UINT32_MAX || allowed_length > UINT32_MAX || event_length > UINT32_MAX ||
|
|
dialect_length > UINT32_MAX || total > CBM_DAEMON_RUNTIME_APPLICATION_PAYLOAD_MAX) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
uint8_t *request = calloc(1, (size_t)total);
|
|
if (!request) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR;
|
|
}
|
|
request[0] = CBM_DAEMON_APPLICATION_REQUEST_SET_CONTEXT;
|
|
application_put_u32(request + 1, (uint32_t)root_length);
|
|
request[5] = allowed_root ? 1U : 0U;
|
|
application_put_u32(request + 6, (uint32_t)allowed_length);
|
|
request[10] = (uint8_t)tool_profile;
|
|
application_put_u32(request + 11, (uint32_t)event_length);
|
|
application_put_u32(request + 15, (uint32_t)dialect_length);
|
|
memcpy(request + APPLICATION_CONTEXT_HEADER_SIZE, session_root, root_length);
|
|
if (allowed_root) {
|
|
memcpy(request + APPLICATION_CONTEXT_HEADER_SIZE + root_length, allowed_root,
|
|
allowed_length);
|
|
}
|
|
if (hook_event) {
|
|
memcpy(request + APPLICATION_CONTEXT_HEADER_SIZE + root_length + allowed_length, hook_event,
|
|
event_length);
|
|
}
|
|
if (hook_dialect) {
|
|
memcpy(request + APPLICATION_CONTEXT_HEADER_SIZE + root_length + allowed_length +
|
|
event_length,
|
|
hook_dialect, dialect_length);
|
|
}
|
|
uint8_t *unexpected = NULL;
|
|
uint32_t unexpected_length = 0;
|
|
cbm_daemon_runtime_application_status_t status = application_client_exchange(
|
|
client, request, (uint32_t)total, &unexpected, &unexpected_length, timeout_ms);
|
|
if (status == CBM_DAEMON_RUNTIME_APPLICATION_OK && (unexpected || unexpected_length != 0)) {
|
|
status = CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR;
|
|
}
|
|
free(unexpected);
|
|
return status;
|
|
}
|
|
|
|
cbm_daemon_runtime_application_status_t cbm_daemon_application_client_set_ui_config(
|
|
cbm_daemon_runtime_client_t *client, uint8_t update_mask, bool ui_enabled, int ui_port,
|
|
uint32_t timeout_ms) {
|
|
const uint8_t valid_mask =
|
|
CBM_DAEMON_APPLICATION_UI_CONFIG_ENABLED | CBM_DAEMON_APPLICATION_UI_CONFIG_PORT;
|
|
bool enabled_present = (update_mask & CBM_DAEMON_APPLICATION_UI_CONFIG_ENABLED) != 0;
|
|
bool port_present = (update_mask & CBM_DAEMON_APPLICATION_UI_CONFIG_PORT) != 0;
|
|
if (!client || update_mask == 0 || (update_mask & (uint8_t)~valid_mask) != 0 ||
|
|
(!enabled_present && ui_enabled) || (!port_present && ui_port != 0) ||
|
|
(port_present && (ui_port <= 0 || ui_port > 65535))) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
uint8_t *request = calloc(1, APPLICATION_UI_CONFIG_REQUEST_SIZE);
|
|
if (!request) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR;
|
|
}
|
|
request[0] = CBM_DAEMON_APPLICATION_REQUEST_SET_UI_CONFIG;
|
|
request[1] = update_mask;
|
|
request[2] = enabled_present && ui_enabled ? 1U : 0U;
|
|
if (port_present) {
|
|
application_put_u32(request + 3, (uint32_t)ui_port);
|
|
}
|
|
uint8_t *unexpected = NULL;
|
|
uint32_t unexpected_length = 0;
|
|
cbm_daemon_runtime_application_status_t status =
|
|
application_client_exchange(client, request, APPLICATION_UI_CONFIG_REQUEST_SIZE,
|
|
&unexpected, &unexpected_length, timeout_ms);
|
|
if (status == CBM_DAEMON_RUNTIME_APPLICATION_OK && (unexpected || unexpected_length != 0)) {
|
|
status = CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR;
|
|
}
|
|
free(unexpected);
|
|
return status;
|
|
}
|
|
|
|
cbm_daemon_runtime_application_status_t cbm_daemon_application_client_ui_readiness_proof(
|
|
cbm_daemon_runtime_client_t *client, const uint8_t challenge[CBM_SHA256_DIGEST_LEN],
|
|
uint8_t proof_out[CBM_SHA256_DIGEST_LEN], uint32_t timeout_ms) {
|
|
if (!client || !challenge || !proof_out) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
cbm_secure_zero(proof_out, CBM_SHA256_DIGEST_LEN);
|
|
uint8_t *request = malloc(APPLICATION_UI_READINESS_REQUEST_SIZE);
|
|
if (!request) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR;
|
|
}
|
|
request[0] = CBM_DAEMON_APPLICATION_REQUEST_UI_READINESS_PROOF;
|
|
memcpy(request + 1, challenge, CBM_SHA256_DIGEST_LEN);
|
|
uint8_t *response = NULL;
|
|
uint32_t response_length = 0;
|
|
cbm_daemon_runtime_application_status_t status =
|
|
application_client_exchange(client, request, APPLICATION_UI_READINESS_REQUEST_SIZE,
|
|
&response, &response_length, timeout_ms);
|
|
if (status == CBM_DAEMON_RUNTIME_APPLICATION_OK) {
|
|
if (!response || response_length != CBM_SHA256_DIGEST_LEN) {
|
|
status = CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR;
|
|
} else {
|
|
memcpy(proof_out, response, CBM_SHA256_DIGEST_LEN);
|
|
}
|
|
}
|
|
if (response) {
|
|
cbm_secure_zero(response, response_length);
|
|
}
|
|
free(response);
|
|
return status;
|
|
}
|
|
|
|
static cbm_daemon_runtime_application_status_t application_client_text_request_tagged(
|
|
cbm_daemon_runtime_client_t *client, cbm_daemon_runtime_application_token_t request_token,
|
|
cbm_daemon_application_request_kind_t kind, const char *text, uint8_t **response_out,
|
|
uint32_t *response_length_out, uint32_t timeout_ms) {
|
|
if (!client || !text || !text[0]) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
size_t text_length = strlen(text);
|
|
if (text_length + 1U > CBM_DAEMON_RUNTIME_APPLICATION_PAYLOAD_MAX) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
uint8_t *request = malloc(text_length + 1U);
|
|
if (!request) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR;
|
|
}
|
|
request[0] = (uint8_t)kind;
|
|
memcpy(request + 1, text, text_length);
|
|
return application_client_exchange_tagged(client, request_token, request,
|
|
(uint32_t)text_length + 1U, response_out,
|
|
response_length_out, timeout_ms);
|
|
}
|
|
|
|
static cbm_daemon_runtime_application_status_t application_client_text_request(
|
|
cbm_daemon_runtime_client_t *client, cbm_daemon_application_request_kind_t kind,
|
|
const char *text, uint8_t **response_out, uint32_t *response_length_out, uint32_t timeout_ms) {
|
|
return application_client_text_request_tagged(
|
|
client, CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID, kind, text, response_out,
|
|
response_length_out, timeout_ms);
|
|
}
|
|
|
|
cbm_daemon_runtime_application_status_t cbm_daemon_application_client_mcp(
|
|
cbm_daemon_runtime_client_t *client, const char *message, uint8_t **response_out,
|
|
uint32_t *response_length_out, uint32_t timeout_ms) {
|
|
return application_client_text_request(client, CBM_DAEMON_APPLICATION_REQUEST_MCP, message,
|
|
response_out, response_length_out, timeout_ms);
|
|
}
|
|
|
|
cbm_daemon_runtime_application_status_t cbm_daemon_application_client_mcp_tagged(
|
|
cbm_daemon_runtime_client_t *client, cbm_daemon_runtime_application_token_t request_token,
|
|
const char *message, uint8_t **response_out, uint32_t *response_length_out,
|
|
uint32_t timeout_ms) {
|
|
return application_client_text_request_tagged(client, request_token,
|
|
CBM_DAEMON_APPLICATION_REQUEST_MCP, message,
|
|
response_out, response_length_out, timeout_ms);
|
|
}
|
|
|
|
cbm_daemon_runtime_application_status_t cbm_daemon_application_client_tool(
|
|
cbm_daemon_runtime_client_t *client, const char *tool_name, const char *args_json,
|
|
uint8_t **response_out, uint32_t *response_length_out, uint32_t timeout_ms) {
|
|
if (!client || !tool_name || !tool_name[0] || !args_json || !args_json[0]) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
size_t tool_length = strlen(tool_name);
|
|
size_t args_length = strlen(args_json);
|
|
uint64_t total = (uint64_t)APPLICATION_TOOL_HEADER_SIZE + tool_length + args_length;
|
|
if (tool_length > UINT32_MAX || total > CBM_DAEMON_RUNTIME_APPLICATION_PAYLOAD_MAX) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_REJECTED;
|
|
}
|
|
uint8_t *request = malloc((size_t)total);
|
|
if (!request) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR;
|
|
}
|
|
request[0] = CBM_DAEMON_APPLICATION_REQUEST_TOOL;
|
|
application_put_u32(request + 1, (uint32_t)tool_length);
|
|
memcpy(request + APPLICATION_TOOL_HEADER_SIZE, tool_name, tool_length);
|
|
memcpy(request + APPLICATION_TOOL_HEADER_SIZE + tool_length, args_json, args_length);
|
|
return application_client_exchange(client, request, (uint32_t)total, response_out,
|
|
response_length_out, timeout_ms);
|
|
}
|
|
|
|
cbm_daemon_runtime_application_status_t cbm_daemon_application_client_hook_augment(
|
|
cbm_daemon_runtime_client_t *client, const char *input_json, uint8_t **response_out,
|
|
uint32_t *response_length_out, uint32_t timeout_ms) {
|
|
return application_client_text_request(client, CBM_DAEMON_APPLICATION_REQUEST_HOOK_AUGMENT,
|
|
input_json, response_out, response_length_out,
|
|
timeout_ms);
|
|
}
|
|
|
|
static int application_background_index(cbm_daemon_application_t *application,
|
|
const char *project_name, const char *root_path,
|
|
bool require_live_watch) {
|
|
if (!application || !project_name || !root_path) {
|
|
return -1;
|
|
}
|
|
char canonical_root[APPLICATION_PATH_CAP];
|
|
struct stat root_status;
|
|
if (!cbm_canonical_path(root_path, canonical_root, sizeof(canonical_root)) ||
|
|
stat(canonical_root, &root_status) != 0 || !S_ISDIR(root_status.st_mode)) {
|
|
return -1;
|
|
}
|
|
yyjson_mut_doc *document = yyjson_mut_doc_new(NULL);
|
|
yyjson_mut_val *root = document ? yyjson_mut_obj(document) : NULL;
|
|
if (!document || !root) {
|
|
yyjson_mut_doc_free(document);
|
|
return -1;
|
|
}
|
|
yyjson_mut_doc_set_root(document, root);
|
|
bool encoded = yyjson_mut_obj_add_strcpy(document, root, "repo_path", canonical_root);
|
|
char *default_project = cbm_project_name_from_path(canonical_root);
|
|
bool custom_project =
|
|
project_name[0] && (!default_project || strcmp(default_project, project_name) != 0);
|
|
if (encoded && custom_project) {
|
|
encoded = yyjson_mut_obj_add_strcpy(document, root, "name", project_name);
|
|
}
|
|
free(default_project);
|
|
char *args = encoded ? yyjson_mut_write(document, 0, NULL) : NULL;
|
|
yyjson_mut_doc_free(document);
|
|
if (!args) {
|
|
return -1;
|
|
}
|
|
char *project_key = application_index_project_key(canonical_root, args);
|
|
if (!project_key) {
|
|
free(args);
|
|
return -1;
|
|
}
|
|
|
|
application_job_subscribe_status_t subscribe_status = APPLICATION_JOB_SUBSCRIBE_UNAVAILABLE;
|
|
application_jobs_reap_completed(application);
|
|
cbm_mutex_lock(&application->mutex);
|
|
cbm_daemon_application_watch_t *watch =
|
|
require_live_watch ? application_find_watch_locked(application, project_name) : NULL;
|
|
bool watch_live = !require_live_watch ||
|
|
(watch && watch->subscribers > 0 && strcmp(watch->root, canonical_root) == 0);
|
|
size_t watch_owner_count = 0;
|
|
bool watch_subscriptions_ok = true;
|
|
cbm_daemon_application_job_t *job =
|
|
watch_live ? application_job_subscribe_locked(application, project_key, canonical_root,
|
|
args, &subscribe_status)
|
|
: NULL;
|
|
if (job && require_live_watch) {
|
|
watch_subscriptions_ok = application_watch_job_subscribe_sessions_locked(
|
|
application, watch, job, &watch_owner_count);
|
|
if (watch_subscriptions_ok && watch_owner_count > 0) {
|
|
job->watcher_waiters++;
|
|
} else if (!watch_subscriptions_ok) {
|
|
subscribe_status = APPLICATION_JOB_SUBSCRIBE_ALLOCATION_FAILED;
|
|
}
|
|
/* application_job_subscribe_locked() lends the caller one ordinary
|
|
* subscriber. A watcher callback is only a storage waiter: exact live
|
|
* session subscriptions above own the physical work. */
|
|
application_job_unsubscribe_locked(job);
|
|
if (!watch_subscriptions_ok || watch_owner_count == 0) {
|
|
job = NULL;
|
|
}
|
|
}
|
|
cbm_mutex_unlock(&application->mutex);
|
|
free(project_key);
|
|
free(args);
|
|
if (!job) {
|
|
if (require_live_watch &&
|
|
(!watch_live || (watch_subscriptions_ok && watch_owner_count == 0))) {
|
|
/* The physical watcher can retain a poll snapshot after the last
|
|
* owning session unwatches it. Treat that stale callback as a
|
|
* harmless skip; no job was admitted. */
|
|
return 1;
|
|
}
|
|
return subscribe_status == APPLICATION_JOB_SUBSCRIBE_OPTIONS_CONFLICT ||
|
|
subscribe_status == APPLICATION_JOB_SUBSCRIBE_BUSY ||
|
|
subscribe_status == APPLICATION_JOB_SUBSCRIBE_CANCELLING
|
|
? 1
|
|
: -1;
|
|
}
|
|
bool successful = false;
|
|
bool cancelled = false;
|
|
for (;;) {
|
|
cbm_mutex_lock(&application->mutex);
|
|
if (job->terminal) {
|
|
successful = job->successful;
|
|
cancelled = job->cancelled;
|
|
if (require_live_watch) {
|
|
application_watch_job_unsubscribe_job_locked(application, job);
|
|
if (job->watcher_waiters > 0) {
|
|
job->watcher_waiters--;
|
|
}
|
|
} else {
|
|
application_job_unsubscribe_locked(job);
|
|
}
|
|
cbm_mutex_unlock(&application->mutex);
|
|
break;
|
|
}
|
|
cbm_mutex_unlock(&application->mutex);
|
|
cbm_usleep(APPLICATION_JOB_POLL_US);
|
|
}
|
|
application_jobs_reap_completed(application);
|
|
return successful ? 0 : (cancelled ? 1 : -1);
|
|
}
|
|
|
|
int cbm_daemon_application_index(cbm_daemon_application_t *application, const char *project_name,
|
|
const char *root_path) {
|
|
return application_background_index(application, project_name, root_path, false);
|
|
}
|
|
|
|
int cbm_daemon_application_watcher_index(const char *project_name, const char *root_path,
|
|
void *context) {
|
|
return application_background_index(context, project_name, root_path, true);
|
|
}
|
|
|
|
size_t cbm_daemon_application_active_jobs(cbm_daemon_application_t *application) {
|
|
if (!application) {
|
|
return 0;
|
|
}
|
|
size_t count = 0;
|
|
cbm_mutex_lock(&application->mutex);
|
|
for (cbm_daemon_application_job_t *job = application->jobs; job; job = job->next) {
|
|
if (!job->terminal) {
|
|
count++;
|
|
}
|
|
}
|
|
cbm_mutex_unlock(&application->mutex);
|
|
return count;
|
|
}
|
|
|
|
size_t cbm_daemon_application_job_subscribers(cbm_daemon_application_t *application,
|
|
const char *project_key) {
|
|
if (!application || !project_key) {
|
|
return 0;
|
|
}
|
|
cbm_mutex_lock(&application->mutex);
|
|
cbm_daemon_application_job_t *job = application_find_job_locked(application, project_key);
|
|
size_t subscribers = job ? job->subscribers : 0;
|
|
cbm_mutex_unlock(&application->mutex);
|
|
return subscribers;
|
|
}
|
|
|
|
size_t cbm_daemon_application_physical_job_limit(cbm_daemon_application_t *application) {
|
|
if (!application) {
|
|
return 0;
|
|
}
|
|
cbm_mutex_lock(&application->mutex);
|
|
size_t limit = application->physical_job_limit;
|
|
cbm_mutex_unlock(&application->mutex);
|
|
return limit;
|
|
}
|
|
|
|
size_t cbm_daemon_application_worker_memory_budget_bytes(cbm_daemon_application_t *application) {
|
|
if (!application) {
|
|
return 0;
|
|
}
|
|
cbm_mutex_lock(&application->mutex);
|
|
size_t budget = application->worker_memory_budget_bytes;
|
|
cbm_mutex_unlock(&application->mutex);
|
|
return budget;
|
|
}
|
|
|
|
bool cbm_daemon_application_session_retains_store_for_test(
|
|
const cbm_daemon_runtime_application_session_t *opaque_session) {
|
|
const cbm_daemon_application_session_t *session =
|
|
(const cbm_daemon_application_session_t *)opaque_session;
|
|
return session && session->mcp && cbm_mcp_server_store(session->mcp) != NULL;
|
|
}
|