* Start watching for file changes. * Returns true if watching started successfully, false otherwise.
()
| 362 | * Returns true if watching started successfully, false otherwise. |
| 363 | */ |
| 364 | start(): boolean { |
| 365 | if (this.recursiveWatcher || this.dirWatchers.size > 0 || this.inert) return true; // Already watching |
| 366 | this.stopped = false; |
| 367 | this.degradedReason = null; |
| 368 | this.lockRetryCount = 0; |
| 369 | this.syncFailureRetryCount = 0; |
| 370 | |
| 371 | // Some environments make filesystem watching unusable — most notably |
| 372 | // WSL2 /mnt/ drives, where the underlying fs.watch calls block long |
| 373 | // enough to break MCP startup handshakes (issue #199). Skip watching |
| 374 | // there; callers fall back to manual `codegraph sync` or git sync hooks. |
| 375 | const disabledReason = watchDisabledReason(this.projectRoot); |
| 376 | if (disabledReason) { |
| 377 | logDebug('File watcher disabled', { reason: disabledReason, projectRoot: this.projectRoot }); |
| 378 | return false; |
| 379 | } |
| 380 | |
| 381 | // Reuse the indexer's ignore set so the watcher and indexer agree on scope. |
| 382 | this.ignoreMatcher = buildScopeIgnore(this.projectRoot); |
| 383 | |
| 384 | try { |
| 385 | if (this.inertForTests) { |
| 386 | // Test-only: install no OS watcher; the seam drives events instead. |
| 387 | this.inert = true; |
| 388 | } else if (supportsRecursiveWatch()) { |
| 389 | this.startRecursive(); |
| 390 | } else { |
| 391 | this.startPerDirectory(); |
| 392 | } |
| 393 | |
| 394 | // The per-directory (Linux) path catches watch-resource exhaustion inside |
| 395 | // watchTree and degrades synchronously rather than throwing, so it never |
| 396 | // reaches the catch below. Surface that as a failed start here so both |
| 397 | // strategies report exhaustion identically (start() === false). |
| 398 | if (this.degradedReason) return false; |
| 399 | |
| 400 | // No async crawl to wait on: as soon as the watch set is installed we |
| 401 | // have a clean baseline (pendingFiles is only populated by post-start |
| 402 | // events). Clear defensively and flip ready. |
| 403 | this.pendingFiles.clear(); |
| 404 | this.ready = true; |
| 405 | for (const cb of this.readyWaiters) cb(); |
| 406 | this.readyWaiters.length = 0; |
| 407 | if (IS_TEST_RUNTIME) liveWatchersForTests.set(this.projectRoot, this); |
| 408 | |
| 409 | logDebug('File watcher started', { |
| 410 | projectRoot: this.projectRoot, |
| 411 | debounceMs: this.debounceMs, |
| 412 | mode: this.inertForTests ? 'inert' : supportsRecursiveWatch() ? 'recursive' : 'per-directory', |
| 413 | watchedDirs: this.dirWatchers.size || undefined, |
| 414 | }); |
| 415 | return true; |
| 416 | } catch (err) { |
| 417 | // Watcher setup failed. Watch-resource exhaustion (EMFILE/ENFILE on the |
| 418 | // recursive path) is terminal — degrade cleanly with one actionable |
| 419 | // warning instead of leaving a half-broken watcher. Everything else |
| 420 | // (permission denied, missing directory) keeps the prior quiet-stop. |
| 421 | if (isWatchResourceExhaustion(err)) { |
nothing calls this directly
no test coverage detected