| 929 | /* ── Watch list management ──────────────────────────────────────── */ |
| 930 | |
| 931 | bool cbm_watcher_watch(cbm_watcher_t *w, const char *project_name, const char *root_path) { |
| 932 | if (!w || !project_name || !project_name[0] || !root_path || !root_path[0] || |
| 933 | atomic_load_explicit(&w->stopped, memory_order_acquire)) { |
| 934 | return false; |
| 935 | } |
| 936 | |
| 937 | project_state_t *s = state_new(project_name, root_path); |
| 938 | if (!s) { |
| 939 | cbm_log_warn("watcher.watch.oom", "project", project_name, "path", root_path); |
| 940 | return false; |
| 941 | } |
| 942 | |
| 943 | cbm_mutex_lock(&w->projects_lock); |
| 944 | if (atomic_load_explicit(&w->stopped, memory_order_acquire)) { |
| 945 | cbm_mutex_unlock(&w->projects_lock); |
| 946 | state_free(s); |
| 947 | return false; |
| 948 | } |
| 949 | project_state_t *old = cbm_ht_get(w->projects, project_name); |
| 950 | if (old && strcmp(old->root_path, s->root_path) == 0) { |
| 951 | /* Multiple daemon sessions may subscribe to the same canonical |
| 952 | * project/root. Re-registering that identical watch must not discard |
| 953 | * its git baseline, pending dirty signature, or immediate-touch state. */ |
| 954 | cbm_mutex_unlock(&w->projects_lock); |
| 955 | state_free(s); |
| 956 | return true; |
| 957 | } |
| 958 | if (old) { |
| 959 | /* A poll snapshot may still be using the old state, including while |
| 960 | * its index callback calls back into this function. Queue it before |
| 961 | * removing the table entry; on OOM, preserve the existing watch. */ |
| 962 | if (!defer_state_free(w, old)) { |
| 963 | cbm_mutex_unlock(&w->projects_lock); |
| 964 | state_free(s); |
| 965 | return false; |
| 966 | } |
| 967 | cbm_ht_delete(w->projects, project_name); |
| 968 | } |
| 969 | cbm_ht_set(w->projects, s->project_name, s); |
| 970 | bool registered = cbm_ht_get(w->projects, project_name) == s; |
| 971 | if (!registered && old) { |
| 972 | /* Restore the original table entry and remove the deferred-free slot. |
| 973 | * The lock keeps this rollback invisible to poll/watch callers. */ |
| 974 | cbm_ht_set(w->projects, old->project_name, old); |
| 975 | if (cbm_ht_get(w->projects, project_name) == old) { |
| 976 | w->pending_free[--w->pending_free_count] = NULL; |
| 977 | } else { |
| 978 | atomic_store_explicit(&old->registered, false, memory_order_release); |
| 979 | if (old->active_git) { |
| 980 | (void)cbm_subprocess_request_cancel(old->active_git); |
| 981 | } |
| 982 | } |
| 983 | } |
| 984 | if (registered && old) { |
| 985 | atomic_store_explicit(&old->registered, false, memory_order_release); |
| 986 | if (old->active_git) { |
| 987 | (void)cbm_subprocess_request_cancel(old->active_git); |
| 988 | } |