mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-10 09:13:53 +08:00
Daemon lifecycle and Windows correctness, verified on a real Windows 11 ARM64 VM through the maintained test-infrastructure/vm drivers, plus the macOS and Linux arm64 suites and the container lint gate. Daemon lifecycle: - daemon start/stop/status subcommands. `daemon start` launches a PERMANENT daemon (spawn shape is byte-exact argv; survives idle periods and session ends) and reports an already-active daemon instead of failing. Permanence is honored at every stop latch: last-committed-client disconnect, host initial-client window, coordinator release, and application final-session close — a permanent daemon also keeps admitting new sessions after its last one closes. - daemon stop refuses while sessions are active and lists the blocking peers (pid/role) that must finish first; an idle daemon drains through the activation-shutdown machinery with the ACK ordered after connection interrupts. A second stop is idempotent. The wire ops are no-cohort first-frame requests with peer fingerprint authentication, so stop/status never conflict with an exact-build admission gate. - One-shot CLI commands now execute through the daemon (index workers keep their local supervised path). A cold CLI run that had to spawn a temporary daemon prints a hint that `daemon start` removes the per-command startup tax; a warm daemon is recycled silently. - Hooks are connect-only fail-open: with no daemon present the hook emits a visible, rate-limited notice (Claude-dialect systemMessage plus stderr for other dialects) and always exits 0 — augmentation is never allowed to block the caller's tool use. - Version skew: a newer-build client automatically drains an older-build permanent daemon (strict semantic-version triples only; dev builds never auto-drain) and the build-conflict message names `cbm daemon stop` as the manual escape hatch. Windows IPC/runtime (real-VM verified): - ipc(win): persistent pending overlapped ConnectNamedPipe. The accept path used to destroy its listening pipe instance on every 20 ms poll timeout; a client attaching in the teardown window was severed or left on an orphaned pipe object whose HELLO no server handle could ever read, absorbing the connect until the client's own timeout expired. The pending connect now survives poll timeouts and nothing is destroyed while a client could be attaching. - ipc(win): drain-before-close for final responses. Closing a named-pipe server handle can discard a just-sent response before the peer reads it (POSIX stream sockets never lose buffered data on close). A bounded cbm_daemon_ipc_connection_drain (read-until-peer-EOF; no-op on POSIX, immediate on interrupted connections) now precedes close in runtime_worker_finish and runtime_reject_inline, so hello-conflict, capacity and disconnect acknowledgements reliably reach the peer. - runtime: CLOSE_INTENT wire frame. A Windows named-pipe client has no transport half-close, so close_begin now announces departure with an explicit frame (ordered after APPLICATION_CANCEL, before the local interrupt); the server releases the client's admission on receipt instead of waiting for the handle to close. Admission-drop timing is now identical to POSIX shutdown() semantics on every platform. - runtime(win): client close cancellation. close_begin serializes with request publication under the send lock, best-effort sends the active token's APPLICATION_CANCEL frame, then interrupts local I/O; the server cancels MCP/subprocess work promptly. Contract tests accept both correct outcomes (interrupted transport or decoded CANCELLED). - runtime: activation acknowledgement ordering. The activation ACK is the requester's license to act on "snapshotted and draining", so every connection interrupt is now initiated before the ACK is sent; a session could previously get one more request serviced after the requester observed the ACK. - service(win): deadline-bounded private-file prepare. The conflict-log prepare retry loop (100 x Sleep(2), which rounds up to the ~16 ms timer granularity) burned ~1.6 s against permanently obstructed paths, stalling hello rejections past the client's timeout. The retry budget is now a 250 ms deadline; transient share collisions still retry. - subprocess(win): cmd.exe /C payload encoder quotes metacharacters correctly (root cause of the git-on-Windows failure cluster). - watcher: SHA-256 buffer sizing (CBM_SZ_64 -> CBM_SZ_128) and a native Windows stop/unwatch cancellation test with exact-image verification. - httpd: send_all writes in bounded 64 KiB slices. A single giant nonblocking send() on Windows is absorbed wholesale into AFD kernel buffering regardless of SO_SNDBUF, so send deadlines and interrupts could never engage against a slow peer (and the full payload was pinned in nonpaged pool). Slicing restores a deterministic backpressure point; a test hook pins SO_SNDBUF for the deadline and interrupt tests. - ui/http: shutdown lifecycle — interrupt checks, response-wide send deadline, explicit connection states, refusal to free a server while a listener-owned connection is active. Windows long-path support: - Central path-aware wide conversion (canonicalize via GetFullPathNameW and prepend the extended-length prefix for absolute paths >=240) at the compat chokepoints (cbm_fopen/compat_fs/mkstemp/mkdtemp), sqlite store opens, and the daemon build-fingerprint/log paths. Deep managed installs (a 64-hex generation directory routinely exceeds MAX_PATH) now index, stage and activate correctly. - activation transaction: its own file APIs and the component-walking ancestry validators now operate in the extended-length namespace; the launcher path is canonicalized (and prefixed when deep) once at entry so every downstream exact-string comparison stays form-consistent. - Executable self-resolution uses the wide APIs (GetModuleFileNameW, GetFileAttributesW) so non-ASCII install paths survive argv[0] resolution. Windows launcher install/uninstall transaction: - FileRenameInfoEx names are NUL-terminated in an over-allocated buffer. FileNameLength governs per the contract, but filter drivers read FileName as NUL-terminated and appended adjacent heap bytes to created names — a flaky, garbage-suffixed rename target. Both the CLI and the launcher rename helpers are fixed. - Uninstall retires state via rename-aside (.cbm -> .cbm-retired-v1-<tag>-<pid>) with the retired tag shortened to 16 hex chars so the bare rename target stays under the FileRenameInfoEx NT-conversion ceiling at guard depths; 64 bits still uniquely identify the generation. - When the running launcher's mapped generation backings pin .cbm against rename, the backings are relocated to activation-<pid>-N .retired tombstones beside the install (a mapped image may be renamed, never deleted; the launcher's liveness-guarded sweep reclaims stale tombstones). Every relocation is recorded, and a FAILED uninstall reverses the moves after restoring .cbm — via MoveFileExW with extended-length paths on both arguments, since the deep generation target is beyond the handle-based rename's bare-path reach — so a restored install keeps its generation backings and stays runnable. - After a committed uninstall the retired tree's backings are relocated out so the tree is shallow enough for the detached cleanup's rd, and the cleanup's working directory strips the extended-length prefix (CreateProcessW lpCurrentDirectory silently ignores prefixed paths). - Files created under Administrators-default-owner directories (CopyFileW destinations, CREATE_NEW tombstones, probe directories) are explicitly owner-stamped so the exact-owner validators hold on runner images; guard fixtures stamp hand-built trees the same way. Diagnostics, tests and infra: - diagnostics: discovery is now an always-delivered JSON control record (new cbm_log_control) that survives CBM_LOG_LEVEL suppression and paths containing spaces; placement honors $TMPDIR with /tmp fallback via a diagnostics-local helper; the soak parser reads the JSON record; documented in docs/CONFIGURATION.md. Red-first coverage for suppressed log levels, TMPDIR-with-spaces, and native Windows output-contract assertions. - tests(win): daemon_ipc/daemon_frontend fixtures now build endpoint parents with production-shaped ancestry (LocalAppData on Windows, via th_secure_runtime_parent_new) — the runtime ancestry validation correctly refuses temp roots whose ancestors grant mutation rights to Authenticated Users (C:/msys64/tmp, GitHub-runner work dirs) — and drive the documented startup-owner publication flow before reading generation-bound endpoint addresses. This turns the 26 Windows failures previously visible in CI's full-test job green without weakening any validation. - tests(win): the launcher guard covers the full permanent-launcher contract including failed-uninstall restore and immediate reinstall after uninstall; new daemon lifecycle and reworked hook-augment guards run the start/recycle/stop flow end to end. - tests: CBM_SKIP_PERF is now actually consumed by the test runner (it was set by CI but never read, so perf suites ran everywhere); four throughput/bench suites are classified as perf, the heavy store_arch suite moved to the slow-timeout tier, and two wall-clock-sensitive assertions were rewritten as invariant checks with coarse hang-detector backstops. - build/test infra: build-dir safety contract, UI dev-proxy security contract, soak daemon-recovery contract, path-safety helper, the Windows VM worktree-sync contract wired into scripts/test.sh, and vm/win.sh guards building its clean embedded-UI product in an isolated BUILD_DIR so it cannot clobber the incremental test build. provision-windows.sh now installs Node.js for the guards UI build. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1400 lines
62 KiB
C
1400 lines
62 KiB
C
/*
|
|
* test_daemon_frontend.c — Exact JSON-RPC cancellation and maintenance.
|
|
*/
|
|
#include "test_framework.h"
|
|
#include "test_helpers.h"
|
|
|
|
#include "daemon/frontend.h"
|
|
#include "daemon/ipc.h"
|
|
#include "daemon/service.h"
|
|
#include "daemon/version_cohort.h"
|
|
#include "foundation/compat.h"
|
|
#include "foundation/compat_thread.h"
|
|
#include "foundation/platform.h"
|
|
|
|
#include <stdint.h>
|
|
#include <stdatomic.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#ifndef _WIN32
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <signal.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
enum { FRONTEND_TEST_PATH_CAP = 1024 };
|
|
|
|
typedef struct {
|
|
char parent[FRONTEND_TEST_PATH_CAP];
|
|
cbm_daemon_ipc_endpoint_t *endpoint;
|
|
cbm_version_cohort_manager_t *manager;
|
|
} frontend_maintenance_fixture_t;
|
|
|
|
static bool frontend_maintenance_fixture_start(frontend_maintenance_fixture_t *fixture,
|
|
const char *tag) {
|
|
memset(fixture, 0, sizeof(*fixture));
|
|
/* Endpoint parents need production-shaped ancestry (LocalAppData on
|
|
* Windows); temp roots are refused by the runtime ancestry validation. */
|
|
if (!th_secure_runtime_parent_new(fixture->parent, sizeof(fixture->parent), tag)) {
|
|
return false;
|
|
}
|
|
fixture->endpoint = cbm_daemon_ipc_endpoint_new("0123456789abcdef", fixture->parent);
|
|
fixture->manager = fixture->endpoint ? cbm_version_cohort_manager_new(fixture->endpoint) : NULL;
|
|
return fixture->endpoint && fixture->manager;
|
|
}
|
|
|
|
static void frontend_maintenance_fixture_finish(frontend_maintenance_fixture_t *fixture) {
|
|
while (fixture->manager &&
|
|
cbm_version_cohort_manager_free(&fixture->manager) != CBM_PRIVATE_FILE_LOCK_OK) {
|
|
cbm_usleep(1000);
|
|
}
|
|
cbm_daemon_ipc_endpoint_free(fixture->endpoint);
|
|
if (fixture->parent[0]) {
|
|
(void)th_rmtree(fixture->parent);
|
|
}
|
|
memset(fixture, 0, sizeof(*fixture));
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
static const char FRONTEND_TEST_BUILD[] =
|
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
static const char FRONTEND_TEST_CACHE[] =
|
|
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
|
|
|
|
enum {
|
|
/* Hang guards, not benchmarks: starved 3-4-core CI runners need several
|
|
* seconds for frontend/daemon startup paths that finish instantly on a
|
|
* dev machine (see the index-supervisor calibration). */
|
|
FRONTEND_EOF_TEST_REQUEST_TIMEOUT_MS = 30000,
|
|
FRONTEND_EOF_TEST_CATASTROPHIC_TIMEOUT_S = 90,
|
|
FRONTEND_BACKPRESSURE_MESSAGE_BYTES = 2 * 1024 * 1024,
|
|
FRONTEND_BACKPRESSURE_FRONTEND_TIMEOUT_S = 90,
|
|
FRONTEND_BACKPRESSURE_DAEMON_TIMEOUT_S = 90,
|
|
FRONTEND_BACKPRESSURE_RUNTIME_TIMEOUT_MS = 90000,
|
|
FRONTEND_BACKPRESSURE_CLEANUP_TIMEOUT_MS = 30000,
|
|
/* The production queue is deliberately bounded below this count. Keep the
|
|
* regression black-box: it must remain valid if the exact capacity changes
|
|
* while still proving that overload cannot hide an already-pending EOF. */
|
|
FRONTEND_EOF_TEST_OVERFLOW_MESSAGES = 32,
|
|
};
|
|
|
|
typedef struct {
|
|
atomic_int requests;
|
|
atomic_int session_cancels;
|
|
atomic_int sessions_opened;
|
|
atomic_int first_session_cancels;
|
|
atomic_int second_session_cancels;
|
|
atomic_bool block_first_request;
|
|
atomic_bool first_request_started;
|
|
int request_observed_fd;
|
|
int session_cancel_fd;
|
|
} frontend_eof_application_context_t;
|
|
|
|
typedef struct {
|
|
frontend_eof_application_context_t *context;
|
|
atomic_bool cancelled;
|
|
int session_index;
|
|
} frontend_eof_application_session_t;
|
|
|
|
typedef struct {
|
|
int fd;
|
|
bool overflow;
|
|
frontend_eof_application_context_t *application;
|
|
atomic_bool finished;
|
|
atomic_bool succeeded;
|
|
} frontend_eof_writer_t;
|
|
|
|
typedef struct {
|
|
char conflict_log[FRONTEND_TEST_PATH_CAP];
|
|
cbm_daemon_ipc_endpoint_t *endpoint;
|
|
cbm_version_cohort_manager_t *manager;
|
|
cbm_daemon_runtime_service_t *service;
|
|
cbm_daemon_runtime_client_t *client;
|
|
frontend_eof_application_context_t application;
|
|
} frontend_eof_fixture_t;
|
|
|
|
static cbm_daemon_build_identity_t frontend_test_identity(void) {
|
|
cbm_daemon_build_identity_t identity = {
|
|
.semantic_version = "2.4.0",
|
|
.build_fingerprint = FRONTEND_TEST_BUILD,
|
|
.cache_fingerprint = FRONTEND_TEST_CACHE,
|
|
.protocol_abi = 3,
|
|
.store_abi = 11,
|
|
.feature_abi = 7,
|
|
};
|
|
return identity;
|
|
}
|
|
|
|
static cbm_daemon_runtime_application_session_t *frontend_eof_application_session_open(
|
|
void *opaque, cbm_daemon_client_id_t client_id, uint64_t authenticated_process_id) {
|
|
frontend_eof_application_context_t *context = opaque;
|
|
frontend_eof_application_session_t *session = calloc(1, sizeof(*session));
|
|
if (!context || !session || client_id == CBM_DAEMON_CLIENT_ID_INVALID ||
|
|
authenticated_process_id == 0) {
|
|
free(session);
|
|
return NULL;
|
|
}
|
|
session->context = context;
|
|
session->session_index =
|
|
atomic_fetch_add_explicit(&context->sessions_opened, 1, memory_order_relaxed);
|
|
atomic_init(&session->cancelled, false);
|
|
return (cbm_daemon_runtime_application_session_t *)session;
|
|
}
|
|
|
|
static cbm_daemon_runtime_application_status_t frontend_eof_application_request(
|
|
void *opaque, 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) {
|
|
(void)request_token;
|
|
frontend_eof_application_context_t *context = opaque;
|
|
frontend_eof_application_session_t *session =
|
|
(frontend_eof_application_session_t *)opaque_session;
|
|
if (!context || !session || session->context != context || !response_out ||
|
|
!response_length_out || (request_length > 0 && !request)) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR;
|
|
}
|
|
*response_out = NULL;
|
|
*response_length_out = 0;
|
|
int request_index = atomic_fetch_add_explicit(&context->requests, 1, memory_order_relaxed);
|
|
if (request_index == 0 &&
|
|
atomic_load_explicit(&context->block_first_request, memory_order_acquire)) {
|
|
atomic_store_explicit(&context->first_request_started, true, memory_order_release);
|
|
while (!atomic_load_explicit(&session->cancelled, memory_order_acquire)) {
|
|
cbm_usleep(1000);
|
|
}
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_CANCELLED;
|
|
}
|
|
if (request_length == 0) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_OK;
|
|
}
|
|
uint8_t *response = malloc(request_length);
|
|
if (!response) {
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR;
|
|
}
|
|
memcpy(response, request, request_length);
|
|
*response_out = response;
|
|
*response_length_out = request_length;
|
|
if (context->request_observed_fd >= 0) {
|
|
const char marker = 'Q';
|
|
(void)write(context->request_observed_fd, &marker, 1);
|
|
}
|
|
return CBM_DAEMON_RUNTIME_APPLICATION_OK;
|
|
}
|
|
|
|
static void frontend_eof_application_request_cancel(
|
|
void *opaque, cbm_daemon_runtime_application_session_t *opaque_session,
|
|
cbm_daemon_runtime_application_token_t request_token) {
|
|
(void)request_token;
|
|
frontend_eof_application_context_t *context = opaque;
|
|
frontend_eof_application_session_t *session =
|
|
(frontend_eof_application_session_t *)opaque_session;
|
|
if (context && session && session->context == context) {
|
|
atomic_store_explicit(&session->cancelled, true, memory_order_release);
|
|
}
|
|
}
|
|
|
|
static void frontend_eof_application_session_cancel(
|
|
void *opaque, cbm_daemon_runtime_application_session_t *opaque_session) {
|
|
frontend_eof_application_context_t *context = opaque;
|
|
frontend_eof_application_session_t *session =
|
|
(frontend_eof_application_session_t *)opaque_session;
|
|
if (context && session && session->context == context) {
|
|
atomic_store_explicit(&session->cancelled, true, memory_order_release);
|
|
if (session->session_index == 0) {
|
|
(void)atomic_fetch_add_explicit(&context->first_session_cancels, 1,
|
|
memory_order_relaxed);
|
|
} else if (session->session_index == 1) {
|
|
(void)atomic_fetch_add_explicit(&context->second_session_cancels, 1,
|
|
memory_order_relaxed);
|
|
}
|
|
/* Publish the total only after the exact-session counter. The daemon
|
|
* fixture acquires this value before inspecting attribution. */
|
|
(void)atomic_fetch_add_explicit(&context->session_cancels, 1, memory_order_release);
|
|
if (context->session_cancel_fd >= 0) {
|
|
const char marker = 'C';
|
|
(void)write(context->session_cancel_fd, &marker, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void frontend_eof_application_session_close(
|
|
void *opaque, cbm_daemon_runtime_application_session_t *opaque_session) {
|
|
frontend_eof_application_context_t *context = opaque;
|
|
frontend_eof_application_session_t *session =
|
|
(frontend_eof_application_session_t *)opaque_session;
|
|
if (context && session && session->context == context) {
|
|
free(session);
|
|
}
|
|
}
|
|
|
|
static bool frontend_eof_write_all(int fd, const char *bytes, size_t length) {
|
|
size_t offset = 0;
|
|
while (offset < length) {
|
|
ssize_t written = write(fd, bytes + offset, length - offset);
|
|
if (written > 0) {
|
|
offset += (size_t)written;
|
|
} else if (written < 0 && errno == EINTR) {
|
|
continue;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void *frontend_eof_writer(void *opaque) {
|
|
frontend_eof_writer_t *writer = opaque;
|
|
static const char first[] =
|
|
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"test/block\",\"params\":{}}\n";
|
|
bool ok = frontend_eof_write_all(writer->fd, first, sizeof(first) - 1);
|
|
uint64_t deadline = cbm_now_ms() + FRONTEND_EOF_TEST_REQUEST_TIMEOUT_MS;
|
|
while (
|
|
ok &&
|
|
!atomic_load_explicit(&writer->application->first_request_started, memory_order_acquire) &&
|
|
cbm_now_ms() < deadline) {
|
|
cbm_usleep(1000);
|
|
}
|
|
ok = ok &&
|
|
atomic_load_explicit(&writer->application->first_request_started, memory_order_acquire);
|
|
for (int index = 0; ok && writer->overflow && index < FRONTEND_EOF_TEST_OVERFLOW_MESSAGES;
|
|
index++) {
|
|
char message[160];
|
|
int written =
|
|
snprintf(message, sizeof(message),
|
|
"{\"jsonrpc\":\"2.0\",\"id\":%d,\"method\":\"test/queued\",\"params\":{}}\n",
|
|
index + 2);
|
|
ok = written > 0 && written < (int)sizeof(message) &&
|
|
frontend_eof_write_all(writer->fd, message, (size_t)written);
|
|
}
|
|
ok = close(writer->fd) == 0 && ok;
|
|
writer->fd = -1;
|
|
atomic_store_explicit(&writer->succeeded, ok, memory_order_release);
|
|
atomic_store_explicit(&writer->finished, true, memory_order_release);
|
|
return NULL;
|
|
}
|
|
|
|
static bool frontend_eof_fixture_start(frontend_eof_fixture_t *fixture, const char *parent) {
|
|
memset(fixture, 0, sizeof(*fixture));
|
|
atomic_init(&fixture->application.requests, 0);
|
|
atomic_init(&fixture->application.session_cancels, 0);
|
|
atomic_init(&fixture->application.sessions_opened, 0);
|
|
atomic_init(&fixture->application.first_session_cancels, 0);
|
|
atomic_init(&fixture->application.second_session_cancels, 0);
|
|
atomic_init(&fixture->application.block_first_request, true);
|
|
atomic_init(&fixture->application.first_request_started, false);
|
|
fixture->application.request_observed_fd = -1;
|
|
fixture->application.session_cancel_fd = -1;
|
|
char key[CBM_DAEMON_KEY_SIZE];
|
|
char build[CBM_DAEMON_BUILD_FINGERPRINT_SIZE];
|
|
int log_written = snprintf(fixture->conflict_log, sizeof(fixture->conflict_log),
|
|
"%s/conflicts.ndjson", parent);
|
|
if (log_written <= 0 || log_written >= (int)sizeof(fixture->conflict_log) ||
|
|
!cbm_daemon_rendezvous_key(key) ||
|
|
!cbm_daemon_runtime_process_build_fingerprint((uint64_t)getpid(), build)) {
|
|
return false;
|
|
}
|
|
fixture->endpoint = cbm_daemon_ipc_endpoint_new(key, parent);
|
|
fixture->manager = fixture->endpoint ? cbm_version_cohort_manager_new(fixture->endpoint) : NULL;
|
|
cbm_daemon_build_identity_t identity = {
|
|
.semantic_version = "2.4.0",
|
|
.build_fingerprint = build,
|
|
.cache_fingerprint = FRONTEND_TEST_CACHE,
|
|
.protocol_abi = 3,
|
|
.store_abi = 11,
|
|
.feature_abi = 7,
|
|
};
|
|
cbm_daemon_runtime_application_callbacks_t callbacks = {
|
|
.context = &fixture->application,
|
|
.session_open = frontend_eof_application_session_open,
|
|
.request = frontend_eof_application_request,
|
|
.request_cancel = frontend_eof_application_request_cancel,
|
|
.session_cancel = frontend_eof_application_session_cancel,
|
|
.session_close = frontend_eof_application_session_close,
|
|
};
|
|
cbm_daemon_runtime_service_config_t config = {
|
|
.endpoint = fixture->endpoint,
|
|
.identity = identity,
|
|
.conflict_log_path = fixture->conflict_log,
|
|
.conflict_log_cap_bytes = 64U * 1024U,
|
|
.max_clients = 4,
|
|
.lease_timeout_ms = 5000,
|
|
.request_timeout_ms = FRONTEND_EOF_TEST_REQUEST_TIMEOUT_MS,
|
|
.shutdown_timeout_ms = FRONTEND_EOF_TEST_REQUEST_TIMEOUT_MS,
|
|
.application = callbacks,
|
|
};
|
|
fixture->service = fixture->manager ? cbm_daemon_runtime_service_start(&config) : NULL;
|
|
cbm_daemon_runtime_connect_result_t connect_result = {0};
|
|
fixture->client = fixture->service
|
|
? cbm_daemon_runtime_client_connect(fixture->endpoint, &identity,
|
|
FRONTEND_EOF_TEST_REQUEST_TIMEOUT_MS,
|
|
&connect_result)
|
|
: NULL;
|
|
return fixture->client && connect_result.status == CBM_DAEMON_RUNTIME_CONNECT_ACCEPTED;
|
|
}
|
|
|
|
static bool frontend_eof_fixture_finish(frontend_eof_fixture_t *fixture) {
|
|
bool ok = true;
|
|
if (fixture->client) {
|
|
ok = cbm_daemon_runtime_client_close(fixture->client,
|
|
FRONTEND_EOF_TEST_REQUEST_TIMEOUT_MS) &&
|
|
ok;
|
|
fixture->client = NULL;
|
|
}
|
|
if (fixture->service) {
|
|
if (cbm_daemon_runtime_service_state(fixture->service) !=
|
|
CBM_DAEMON_RUNTIME_SERVICE_EXITED) {
|
|
ok = cbm_daemon_runtime_service_stop(fixture->service,
|
|
FRONTEND_EOF_TEST_REQUEST_TIMEOUT_MS) &&
|
|
ok;
|
|
}
|
|
ok = cbm_daemon_runtime_service_free(fixture->service) && ok;
|
|
fixture->service = NULL;
|
|
}
|
|
if (fixture->manager) {
|
|
ok = cbm_version_cohort_manager_free(&fixture->manager) == CBM_PRIVATE_FILE_LOCK_OK && ok;
|
|
}
|
|
cbm_daemon_ipc_endpoint_free(fixture->endpoint);
|
|
fixture->endpoint = NULL;
|
|
return ok;
|
|
}
|
|
|
|
static int frontend_eof_child_run(const char *parent, bool overflow) {
|
|
frontend_eof_fixture_t fixture;
|
|
if (!frontend_eof_fixture_start(&fixture, parent)) {
|
|
(void)frontend_eof_fixture_finish(&fixture);
|
|
return 70;
|
|
}
|
|
int input_pipe[2] = {-1, -1};
|
|
if (pipe(input_pipe) != 0) {
|
|
(void)frontend_eof_fixture_finish(&fixture);
|
|
return 71;
|
|
}
|
|
FILE *input = fdopen(input_pipe[0], "rb");
|
|
FILE *output = tmpfile();
|
|
frontend_eof_writer_t writer = {
|
|
.fd = input_pipe[1],
|
|
.overflow = overflow,
|
|
.application = &fixture.application,
|
|
};
|
|
atomic_init(&writer.finished, false);
|
|
atomic_init(&writer.succeeded, false);
|
|
cbm_thread_t writer_thread;
|
|
bool writer_started =
|
|
input && output && cbm_thread_create(&writer_thread, 0, frontend_eof_writer, &writer) == 0;
|
|
if (!writer_started) {
|
|
if (input) {
|
|
(void)fclose(input);
|
|
} else {
|
|
(void)close(input_pipe[0]);
|
|
}
|
|
(void)close(input_pipe[1]);
|
|
if (output) {
|
|
(void)fclose(output);
|
|
}
|
|
(void)frontend_eof_fixture_finish(&fixture);
|
|
return 72;
|
|
}
|
|
|
|
cbm_daemon_runtime_client_t *frontend_client = fixture.client;
|
|
fixture.client = NULL; /* cbm_daemon_frontend_mcp_run consumes it. */
|
|
int result = cbm_daemon_frontend_mcp_run(frontend_client, fixture.manager, input, output);
|
|
bool joined = cbm_thread_join(&writer_thread) == 0;
|
|
bool writer_ok = atomic_load_explicit(&writer.finished, memory_order_acquire) &&
|
|
atomic_load_explicit(&writer.succeeded, memory_order_acquire);
|
|
bool request_started =
|
|
atomic_load_explicit(&fixture.application.first_request_started, memory_order_acquire);
|
|
bool session_cancelled =
|
|
atomic_load_explicit(&fixture.application.session_cancels, memory_order_acquire) > 0;
|
|
bool input_closed = fclose(input) == 0;
|
|
bool output_closed = fclose(output) == 0;
|
|
bool streams_closed = input_closed && output_closed;
|
|
bool fixture_closed = frontend_eof_fixture_finish(&fixture);
|
|
bool result_matches_contract = overflow ? result < 0 : result == 0;
|
|
return result_matches_contract && joined && writer_ok && request_started && session_cancelled &&
|
|
streams_closed && fixture_closed
|
|
? 0
|
|
: 73;
|
|
}
|
|
|
|
static bool frontend_eof_run_isolated(const char *tag, bool overflow) {
|
|
char parent[FRONTEND_TEST_PATH_CAP];
|
|
int written =
|
|
snprintf(parent, sizeof(parent), "%s/cbm-frontend-eof-%s-XXXXXX", cbm_tmpdir(), tag);
|
|
if (written <= 0 || written >= (int)sizeof(parent) || !cbm_mkdtemp(parent)) {
|
|
return false;
|
|
}
|
|
pid_t child = fork();
|
|
if (child == 0) {
|
|
(void)signal(SIGALRM, SIG_DFL);
|
|
/* This alarm bounds a broken isolated harness, not product behavior.
|
|
* Valid startup, drain/watchdog, close, and fixture teardown can exceed
|
|
* ten seconds under sanitizers while remaining within their own bounds. */
|
|
(void)alarm(FRONTEND_EOF_TEST_CATASTROPHIC_TIMEOUT_S);
|
|
_exit(frontend_eof_child_run(parent, overflow));
|
|
}
|
|
int status = 0;
|
|
pid_t waited;
|
|
do {
|
|
waited = child > 0 ? waitpid(child, &status, 0) : -1;
|
|
} while (waited < 0 && errno == EINTR);
|
|
bool cleaned = th_rmtree(parent) == 0;
|
|
bool child_ok = child > 0 && waited == child && WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
|
if (!child_ok) {
|
|
if (child <= 0) {
|
|
(void)fprintf(stderr, "frontend EOF fixture %s: fork failed\n", tag);
|
|
} else if (waited != child) {
|
|
(void)fprintf(stderr, "frontend EOF fixture %s: waitpid failed errno=%d\n", tag, errno);
|
|
} else if (WIFSIGNALED(status)) {
|
|
(void)fprintf(stderr, "frontend EOF fixture %s: child signal=%d\n", tag,
|
|
WTERMSIG(status));
|
|
} else if (WIFEXITED(status)) {
|
|
(void)fprintf(stderr, "frontend EOF fixture %s: child exit=%d\n", tag,
|
|
WEXITSTATUS(status));
|
|
} else {
|
|
(void)fprintf(stderr, "frontend EOF fixture %s: unexpected child status\n", tag);
|
|
}
|
|
}
|
|
if (!cleaned) {
|
|
(void)fprintf(stderr, "frontend EOF fixture %s: cleanup failed\n", tag);
|
|
}
|
|
return child_ok && cleaned;
|
|
}
|
|
|
|
static void frontend_test_release_lease(cbm_version_cohort_lease_t **lease) {
|
|
while (lease && *lease && cbm_version_cohort_lease_release(lease) != CBM_PRIVATE_FILE_LOCK_OK) {
|
|
cbm_usleep(1000);
|
|
}
|
|
}
|
|
|
|
static bool frontend_test_release_lease_until(cbm_version_cohort_lease_t **lease,
|
|
uint64_t deadline_ms) {
|
|
while (lease && *lease) {
|
|
if (cbm_version_cohort_lease_release(lease) == CBM_PRIVATE_FILE_LOCK_OK) {
|
|
continue;
|
|
}
|
|
if (cbm_now_ms() >= deadline_ms) {
|
|
return false;
|
|
}
|
|
cbm_usleep(1000);
|
|
}
|
|
return !lease || !*lease;
|
|
}
|
|
|
|
static bool frontend_test_read_byte(int fd, char *observed, uint64_t deadline_ms) {
|
|
if (!observed) {
|
|
return false;
|
|
}
|
|
*observed = '\0';
|
|
int flags = fcntl(fd, F_GETFL, 0);
|
|
if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) != 0) {
|
|
return false;
|
|
}
|
|
for (;;) {
|
|
char value = '\0';
|
|
ssize_t count = read(fd, &value, 1);
|
|
if (count == 1) {
|
|
*observed = value;
|
|
return true;
|
|
}
|
|
if (count == 0 || (count < 0 && errno != EAGAIN && errno != EWOULDBLOCK) ||
|
|
cbm_now_ms() >= deadline_ms) {
|
|
return false;
|
|
}
|
|
cbm_usleep(1000);
|
|
}
|
|
}
|
|
|
|
static bool frontend_test_wait_byte(int fd, char expected, uint64_t deadline_ms) {
|
|
char observed = '\0';
|
|
return frontend_test_read_byte(fd, &observed, deadline_ms) && observed == expected;
|
|
}
|
|
|
|
typedef struct {
|
|
int fd;
|
|
atomic_bool finished;
|
|
atomic_bool succeeded;
|
|
} frontend_backpressure_writer_t;
|
|
|
|
static cbm_version_cohort_quiesce_result_t frontend_test_quiesce_requested(void *context);
|
|
|
|
static bool frontend_backpressure_identity(cbm_daemon_build_identity_t *identity,
|
|
const char build[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
|
|
if (!identity || !build || strlen(build) != CBM_DAEMON_BUILD_FINGERPRINT_SIZE - 1) {
|
|
return false;
|
|
}
|
|
*identity = (cbm_daemon_build_identity_t){
|
|
.semantic_version = "2.4.0",
|
|
.build_fingerprint = build,
|
|
.cache_fingerprint = FRONTEND_TEST_CACHE,
|
|
.protocol_abi = 3,
|
|
.store_abi = 11,
|
|
.feature_abi = 7,
|
|
};
|
|
return true;
|
|
}
|
|
|
|
static void *frontend_backpressure_writer(void *opaque) {
|
|
frontend_backpressure_writer_t *writer = opaque;
|
|
static const char prefix[] = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"test/large\","
|
|
"\"params\":{\"blob\":\"";
|
|
static const char suffix[] = "\"}}\n";
|
|
char *message = malloc(FRONTEND_BACKPRESSURE_MESSAGE_BYTES);
|
|
size_t prefix_length = sizeof(prefix) - 1;
|
|
size_t suffix_length = sizeof(suffix) - 1;
|
|
bool valid = message && prefix_length + suffix_length < FRONTEND_BACKPRESSURE_MESSAGE_BYTES;
|
|
if (valid) {
|
|
memcpy(message, prefix, prefix_length);
|
|
memset(message + prefix_length, 'x',
|
|
FRONTEND_BACKPRESSURE_MESSAGE_BYTES - prefix_length - suffix_length);
|
|
memcpy(message + FRONTEND_BACKPRESSURE_MESSAGE_BYTES - suffix_length, suffix,
|
|
suffix_length);
|
|
valid = frontend_eof_write_all(writer->fd, message, FRONTEND_BACKPRESSURE_MESSAGE_BYTES);
|
|
}
|
|
free(message);
|
|
valid = close(writer->fd) == 0 && valid;
|
|
writer->fd = -1;
|
|
atomic_store_explicit(&writer->succeeded, valid, memory_order_release);
|
|
atomic_store_explicit(&writer->finished, true, memory_order_release);
|
|
return NULL;
|
|
}
|
|
|
|
static int frontend_backpressure_daemon_run(const char *parent, int ready_fd, int cancel_fd,
|
|
int control_fd,
|
|
const char build[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
|
|
(void)alarm(FRONTEND_BACKPRESSURE_DAEMON_TIMEOUT_S);
|
|
char key[CBM_DAEMON_KEY_SIZE];
|
|
char conflict_log[FRONTEND_TEST_PATH_CAP];
|
|
cbm_daemon_build_identity_t identity = {0};
|
|
int log_written = snprintf(conflict_log, sizeof(conflict_log), "%s/conflicts.ndjson", parent);
|
|
cbm_daemon_ipc_endpoint_t *endpoint =
|
|
log_written > 0 && log_written < (int)sizeof(conflict_log) &&
|
|
cbm_daemon_rendezvous_key(key) && frontend_backpressure_identity(&identity, build)
|
|
? cbm_daemon_ipc_endpoint_new(key, parent)
|
|
: NULL;
|
|
frontend_eof_application_context_t application;
|
|
memset(&application, 0, sizeof(application));
|
|
atomic_init(&application.requests, 0);
|
|
atomic_init(&application.session_cancels, 0);
|
|
atomic_init(&application.sessions_opened, 0);
|
|
atomic_init(&application.first_session_cancels, 0);
|
|
atomic_init(&application.second_session_cancels, 0);
|
|
atomic_init(&application.block_first_request, false);
|
|
atomic_init(&application.first_request_started, false);
|
|
application.request_observed_fd = ready_fd;
|
|
application.session_cancel_fd = cancel_fd;
|
|
cbm_daemon_runtime_application_callbacks_t callbacks = {
|
|
.context = &application,
|
|
.session_open = frontend_eof_application_session_open,
|
|
.request = frontend_eof_application_request,
|
|
.request_cancel = frontend_eof_application_request_cancel,
|
|
.session_cancel = frontend_eof_application_session_cancel,
|
|
.session_close = frontend_eof_application_session_close,
|
|
};
|
|
cbm_daemon_runtime_service_config_t config = {
|
|
.endpoint = endpoint,
|
|
.identity = identity,
|
|
.conflict_log_path = conflict_log,
|
|
.conflict_log_cap_bytes = 64U * 1024U,
|
|
.max_clients = 4,
|
|
.lease_timeout_ms = 30000,
|
|
.request_timeout_ms = FRONTEND_EOF_TEST_REQUEST_TIMEOUT_MS,
|
|
.shutdown_timeout_ms = FRONTEND_EOF_TEST_REQUEST_TIMEOUT_MS,
|
|
.application = callbacks,
|
|
};
|
|
cbm_daemon_runtime_service_t *service =
|
|
endpoint ? cbm_daemon_runtime_service_start(&config) : NULL;
|
|
const char ready = 'R';
|
|
bool announced = service && write(ready_fd, &ready, 1) == 1;
|
|
uint64_t cancellation_deadline = cbm_now_ms() + FRONTEND_BACKPRESSURE_RUNTIME_TIMEOUT_MS;
|
|
while (announced &&
|
|
atomic_load_explicit(&application.session_cancels, memory_order_acquire) == 0 &&
|
|
cbm_now_ms() < cancellation_deadline) {
|
|
cbm_usleep(1000);
|
|
}
|
|
int target_cancellations =
|
|
atomic_load_explicit(&application.session_cancels, memory_order_acquire);
|
|
int sentinel_cancellations_before_close =
|
|
atomic_load_explicit(&application.first_session_cancels, memory_order_acquire);
|
|
int target_session_cancellations =
|
|
atomic_load_explicit(&application.second_session_cancels, memory_order_acquire);
|
|
bool target_cancelled_exactly_once = announced && target_cancellations == 1 &&
|
|
sentinel_cancellations_before_close == 0 &&
|
|
target_session_cancellations == 1;
|
|
const char checkpoint = target_cancelled_exactly_once ? 'T' : 'E';
|
|
bool checkpoint_written = write(ready_fd, &checkpoint, 1) == 1;
|
|
char control = '\0';
|
|
bool shutdown_released =
|
|
checkpoint_written &&
|
|
frontend_test_read_byte(control_fd, &control,
|
|
cbm_now_ms() + FRONTEND_BACKPRESSURE_RUNTIME_TIMEOUT_MS) &&
|
|
control == 'S';
|
|
int final_cancellations =
|
|
atomic_load_explicit(&application.session_cancels, memory_order_acquire);
|
|
int sentinel_session_cancellations =
|
|
atomic_load_explicit(&application.first_session_cancels, memory_order_acquire);
|
|
int final_target_session_cancellations =
|
|
atomic_load_explicit(&application.second_session_cancels, memory_order_acquire);
|
|
bool sentinel_closed_exactly_once = final_cancellations == 2 &&
|
|
sentinel_session_cancellations == 1 &&
|
|
final_target_session_cancellations == 1;
|
|
bool stopped =
|
|
service && cbm_daemon_runtime_service_stop(service, FRONTEND_EOF_TEST_REQUEST_TIMEOUT_MS);
|
|
bool exited = stopped && cbm_daemon_runtime_service_wait_exited(service, 0);
|
|
bool freed = exited && cbm_daemon_runtime_service_free(service);
|
|
cbm_daemon_ipc_endpoint_free(endpoint);
|
|
const char done = target_cancelled_exactly_once && shutdown_released &&
|
|
sentinel_closed_exactly_once && stopped && exited && freed
|
|
? 'D'
|
|
: 'E';
|
|
bool finished = write(ready_fd, &done, 1) == 1;
|
|
(void)close(ready_fd);
|
|
(void)close(cancel_fd);
|
|
(void)close(control_fd);
|
|
return announced && target_cancelled_exactly_once && checkpoint_written && shutdown_released &&
|
|
sentinel_closed_exactly_once && stopped && exited && freed && finished
|
|
? 0
|
|
: 80;
|
|
}
|
|
|
|
static int frontend_backpressure_frontend_run(const char *parent, int input_fd, int input_write_fd,
|
|
int output_fd,
|
|
const char build[CBM_DAEMON_BUILD_FINGERPRINT_SIZE],
|
|
bool keep_input_open) {
|
|
(void)alarm(FRONTEND_BACKPRESSURE_FRONTEND_TIMEOUT_S);
|
|
char key[CBM_DAEMON_KEY_SIZE];
|
|
cbm_daemon_build_identity_t identity = {0};
|
|
cbm_daemon_ipc_endpoint_t *endpoint =
|
|
cbm_daemon_rendezvous_key(key) && frontend_backpressure_identity(&identity, build)
|
|
? cbm_daemon_ipc_endpoint_new(key, parent)
|
|
: NULL;
|
|
cbm_version_cohort_manager_t *manager =
|
|
endpoint ? cbm_version_cohort_manager_new(endpoint) : NULL;
|
|
cbm_version_cohort_lease_t *participant = NULL;
|
|
cbm_daemon_conflict_t conflict;
|
|
cbm_version_cohort_status_t admitted =
|
|
manager ? cbm_version_cohort_acquire(manager, &identity, cbm_now_ms() + 2000U, &participant,
|
|
&conflict)
|
|
: CBM_VERSION_COHORT_IO;
|
|
cbm_daemon_runtime_connect_result_t connect_result = {0};
|
|
cbm_daemon_runtime_client_t *client =
|
|
admitted == CBM_VERSION_COHORT_OK
|
|
? cbm_daemon_runtime_client_connect(
|
|
endpoint, &identity, FRONTEND_EOF_TEST_REQUEST_TIMEOUT_MS, &connect_result)
|
|
: NULL;
|
|
FILE *input = client ? fdopen(input_fd, "rb") : NULL;
|
|
FILE *output = input ? fdopen(output_fd, "wb") : NULL;
|
|
int input_hold_fd = keep_input_open && output ? dup(input_write_fd) : -1;
|
|
frontend_backpressure_writer_t writer = {
|
|
.fd = input_write_fd,
|
|
};
|
|
atomic_init(&writer.finished, false);
|
|
atomic_init(&writer.succeeded, false);
|
|
cbm_thread_t writer_thread;
|
|
bool writer_started =
|
|
output && (!keep_input_open || input_hold_fd >= 0) &&
|
|
cbm_thread_create(&writer_thread, 0, frontend_backpressure_writer, &writer) == 0;
|
|
if (!writer_started) {
|
|
if (client) {
|
|
(void)cbm_daemon_runtime_client_close(client, FRONTEND_EOF_TEST_REQUEST_TIMEOUT_MS);
|
|
}
|
|
if (input) {
|
|
(void)fclose(input);
|
|
} else {
|
|
(void)close(input_fd);
|
|
}
|
|
if (output) {
|
|
(void)fclose(output);
|
|
} else {
|
|
(void)close(output_fd);
|
|
}
|
|
(void)close(input_write_fd);
|
|
if (input_hold_fd >= 0) {
|
|
(void)close(input_hold_fd);
|
|
}
|
|
frontend_test_release_lease(&participant);
|
|
if (manager) {
|
|
(void)cbm_version_cohort_manager_free(&manager);
|
|
}
|
|
cbm_daemon_ipc_endpoint_free(endpoint);
|
|
return 81;
|
|
}
|
|
|
|
int result = cbm_daemon_frontend_mcp_run(client, manager, input, output);
|
|
bool joined = cbm_thread_join(&writer_thread) == 0;
|
|
bool writer_ok = atomic_load_explicit(&writer.finished, memory_order_acquire) &&
|
|
atomic_load_explicit(&writer.succeeded, memory_order_acquire);
|
|
bool input_closed = fclose(input) == 0;
|
|
bool output_closed = fclose(output) == 0;
|
|
bool input_hold_closed = input_hold_fd < 0 || close(input_hold_fd) == 0;
|
|
frontend_test_release_lease(&participant);
|
|
bool manager_closed = cbm_version_cohort_manager_free(&manager) == CBM_PRIVATE_FILE_LOCK_OK;
|
|
cbm_daemon_ipc_endpoint_free(endpoint);
|
|
return result < 0 && joined && writer_ok && input_closed && output_closed &&
|
|
input_hold_closed && manager_closed
|
|
? 47
|
|
: 82;
|
|
}
|
|
|
|
static bool frontend_backpressure_run_isolated(bool maintenance) {
|
|
char parent[FRONTEND_TEST_PATH_CAP];
|
|
char build[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
|
int path_written = snprintf(parent, sizeof(parent),
|
|
maintenance ? "%s/cbm-frontend-maintenance-backpressure-XXXXXX"
|
|
: "%s/cbm-frontend-backpressure-XXXXXX",
|
|
cbm_tmpdir());
|
|
int ready_pipe[2] = {-1, -1};
|
|
int cancel_pipe[2] = {-1, -1};
|
|
int control_pipe[2] = {-1, -1};
|
|
bool directory_ready =
|
|
path_written > 0 && path_written < (int)sizeof(parent) && cbm_mkdtemp(parent);
|
|
bool identity_ready =
|
|
directory_ready && cbm_daemon_runtime_process_build_fingerprint((uint64_t)getpid(), build);
|
|
bool prepared = identity_ready && pipe(ready_pipe) == 0 && pipe(cancel_pipe) == 0 &&
|
|
pipe(control_pipe) == 0;
|
|
if (!prepared) {
|
|
for (size_t index = 0; index < 2; index++) {
|
|
if (ready_pipe[index] >= 0) {
|
|
(void)close(ready_pipe[index]);
|
|
}
|
|
if (cancel_pipe[index] >= 0) {
|
|
(void)close(cancel_pipe[index]);
|
|
}
|
|
if (control_pipe[index] >= 0) {
|
|
(void)close(control_pipe[index]);
|
|
}
|
|
}
|
|
if (directory_ready) {
|
|
(void)th_rmtree(parent);
|
|
}
|
|
return false;
|
|
}
|
|
pid_t daemon = prepared ? fork() : -1;
|
|
if (daemon == 0) {
|
|
(void)signal(SIGALRM, SIG_DFL);
|
|
(void)close(ready_pipe[0]);
|
|
(void)close(cancel_pipe[0]);
|
|
(void)close(control_pipe[1]);
|
|
_exit(frontend_backpressure_daemon_run(parent, ready_pipe[1], cancel_pipe[1],
|
|
control_pipe[0], build));
|
|
}
|
|
if (prepared) {
|
|
(void)close(ready_pipe[1]);
|
|
(void)close(cancel_pipe[1]);
|
|
(void)close(control_pipe[0]);
|
|
}
|
|
char announced_marker = '\0';
|
|
/* The daemon child runs cbm_daemon_runtime_service_start before it can
|
|
* announce, and that includes the cohort-claim path whose own internal
|
|
* budget is HOST_DAEMON_CLAIM_TIMEOUT_MS (12s). An announce deadline
|
|
* below that budget SIGKILLs a slow-but-legitimate startup — on
|
|
* oversubscribed CI runners this fired every round (announced=0,
|
|
* daemon_signal=9) while fast local machines never saw it. Keep the
|
|
* deadline comfortably above the daemon's own worst-case budget. */
|
|
bool announced_read = daemon > 0 && frontend_test_read_byte(ready_pipe[0], &announced_marker,
|
|
cbm_now_ms() + 30000U);
|
|
bool announced = announced_read && announced_marker == 'R';
|
|
|
|
/* This raw runtime client intentionally owns no cohort lease. It is a
|
|
* sentinel for session isolation: maintenance must remove the target
|
|
* frontend without touching this independently authenticated session. Its
|
|
* completed admission precedes the target fork, so the daemon fixture can
|
|
* attribute session-cancel callbacks to sentinel index 0 and target index
|
|
* 1 rather than relying on a global callback count. */
|
|
char sentinel_key[CBM_DAEMON_KEY_SIZE];
|
|
cbm_daemon_build_identity_t sentinel_identity = {0};
|
|
cbm_daemon_ipc_endpoint_t *sentinel_endpoint =
|
|
announced && cbm_daemon_rendezvous_key(sentinel_key) &&
|
|
frontend_backpressure_identity(&sentinel_identity, build)
|
|
? cbm_daemon_ipc_endpoint_new(sentinel_key, parent)
|
|
: NULL;
|
|
cbm_daemon_runtime_connect_result_t sentinel_connect = {0};
|
|
cbm_daemon_runtime_client_t *sentinel =
|
|
sentinel_endpoint ? cbm_daemon_runtime_client_connect(sentinel_endpoint, &sentinel_identity,
|
|
FRONTEND_EOF_TEST_REQUEST_TIMEOUT_MS,
|
|
&sentinel_connect)
|
|
: NULL;
|
|
bool sentinel_ready =
|
|
sentinel && sentinel_connect.status == CBM_DAEMON_RUNTIME_CONNECT_ACCEPTED;
|
|
if (!sentinel_ready && daemon > 0) {
|
|
(void)kill(daemon, SIGKILL);
|
|
}
|
|
|
|
int input_pipe[2] = {-1, -1};
|
|
int output_pipe[2] = {-1, -1};
|
|
bool input_ready = sentinel_ready && pipe(input_pipe) == 0;
|
|
bool frontend_pipes = input_ready && pipe(output_pipe) == 0;
|
|
if (input_ready && !frontend_pipes) {
|
|
(void)close(input_pipe[0]);
|
|
(void)close(input_pipe[1]);
|
|
input_pipe[0] = -1;
|
|
input_pipe[1] = -1;
|
|
}
|
|
pid_t frontend = frontend_pipes ? fork() : -1;
|
|
if (frontend == 0) {
|
|
(void)signal(SIGALRM, SIG_DFL);
|
|
(void)close(output_pipe[0]);
|
|
(void)close(ready_pipe[0]);
|
|
(void)close(cancel_pipe[0]);
|
|
(void)close(control_pipe[1]);
|
|
_exit(frontend_backpressure_frontend_run(parent, input_pipe[0], input_pipe[1],
|
|
output_pipe[1], build, maintenance));
|
|
}
|
|
if (frontend_pipes) {
|
|
(void)close(input_pipe[0]);
|
|
(void)close(input_pipe[1]);
|
|
(void)close(output_pipe[1]);
|
|
}
|
|
|
|
char request_marker = '\0';
|
|
bool request_read = frontend > 0 && frontend_test_read_byte(ready_pipe[0], &request_marker,
|
|
cbm_now_ms() + 4000U);
|
|
bool request_observed = request_read && request_marker == 'Q';
|
|
char output_marker = '\0';
|
|
bool output_observed =
|
|
!maintenance || (request_observed && frontend_test_read_byte(output_pipe[0], &output_marker,
|
|
cbm_now_ms() + 4000U));
|
|
|
|
cbm_daemon_ipc_endpoint_t *mutation_endpoint = NULL;
|
|
cbm_version_cohort_manager_t *mutation_manager = NULL;
|
|
cbm_version_cohort_lease_t *mutation = NULL;
|
|
cbm_version_cohort_quiesce_result_t quiesce = CBM_VERSION_COHORT_QUIESCE_NOT_NEEDED;
|
|
cbm_version_cohort_status_t mutation_status = CBM_VERSION_COHORT_OK;
|
|
if (maintenance) {
|
|
char mutation_key[CBM_DAEMON_KEY_SIZE];
|
|
mutation_endpoint = output_observed && cbm_daemon_rendezvous_key(mutation_key)
|
|
? cbm_daemon_ipc_endpoint_new(mutation_key, parent)
|
|
: NULL;
|
|
mutation_manager =
|
|
mutation_endpoint ? cbm_version_cohort_manager_new(mutation_endpoint) : NULL;
|
|
mutation_status = mutation_manager
|
|
? cbm_version_cohort_reserve_for_mutation(
|
|
mutation_manager, cbm_now_ms() + 8000U,
|
|
frontend_test_quiesce_requested, NULL, &quiesce, &mutation)
|
|
: CBM_VERSION_COHORT_IO;
|
|
if (mutation_status != CBM_VERSION_COHORT_OK && frontend > 0) {
|
|
(void)kill(frontend, SIGKILL);
|
|
}
|
|
}
|
|
|
|
int frontend_status = 0;
|
|
pid_t frontend_waited;
|
|
if (frontend > 0) {
|
|
do {
|
|
frontend_waited = waitpid(frontend, &frontend_status, 0);
|
|
} while (frontend_waited < 0 && errno == EINTR);
|
|
} else {
|
|
frontend_waited = -1;
|
|
}
|
|
char cancel_marker = '\0';
|
|
bool cancel_read = request_observed && frontend_test_read_byte(cancel_pipe[0], &cancel_marker,
|
|
cbm_now_ms() + 4000U);
|
|
bool cancelled = cancel_read && cancel_marker == 'C';
|
|
char checkpoint_marker = '\0';
|
|
bool checkpoint_read =
|
|
frontend_test_read_byte(ready_pipe[0], &checkpoint_marker, cbm_now_ms() + 4000U);
|
|
bool target_cancelled_exactly_once = checkpoint_read && checkpoint_marker == 'T';
|
|
bool sentinel_usable =
|
|
cancelled && target_cancelled_exactly_once && sentinel_ready &&
|
|
cbm_daemon_runtime_client_heartbeat(sentinel, FRONTEND_EOF_TEST_REQUEST_TIMEOUT_MS);
|
|
bool sentinel_closed =
|
|
sentinel && cbm_daemon_runtime_client_close(sentinel, FRONTEND_EOF_TEST_REQUEST_TIMEOUT_MS);
|
|
sentinel = NULL;
|
|
cbm_daemon_ipc_endpoint_free(sentinel_endpoint);
|
|
sentinel_endpoint = NULL;
|
|
const char shutdown = 'S';
|
|
bool shutdown_sent = checkpoint_read && write(control_pipe[1], &shutdown, 1) == 1;
|
|
if (control_pipe[1] >= 0) {
|
|
(void)close(control_pipe[1]);
|
|
control_pipe[1] = -1;
|
|
}
|
|
char done_marker = '\0';
|
|
bool done_read =
|
|
shutdown_sent && frontend_test_read_byte(ready_pipe[0], &done_marker, cbm_now_ms() + 4000U);
|
|
bool daemon_done = done_read && done_marker == 'D';
|
|
if (!daemon_done && daemon > 0) {
|
|
(void)kill(daemon, SIGKILL);
|
|
}
|
|
int daemon_status = 0;
|
|
pid_t daemon_waited;
|
|
do {
|
|
daemon_waited = daemon > 0 ? waitpid(daemon, &daemon_status, 0) : -1;
|
|
} while (daemon_waited < 0 && errno == EINTR);
|
|
|
|
uint64_t cleanup_now = cbm_now_ms();
|
|
uint64_t cleanup_deadline = cleanup_now > UINT64_MAX - FRONTEND_BACKPRESSURE_CLEANUP_TIMEOUT_MS
|
|
? UINT64_MAX
|
|
: cleanup_now + FRONTEND_BACKPRESSURE_CLEANUP_TIMEOUT_MS;
|
|
bool mutation_released = frontend_test_release_lease_until(&mutation, cleanup_deadline);
|
|
while (mutation_released && mutation_manager && cbm_now_ms() < cleanup_deadline &&
|
|
cbm_version_cohort_manager_free(&mutation_manager) != CBM_PRIVATE_FILE_LOCK_OK) {
|
|
cbm_usleep(1000);
|
|
}
|
|
bool mutation_manager_closed = mutation_manager == NULL;
|
|
cbm_daemon_ipc_endpoint_free(mutation_endpoint);
|
|
|
|
if (output_pipe[0] >= 0) {
|
|
(void)close(output_pipe[0]);
|
|
}
|
|
if (ready_pipe[0] >= 0) {
|
|
(void)close(ready_pipe[0]);
|
|
}
|
|
if (cancel_pipe[0] >= 0) {
|
|
(void)close(cancel_pipe[0]);
|
|
}
|
|
if (control_pipe[1] >= 0) {
|
|
(void)close(control_pipe[1]);
|
|
}
|
|
bool cleaned = prepared && th_rmtree(parent) == 0;
|
|
bool frontend_exited_boundedly = frontend_waited == frontend && WIFEXITED(frontend_status) &&
|
|
(maintenance ? WEXITSTATUS(frontend_status) == EXIT_SUCCESS
|
|
: (WEXITSTATUS(frontend_status) == EXIT_FAILURE ||
|
|
WEXITSTATUS(frontend_status) == 47));
|
|
bool daemon_clean =
|
|
daemon_waited == daemon && WIFEXITED(daemon_status) && WEXITSTATUS(daemon_status) == 0;
|
|
bool maintenance_completed =
|
|
!maintenance || (output_observed && mutation_status == CBM_VERSION_COHORT_OK &&
|
|
quiesce == CBM_VERSION_COHORT_QUIESCE_REQUESTED);
|
|
bool passed = prepared && announced && sentinel_ready && frontend_pipes && frontend > 0 &&
|
|
request_observed && maintenance_completed && frontend_exited_boundedly &&
|
|
cancelled && target_cancelled_exactly_once && sentinel_usable &&
|
|
sentinel_closed && shutdown_sent && daemon_done && daemon_clean &&
|
|
mutation_released && mutation_manager_closed && cleaned;
|
|
if (!passed) {
|
|
bool frontend_reaped = frontend > 0 && frontend_waited == frontend;
|
|
bool daemon_reaped = daemon > 0 && daemon_waited == daemon;
|
|
int frontend_exit =
|
|
frontend_reaped && WIFEXITED(frontend_status) ? WEXITSTATUS(frontend_status) : -1;
|
|
int frontend_signal =
|
|
frontend_reaped && WIFSIGNALED(frontend_status) ? WTERMSIG(frontend_status) : 0;
|
|
int daemon_exit =
|
|
daemon_reaped && WIFEXITED(daemon_status) ? WEXITSTATUS(daemon_status) : -1;
|
|
int daemon_signal =
|
|
daemon_reaped && WIFSIGNALED(daemon_status) ? WTERMSIG(daemon_status) : 0;
|
|
fprintf(stderr,
|
|
"frontend backpressure fixture failed: maintenance=%d prepared=%d daemon_pid=%ld "
|
|
"announced=%d announced_marker=0x%02x sentinel_ready=%d frontend_pipes=%d "
|
|
"frontend_pid=%ld frontend_reaped=%d frontend_status=0x%x "
|
|
"frontend_exit=%d frontend_signal=%d request_read=%d "
|
|
"request_marker=0x%02x output_observed=%d output_marker=0x%02x "
|
|
"mutation_status=%d quiesce=%d cancel_read=%d cancel_marker=0x%02x "
|
|
"checkpoint_read=%d checkpoint_marker=0x%02x sentinel_usable=%d "
|
|
"sentinel_closed=%d shutdown_sent=%d "
|
|
"done_read=%d done_marker=0x%02x daemon_reaped=%d "
|
|
"daemon_status=0x%x daemon_exit=%d daemon_signal=%d "
|
|
"mutation_released=%d mutation_manager_closed=%d cleaned=%d\n",
|
|
maintenance, prepared, (long)daemon, announced,
|
|
(unsigned int)(unsigned char)announced_marker, sentinel_ready, frontend_pipes,
|
|
(long)frontend, frontend_reaped, frontend_status, frontend_exit, frontend_signal,
|
|
request_read, (unsigned int)(unsigned char)request_marker, output_observed,
|
|
(unsigned int)(unsigned char)output_marker, mutation_status, quiesce, cancel_read,
|
|
(unsigned int)(unsigned char)cancel_marker, checkpoint_read,
|
|
(unsigned int)(unsigned char)checkpoint_marker, sentinel_usable, sentinel_closed,
|
|
shutdown_sent, done_read, (unsigned int)(unsigned char)done_marker, daemon_reaped,
|
|
daemon_status, daemon_exit, daemon_signal, mutation_released,
|
|
mutation_manager_closed, cleaned);
|
|
}
|
|
return passed;
|
|
}
|
|
|
|
static cbm_version_cohort_quiesce_result_t frontend_test_quiesce_requested(void *context) {
|
|
(void)context;
|
|
return CBM_VERSION_COHORT_QUIESCE_REQUESTED;
|
|
}
|
|
|
|
static bool frontend_test_cancel_active(void *context) {
|
|
int fd = *(int *)context;
|
|
const char marker = 'C';
|
|
return write(fd, &marker, 1) == 1;
|
|
}
|
|
|
|
static bool frontend_test_cancel_and_mark(void *context) {
|
|
atomic_bool *cancelled = context;
|
|
atomic_store_explicit(cancelled, true, memory_order_release);
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
TEST(daemon_frontend_recognizes_exact_cancellation_notification) {
|
|
ASSERT_TRUE(cbm_daemon_frontend_is_cancellation_notification(
|
|
"{\"jsonrpc\":\"2.0\",\"method\":\"notifications/cancelled\","
|
|
"\"params\":{\"requestId\":7}}"));
|
|
ASSERT_TRUE(cbm_daemon_frontend_is_cancellation_notification(
|
|
"{ \"params\": {}, \"method\": \"notifications/cancelled\", "
|
|
"\"jsonrpc\": \"2.0\" }"));
|
|
PASS();
|
|
}
|
|
|
|
/* RED on the whole-session cancellation shortcut: merely recognizing the
|
|
* notification method is not authority to close a session. The requestId
|
|
* must match the exact numeric/string identity currently being executed. */
|
|
TEST(daemon_frontend_correlates_cancellation_to_exact_request) {
|
|
ASSERT_TRUE(cbm_daemon_frontend_cancellation_matches_request(
|
|
"{\"jsonrpc\":\"2.0\",\"method\":\"notifications/cancelled\","
|
|
"\"params\":{\"requestId\":7}}",
|
|
7, NULL));
|
|
ASSERT_FALSE(cbm_daemon_frontend_cancellation_matches_request(
|
|
"{\"jsonrpc\":\"2.0\",\"method\":\"notifications/cancelled\","
|
|
"\"params\":{\"requestId\":8}}",
|
|
7, NULL));
|
|
ASSERT_TRUE(cbm_daemon_frontend_cancellation_matches_request(
|
|
"{\"jsonrpc\":\"2.0\",\"method\":\"notifications/cancelled\","
|
|
"\"params\":{\"requestId\":\"request-7\"}}",
|
|
-1, "request-7"));
|
|
ASSERT_FALSE(cbm_daemon_frontend_cancellation_matches_request(
|
|
"{\"jsonrpc\":\"2.0\",\"method\":\"notifications/cancelled\","
|
|
"\"params\":{\"requestId\":7}}",
|
|
-1, "7"));
|
|
ASSERT_FALSE(cbm_daemon_frontend_cancellation_matches_request(
|
|
"{\"jsonrpc\":\"2.0\",\"method\":\"notifications/cancelled\","
|
|
"\"params\":{}}",
|
|
7, NULL));
|
|
ASSERT_FALSE(
|
|
cbm_daemon_frontend_cancellation_matches_request("{\"jsonrpc\":\"2.0\",\"id\":9,"
|
|
"\"method\":\"notifications/cancelled\","
|
|
"\"params\":{\"requestId\":7}}",
|
|
7, NULL));
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_frontend_ignores_cancellation_text_in_string_content) {
|
|
ASSERT_FALSE(cbm_daemon_frontend_is_cancellation_notification(
|
|
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\","
|
|
"\"params\":{\"name\":\"search_graph\",\"arguments\":{\"query\":"
|
|
"\"notifications/cancelled\"}}}"));
|
|
ASSERT_FALSE(cbm_daemon_frontend_is_cancellation_notification(
|
|
"{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\","
|
|
"\"params\":{\"method\":\"notifications/cancelled\"}}"));
|
|
PASS();
|
|
}
|
|
|
|
TEST(daemon_frontend_rejects_non_notification_cancellation_shapes) {
|
|
ASSERT_FALSE(cbm_daemon_frontend_is_cancellation_notification(
|
|
"{\"jsonrpc\":\"2.0\",\"id\":3,"
|
|
"\"method\":\"notifications/cancelled\"}"));
|
|
ASSERT_FALSE(cbm_daemon_frontend_is_cancellation_notification(
|
|
"{\"jsonrpc\":\"2.0\",\"method\":\"notifications/cancelled-extra\"}"));
|
|
ASSERT_FALSE(cbm_daemon_frontend_is_cancellation_notification(
|
|
"{\"jsonrpc\":\"2.0\",\"method\":\"prefix/notifications/cancelled\"}"));
|
|
ASSERT_FALSE(cbm_daemon_frontend_is_cancellation_notification(
|
|
"{\"jsonrpc\":\"2.0\",\"params\":{\"method\":"
|
|
"\"notifications/cancelled\"}}"));
|
|
ASSERT_FALSE(cbm_daemon_frontend_is_cancellation_notification("not-json"));
|
|
ASSERT_FALSE(cbm_daemon_frontend_is_cancellation_notification(NULL));
|
|
PASS();
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
/* RED: an MCP frontend's main thread can remain blocked forever in stdio while
|
|
* install/update/uninstall owns maintenance intent. The already-present
|
|
* frontend worker must observe that native intent and terminate this stateless
|
|
* child, releasing its cohort lease and kernel IPC ownership. A deliberately
|
|
* invalid client pointer is safe only if the maintenance path exits before
|
|
* ordinary EOF/session-close handling, which also pins that exact path. */
|
|
TEST(daemon_frontend_maintenance_exits_while_stdio_reader_is_blocked) {
|
|
frontend_maintenance_fixture_t fixture;
|
|
ASSERT_TRUE(frontend_maintenance_fixture_start(&fixture, "blocked-stdio"));
|
|
int input_pipe[2] = {-1, -1};
|
|
int ready_pipe[2] = {-1, -1};
|
|
bool pipes_ready = pipe(input_pipe) == 0 && pipe(ready_pipe) == 0;
|
|
pid_t child = pipes_ready ? fork() : -1;
|
|
if (child == 0) {
|
|
(void)close(input_pipe[1]);
|
|
(void)close(ready_pipe[0]);
|
|
cbm_daemon_ipc_endpoint_t *endpoint =
|
|
cbm_daemon_ipc_endpoint_new("0123456789abcdef", fixture.parent);
|
|
cbm_version_cohort_manager_t *manager =
|
|
endpoint ? cbm_version_cohort_manager_new(endpoint) : NULL;
|
|
cbm_version_cohort_lease_t *participant = NULL;
|
|
cbm_daemon_conflict_t conflict;
|
|
cbm_daemon_build_identity_t identity = frontend_test_identity();
|
|
cbm_version_cohort_status_t admitted =
|
|
manager ? cbm_version_cohort_acquire(manager, &identity, cbm_now_ms() + 2000U,
|
|
&participant, &conflict)
|
|
: CBM_VERSION_COHORT_IO;
|
|
FILE *input = admitted == CBM_VERSION_COHORT_OK ? fdopen(input_pipe[0], "rb") : NULL;
|
|
FILE *output = input ? tmpfile() : NULL;
|
|
const char ready = 'R';
|
|
bool announced = output && write(ready_pipe[1], &ready, 1) == 1;
|
|
(void)close(ready_pipe[1]);
|
|
if (!announced) {
|
|
_exit(70);
|
|
}
|
|
int result = cbm_daemon_frontend_mcp_run((cbm_daemon_runtime_client_t *)(uintptr_t)1,
|
|
manager, input, output);
|
|
(void)result;
|
|
_exit(71);
|
|
}
|
|
|
|
if (pipes_ready) {
|
|
(void)close(input_pipe[0]);
|
|
(void)close(ready_pipe[1]);
|
|
}
|
|
bool announced = child > 0 && frontend_test_wait_byte(ready_pipe[0], 'R', cbm_now_ms() + 2000U);
|
|
cbm_version_cohort_lease_t *mutation = NULL;
|
|
cbm_version_cohort_quiesce_result_t quiesce = CBM_VERSION_COHORT_QUIESCE_NOT_NEEDED;
|
|
cbm_version_cohort_status_t status =
|
|
announced ? cbm_version_cohort_reserve_for_mutation(fixture.manager, cbm_now_ms() + 3000U,
|
|
frontend_test_quiesce_requested, NULL,
|
|
&quiesce, &mutation)
|
|
: CBM_VERSION_COHORT_IO;
|
|
if (status != CBM_VERSION_COHORT_OK && child > 0) {
|
|
(void)kill(child, SIGKILL);
|
|
}
|
|
int child_status = 0;
|
|
bool waited = child > 0 && waitpid(child, &child_status, 0) == child;
|
|
if (pipes_ready) {
|
|
(void)close(input_pipe[1]);
|
|
(void)close(ready_pipe[0]);
|
|
}
|
|
frontend_test_release_lease(&mutation);
|
|
frontend_maintenance_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(pipes_ready);
|
|
ASSERT_TRUE(child > 0);
|
|
ASSERT_TRUE(announced);
|
|
ASSERT_EQ(status, CBM_VERSION_COHORT_OK);
|
|
ASSERT_EQ(quiesce, CBM_VERSION_COHORT_QUIESCE_REQUESTED);
|
|
ASSERT_TRUE(waited);
|
|
ASSERT_TRUE(WIFEXITED(child_status));
|
|
ASSERT_EQ(WEXITSTATUS(child_status), 0);
|
|
PASS();
|
|
}
|
|
|
|
/* RED: local CLI/worker work has no standing daemon session to interrupt. Its
|
|
* command-lifetime monitor must request cooperative cancellation once, allow a
|
|
* bounded grace, then hard-exit the isolated process so SQLite/native locks
|
|
* roll back and the mutation barrier can prove that every lifetime participant
|
|
* is gone. */
|
|
TEST(daemon_local_participant_monitor_cancels_then_bounds_active_operation) {
|
|
frontend_maintenance_fixture_t fixture;
|
|
ASSERT_TRUE(frontend_maintenance_fixture_start(&fixture, "local-monitor"));
|
|
int ready_pipe[2] = {-1, -1};
|
|
int cancel_pipe[2] = {-1, -1};
|
|
bool pipes_ready = pipe(ready_pipe) == 0 && pipe(cancel_pipe) == 0;
|
|
pid_t child = pipes_ready ? fork() : -1;
|
|
if (child == 0) {
|
|
(void)close(ready_pipe[0]);
|
|
(void)close(cancel_pipe[0]);
|
|
cbm_daemon_ipc_endpoint_t *endpoint =
|
|
cbm_daemon_ipc_endpoint_new("0123456789abcdef", fixture.parent);
|
|
cbm_version_cohort_manager_t *manager =
|
|
endpoint ? cbm_version_cohort_manager_new(endpoint) : NULL;
|
|
cbm_version_cohort_lease_t *participant = NULL;
|
|
cbm_daemon_conflict_t conflict;
|
|
cbm_daemon_build_identity_t identity = frontend_test_identity();
|
|
cbm_version_cohort_status_t admitted =
|
|
manager ? cbm_version_cohort_acquire(manager, &identity, cbm_now_ms() + 2000U,
|
|
&participant, &conflict)
|
|
: CBM_VERSION_COHORT_IO;
|
|
cbm_daemon_maintenance_monitor_t *monitor =
|
|
admitted == CBM_VERSION_COHORT_OK
|
|
? cbm_daemon_maintenance_monitor_start(manager, frontend_test_cancel_active,
|
|
&cancel_pipe[1], 37, "test-local-operation")
|
|
: NULL;
|
|
const char ready = 'R';
|
|
bool announced = monitor && write(ready_pipe[1], &ready, 1) == 1;
|
|
(void)close(ready_pipe[1]);
|
|
if (!announced) {
|
|
_exit(72);
|
|
}
|
|
for (;;) {
|
|
cbm_usleep(100000);
|
|
}
|
|
}
|
|
|
|
if (pipes_ready) {
|
|
(void)close(ready_pipe[1]);
|
|
(void)close(cancel_pipe[1]);
|
|
}
|
|
bool announced = child > 0 && frontend_test_wait_byte(ready_pipe[0], 'R', cbm_now_ms() + 2000U);
|
|
cbm_version_cohort_lease_t *mutation = NULL;
|
|
cbm_version_cohort_quiesce_result_t quiesce = CBM_VERSION_COHORT_QUIESCE_NOT_NEEDED;
|
|
cbm_version_cohort_status_t status =
|
|
announced ? cbm_version_cohort_reserve_for_mutation(fixture.manager, cbm_now_ms() + 5000U,
|
|
frontend_test_quiesce_requested, NULL,
|
|
&quiesce, &mutation)
|
|
: CBM_VERSION_COHORT_IO;
|
|
bool cancelled = status == CBM_VERSION_COHORT_OK &&
|
|
frontend_test_wait_byte(cancel_pipe[0], 'C', cbm_now_ms() + 1000U);
|
|
if (status != CBM_VERSION_COHORT_OK && child > 0) {
|
|
(void)kill(child, SIGKILL);
|
|
}
|
|
int child_status = 0;
|
|
bool waited = child > 0 && waitpid(child, &child_status, 0) == child;
|
|
if (pipes_ready) {
|
|
(void)close(ready_pipe[0]);
|
|
(void)close(cancel_pipe[0]);
|
|
}
|
|
frontend_test_release_lease(&mutation);
|
|
frontend_maintenance_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(pipes_ready);
|
|
ASSERT_TRUE(child > 0);
|
|
ASSERT_TRUE(announced);
|
|
ASSERT_EQ(status, CBM_VERSION_COHORT_OK);
|
|
ASSERT_EQ(quiesce, CBM_VERSION_COHORT_QUIESCE_REQUESTED);
|
|
ASSERT_TRUE(cancelled);
|
|
ASSERT_TRUE(waited);
|
|
ASSERT_TRUE(WIFEXITED(child_status));
|
|
ASSERT_EQ(WEXITSTATUS(child_status), 37);
|
|
PASS();
|
|
}
|
|
|
|
/* RED: supervised process-tree cancellation permits one second of graceful
|
|
* shutdown followed by one second of forced containment. The maintenance
|
|
* observer must not _Exit the owning process before that bounded supervisor
|
|
* window can finish and join the observer normally. */
|
|
TEST(daemon_local_participant_monitor_allows_supervisor_containment_window) {
|
|
frontend_maintenance_fixture_t fixture;
|
|
ASSERT_TRUE(frontend_maintenance_fixture_start(&fixture, "monitor-window"));
|
|
int ready_pipe[2] = {-1, -1};
|
|
bool pipe_ready = pipe(ready_pipe) == 0;
|
|
pid_t child = pipe_ready ? fork() : -1;
|
|
if (child == 0) {
|
|
(void)close(ready_pipe[0]);
|
|
cbm_daemon_ipc_endpoint_t *endpoint =
|
|
cbm_daemon_ipc_endpoint_new("0123456789abcdef", fixture.parent);
|
|
cbm_version_cohort_manager_t *manager =
|
|
endpoint ? cbm_version_cohort_manager_new(endpoint) : NULL;
|
|
cbm_version_cohort_lease_t *participant = NULL;
|
|
cbm_daemon_conflict_t conflict;
|
|
cbm_daemon_build_identity_t identity = frontend_test_identity();
|
|
cbm_version_cohort_status_t admitted =
|
|
manager ? cbm_version_cohort_acquire(manager, &identity, cbm_now_ms() + 2000U,
|
|
&participant, &conflict)
|
|
: CBM_VERSION_COHORT_IO;
|
|
atomic_bool cancelled;
|
|
atomic_init(&cancelled, false);
|
|
cbm_daemon_maintenance_monitor_t *monitor =
|
|
admitted == CBM_VERSION_COHORT_OK
|
|
? cbm_daemon_maintenance_monitor_start(manager, frontend_test_cancel_and_mark,
|
|
&cancelled, 38, "test-supervisor-window")
|
|
: NULL;
|
|
const char ready = 'R';
|
|
bool announced = monitor && write(ready_pipe[1], &ready, 1) == 1;
|
|
(void)close(ready_pipe[1]);
|
|
if (!announced) {
|
|
_exit(72);
|
|
}
|
|
while (!atomic_load_explicit(&cancelled, memory_order_acquire)) {
|
|
cbm_usleep(1000);
|
|
}
|
|
|
|
/* Model the maximum graceful + forced-settle supervisor bounds. */
|
|
cbm_usleep(2100U * 1000U);
|
|
bool stopped = cbm_daemon_maintenance_monitor_stop(&monitor);
|
|
frontend_test_release_lease(&participant);
|
|
while (manager && cbm_version_cohort_manager_free(&manager) != CBM_PRIVATE_FILE_LOCK_OK) {
|
|
cbm_usleep(1000);
|
|
}
|
|
cbm_daemon_ipc_endpoint_free(endpoint);
|
|
_exit(stopped ? 0 : 73);
|
|
}
|
|
|
|
if (pipe_ready) {
|
|
(void)close(ready_pipe[1]);
|
|
}
|
|
bool announced = child > 0 && frontend_test_wait_byte(ready_pipe[0], 'R', cbm_now_ms() + 2000U);
|
|
cbm_version_cohort_lease_t *mutation = NULL;
|
|
cbm_version_cohort_quiesce_result_t quiesce = CBM_VERSION_COHORT_QUIESCE_NOT_NEEDED;
|
|
cbm_version_cohort_status_t status =
|
|
announced ? cbm_version_cohort_reserve_for_mutation(fixture.manager, cbm_now_ms() + 5000U,
|
|
frontend_test_quiesce_requested, NULL,
|
|
&quiesce, &mutation)
|
|
: CBM_VERSION_COHORT_IO;
|
|
if (status != CBM_VERSION_COHORT_OK && child > 0) {
|
|
(void)kill(child, SIGKILL);
|
|
}
|
|
int child_status = 0;
|
|
bool waited = child > 0 && waitpid(child, &child_status, 0) == child;
|
|
if (pipe_ready) {
|
|
(void)close(ready_pipe[0]);
|
|
}
|
|
frontend_test_release_lease(&mutation);
|
|
frontend_maintenance_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(pipe_ready);
|
|
ASSERT_TRUE(child > 0);
|
|
ASSERT_TRUE(announced);
|
|
ASSERT_EQ(status, CBM_VERSION_COHORT_OK);
|
|
ASSERT_EQ(quiesce, CBM_VERSION_COHORT_QUIESCE_REQUESTED);
|
|
ASSERT_TRUE(waited);
|
|
ASSERT_TRUE(WIFEXITED(child_status));
|
|
ASSERT_EQ(WEXITSTATUS(child_status), 0);
|
|
PASS();
|
|
}
|
|
|
|
/* RED: once a long request is active, a buffered writer can fill the bounded
|
|
* frontend queue and close its pipe. Waiting indefinitely for queue capacity
|
|
* prevents the sole reader from observing that already-pending EOF, so neither
|
|
* the request nor its daemon session is cancelled. Overload must instead fail
|
|
* the frontend promptly and close/cancel the authenticated session. */
|
|
TEST(daemon_frontend_over_capacity_input_cannot_hide_eof_behind_active_request) {
|
|
ASSERT_TRUE(frontend_eof_run_isolated("overflow", true));
|
|
PASS();
|
|
}
|
|
|
|
/* Clean EOF is the normal MCP-session ownership boundary. Accepted work gets a
|
|
* bounded drain opportunity; work still active at the deadline is cancelled
|
|
* with that exact session, while the frontend itself reports a clean close. */
|
|
TEST(daemon_frontend_eof_drain_timeout_cancels_and_returns_success) {
|
|
ASSERT_TRUE(frontend_eof_run_isolated("timeout", false));
|
|
PASS();
|
|
}
|
|
|
|
/* A daemon response can finish its IPC exchange and then block forever while
|
|
* writing to an agent that stopped reading stdout. EOF must still bound the
|
|
* thin frontend process. Keep the daemon in a separate child so the frontend's
|
|
* terminal watchdog closes a real kernel connection; the daemon must observe
|
|
* that close, cancel the exact session, and shut down normally. */
|
|
TEST(daemon_frontend_stdout_backpressure_eof_fail_stops_and_cancels_session) {
|
|
ASSERT_TRUE(frontend_backpressure_run_isolated(false));
|
|
PASS();
|
|
}
|
|
|
|
/* Maintenance must not depend on either MCP stdio thread making progress.
|
|
* Keep stdin open, fill stdout with a completed daemon response, then publish
|
|
* activation intent. The independent monitor must release the cohort lease and
|
|
* kernel session within its bound; the daemon must cancel that exact session. */
|
|
TEST(daemon_frontend_stdout_backpressure_maintenance_stops_and_cancels_session) {
|
|
ASSERT_TRUE(frontend_backpressure_run_isolated(true));
|
|
PASS();
|
|
}
|
|
#endif
|
|
|
|
TEST(daemon_local_participant_monitor_joins_before_manager_teardown) {
|
|
frontend_maintenance_fixture_t fixture;
|
|
ASSERT_TRUE(frontend_maintenance_fixture_start(&fixture, "monitor-join"));
|
|
cbm_daemon_maintenance_monitor_t *monitor = cbm_daemon_maintenance_monitor_start(
|
|
fixture.manager, NULL, NULL, EXIT_FAILURE, "test-idle-command");
|
|
bool started = monitor != NULL;
|
|
bool stopped = started && cbm_daemon_maintenance_monitor_stop(&monitor);
|
|
bool consumed = monitor == NULL;
|
|
frontend_maintenance_fixture_finish(&fixture);
|
|
|
|
ASSERT_TRUE(started);
|
|
ASSERT_TRUE(stopped);
|
|
ASSERT_TRUE(consumed);
|
|
PASS();
|
|
}
|
|
|
|
SUITE(daemon_frontend) {
|
|
RUN_TEST(daemon_frontend_recognizes_exact_cancellation_notification);
|
|
RUN_TEST(daemon_frontend_correlates_cancellation_to_exact_request);
|
|
RUN_TEST(daemon_frontend_ignores_cancellation_text_in_string_content);
|
|
RUN_TEST(daemon_frontend_rejects_non_notification_cancellation_shapes);
|
|
#ifndef _WIN32
|
|
RUN_TEST(daemon_frontend_maintenance_exits_while_stdio_reader_is_blocked);
|
|
RUN_TEST(daemon_local_participant_monitor_cancels_then_bounds_active_operation);
|
|
RUN_TEST(daemon_local_participant_monitor_allows_supervisor_containment_window);
|
|
RUN_TEST(daemon_frontend_over_capacity_input_cannot_hide_eof_behind_active_request);
|
|
RUN_TEST(daemon_frontend_eof_drain_timeout_cancels_and_returns_success);
|
|
RUN_TEST(daemon_frontend_stdout_backpressure_eof_fail_stops_and_cancels_session);
|
|
RUN_TEST(daemon_frontend_stdout_backpressure_maintenance_stops_and_cancels_session);
|
|
#endif
|
|
RUN_TEST(daemon_local_participant_monitor_joins_before_manager_teardown);
|
|
}
|