* Add an inotify watch for `dir` and recurse into its non-ignored * subdirectories. When `markExisting` is true (a directory that appeared * AFTER startup), the source files already inside it are recorded as pending * — this closes the `mkdir + write` race where files created before the new
(dir: string, markExisting: boolean)
| 470 | * sync owns the baseline). |
| 471 | */ |
| 472 | private watchTree(dir: string, markExisting: boolean): void { |
| 473 | // A degrade() mid-walk (exhaustion on an earlier directory) calls stop(), |
| 474 | // which sets `stopped`; bail so the recursion unwinds without adding more |
| 475 | // watches to a watcher that is shutting down. `inotifyLimitWarned` does the |
| 476 | // same after ENOSPC — the kernel budget is gone, so stop trying the rest of |
| 477 | // the tree (every add would fail) while keeping the watches already set. |
| 478 | if (this.stopped || this.degradedReason || this.inotifyLimitWarned) return; |
| 479 | if (this.dirWatchers.has(dir)) return; |
| 480 | if (this.dirWatchers.size >= maxDirWatches()) { |
| 481 | if (!this.dirCapWarned) { |
| 482 | this.dirCapWarned = true; |
| 483 | logWarn('File watcher hit directory-watch cap; remaining subtrees rely on manual/periodic sync', { |
| 484 | cap: maxDirWatches(), |
| 485 | }); |
| 486 | } |
| 487 | return; |
| 488 | } |
| 489 | |
| 490 | let w: fs.FSWatcher; |
| 491 | try { |
| 492 | w = watchImpl(dir, { persistent: true }, (_event, filename) => |
| 493 | this.handleDirEvent(dir, filename) |
| 494 | ); |
| 495 | } catch (err) { |
| 496 | // EMFILE/ENFILE means the PROCESS is out of descriptors — every further |
| 497 | // directory would fail too, so degrade the whole watcher rather than |
| 498 | // limping along with a partial watch set. |
| 499 | if (isWatchResourceExhaustion(err)) { |
| 500 | this.degrade(EXHAUSTION_REASON, { error: String(err), dir }); |
| 501 | } else if (isInotifyWatchExhaustion(err)) { |
| 502 | // ENOSPC = inotify watch budget exhausted. NON-fatal: keep the watches |
| 503 | // we have and tell the user the knob to raise (warn once). |
| 504 | this.warnInotifyLimit({ error: String(err), dir }); |
| 505 | } |
| 506 | // ENOENT / EACCES on a single directory stays non-fatal: skip it quietly. |
| 507 | return; |
| 508 | } |
| 509 | w.on('error', (err: unknown) => { |
| 510 | if (isWatchResourceExhaustion(err)) { |
| 511 | this.degrade(EXHAUSTION_REASON, { error: String(err), dir }); |
| 512 | return; |
| 513 | } |
| 514 | if (isInotifyWatchExhaustion(err)) { |
| 515 | this.warnInotifyLimit({ error: String(err), dir }); |
| 516 | } |
| 517 | this.unwatchDir(dir); |
| 518 | }); |
| 519 | this.dirWatchers.set(dir, w); |
| 520 | |
| 521 | let entries: fs.Dirent[]; |
| 522 | try { |
| 523 | entries = fs.readdirSync(dir, { withFileTypes: true }); |
| 524 | } catch { |
| 525 | return; |
| 526 | } |
| 527 | for (const entry of entries) { |
| 528 | const child = path.join(dir, entry.name); |
| 529 | if (entry.isDirectory()) { |
no test coverage detected