Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/watcher/watcher.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "foundation/compat.h"
#include "foundation/compat_thread.h"
#include "foundation/compat_fs.h"
#include "foundation/platform.h"
#include "foundation/str_util.h"

#include <stdio.h>
Expand All @@ -39,6 +40,7 @@ typedef struct {
char last_head[CBM_SZ_64]; /* git HEAD hash */
bool is_git; /* false → skip polling */
bool baseline_done; /* true after first poll */
int missing_root_count; /* consecutive polls where root_path was absent */
int file_count; /* approximate, for interval calc */
int interval_ms; /* adaptive poll interval */
int64_t next_poll_ns; /* next poll time (monotonic ns) */
Expand All @@ -65,6 +67,7 @@ struct cbm_watcher {
#define POLL_BASE_MS 5000
#define POLL_FILE_STEP 500 /* add 1s per this many files */
#define POLL_MAX_MS 60000
#define MISSING_ROOT_DELETE_AFTER 3

/* Sleep chunk for responsive shutdown (ms) */
#define SLEEP_CHUNK_MS 500
Expand Down Expand Up @@ -241,6 +244,32 @@ static void state_free(project_state_t *s) {
free(s);
}

static bool root_path_exists(const char *root_path) {
struct stat st;
return root_path && stat(root_path, &st) == 0 && S_ISDIR(st.st_mode);
}

static void delete_cached_project_db(const char *project_name) {
if (!cbm_validate_project_name(project_name)) {
return;
}

const char *cache_dir = cbm_resolve_cache_dir();
if (!cache_dir) {
return;
}

char path[CBM_SZ_1K];
char wal[CBM_SZ_1K];
char shm[CBM_SZ_1K];
snprintf(path, sizeof(path), "%s/%s.db", cache_dir, project_name);
snprintf(wal, sizeof(wal), "%s-wal", path);
snprintf(shm, sizeof(shm), "%s-shm", path);
(void)cbm_unlink(path);
(void)cbm_unlink(wal);
(void)cbm_unlink(shm);
}

/* Hash table foreach callback to free state entries */
static void free_state_entry(const char *key, void *val, void *ud) {
(void)key;
Expand Down Expand Up @@ -410,6 +439,30 @@ typedef struct {
int reindexed;
} poll_ctx_t;

static void prune_missing_project(cbm_watcher_t *w, project_state_t *s) {
if (!w || !s || !s->project_name) {
return;
}

char project_name[CBM_SZ_1K];
snprintf(project_name, sizeof(project_name), "%s", s->project_name);

bool removed = false;
cbm_mutex_lock(&w->projects_lock);
project_state_t *current = cbm_ht_get(w->projects, project_name);
if (current == s) {
delete_cached_project_db(project_name);
cbm_ht_delete(w->projects, project_name);
state_free(s);
removed = true;
}
cbm_mutex_unlock(&w->projects_lock);

if (removed) {
cbm_log_info("watcher.root_pruned", "project", project_name);
}
}

static void poll_project(const char *key, void *val, void *ud) {
(void)key;
poll_ctx_t *ctx = ud;
Expand All @@ -418,6 +471,19 @@ static void poll_project(const char *key, void *val, void *ud) {
return;
}

if (!root_path_exists(s->root_path)) {
s->missing_root_count++;
cbm_log_warn("watcher.root_missing", "project", s->project_name, "path", s->root_path);
if (s->missing_root_count >= MISSING_ROOT_DELETE_AFTER) {
prune_missing_project(ctx->w, s);
}
return;
}
if (s->missing_root_count > 0) {
cbm_log_info("watcher.root_restored", "project", s->project_name, "path", s->root_path);
s->missing_root_count = 0;
}

/* Initialize baseline on first poll */
if (!s->baseline_done) {
init_baseline(s);
Expand Down
69 changes: 69 additions & 0 deletions tests/test_watcher.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* poll_once behavior.
*/
#include "../src/foundation/compat.h"
#include "../src/foundation/platform.h"
#include "test_framework.h"
#include "test_helpers.h"
#include <watcher/watcher.h>
Expand Down Expand Up @@ -190,6 +191,73 @@ TEST(watcher_poll_nonexistent_path) {
PASS();
}

TEST(watcher_prunes_sustained_missing_root) {
char rootdir[256];
snprintf(rootdir, sizeof(rootdir), "/tmp/cbm_watcher_stale_root_XXXXXX");
if (!cbm_mkdtemp(rootdir)) {
FAIL("cbm_mkdtemp root failed");
}

char cachedir[256];
snprintf(cachedir, sizeof(cachedir), "/tmp/cbm_watcher_stale_cache_XXXXXX");
if (!cbm_mkdtemp(cachedir)) {
th_rmtree(rootdir);
FAIL("cbm_mkdtemp cache failed");
}

char saved_cache_dir[1024];
bool had_cache_dir =
cbm_safe_getenv("CBM_CACHE_DIR", saved_cache_dir, sizeof(saved_cache_dir), NULL) != NULL;
cbm_setenv("CBM_CACHE_DIR", cachedir, 1);

char db_path[512];
char wal_path[512];
char shm_path[512];
snprintf(db_path, sizeof(db_path), "%s/stale-project.db", cachedir);
snprintf(wal_path, sizeof(wal_path), "%s/stale-project.db-wal", cachedir);
snprintf(shm_path, sizeof(shm_path), "%s/stale-project.db-shm", cachedir);
th_write_file(db_path, "db\n");
th_write_file(wal_path, "wal\n");
th_write_file(shm_path, "shm\n");

cbm_store_t *store = cbm_store_open_memory();
cbm_watcher_t *w = cbm_watcher_new(store, index_callback, NULL);
cbm_watcher_watch(w, "stale-project", rootdir);
ASSERT_EQ(cbm_watcher_watch_count(w), 1);

/* Existing root: first poll initializes baseline only. */
cbm_watcher_poll_once(w);
ASSERT_EQ(cbm_watcher_watch_count(w), 1);

th_rmtree(rootdir);

/* Transient miss: keep the project and cached DB. */
cbm_watcher_touch(w, "stale-project");
cbm_watcher_poll_once(w);
ASSERT_EQ(cbm_watcher_watch_count(w), 1);
ASSERT_EQ(access(db_path, F_OK), 0);

/* Sustained absence: prune watcher state and cached DB files. */
cbm_watcher_touch(w, "stale-project");
cbm_watcher_poll_once(w);
cbm_watcher_touch(w, "stale-project");
cbm_watcher_poll_once(w);
ASSERT_EQ(cbm_watcher_watch_count(w), 0);
ASSERT_NEQ(access(db_path, F_OK), 0);
ASSERT_NEQ(access(wal_path, F_OK), 0);
ASSERT_NEQ(access(shm_path, F_OK), 0);

cbm_watcher_free(w);
cbm_store_close(store);
if (had_cache_dir) {
cbm_setenv("CBM_CACHE_DIR", saved_cache_dir, 1);
} else {
cbm_unsetenv("CBM_CACHE_DIR");
}
th_rmtree(cachedir);
PASS();
}

TEST(watcher_poll_this_repo) {
/* Use this project's own repo as a real git repo test */
cbm_store_t *store = cbm_store_open_memory();
Expand Down Expand Up @@ -1508,6 +1576,7 @@ SUITE(watcher) {
/* Polling */
RUN_TEST(watcher_poll_no_projects);
RUN_TEST(watcher_poll_nonexistent_path);
RUN_TEST(watcher_prunes_sustained_missing_root);
RUN_TEST(watcher_poll_this_repo);
RUN_TEST(watcher_stop_flag);

Expand Down
Loading