Files
codebase-memory-mcp/src/daemon/version_cohort.c
T
Shane McCarronandClaude Opus 5 3cf2a6663f fix(daemon): address QA round 1
- Separate loss from transient failure. Touch helpers now report OK /
  lost / transient (version_cohort: OK/UNSAFE/IO; ipc: 1/0/-1). Only a
  proven unlink or replacement stops the daemon. A refresh failure on a
  still-valid file (EROFS, EPERM, EMFILE on the marker reopen) logs
  daemon.coordination_touch_failed once, retries every tick, and logs
  daemon.coordination_touch_recovered when it clears.
- Cohort and claim locks now prove the path still names the held inode
  (private_file_revalidate against the stored directory + name), not
  just st_nlink, so rename-away, hard-link+unlink and a replaced
  directory are caught like unlink.
- The runtime service's self-owned participant guard is refreshed by
  the listener heartbeat too.
- Tests: rename-away detection; cohort lease/claim touch (lease with
  only lifetime held is OK, each unlinked file reported independently);
  IPC listener touch now checks timestamps are actually refreshed and
  covers the participant guard and identity-marker replacement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-09-21 13:02:07 -05:00

1083 lines
47 KiB
C

/* version_cohort.c — Short transition lock + process-lifetime SH/EX barrier. */
#include "daemon/version_cohort.h"
#include "foundation/compat.h"
#include "foundation/compat_thread.h"
#include "foundation/log.h"
#include "foundation/platform.h"
#include "foundation/private_file_lock_internal.h"
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#else
#include <unistd.h>
#endif
enum {
VERSION_COHORT_RETRY_US = 1000,
VERSION_COHORT_CLEANUP_TIMEOUT_MS = 500,
VERSION_COHORT_RECORD_MAGIC_SIZE = 8,
VERSION_COHORT_RECORD_VERSION_OFFSET = VERSION_COHORT_RECORD_MAGIC_SIZE,
VERSION_COHORT_RECORD_BUILD_OFFSET =
VERSION_COHORT_RECORD_VERSION_OFFSET + CBM_DAEMON_VERSION_TEXT_SIZE,
VERSION_COHORT_RECORD_CACHE_OFFSET =
VERSION_COHORT_RECORD_BUILD_OFFSET + CBM_DAEMON_BUILD_FINGERPRINT_SIZE,
VERSION_COHORT_RECORD_PROTOCOL_OFFSET =
VERSION_COHORT_RECORD_CACHE_OFFSET + CBM_DAEMON_BUILD_FINGERPRINT_SIZE,
VERSION_COHORT_RECORD_STORE_OFFSET = VERSION_COHORT_RECORD_PROTOCOL_OFFSET + 4,
VERSION_COHORT_RECORD_FEATURE_OFFSET = VERSION_COHORT_RECORD_STORE_OFFSET + 4,
VERSION_COHORT_RECORD_SIZE = VERSION_COHORT_RECORD_FEATURE_OFFSET + 4,
VERSION_COHORT_LOG_CAP = 1024 * 1024,
VERSION_COHORT_PATH_CAP = 4096,
};
static const unsigned char VERSION_COHORT_RECORD_MAGIC[VERSION_COHORT_RECORD_MAGIC_SIZE] = {
'C', 'B', 'M', 'C', 'O', 'H', 2, 0,
};
static const char VERSION_COHORT_ADMISSION_FILE[] = "cbm-version-cohort-admission-v1.lock";
static const char VERSION_COHORT_LIFETIME_FILE[] = "cbm-version-cohort-lifetime-v1.lock";
static const char VERSION_COHORT_MAINTENANCE_FILE[] = "cbm-version-cohort-maintenance-v1.lock";
static const char VERSION_COHORT_DAEMON_FILE[] = "cbm-version-cohort-daemon-v1.lock";
struct cbm_version_cohort_manager {
cbm_private_lock_directory_t *directory;
cbm_mutex_t mutex;
size_t lease_count;
uint64_t owner_pid;
bool closing;
};
struct cbm_version_cohort_lease {
cbm_version_cohort_manager_t *manager;
cbm_private_file_lock_t *maintenance;
cbm_private_file_lock_t *admission;
cbm_private_file_lock_t *lifetime;
bool registered;
};
struct cbm_version_cohort_daemon_claim {
cbm_version_cohort_manager_t *manager;
cbm_private_file_lock_t *marker;
bool registered;
};
static uint64_t version_cohort_current_pid(void) {
#ifdef _WIN32
return (uint64_t)GetCurrentProcessId();
#else
return (uint64_t)getpid();
#endif
}
static uint64_t version_cohort_deadline_after(uint32_t timeout_ms) {
uint64_t now = cbm_now_ms();
return now > UINT64_MAX - timeout_ms ? UINT64_MAX : now + timeout_ms;
}
/* flock's LOCK_NB retries never queue in the kernel, so fairness comes only
* from the retry schedule. Two participants retrying with the SAME fixed
* period can phase-lock — every try landing inside the peer's brief hold
* window — starving an EX acquisition until one side's budget expires. A
* per-process, time-scattered jitter decorrelates the schedules. */
static void version_cohort_retry_sleep(void) {
uint64_t mix = (cbm_now_ms() + version_cohort_current_pid()) * 2654435761ULL;
cbm_usleep(VERSION_COHORT_RETRY_US / 2U + (uint32_t)(mix % VERSION_COHORT_RETRY_US));
}
static cbm_private_file_lock_status_t version_cohort_lock_release_until(
cbm_private_file_lock_t **lock_io, uint64_t deadline_ms) {
cbm_private_file_lock_status_t result = CBM_PRIVATE_FILE_LOCK_OK;
if (!lock_io) {
return CBM_PRIVATE_FILE_LOCK_IO;
}
while (*lock_io) {
cbm_private_file_lock_status_t status = cbm_private_file_lock_release(lock_io);
if (status != CBM_PRIVATE_FILE_LOCK_OK) {
result = status;
}
if (!*lock_io) {
break;
}
if (cbm_now_ms() >= deadline_ms) {
return result == CBM_PRIVATE_FILE_LOCK_OK ? CBM_PRIVATE_FILE_LOCK_IO : result;
}
version_cohort_retry_sleep();
}
return result;
}
static _Noreturn void version_cohort_cleanup_fail_stop(const char *component) {
/* Observer APIs cannot return an opaque cleanup handle. Once their finite
* retry budget is exhausted, process exit is the only way to avoid losing
* retry authority while a coordination marker remains owned. */
cbm_log_error("daemon.forced_shutdown", "component", component, "action",
"coordination_cleanup");
(void)fflush(stdout);
(void)fflush(stderr);
#ifdef _WIN32
(void)TerminateProcess(GetCurrentProcess(), EXIT_FAILURE);
abort();
#else
_exit(EXIT_FAILURE);
#endif
}
#ifndef _WIN32
static void version_cohort_startup_lock_release_complete(cbm_daemon_ipc_startup_lock_t **lock_io) {
uint64_t deadline = version_cohort_deadline_after(VERSION_COHORT_CLEANUP_TIMEOUT_MS);
while (lock_io && *lock_io) {
(void)cbm_daemon_ipc_startup_lock_release(lock_io);
if (!*lock_io) {
return;
}
if (cbm_now_ms() >= deadline) {
version_cohort_cleanup_fail_stop("startup_lock_cleanup");
}
version_cohort_retry_sleep();
}
}
#endif
static bool version_cohort_identity_valid(const cbm_daemon_build_identity_t *identity) {
const char *cache = identity ? identity->cache_fingerprint : NULL;
if (!cache) {
return false;
}
size_t cache_length = 0;
while (cache_length < CBM_DAEMON_BUILD_FINGERPRINT_SIZE && cache[cache_length]) {
unsigned char ch = (unsigned char)cache[cache_length];
if (!((ch >= (unsigned char)'0' && ch <= (unsigned char)'9') ||
(ch >= (unsigned char)'a' && ch <= (unsigned char)'f'))) {
return false;
}
cache_length++;
}
if (cache_length != CBM_DAEMON_BUILD_FINGERPRINT_SIZE - 1U || cache[cache_length] != '\0') {
return false;
}
cbm_daemon_conflict_t comparison;
return cbm_daemon_hello_compare(identity, identity, &comparison) == CBM_DAEMON_HELLO_COMPATIBLE;
}
static void version_cohort_u32_encode(unsigned char out[4], uint32_t value) {
out[0] = (unsigned char)(value & 0xffU);
out[1] = (unsigned char)((value >> 8) & 0xffU);
out[2] = (unsigned char)((value >> 16) & 0xffU);
out[3] = (unsigned char)((value >> 24) & 0xffU);
}
static uint32_t version_cohort_u32_decode(const unsigned char in[4]) {
return (uint32_t)in[0] | ((uint32_t)in[1] << 8) | ((uint32_t)in[2] << 16) |
((uint32_t)in[3] << 24);
}
static bool version_cohort_record_encode(const cbm_daemon_build_identity_t *identity,
unsigned char out[VERSION_COHORT_RECORD_SIZE]) {
if (!out || !version_cohort_identity_valid(identity)) {
return false;
}
memset(out, 0, VERSION_COHORT_RECORD_SIZE);
memcpy(out, VERSION_COHORT_RECORD_MAGIC, VERSION_COHORT_RECORD_MAGIC_SIZE);
size_t version_length = strlen(identity->semantic_version);
size_t build_length = strlen(identity->build_fingerprint);
memcpy(out + VERSION_COHORT_RECORD_VERSION_OFFSET, identity->semantic_version,
version_length + 1);
memcpy(out + VERSION_COHORT_RECORD_BUILD_OFFSET, identity->build_fingerprint, build_length + 1);
if (identity->cache_fingerprint) {
size_t cache_length = strlen(identity->cache_fingerprint);
memcpy(out + VERSION_COHORT_RECORD_CACHE_OFFSET, identity->cache_fingerprint,
cache_length + 1);
}
version_cohort_u32_encode(out + VERSION_COHORT_RECORD_PROTOCOL_OFFSET, identity->protocol_abi);
version_cohort_u32_encode(out + VERSION_COHORT_RECORD_STORE_OFFSET, identity->store_abi);
version_cohort_u32_encode(out + VERSION_COHORT_RECORD_FEATURE_OFFSET, identity->feature_abi);
return true;
}
static bool version_cohort_record_decode(const unsigned char record[VERSION_COHORT_RECORD_SIZE],
size_t length, cbm_daemon_build_identity_t *identity_out,
char version_out[CBM_DAEMON_VERSION_TEXT_SIZE],
char build_out[CBM_DAEMON_BUILD_FINGERPRINT_SIZE],
char cache_out[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
if (!record || length != VERSION_COHORT_RECORD_SIZE || !identity_out || !version_out ||
!build_out || !cache_out ||
memcmp(record, VERSION_COHORT_RECORD_MAGIC, VERSION_COHORT_RECORD_MAGIC_SIZE) != 0) {
return false;
}
memcpy(version_out, record + VERSION_COHORT_RECORD_VERSION_OFFSET,
CBM_DAEMON_VERSION_TEXT_SIZE);
memcpy(build_out, record + VERSION_COHORT_RECORD_BUILD_OFFSET,
CBM_DAEMON_BUILD_FINGERPRINT_SIZE);
memcpy(cache_out, record + VERSION_COHORT_RECORD_CACHE_OFFSET,
CBM_DAEMON_BUILD_FINGERPRINT_SIZE);
cbm_daemon_build_identity_t identity = {
.semantic_version = version_out,
.build_fingerprint = build_out,
.cache_fingerprint = cache_out[0] ? cache_out : NULL,
.protocol_abi = version_cohort_u32_decode(record + VERSION_COHORT_RECORD_PROTOCOL_OFFSET),
.store_abi = version_cohort_u32_decode(record + VERSION_COHORT_RECORD_STORE_OFFSET),
.feature_abi = version_cohort_u32_decode(record + VERSION_COHORT_RECORD_FEATURE_OFFSET),
};
if (!version_cohort_identity_valid(&identity)) {
return false;
}
*identity_out = identity;
return true;
}
static cbm_version_cohort_status_t version_cohort_status_from_lock(
cbm_private_file_lock_status_t status) {
switch (status) {
case CBM_PRIVATE_FILE_LOCK_OK:
return CBM_VERSION_COHORT_OK;
case CBM_PRIVATE_FILE_LOCK_BUSY:
return CBM_VERSION_COHORT_BUSY;
case CBM_PRIVATE_FILE_LOCK_UNSAFE:
return CBM_VERSION_COHORT_UNSAFE;
case CBM_PRIVATE_FILE_LOCK_IO:
default:
return CBM_VERSION_COHORT_IO;
}
}
static cbm_private_file_lock_status_t version_cohort_lock_until(
cbm_version_cohort_manager_t *manager, const char *base_name, cbm_private_file_lock_mode_t mode,
uint64_t deadline_ms, cbm_private_file_lock_t **lock_out) {
for (;;) {
cbm_private_file_lock_status_t status =
cbm_private_file_lock_try_acquire(manager->directory, base_name, mode, lock_out);
if (status != CBM_PRIVATE_FILE_LOCK_BUSY ||
(deadline_ms != UINT64_MAX && cbm_now_ms() >= deadline_ms)) {
return status;
}
version_cohort_retry_sleep();
}
}
static cbm_version_cohort_lease_t *version_cohort_lease_new(cbm_version_cohort_manager_t *manager) {
if (!manager) {
return NULL;
}
cbm_version_cohort_lease_t *lease = calloc(1, sizeof(*lease));
if (!lease) {
return NULL;
}
cbm_mutex_lock(&manager->mutex);
bool admitted = !manager->closing && manager->owner_pid == version_cohort_current_pid();
if (admitted) {
manager->lease_count++;
}
cbm_mutex_unlock(&manager->mutex);
if (!admitted) {
free(lease);
return NULL;
}
lease->manager = manager;
lease->registered = true;
return lease;
}
static bool version_cohort_manager_register(cbm_version_cohort_manager_t *manager) {
if (!manager) {
return false;
}
cbm_mutex_lock(&manager->mutex);
bool admitted = !manager->closing && manager->owner_pid == version_cohort_current_pid();
if (admitted) {
manager->lease_count++;
}
cbm_mutex_unlock(&manager->mutex);
return admitted;
}
static bool version_cohort_manager_unregister(cbm_version_cohort_manager_t *manager) {
if (!manager) {
return false;
}
cbm_mutex_lock(&manager->mutex);
bool registered =
manager->owner_pid == version_cohort_current_pid() && manager->lease_count > 0;
if (registered) {
manager->lease_count--;
}
cbm_mutex_unlock(&manager->mutex);
return registered;
}
cbm_private_file_lock_status_t cbm_version_cohort_lease_release(
cbm_version_cohort_lease_t **lease_io) {
if (!lease_io || !*lease_io) {
return CBM_PRIVATE_FILE_LOCK_IO;
}
cbm_version_cohort_lease_t *lease = *lease_io;
cbm_private_file_lock_status_t result = CBM_PRIVATE_FILE_LOCK_OK;
if (lease->lifetime) {
cbm_private_file_lock_status_t status = cbm_private_file_lock_release(&lease->lifetime);
if (status != CBM_PRIVATE_FILE_LOCK_OK) {
result = status;
}
}
if (lease->lifetime) {
return CBM_PRIVATE_FILE_LOCK_IO;
}
if (lease->admission) {
cbm_private_file_lock_status_t status = cbm_private_file_lock_release(&lease->admission);
if (status != CBM_PRIVATE_FILE_LOCK_OK) {
result = status;
}
}
if (lease->admission) {
return CBM_PRIVATE_FILE_LOCK_IO;
}
if (lease->maintenance) {
cbm_private_file_lock_status_t status = cbm_private_file_lock_release(&lease->maintenance);
if (status != CBM_PRIVATE_FILE_LOCK_OK) {
result = status;
}
}
if (lease->maintenance) {
return CBM_PRIVATE_FILE_LOCK_IO;
}
if (lease->registered && lease->manager) {
cbm_mutex_lock(&lease->manager->mutex);
if (lease->manager->lease_count > 0) {
lease->manager->lease_count--;
} else {
result = CBM_PRIVATE_FILE_LOCK_IO;
}
cbm_mutex_unlock(&lease->manager->mutex);
lease->registered = false;
}
free(lease);
*lease_io = NULL;
return result;
}
/* Folds one lock's touch into an accumulated result: a lost file (UNSAFE)
* outranks a transient refresh failure (IO), which outranks OK. A lease holds
* only lifetime after admission, so absent locks are skipped. */
static cbm_version_cohort_status_t version_cohort_lock_touch(cbm_private_file_lock_t *lock,
cbm_version_cohort_status_t result) {
if (!lock || result == CBM_VERSION_COHORT_UNSAFE) {
return result;
}
cbm_private_file_lock_status_t status = cbm_private_file_lock_touch(lock);
if (status == CBM_PRIVATE_FILE_LOCK_UNSAFE) {
return CBM_VERSION_COHORT_UNSAFE;
}
return status == CBM_PRIVATE_FILE_LOCK_OK ? result : CBM_VERSION_COHORT_IO;
}
cbm_version_cohort_status_t cbm_version_cohort_lease_touch(cbm_version_cohort_lease_t *lease) {
if (!lease || !lease->lifetime) {
return CBM_VERSION_COHORT_UNSAFE;
}
cbm_version_cohort_status_t result =
version_cohort_lock_touch(lease->lifetime, CBM_VERSION_COHORT_OK);
result = version_cohort_lock_touch(lease->admission, result);
return version_cohort_lock_touch(lease->maintenance, result);
}
cbm_version_cohort_status_t cbm_version_cohort_daemon_claim_touch(
cbm_version_cohort_daemon_claim_t *claim) {
return claim && claim->marker ? version_cohort_lock_touch(claim->marker, CBM_VERSION_COHORT_OK)
: CBM_VERSION_COHORT_UNSAFE;
}
static cbm_version_cohort_status_t version_cohort_failed(cbm_version_cohort_lease_t *lease,
cbm_version_cohort_status_t status,
cbm_version_cohort_lease_t **lease_out) {
cbm_version_cohort_lease_t *cleanup = lease;
cbm_private_file_lock_status_t cleanup_status = cbm_version_cohort_lease_release(&cleanup);
if (cleanup) {
*lease_out = cleanup;
return CBM_VERSION_COHORT_IO;
}
return cleanup_status == CBM_PRIVATE_FILE_LOCK_OK ? status : CBM_VERSION_COHORT_IO;
}
cbm_version_cohort_manager_t *cbm_version_cohort_manager_new(
const cbm_daemon_ipc_endpoint_t *endpoint) {
cbm_private_lock_directory_t *directory = NULL;
cbm_private_file_lock_status_t directory_status =
cbm_daemon_ipc_private_lock_directory_new(endpoint, &directory);
if (directory_status != CBM_PRIVATE_FILE_LOCK_OK || !directory) {
if (directory) {
cbm_private_lock_directory_close(directory);
}
return NULL;
}
cbm_version_cohort_manager_t *manager = calloc(1, sizeof(*manager));
if (!manager) {
cbm_private_lock_directory_close(directory);
return NULL;
}
manager->directory = directory;
manager->owner_pid = version_cohort_current_pid();
cbm_mutex_init(&manager->mutex);
return manager;
}
static cbm_version_cohort_status_t version_cohort_claim_new(
cbm_version_cohort_lease_t *lease, const cbm_daemon_build_identity_t *identity,
uint64_t deadline_ms) {
unsigned char record[VERSION_COHORT_RECORD_SIZE];
if (!version_cohort_record_encode(identity, record) ||
cbm_private_file_lock_payload_write(lease->lifetime, record, sizeof(record)) !=
CBM_PRIVATE_FILE_LOCK_OK) {
return CBM_VERSION_COHORT_IO;
}
cbm_private_file_lock_status_t release_status = cbm_private_file_lock_release(&lease->lifetime);
if (release_status != CBM_PRIVATE_FILE_LOCK_OK) {
return CBM_VERSION_COHORT_IO;
}
return version_cohort_status_from_lock(
version_cohort_lock_until(lease->manager, VERSION_COHORT_LIFETIME_FILE,
CBM_PRIVATE_FILE_LOCK_SH, deadline_ms, &lease->lifetime));
}
static cbm_version_cohort_status_t version_cohort_acquire_once(
cbm_version_cohort_manager_t *manager, const cbm_daemon_build_identity_t *identity,
uint64_t deadline_ms, cbm_version_cohort_lease_t **lease_out,
cbm_daemon_conflict_t *conflict_out) {
if (lease_out) {
*lease_out = NULL;
}
if (conflict_out) {
memset(conflict_out, 0, sizeof(*conflict_out));
}
if (!manager || !identity || !lease_out || !conflict_out ||
!version_cohort_identity_valid(identity)) {
return CBM_VERSION_COHORT_UNSAFE;
}
cbm_version_cohort_lease_t *lease = version_cohort_lease_new(manager);
if (!lease) {
return CBM_VERSION_COHORT_IO;
}
/* Global order is maintenance -> admission -> lifetime. Retaining SH
* through admission closes the probe/admission race: an activation cannot
* publish EX intent until this already-started transition has finished,
* while every later participant fails its non-blocking SH attempt. */
cbm_private_file_lock_status_t lock_status =
cbm_private_file_lock_try_acquire(manager->directory, VERSION_COHORT_MAINTENANCE_FILE,
CBM_PRIVATE_FILE_LOCK_SH, &lease->maintenance);
if (lock_status != CBM_PRIVATE_FILE_LOCK_OK) {
return version_cohort_failed(lease, version_cohort_status_from_lock(lock_status),
lease_out);
}
lock_status =
version_cohort_lock_until(manager, VERSION_COHORT_ADMISSION_FILE, CBM_PRIVATE_FILE_LOCK_EX,
deadline_ms, &lease->admission);
if (lock_status != CBM_PRIVATE_FILE_LOCK_OK) {
return version_cohort_failed(lease, version_cohort_status_from_lock(lock_status),
lease_out);
}
lock_status =
cbm_private_file_lock_try_acquire(manager->directory, VERSION_COHORT_LIFETIME_FILE,
CBM_PRIVATE_FILE_LOCK_EX, &lease->lifetime);
cbm_version_cohort_status_t status = version_cohort_status_from_lock(lock_status);
if (status == CBM_VERSION_COHORT_OK) {
/* No live holder: this participant CLAIMS the cohort and NO identity
* comparison happens — a mismatched build is admitted, not refused.
* That is correct when nothing is running, and it is the only path by
* which a mismatched client can join. Name it: "was the lifetime lock
* held?" is exactly what separates a local run (conflict raised) from
* a CI run (client admitted), and it was not observable in any log. */
/* Key wording matters here: the old name "claimed_unheld" read as a
* stale-claim anomaly and derailed the 2026-08-29 zombie diagnosis,
* when the record actually marks the HEALTHY fresh-claim path that
* every normal start logs. */
cbm_log_info("version_cohort.claimed_fresh", "prior_holder", "none", "build",
identity->build_fingerprint ? identity->build_fingerprint : "<null>");
status = version_cohort_claim_new(lease, identity, deadline_ms);
} else if (status == CBM_VERSION_COHORT_BUSY) {
lock_status =
cbm_private_file_lock_try_acquire(manager->directory, VERSION_COHORT_LIFETIME_FILE,
CBM_PRIVATE_FILE_LOCK_SH, &lease->lifetime);
status = version_cohort_status_from_lock(lock_status);
if (status == CBM_VERSION_COHORT_OK) {
unsigned char record[VERSION_COHORT_RECORD_SIZE];
size_t record_length = 0;
char active_version[CBM_DAEMON_VERSION_TEXT_SIZE];
char active_build[CBM_DAEMON_BUILD_FINGERPRINT_SIZE];
char active_cache[CBM_DAEMON_BUILD_FINGERPRINT_SIZE];
cbm_daemon_build_identity_t active;
cbm_private_file_lock_status_t payload_status = cbm_private_file_lock_payload_read(
lease->lifetime, record, sizeof(record), &record_length);
bool decoded = payload_status == CBM_PRIVATE_FILE_LOCK_OK &&
version_cohort_record_decode(record, record_length, &active,
active_version, active_build, active_cache);
if (!decoded) {
/* The active lifetime record could not be read/decoded from a
* peer holder - names whether the cross-process payload was
* unreadable vs present-but-malformed (a Windows lock-payload
* visibility question the unix flock path does not hit). */
char len_text[16];
(void)snprintf(len_text, sizeof(len_text), "%zu", record_length);
cbm_log_warn("version_cohort.record_undecodable", "payload_status",
payload_status == CBM_PRIVATE_FILE_LOCK_OK ? "read_ok" : "read_failed",
"bytes", len_text);
status = CBM_VERSION_COHORT_UNSAFE;
} else {
cbm_daemon_hello_status_t comparison =
cbm_daemon_hello_compare(&active, identity, conflict_out);
if (comparison == CBM_DAEMON_HELLO_INVALID) {
/* The record decoded but hello_compare rejected an identity
* field as invalid (empty version/fingerprint/cache). Name
* the decoded active values so the malformed field on the
* Windows cross-process read path is visible. */
cbm_log_warn(
"version_cohort.active_identity_invalid", "version",
active.semantic_version ? active.semantic_version : "<null>", "build",
active.build_fingerprint ? active.build_fingerprint : "<null>", "cache",
active.cache_fingerprint ? active.cache_fingerprint : "<null>");
}
const char *active_cache_fingerprint =
active.cache_fingerprint ? active.cache_fingerprint : "";
const char *requested_cache_fingerprint =
identity->cache_fingerprint ? identity->cache_fingerprint : "";
if (comparison == CBM_DAEMON_HELLO_COMPATIBLE &&
strcmp(active_cache_fingerprint, requested_cache_fingerprint) != 0) {
comparison = CBM_DAEMON_HELLO_CACHE_CONFLICT;
conflict_out->status = comparison;
(void)snprintf(conflict_out->active_cache_fingerprint,
sizeof(conflict_out->active_cache_fingerprint), "%s",
active_cache_fingerprint);
(void)snprintf(conflict_out->requested_cache_fingerprint,
sizeof(conflict_out->requested_cache_fingerprint), "%s",
requested_cache_fingerprint);
}
if (comparison == CBM_DAEMON_HELLO_COMPATIBLE) {
status = CBM_VERSION_COHORT_OK;
} else if (comparison == CBM_DAEMON_HELLO_INVALID) {
status = CBM_VERSION_COHORT_UNSAFE;
} else {
cbm_private_file_lock_status_t release_status =
cbm_private_file_lock_release(&lease->lifetime);
if (release_status != CBM_PRIVATE_FILE_LOCK_OK) {
status = CBM_VERSION_COHORT_IO;
} else {
lock_status = cbm_private_file_lock_try_acquire(
manager->directory, VERSION_COHORT_LIFETIME_FILE,
CBM_PRIVATE_FILE_LOCK_EX, &lease->lifetime);
status = version_cohort_status_from_lock(lock_status);
if (status == CBM_VERSION_COHORT_OK) {
status = version_cohort_claim_new(lease, identity, deadline_ms);
} else if (status == CBM_VERSION_COHORT_BUSY) {
status = CBM_VERSION_COHORT_CONFLICT;
}
}
}
}
}
}
if (status != CBM_VERSION_COHORT_OK) {
return version_cohort_failed(lease, status, lease_out);
}
cbm_private_file_lock_status_t admission_release =
cbm_private_file_lock_release(&lease->admission);
if (admission_release != CBM_PRIVATE_FILE_LOCK_OK) {
return version_cohort_failed(lease, CBM_VERSION_COHORT_IO, lease_out);
}
cbm_private_file_lock_status_t maintenance_release =
cbm_private_file_lock_release(&lease->maintenance);
if (maintenance_release != CBM_PRIVATE_FILE_LOCK_OK) {
return version_cohort_failed(lease, CBM_VERSION_COHORT_IO, lease_out);
}
*lease_out = lease;
return CBM_VERSION_COHORT_OK;
}
#ifdef CBM_ENABLE_TEST_SEAMS
/* #2046 test seam: counts conflict retries so a test can prove the waiting
* participant actually met the mismatched holder before that holder left —
* without it, "admitted after release" is indistinguishable from "arrived
* after release" and the test passes vacuously. */
static atomic_uint_fast64_t version_cohort_conflict_retry_seam;
uint64_t cbm_version_cohort_conflict_retries_for_testing(void) {
return atomic_load_explicit(&version_cohort_conflict_retry_seam, memory_order_acquire);
}
static void version_cohort_note_conflict_retry(void) {
(void)atomic_fetch_add_explicit(&version_cohort_conflict_retry_seam, 1, memory_order_release);
}
#else
static void version_cohort_note_conflict_retry(void) {}
#endif
static const char *version_cohort_conflict_kind(const cbm_daemon_conflict_t *conflict) {
switch (conflict->status) {
case CBM_DAEMON_HELLO_VERSION_CONFLICT:
return "version";
case CBM_DAEMON_HELLO_BUILD_CONFLICT:
return "build";
case CBM_DAEMON_HELLO_PROTOCOL_ABI_CONFLICT:
return "protocol_abi";
case CBM_DAEMON_HELLO_STORE_ABI_CONFLICT:
return "store_abi";
case CBM_DAEMON_HELLO_FEATURE_ABI_CONFLICT:
return "feature_abi";
case CBM_DAEMON_HELLO_CACHE_CONFLICT:
return "cache_root";
default:
return "unknown";
}
}
/* A mismatched live holder is not necessarily a peer that will stay: an
* internal daemon keeps its lifetime SH lock for the few hundred milliseconds
* between its last client leaving and its teardown finishing, and a CLI under
* another CBM_CACHE_DIR arriving inside that window used to be refused with a
* cache-root conflict although nothing was running any more (#2046; the
* refusal was masked only by the local CLI's own slow startup). host.c already
* waits out the same handoff for the daemon claim marker. Admission therefore
* retries a conflict until the caller's deadline, holding NO guard between
* attempts so compatible peers and activations are never queued behind the
* wait. On deadline the last observed conflict is reported exactly as before;
* an indefinite deadline keeps failing fast because a mismatched holder may
* never leave. */
cbm_version_cohort_status_t cbm_version_cohort_acquire(cbm_version_cohort_manager_t *manager,
const cbm_daemon_build_identity_t *identity,
uint64_t deadline_ms,
cbm_version_cohort_lease_t **lease_out,
cbm_daemon_conflict_t *conflict_out) {
bool retry_logged = false;
for (;;) {
cbm_version_cohort_status_t status =
version_cohort_acquire_once(manager, identity, deadline_ms, lease_out, conflict_out);
if (status != CBM_VERSION_COHORT_CONFLICT || deadline_ms == UINT64_MAX ||
cbm_now_ms() >= deadline_ms) {
return status;
}
if (!retry_logged) {
retry_logged = true;
cbm_log_info("version_cohort.conflict_retry", "reason",
version_cohort_conflict_kind(conflict_out), "active_build",
conflict_out->active_build_fingerprint);
}
version_cohort_note_conflict_retry();
version_cohort_retry_sleep();
}
}
static cbm_version_cohort_status_t version_cohort_reserve_for_mutation_internal(
cbm_version_cohort_manager_t *manager, uint64_t deadline_ms,
cbm_version_cohort_quiesce_fn quiesce, void *quiesce_context,
cbm_version_cohort_quiesce_result_t *quiesce_result_out, cbm_version_cohort_lease_t **lease_out,
bool require_finite_deadline) {
if (lease_out) {
*lease_out = NULL;
}
if (quiesce_result_out) {
*quiesce_result_out = CBM_VERSION_COHORT_QUIESCE_NOT_NEEDED;
}
if (!manager || !quiesce_result_out || !lease_out ||
(require_finite_deadline && deadline_ms == UINT64_MAX)) {
return CBM_VERSION_COHORT_UNSAFE;
}
cbm_version_cohort_lease_t *lease = version_cohort_lease_new(manager);
if (!lease) {
return CBM_VERSION_COHORT_IO;
}
/* Publish crash-safe intent before waiting for an admission already in
* flight. Normal admissions retain maintenance SH until their admission
* transition ends, giving every participant the same deadlock-free native
* order: maintenance -> admission -> lifetime. */
cbm_private_file_lock_status_t lock_status =
version_cohort_lock_until(manager, VERSION_COHORT_MAINTENANCE_FILE,
CBM_PRIVATE_FILE_LOCK_EX, deadline_ms, &lease->maintenance);
if (lock_status != CBM_PRIVATE_FILE_LOCK_OK) {
return version_cohort_failed(lease, version_cohort_status_from_lock(lock_status),
lease_out);
}
lock_status =
version_cohort_lock_until(manager, VERSION_COHORT_ADMISSION_FILE, CBM_PRIVATE_FILE_LOCK_EX,
deadline_ms, &lease->admission);
if (lock_status != CBM_PRIVATE_FILE_LOCK_OK) {
return version_cohort_failed(lease, version_cohort_status_from_lock(lock_status),
lease_out);
}
lock_status =
cbm_private_file_lock_try_acquire(manager->directory, VERSION_COHORT_LIFETIME_FILE,
CBM_PRIVATE_FILE_LOCK_EX, &lease->lifetime);
if (lock_status == CBM_PRIVATE_FILE_LOCK_BUSY) {
if (!quiesce) {
*quiesce_result_out = CBM_VERSION_COHORT_QUIESCE_REFUSED;
return version_cohort_failed(lease, CBM_VERSION_COHORT_BUSY, lease_out);
}
/* Admission remains exclusively locked across the callback and wait.
* Consequently the active lifetime set can only shrink, and no new
* participant can race mutation after accepting the quiesce request. */
cbm_version_cohort_quiesce_result_t quiesce_result = quiesce(quiesce_context);
*quiesce_result_out = quiesce_result;
if (quiesce_result == CBM_VERSION_COHORT_QUIESCE_REFUSED) {
return version_cohort_failed(lease, CBM_VERSION_COHORT_BUSY, lease_out);
}
if (quiesce_result == CBM_VERSION_COHORT_QUIESCE_ERROR) {
return version_cohort_failed(lease, CBM_VERSION_COHORT_IO, lease_out);
}
if (quiesce_result != CBM_VERSION_COHORT_QUIESCE_REQUESTED) {
return version_cohort_failed(lease, CBM_VERSION_COHORT_UNSAFE, lease_out);
}
lock_status =
version_cohort_lock_until(manager, VERSION_COHORT_LIFETIME_FILE,
CBM_PRIVATE_FILE_LOCK_EX, deadline_ms, &lease->lifetime);
}
if (lock_status != CBM_PRIVATE_FILE_LOCK_OK) {
return version_cohort_failed(lease, version_cohort_status_from_lock(lock_status),
lease_out);
}
/* All three locks intentionally remain in the lease. Maintenance makes
* admission fail fast, admission closes the transition, and lifetime
* proves all earlier participants have drained. */
*lease_out = lease;
return CBM_VERSION_COHORT_OK;
}
cbm_version_cohort_status_t cbm_version_cohort_reserve_for_mutation(
cbm_version_cohort_manager_t *manager, uint64_t deadline_ms,
cbm_version_cohort_quiesce_fn quiesce, void *quiesce_context,
cbm_version_cohort_quiesce_result_t *quiesce_result_out,
cbm_version_cohort_lease_t **lease_out) {
return version_cohort_reserve_for_mutation_internal(
manager, deadline_ms, quiesce, quiesce_context, quiesce_result_out, lease_out, true);
}
cbm_version_cohort_status_t cbm_version_cohort_reserve_exclusive(
cbm_version_cohort_manager_t *manager, uint64_t deadline_ms,
cbm_version_cohort_lease_t **lease_out) {
cbm_version_cohort_quiesce_result_t ignored_quiesce;
return version_cohort_reserve_for_mutation_internal(manager, deadline_ms, NULL, NULL,
&ignored_quiesce, lease_out, false);
}
static cbm_version_cohort_maintenance_presence_t version_cohort_maintenance_presence_internal(
cbm_version_cohort_manager_t *manager, bool terminal_observer) {
if (!manager || !version_cohort_manager_register(manager)) {
return CBM_VERSION_COHORT_MAINTENANCE_UNSAFE;
}
cbm_version_cohort_maintenance_presence_t presence = CBM_VERSION_COHORT_MAINTENANCE_IO;
cbm_private_file_lock_t *observer = NULL;
cbm_private_file_lock_status_t status = cbm_private_file_lock_try_acquire(
manager->directory, VERSION_COHORT_MAINTENANCE_FILE, CBM_PRIVATE_FILE_LOCK_SH, &observer);
if (status == CBM_PRIVATE_FILE_LOCK_OK && observer) {
presence = CBM_VERSION_COHORT_MAINTENANCE_ABSENT;
} else if (status == CBM_PRIVATE_FILE_LOCK_BUSY) {
presence = CBM_VERSION_COHORT_MAINTENANCE_REQUESTED;
} else if (status == CBM_PRIVATE_FILE_LOCK_UNSAFE) {
presence = CBM_VERSION_COHORT_MAINTENANCE_UNSAFE;
}
cbm_private_file_lock_status_t observer_release = version_cohort_lock_release_until(
&observer, version_cohort_deadline_after(VERSION_COHORT_CLEANUP_TIMEOUT_MS));
if (observer_release != CBM_PRIVATE_FILE_LOCK_OK) {
presence = CBM_VERSION_COHORT_MAINTENANCE_IO;
}
if (observer) {
if (terminal_observer) {
/* The caller's contract is immediate no-I/O process termination.
* Returning the error preserves that terminal thread's ability to
* exit even when structured logging or stdio is backpressured. The
* native handle intentionally remains process-owned until exit. */
return CBM_VERSION_COHORT_MAINTENANCE_IO;
}
version_cohort_cleanup_fail_stop("maintenance_observer_cleanup");
}
if (!version_cohort_manager_unregister(manager)) {
presence = CBM_VERSION_COHORT_MAINTENANCE_IO;
}
return presence;
}
cbm_version_cohort_maintenance_presence_t cbm_version_cohort_maintenance_presence(
cbm_version_cohort_manager_t *manager) {
return version_cohort_maintenance_presence_internal(manager, false);
}
cbm_version_cohort_maintenance_presence_t cbm_version_cohort_maintenance_presence_terminal(
cbm_version_cohort_manager_t *manager) {
return version_cohort_maintenance_presence_internal(manager, true);
}
cbm_version_cohort_status_t cbm_version_cohort_daemon_claim_acquire(
cbm_version_cohort_manager_t *manager, cbm_version_cohort_daemon_claim_t **claim_out) {
if (claim_out) {
*claim_out = NULL;
}
if (!manager || !claim_out) {
return CBM_VERSION_COHORT_UNSAFE;
}
cbm_version_cohort_daemon_claim_t *claim = calloc(1, sizeof(*claim));
if (!claim || !version_cohort_manager_register(manager)) {
free(claim);
return CBM_VERSION_COHORT_IO;
}
claim->manager = manager;
claim->registered = true;
cbm_private_file_lock_status_t lock_status = cbm_private_file_lock_try_acquire(
manager->directory, VERSION_COHORT_DAEMON_FILE, CBM_PRIVATE_FILE_LOCK_EX, &claim->marker);
if (lock_status == CBM_PRIVATE_FILE_LOCK_OK) {
*claim_out = claim;
return CBM_VERSION_COHORT_OK;
}
cbm_private_file_lock_status_t cleanup_status = version_cohort_lock_release_until(
&claim->marker, version_cohort_deadline_after(VERSION_COHORT_CLEANUP_TIMEOUT_MS));
if (claim->marker) {
*claim_out = claim;
return CBM_VERSION_COHORT_IO;
}
bool unregistered = version_cohort_manager_unregister(manager);
free(claim);
return cleanup_status == CBM_PRIVATE_FILE_LOCK_OK && unregistered
? version_cohort_status_from_lock(lock_status)
: CBM_VERSION_COHORT_IO;
}
cbm_private_file_lock_status_t cbm_version_cohort_daemon_claim_release(
cbm_version_cohort_daemon_claim_t **claim_io) {
if (!claim_io || !*claim_io) {
return CBM_PRIVATE_FILE_LOCK_IO;
}
cbm_version_cohort_daemon_claim_t *claim = *claim_io;
cbm_private_file_lock_status_t result = CBM_PRIVATE_FILE_LOCK_OK;
if (claim->marker) {
result = cbm_private_file_lock_release(&claim->marker);
}
if (claim->marker) {
return CBM_PRIVATE_FILE_LOCK_IO;
}
if (claim->registered) {
if (!version_cohort_manager_unregister(claim->manager)) {
result = CBM_PRIVATE_FILE_LOCK_IO;
}
claim->registered = false;
}
free(claim);
*claim_io = NULL;
return result;
}
cbm_version_cohort_daemon_presence_t cbm_version_cohort_daemon_claim_presence(
cbm_version_cohort_manager_t *manager) {
if (!manager || !version_cohort_manager_register(manager)) {
return CBM_VERSION_COHORT_DAEMON_UNSAFE;
}
cbm_version_cohort_daemon_presence_t presence = CBM_VERSION_COHORT_DAEMON_IO;
cbm_private_file_lock_t *marker = NULL;
cbm_private_file_lock_status_t marker_status = cbm_private_file_lock_try_acquire(
manager->directory, VERSION_COHORT_DAEMON_FILE, CBM_PRIVATE_FILE_LOCK_EX, &marker);
if (marker_status == CBM_PRIVATE_FILE_LOCK_BUSY) {
presence = CBM_VERSION_COHORT_DAEMON_COORDINATED;
} else if (marker_status == CBM_PRIVATE_FILE_LOCK_OK && marker) {
presence = CBM_VERSION_COHORT_DAEMON_ABSENT;
} else if (marker_status == CBM_PRIVATE_FILE_LOCK_UNSAFE) {
presence = CBM_VERSION_COHORT_DAEMON_UNSAFE;
}
cbm_private_file_lock_status_t marker_release = version_cohort_lock_release_until(
&marker, version_cohort_deadline_after(VERSION_COHORT_CLEANUP_TIMEOUT_MS));
if (marker_release != CBM_PRIVATE_FILE_LOCK_OK) {
presence = CBM_VERSION_COHORT_DAEMON_IO;
}
if (marker) {
version_cohort_cleanup_fail_stop("daemon_marker_observer_cleanup");
}
if (!version_cohort_manager_unregister(manager)) {
presence = CBM_VERSION_COHORT_DAEMON_IO;
}
return presence;
}
static cbm_version_cohort_daemon_presence_t version_cohort_active_daemon_presence(
cbm_version_cohort_manager_t *manager, const cbm_daemon_ipc_endpoint_t *endpoint,
const cbm_daemon_ipc_local_transition_t *transition) {
cbm_version_cohort_daemon_presence_t presence = CBM_VERSION_COHORT_DAEMON_IO;
cbm_private_file_lock_t *marker = NULL;
cbm_private_file_lock_status_t marker_status = cbm_private_file_lock_try_acquire(
manager->directory, VERSION_COHORT_DAEMON_FILE, CBM_PRIVATE_FILE_LOCK_EX, &marker);
if (marker_status == CBM_PRIVATE_FILE_LOCK_BUSY) {
presence = CBM_VERSION_COHORT_DAEMON_COORDINATED;
} else if (marker_status == CBM_PRIVATE_FILE_LOCK_OK) {
if (!transition) {
presence = CBM_VERSION_COHORT_DAEMON_UNCOORDINATED;
} else {
/* The sealed transition prevents a replacement daemon from
* starting. Recheck lifetime while retaining the marker EX lock:
* shutdown may have released lifetime after our first observation
* but before the marker became available. */
int lifetime = cbm_daemon_ipc_local_transition_lifetime_probe(endpoint, transition);
presence = lifetime == 0 ? CBM_VERSION_COHORT_DAEMON_ABSENT
: lifetime == 1 ? CBM_VERSION_COHORT_DAEMON_UNCOORDINATED
: CBM_VERSION_COHORT_DAEMON_UNSAFE;
}
} else if (marker_status == CBM_PRIVATE_FILE_LOCK_UNSAFE) {
presence = CBM_VERSION_COHORT_DAEMON_UNSAFE;
}
cbm_private_file_lock_status_t marker_release = version_cohort_lock_release_until(
&marker, version_cohort_deadline_after(VERSION_COHORT_CLEANUP_TIMEOUT_MS));
if (marker_release != CBM_PRIVATE_FILE_LOCK_OK) {
presence = CBM_VERSION_COHORT_DAEMON_IO;
}
if (marker) {
version_cohort_cleanup_fail_stop("active_daemon_marker_cleanup");
}
return presence;
}
cbm_version_cohort_daemon_presence_t cbm_version_cohort_daemon_presence(
cbm_version_cohort_manager_t *manager, const cbm_daemon_ipc_endpoint_t *endpoint) {
if (!manager || !endpoint || !version_cohort_manager_register(manager)) {
return CBM_VERSION_COHORT_DAEMON_UNSAFE;
}
cbm_version_cohort_daemon_presence_t presence = CBM_VERSION_COHORT_DAEMON_IO;
cbm_version_cohort_daemon_presence_t claim = cbm_version_cohort_daemon_claim_presence(manager);
if (claim == CBM_VERSION_COHORT_DAEMON_COORDINATED) {
presence = claim;
} else if (claim != CBM_VERSION_COHORT_DAEMON_ABSENT) {
presence = claim;
} else {
int lifetime = cbm_daemon_ipc_lifetime_reservation_probe(endpoint);
if (lifetime == 0) {
#ifdef _WIN32
int legacy = cbm_daemon_ipc_legacy_generation_probe(endpoint);
if (legacy == 0) {
presence = CBM_VERSION_COHORT_DAEMON_ABSENT;
} else if (legacy == 1) {
presence = CBM_VERSION_COHORT_DAEMON_UNCOORDINATED;
} else {
presence = CBM_VERSION_COHORT_DAEMON_UNSAFE;
}
#else
cbm_daemon_ipc_startup_lock_t *startup = NULL;
int startup_status = cbm_daemon_ipc_startup_lock_try_acquire(endpoint, &startup);
if (startup_status == 0) {
presence = CBM_VERSION_COHORT_DAEMON_UNCOORDINATED;
} else if (startup_status < 0 || !startup) {
presence = CBM_VERSION_COHORT_DAEMON_UNSAFE;
} else {
int cleanup = cbm_daemon_ipc_stale_generation_cleanup(endpoint, startup);
if (cleanup == 1) {
presence = CBM_VERSION_COHORT_DAEMON_ABSENT;
} else if (cleanup == 0) {
presence = CBM_VERSION_COHORT_DAEMON_UNCOORDINATED;
} else {
presence = CBM_VERSION_COHORT_DAEMON_UNSAFE;
}
}
version_cohort_startup_lock_release_complete(&startup);
#endif
} else if (lifetime == 1) {
presence = version_cohort_active_daemon_presence(manager, NULL, NULL);
}
}
if (!version_cohort_manager_unregister(manager)) {
presence = CBM_VERSION_COHORT_DAEMON_IO;
}
return presence;
}
cbm_version_cohort_daemon_presence_t cbm_version_cohort_daemon_presence_under_transition(
cbm_version_cohort_manager_t *manager, const cbm_daemon_ipc_endpoint_t *endpoint,
const cbm_daemon_ipc_local_transition_t *transition) {
if (!manager || !endpoint || !transition || !version_cohort_manager_register(manager)) {
return CBM_VERSION_COHORT_DAEMON_UNSAFE;
}
int transition_status = cbm_daemon_ipc_local_transition_lifetime_probe(endpoint, transition);
/* After validating the guard, classify through the marker. If it is free,
* keep it exclusively locked while observing lifetime so daemon shutdown
* cannot become a false conflict. The retained startup transition prevents
* a replacement generation. */
cbm_version_cohort_daemon_presence_t presence =
transition_status < 0
? CBM_VERSION_COHORT_DAEMON_UNSAFE
: version_cohort_active_daemon_presence(manager, endpoint, transition);
if (!version_cohort_manager_unregister(manager)) {
presence = CBM_VERSION_COHORT_DAEMON_IO;
}
return presence;
}
cbm_private_file_lock_status_t cbm_version_cohort_manager_free(
cbm_version_cohort_manager_t **manager_io) {
if (!manager_io || !*manager_io) {
return CBM_PRIVATE_FILE_LOCK_IO;
}
cbm_version_cohort_manager_t *manager = *manager_io;
cbm_mutex_lock(&manager->mutex);
bool leases_active = manager->lease_count != 0;
bool can_free =
manager->owner_pid == version_cohort_current_pid() && !leases_active && !manager->closing;
if (can_free) {
manager->closing = true;
}
cbm_mutex_unlock(&manager->mutex);
if (!can_free) {
return leases_active ? CBM_PRIVATE_FILE_LOCK_BUSY : CBM_PRIVATE_FILE_LOCK_IO;
}
cbm_private_lock_directory_close(manager->directory);
manager->directory = NULL;
cbm_mutex_destroy(&manager->mutex);
free(manager);
*manager_io = NULL;
return CBM_PRIVATE_FILE_LOCK_OK;
}
bool cbm_version_cohort_log_conflict(const cbm_daemon_conflict_t *conflict) {
const char *cache = cbm_resolve_cache_dir();
if (!cache || !cache[0] || !conflict) {
return false;
}
char logs[VERSION_COHORT_PATH_CAP];
char path[VERSION_COHORT_PATH_CAP];
int logs_written = snprintf(logs, sizeof(logs), "%s/logs", cache);
int path_written = snprintf(path, sizeof(path), "%s/daemon-conflicts.ndjson", logs);
if (logs_written <= 0 || logs_written >= (int)sizeof(logs) || path_written <= 0 ||
path_written >= (int)sizeof(path)) {
return false;
}
FILE *seed =
cbm_daemon_ipc_private_log_open(logs, "daemon-conflicts.ndjson", VERSION_COHORT_LOG_CAP);
if (!seed) {
return false;
}
bool seeded = fclose(seed) == 0;
return seeded && cbm_daemon_conflict_log_append(path, conflict, VERSION_COHORT_LOG_CAP);
}
bool cbm_version_cohort_log_uncoordinated_daemon(const cbm_daemon_build_identity_t *requested) {
if (!version_cohort_identity_valid(requested)) {
return false;
}
cbm_daemon_conflict_t conflict = {
.status = CBM_DAEMON_HELLO_BUILD_CONFLICT,
};
(void)snprintf(conflict.active_version, sizeof(conflict.active_version), "%s",
"pre-cohort/unknown");
memset(conflict.active_build_fingerprint, '0', sizeof(conflict.active_build_fingerprint) - 1);
conflict.active_build_fingerprint[sizeof(conflict.active_build_fingerprint) - 1] = '\0';
(void)snprintf(conflict.requested_version, sizeof(conflict.requested_version), "%s",
requested->semantic_version);
(void)snprintf(conflict.requested_build_fingerprint,
sizeof(conflict.requested_build_fingerprint), "%s",
requested->build_fingerprint);
return cbm_version_cohort_log_conflict(&conflict);
}