* Start watching for file changes. * Returns true if watching started successfully, false otherwise.
()
| 325 | * Returns true if watching started successfully, false otherwise. |
| 326 | */ |
| 327 | start(): boolean { |
| 328 | if (this.recursiveWatcher || this.dirWatchers.size > 0 || this.inert) return true; // Already watching |
| 329 | this.stopped = false; |
| 330 | this.degradedReason = null; |
| 331 | this.lockRetryCount = 0; |
| 332 | |
| 333 | // Some environments make filesystem watching unusable — most notably |
| 334 | // WSL2 /mnt/ drives, where the underlying fs.watch calls block long |
| 335 | // enough to break MCP startup handshakes (issue #199). Skip watching |
| 336 | // there; callers fall back to manual `codegraph sync` or git sync hooks. |
| 337 | const disabledReason = watchDisabledReason(this.projectRoot); |
| 338 | if (disabledReason) { |
| 339 | logDebug('File watcher disabled', { reason: disabledReason, projectRoot: this.projectRoot }); |
| 340 | return false; |
| 341 | } |
| 342 | |
| 343 | // Reuse the indexer's ignore set so the watcher and indexer agree on scope. |
| 344 | this.ignoreMatcher = buildScopeIgnore(this.projectRoot); |
| 345 | |
| 346 | try { |
| 347 | if (this.inertForTests) { |
| 348 | // Test-only: install no OS watcher; the seam drives events instead. |
| 349 | this.inert = true; |
| 350 | } else if (supportsRecursiveWatch()) { |
| 351 | this.startRecursive(); |
| 352 | } else { |
| 353 | this.startPerDirectory(); |
| 354 | } |
| 355 | |
| 356 | // The per-directory (Linux) path catches watch-resource exhaustion inside |
| 357 | // watchTree and degrades synchronously rather than throwing, so it never |
| 358 | // reaches the catch below. Surface that as a failed start here so both |
| 359 | // strategies report exhaustion identically (start() === false). |
| 360 | if (this.degradedReason) return false; |
| 361 | |
| 362 | // No async crawl to wait on: as soon as the watch set is installed we |
| 363 | // have a clean baseline (pendingFiles is only populated by post-start |
| 364 | // events). Clear defensively and flip ready. |
| 365 | this.pendingFiles.clear(); |
| 366 | this.ready = true; |
| 367 | for (const cb of this.readyWaiters) cb(); |
| 368 | this.readyWaiters.length = 0; |
| 369 | if (IS_TEST_RUNTIME) liveWatchersForTests.set(this.projectRoot, this); |
| 370 | |
| 371 | logDebug('File watcher started', { |
| 372 | projectRoot: this.projectRoot, |
| 373 | debounceMs: this.debounceMs, |
| 374 | mode: this.inertForTests ? 'inert' : supportsRecursiveWatch() ? 'recursive' : 'per-directory', |
| 375 | watchedDirs: this.dirWatchers.size || undefined, |
| 376 | }); |
| 377 | return true; |
| 378 | } catch (err) { |
| 379 | // Watcher setup failed. Watch-resource exhaustion (EMFILE/ENFILE on the |
| 380 | // recursive path) is terminal — degrade cleanly with one actionable |
| 381 | // warning instead of leaving a half-broken watcher. Everything else |
| 382 | // (permission denied, missing directory) keeps the prior quiet-stop. |
| 383 | if (isWatchResourceExhaustion(err)) { |
| 384 | this.degrade(EXHAUSTION_REASON, { error: String(err) }); |
nothing calls this directly
no test coverage detected