* 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)
| 432 | * sync owns the baseline). |
| 433 | */ |
| 434 | private watchTree(dir: string, markExisting: boolean): void { |
| 435 | // A degrade() mid-walk (exhaustion on an earlier directory) calls stop(), |
| 436 | // which sets `stopped`; bail so the recursion unwinds without adding more |
| 437 | // watches to a watcher that is shutting down. `inotifyLimitWarned` does the |
| 438 | // same after ENOSPC — the kernel budget is gone, so stop trying the rest of |
| 439 | // the tree (every add would fail) while keeping the watches already set. |
| 440 | if (this.stopped || this.degradedReason || this.inotifyLimitWarned) return; |
| 441 | if (this.dirWatchers.has(dir)) return; |
| 442 | if (this.dirWatchers.size >= maxDirWatches()) { |
| 443 | if (!this.dirCapWarned) { |
| 444 | this.dirCapWarned = true; |
| 445 | logWarn('File watcher hit directory-watch cap; remaining subtrees rely on manual/periodic sync', { |
| 446 | cap: maxDirWatches(), |
| 447 | }); |
| 448 | } |
| 449 | return; |
| 450 | } |
| 451 | |
| 452 | let w: fs.FSWatcher; |
| 453 | try { |
| 454 | w = watchImpl(dir, { persistent: true }, (_event, filename) => |
| 455 | this.handleDirEvent(dir, filename) |
| 456 | ); |
| 457 | } catch (err) { |
| 458 | // EMFILE/ENFILE means the PROCESS is out of descriptors — every further |
| 459 | // directory would fail too, so degrade the whole watcher rather than |
| 460 | // limping along with a partial watch set. |
| 461 | if (isWatchResourceExhaustion(err)) { |
| 462 | this.degrade(EXHAUSTION_REASON, { error: String(err), dir }); |
| 463 | } else if (isInotifyWatchExhaustion(err)) { |
| 464 | // ENOSPC = inotify watch budget exhausted. NON-fatal: keep the watches |
| 465 | // we have and tell the user the knob to raise (warn once). |
| 466 | this.warnInotifyLimit({ error: String(err), dir }); |
| 467 | } |
| 468 | // ENOENT / EACCES on a single directory stays non-fatal: skip it quietly. |
| 469 | return; |
| 470 | } |
| 471 | w.on('error', (err: unknown) => { |
| 472 | if (isWatchResourceExhaustion(err)) { |
| 473 | this.degrade(EXHAUSTION_REASON, { error: String(err), dir }); |
| 474 | return; |
| 475 | } |
| 476 | if (isInotifyWatchExhaustion(err)) { |
| 477 | this.warnInotifyLimit({ error: String(err), dir }); |
| 478 | } |
| 479 | this.unwatchDir(dir); |
| 480 | }); |
| 481 | this.dirWatchers.set(dir, w); |
| 482 | |
| 483 | let entries: fs.Dirent[]; |
| 484 | try { |
| 485 | entries = fs.readdirSync(dir, { withFileTypes: true }); |
| 486 | } catch { |
| 487 | return; |
| 488 | } |
| 489 | for (const entry of entries) { |
| 490 | const child = path.join(dir, entry.name); |
| 491 | if (entry.isDirectory()) { |
no test coverage detected