| 1219 | } |
| 1220 | |
| 1221 | static void poll_project(const char *key, void *val, void *ud) { |
| 1222 | (void)key; |
| 1223 | poll_ctx_t *ctx = ud; |
| 1224 | project_state_t *s = val; |
| 1225 | if (!s || !atomic_load_explicit(&s->registered, memory_order_acquire)) { |
| 1226 | return; |
| 1227 | } |
| 1228 | |
| 1229 | /* Stale-root pruning (#286): classify the root BEFORE the baseline / |
| 1230 | * is_git / interval gates so vanished roots are noticed even for |
| 1231 | * non-git projects and regardless of adaptive backoff. */ |
| 1232 | int stat_errno = 0; |
| 1233 | root_status_t rs = root_status(s->root_path, &stat_errno); |
| 1234 | if (rs == ROOT_UNCERTAIN) { |
| 1235 | /* EACCES / EIO / network blip / TCC revocation — the root may still |
| 1236 | * exist. Never count toward pruning; restart the streak so only an |
| 1237 | * uninterrupted run of genuine ENOENT/ENOTDIR observations can |
| 1238 | * delete user data. */ |
| 1239 | if (s->missing_root_count > 0) { |
| 1240 | s->missing_root_count = 0; |
| 1241 | s->first_missing_ms = 0; |
| 1242 | } |
| 1243 | cbm_log_warn("watcher.root_stat_error", "project", s->project_name, "path", s->root_path, |
| 1244 | "errno", itoa_buf(stat_errno)); |
| 1245 | return; |
| 1246 | } |
| 1247 | if (rs == ROOT_MISSING) { |
| 1248 | uint64_t now_ms = cbm_now_ms(); |
| 1249 | if (s->missing_root_count == 0) { |
| 1250 | s->first_missing_ms = now_ms; |
| 1251 | } |
| 1252 | s->missing_root_count++; |
| 1253 | cbm_log_warn("watcher.root_missing", "project", s->project_name, "path", s->root_path, |
| 1254 | "polls", itoa_buf(s->missing_root_count)); |
| 1255 | if (s->missing_root_count >= MISSING_ROOT_DELETE_AFTER && |
| 1256 | now_ms - s->first_missing_ms >= (uint64_t)prune_grace_s() * CBM_MSEC_PER_SEC) { |
| 1257 | prune_missing_project(ctx->w, s); |
| 1258 | } |
| 1259 | return; |
| 1260 | } |
| 1261 | if (s->missing_root_count > 0) { |
| 1262 | cbm_log_info("watcher.root_restored", "project", s->project_name, "path", s->root_path); |
| 1263 | s->missing_root_count = 0; |
| 1264 | s->first_missing_ms = 0; |
| 1265 | } |
| 1266 | |
| 1267 | /* Initialize baseline on first poll */ |
| 1268 | if (!s->baseline_done) { |
| 1269 | (void)init_baseline(ctx->w, s); |
| 1270 | return; |
| 1271 | } |
| 1272 | |
| 1273 | /* Skip non-git projects */ |
| 1274 | if (!s->is_git) { |
| 1275 | return; |
| 1276 | } |
| 1277 | |
| 1278 | /* Respect adaptive interval */ |
no test coverage detected