DirectoryWatcher.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { EventEmitter } = require("events");
  7. const path = require("path");
  8. const fs = require("graceful-fs");
  9. const watchEventSource = require("./watchEventSource");
  10. /** @typedef {import("./index").IgnoredFunction} IgnoredFunction */
  11. /** @typedef {import("./index").EventType} EventType */
  12. /** @typedef {import("./index").TimeInfoEntries} TimeInfoEntries */
  13. /** @typedef {import("./index").Entry} Entry */
  14. /** @typedef {import("./index").ExistenceOnlyTimeEntry} ExistenceOnlyTimeEntry */
  15. /** @typedef {import("./index").OnlySafeTimeEntry} OnlySafeTimeEntry */
  16. /** @typedef {import("./index").EventMap} EventMap */
  17. /** @typedef {import("./getWatcherManager").WatcherManager} WatcherManager */
  18. /** @typedef {import("./watchEventSource").Watcher} EventSourceWatcher */
  19. /** @type {ExistenceOnlyTimeEntry} */
  20. const EXISTANCE_ONLY_TIME_ENTRY = Object.freeze({});
  21. let FS_ACCURACY = 2000;
  22. const IS_OSX = require("os").platform() === "darwin";
  23. const IS_WIN = require("os").platform() === "win32";
  24. const { WATCHPACK_POLLING, WATCHPACK_RETRIES } = process.env;
  25. const FORCE_POLLING =
  26. // @ts-expect-error avoid additional checks
  27. `${+WATCHPACK_POLLING}` === WATCHPACK_POLLING
  28. ? +WATCHPACK_POLLING
  29. : Boolean(WATCHPACK_POLLING) && WATCHPACK_POLLING !== "false";
  30. // Number of retries (and delay between retries, in ms) when an fs operation
  31. // returns EBUSY. EBUSY is transient on Windows when an AV scanner, indexer,
  32. // or another process briefly locks a file; retrying avoids incorrectly
  33. // reporting the file as removed (see webpack/watchpack#223, #44).
  34. //
  35. // Configurable via `WATCHPACK_RETRIES` env var: an integer >= 0, or "false"
  36. // to disable retries entirely. Unset / invalid values fall back to 3.
  37. const BUSY_RETRIES = (() => {
  38. if (WATCHPACK_RETRIES === undefined) return 3;
  39. if (WATCHPACK_RETRIES === "false") return 0;
  40. const n = Number(WATCHPACK_RETRIES);
  41. return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 3;
  42. })();
  43. const BUSY_RETRY_DELAY = 100;
  44. /**
  45. * @param {string} str string
  46. * @returns {string} lower cased string
  47. */
  48. function withoutCase(str) {
  49. return str.toLowerCase();
  50. }
  51. /**
  52. * @param {number} times times
  53. * @param {() => void} callback callback
  54. * @returns {() => void} result
  55. */
  56. function needCalls(times, callback) {
  57. return function needCallsCallback() {
  58. if (--times === 0) {
  59. return callback();
  60. }
  61. };
  62. }
  63. /**
  64. * @param {Entry} entry entry
  65. */
  66. function fixupEntryAccuracy(entry) {
  67. if (entry.accuracy > FS_ACCURACY) {
  68. entry.safeTime = entry.safeTime - entry.accuracy + FS_ACCURACY;
  69. entry.accuracy = FS_ACCURACY;
  70. }
  71. }
  72. /**
  73. * @param {number=} mtime mtime
  74. */
  75. function ensureFsAccuracy(mtime) {
  76. if (!mtime) return;
  77. if (FS_ACCURACY > 1 && mtime % 1 !== 0) FS_ACCURACY = 1;
  78. else if (FS_ACCURACY > 10 && mtime % 10 !== 0) FS_ACCURACY = 10;
  79. else if (FS_ACCURACY > 100 && mtime % 100 !== 0) FS_ACCURACY = 100;
  80. else if (FS_ACCURACY > 1000 && mtime % 1000 !== 0) FS_ACCURACY = 1000;
  81. }
  82. /**
  83. * Call `fs.lstat` with retries on EBUSY. Transient EBUSY errors are common
  84. * on Windows when another process (AV scanner, indexer, editor) holds an
  85. * open handle on the file. See webpack/watchpack#223, #44.
  86. *
  87. * The retry count is taken from the `WATCHPACK_RETRIES` env var (default
  88. * 3, set to "0" or "false" to disable retrying). The hot path is a single
  89. * `fs.lstat` call with one inline callback; the timer and the recursive
  90. * call are only scheduled when an EBUSY is actually observed.
  91. * @param {string} target target path
  92. * @param {{ closed: boolean }} watcher owning watcher (checked between retries)
  93. * @param {(err: NodeJS.ErrnoException | null, stats: import("fs").Stats) => void} callback callback
  94. * @param {number=} remaining retries remaining (defaults to `BUSY_RETRIES`)
  95. */
  96. function lstatWithRetry(target, watcher, callback, remaining = BUSY_RETRIES) {
  97. fs.lstat(target, (err, stats) => {
  98. if (
  99. err &&
  100. /** @type {NodeJS.ErrnoException} */ (err).code === "EBUSY" &&
  101. remaining > 0 &&
  102. !watcher.closed
  103. ) {
  104. setTimeout(
  105. () => lstatWithRetry(target, watcher, callback, remaining - 1),
  106. BUSY_RETRY_DELAY,
  107. );
  108. return;
  109. }
  110. callback(err, stats);
  111. });
  112. }
  113. /**
  114. * @typedef {object} FileWatcherEvents
  115. * @property {(type: EventType) => void} initial-missing initial missing event
  116. * @property {(mtime: number, type: EventType, initial: boolean) => void} change change event
  117. * @property {(type: EventType) => void} remove remove event
  118. * @property {() => void} closed closed event
  119. */
  120. /**
  121. * @typedef {object} DirectoryWatcherEvents
  122. * @property {(type: EventType) => void} initial-missing initial missing event
  123. * @property {((file: string, mtime: number, type: EventType, initial: boolean) => void)} change change event
  124. * @property {(type: EventType) => void} remove remove event
  125. * @property {() => void} closed closed event
  126. */
  127. /**
  128. * @template {EventMap} T
  129. * @extends {EventEmitter<{ [K in keyof T]: Parameters<T[K]> }>}
  130. */
  131. class Watcher extends EventEmitter {
  132. /**
  133. * @param {DirectoryWatcher} directoryWatcher a directory watcher
  134. * @param {string} target a target to watch
  135. * @param {number=} startTime start time
  136. */
  137. constructor(directoryWatcher, target, startTime) {
  138. super();
  139. this.directoryWatcher = directoryWatcher;
  140. this.path = target;
  141. this.startTime = startTime && +startTime;
  142. }
  143. /**
  144. * @param {number} mtime mtime
  145. * @param {boolean} initial true when initial, otherwise false
  146. * @returns {boolean} true of start time less than mtile, otherwise false
  147. */
  148. checkStartTime(mtime, initial) {
  149. const { startTime } = this;
  150. if (typeof startTime !== "number") return !initial;
  151. return startTime <= mtime;
  152. }
  153. close() {
  154. // @ts-expect-error bad typing in EventEmitter
  155. this.emit("closed");
  156. }
  157. }
  158. /** @typedef {Set<string>} InitialScanRemoved */
  159. /**
  160. * @typedef {object} WatchpackEvents
  161. * @property {(target: string, mtime: string, type: EventType, initial: boolean) => void} change change event
  162. * @property {() => void} closed closed event
  163. */
  164. /**
  165. * @typedef {object} DirectoryWatcherOptions
  166. * @property {boolean=} followSymlinks true when need to resolve symlinks and watch symlink and real file, otherwise false
  167. * @property {IgnoredFunction=} ignored ignore some files from watching (glob pattern or regexp)
  168. * @property {number | boolean=} poll true when need to enable polling mode for watching, otherwise false
  169. */
  170. /**
  171. * @extends {EventEmitter<{ [K in keyof WatchpackEvents]: Parameters<WatchpackEvents[K]> }>}
  172. */
  173. class DirectoryWatcher extends EventEmitter {
  174. /**
  175. * @param {WatcherManager} watcherManager a watcher manager
  176. * @param {string} directoryPath directory path
  177. * @param {DirectoryWatcherOptions=} options options
  178. */
  179. constructor(watcherManager, directoryPath, options = {}) {
  180. super();
  181. if (FORCE_POLLING) {
  182. options.poll = FORCE_POLLING;
  183. }
  184. this.watcherManager = watcherManager;
  185. this.options = options;
  186. this.path = directoryPath;
  187. // safeTime is the point in time after which reading is safe to be unchanged
  188. // timestamp is a value that should be compared with another timestamp (mtime)
  189. /** @type {Map<string, Entry>} */
  190. this.files = new Map();
  191. /** @type {Map<string, number>} */
  192. this.filesWithoutCase = new Map();
  193. /** @type {Map<string, Watcher<DirectoryWatcherEvents> | boolean>} */
  194. this.directories = new Map();
  195. /** @type {Map<string, Watcher<FileWatcherEvents>>} */
  196. this._symlinkTargetWatchers = new Map();
  197. this.lastWatchEvent = 0;
  198. this.initialScan = true;
  199. this.ignored = options.ignored || (() => false);
  200. this.nestedWatching = false;
  201. /** @type {number | false} */
  202. this.polledWatching =
  203. typeof options.poll === "number"
  204. ? options.poll
  205. : options.poll
  206. ? 5007
  207. : false;
  208. /** @type {undefined | NodeJS.Timeout} */
  209. this.timeout = undefined;
  210. /** @type {null | InitialScanRemoved} */
  211. this.initialScanRemoved = new Set();
  212. /** @type {undefined | number} */
  213. this.initialScanFinished = undefined;
  214. /** @type {Map<string, Set<Watcher<DirectoryWatcherEvents> | Watcher<FileWatcherEvents>>>} */
  215. this.watchers = new Map();
  216. /** @type {Watcher<FileWatcherEvents> | null} */
  217. this.parentWatcher = null;
  218. this.refs = 0;
  219. /** @type {Map<string, boolean>} */
  220. this._activeEvents = new Map();
  221. this.closed = false;
  222. this.scanning = false;
  223. this.scanAgain = false;
  224. this.scanAgainInitial = false;
  225. this.createWatcher();
  226. this.doScan(true);
  227. }
  228. createWatcher() {
  229. try {
  230. if (this.polledWatching) {
  231. /** @type {EventSourceWatcher} */
  232. (this.watcher) = /** @type {EventSourceWatcher} */ ({
  233. close: () => {
  234. if (this.timeout) {
  235. clearTimeout(this.timeout);
  236. this.timeout = undefined;
  237. }
  238. },
  239. });
  240. } else {
  241. if (IS_OSX) {
  242. this.watchInParentDirectory();
  243. }
  244. this.watcher =
  245. /** @type {EventSourceWatcher} */
  246. (watchEventSource.watch(this.path));
  247. this.watcher.on("change", this.onWatchEvent.bind(this));
  248. this.watcher.on("error", this.onWatcherError.bind(this));
  249. }
  250. } catch (err) {
  251. this.onWatcherError(err);
  252. }
  253. }
  254. /**
  255. * @template {(watcher: Watcher<EventMap>) => void} T
  256. * @param {string} path path
  257. * @param {T} fn function
  258. */
  259. forEachWatcher(path, fn) {
  260. const watchers = this.watchers.get(withoutCase(path));
  261. if (watchers !== undefined) {
  262. for (const w of watchers) {
  263. fn(w);
  264. }
  265. }
  266. }
  267. /**
  268. * @param {string} itemPath an item path
  269. * @param {boolean} initial true when initial, otherwise false
  270. * @param {EventType} type even type
  271. */
  272. setMissing(itemPath, initial, type) {
  273. if (this.initialScan) {
  274. /** @type {InitialScanRemoved} */
  275. (this.initialScanRemoved).add(itemPath);
  276. }
  277. const oldDirectory = this.directories.get(itemPath);
  278. if (oldDirectory) {
  279. if (this.nestedWatching) {
  280. /** @type {Watcher<DirectoryWatcherEvents>} */
  281. (oldDirectory).close();
  282. }
  283. this.directories.delete(itemPath);
  284. this.forEachWatcher(itemPath, (w) => w.emit("remove", type));
  285. if (!initial) {
  286. this.forEachWatcher(this.path, (w) =>
  287. w.emit("change", itemPath, null, type, initial),
  288. );
  289. }
  290. }
  291. const oldFile = this.files.get(itemPath);
  292. if (oldFile) {
  293. this.files.delete(itemPath);
  294. const key = withoutCase(itemPath);
  295. const count = /** @type {number} */ (this.filesWithoutCase.get(key)) - 1;
  296. if (count <= 0) {
  297. this.filesWithoutCase.delete(key);
  298. this.forEachWatcher(itemPath, (w) => w.emit("remove", type));
  299. } else {
  300. this.filesWithoutCase.set(key, count);
  301. }
  302. if (!initial) {
  303. this.forEachWatcher(this.path, (w) =>
  304. w.emit("change", itemPath, null, type, initial),
  305. );
  306. }
  307. }
  308. }
  309. /**
  310. * @param {string} target a target to set file time
  311. * @param {number} mtime mtime
  312. * @param {boolean} initial true when initial, otherwise false
  313. * @param {boolean} ignoreWhenEqual true to ignore when equal, otherwise false
  314. * @param {EventType} type type
  315. */
  316. setFileTime(target, mtime, initial, ignoreWhenEqual, type) {
  317. const now = Date.now();
  318. if (this.ignored(target)) return;
  319. const old = this.files.get(target);
  320. let safeTime;
  321. let accuracy;
  322. if (initial) {
  323. safeTime = Math.min(now, mtime) + FS_ACCURACY;
  324. accuracy = FS_ACCURACY;
  325. } else {
  326. safeTime = now;
  327. accuracy = 0;
  328. if (old && old.timestamp === mtime && mtime + FS_ACCURACY < now) {
  329. // We are sure that mtime is untouched
  330. // This can be caused by some file attribute change
  331. // e. g. when access time has been changed
  332. // but the file content is untouched
  333. return;
  334. }
  335. }
  336. if (ignoreWhenEqual && old && old.timestamp === mtime) return;
  337. this.files.set(target, {
  338. safeTime,
  339. accuracy,
  340. timestamp: mtime,
  341. });
  342. if (!old) {
  343. const key = withoutCase(target);
  344. const count = this.filesWithoutCase.get(key);
  345. this.filesWithoutCase.set(key, (count || 0) + 1);
  346. if (count !== undefined) {
  347. // There is already a file with case-insensitive-equal name
  348. // On a case-insensitive filesystem we may miss the renaming
  349. // when only casing is changed.
  350. // To be sure that our information is correct
  351. // we trigger a rescan here
  352. this.doScan(false);
  353. }
  354. this.forEachWatcher(target, (w) => {
  355. if (!initial || w.checkStartTime(safeTime, initial)) {
  356. w.emit("change", mtime, type);
  357. }
  358. });
  359. } else if (!initial) {
  360. this.forEachWatcher(target, (w) => w.emit("change", mtime, type));
  361. }
  362. this.forEachWatcher(this.path, (w) => {
  363. if (!initial || w.checkStartTime(safeTime, initial)) {
  364. w.emit("change", target, safeTime, type, initial);
  365. }
  366. });
  367. }
  368. /**
  369. * @param {string} directoryPath directory path
  370. * @param {number} birthtime birthtime
  371. * @param {boolean} initial true when initial, otherwise false
  372. * @param {EventType} type even type
  373. */
  374. setDirectory(directoryPath, birthtime, initial, type) {
  375. if (this.ignored(directoryPath)) return;
  376. if (directoryPath === this.path) {
  377. if (!initial) {
  378. this.forEachWatcher(this.path, (w) =>
  379. w.emit("change", directoryPath, birthtime, type, initial),
  380. );
  381. }
  382. } else {
  383. const old = this.directories.get(directoryPath);
  384. if (!old) {
  385. const now = Date.now();
  386. if (this.nestedWatching) {
  387. this.createNestedWatcher(directoryPath);
  388. } else {
  389. this.directories.set(directoryPath, true);
  390. }
  391. const safeTime = initial ? Math.min(now, birthtime) + FS_ACCURACY : now;
  392. this.forEachWatcher(directoryPath, (w) => {
  393. if (!initial || w.checkStartTime(safeTime, false)) {
  394. w.emit("change", birthtime, type);
  395. }
  396. });
  397. this.forEachWatcher(this.path, (w) => {
  398. if (!initial || w.checkStartTime(safeTime, initial)) {
  399. w.emit("change", directoryPath, safeTime, type, initial);
  400. }
  401. });
  402. }
  403. }
  404. }
  405. /**
  406. * @param {string} directoryPath directory path
  407. */
  408. createNestedWatcher(directoryPath) {
  409. const watcher = this.watcherManager.watchDirectory(directoryPath, 1);
  410. watcher.on("change", (target, mtime, type, initial) => {
  411. this.forEachWatcher(this.path, (w) => {
  412. if (!initial || w.checkStartTime(mtime, initial)) {
  413. w.emit("change", target, mtime, type, initial);
  414. }
  415. });
  416. });
  417. this.directories.set(directoryPath, watcher);
  418. }
  419. /**
  420. * @param {boolean} flag true when nested, otherwise false
  421. */
  422. setNestedWatching(flag) {
  423. if (this.nestedWatching !== Boolean(flag)) {
  424. this.nestedWatching = Boolean(flag);
  425. if (this.nestedWatching) {
  426. for (const directory of this.directories.keys()) {
  427. this.createNestedWatcher(directory);
  428. }
  429. } else {
  430. for (const [directory, watcher] of this.directories) {
  431. /** @type {Watcher<DirectoryWatcherEvents>} */
  432. (watcher).close();
  433. this.directories.set(directory, true);
  434. }
  435. for (const w of this._symlinkTargetWatchers.values()) {
  436. w.close();
  437. }
  438. this._symlinkTargetWatchers.clear();
  439. }
  440. }
  441. }
  442. /**
  443. * @param {string} target a target to watch
  444. * @param {number=} startTime start time
  445. * @returns {Watcher<DirectoryWatcherEvents> | Watcher<FileWatcherEvents>} watcher
  446. */
  447. watch(target, startTime) {
  448. const key = withoutCase(target);
  449. let watchers = this.watchers.get(key);
  450. if (watchers === undefined) {
  451. watchers = new Set();
  452. this.watchers.set(key, watchers);
  453. }
  454. this.refs++;
  455. const watcher =
  456. /** @type {Watcher<DirectoryWatcherEvents> | Watcher<FileWatcherEvents>} */
  457. (new Watcher(this, target, startTime));
  458. watcher.on("closed", () => {
  459. if (--this.refs <= 0) {
  460. this.close();
  461. return;
  462. }
  463. watchers.delete(watcher);
  464. if (watchers.size === 0) {
  465. this.watchers.delete(key);
  466. if (this.path === target) this.setNestedWatching(false);
  467. }
  468. });
  469. watchers.add(watcher);
  470. let safeTime;
  471. if (target === this.path) {
  472. this.setNestedWatching(true);
  473. safeTime = this.lastWatchEvent;
  474. for (const entry of this.files.values()) {
  475. fixupEntryAccuracy(entry);
  476. safeTime = Math.max(safeTime, entry.safeTime);
  477. }
  478. } else {
  479. const entry = this.files.get(target);
  480. if (entry) {
  481. fixupEntryAccuracy(entry);
  482. safeTime = entry.safeTime;
  483. } else {
  484. safeTime = 0;
  485. }
  486. }
  487. if (safeTime) {
  488. if (startTime && safeTime >= startTime) {
  489. process.nextTick(() => {
  490. if (this.closed) return;
  491. if (target === this.path) {
  492. /** @type {Watcher<DirectoryWatcherEvents>} */
  493. (watcher).emit(
  494. "change",
  495. target,
  496. safeTime,
  497. "watch (outdated on attach)",
  498. true,
  499. );
  500. } else {
  501. /** @type {Watcher<FileWatcherEvents>} */
  502. (watcher).emit(
  503. "change",
  504. safeTime,
  505. "watch (outdated on attach)",
  506. true,
  507. );
  508. }
  509. });
  510. }
  511. } else if (this.initialScan) {
  512. if (
  513. /** @type {InitialScanRemoved} */
  514. (this.initialScanRemoved).has(target)
  515. ) {
  516. process.nextTick(() => {
  517. if (this.closed) return;
  518. watcher.emit("remove");
  519. });
  520. }
  521. } else if (
  522. target !== this.path &&
  523. !this.directories.has(target) &&
  524. watcher.checkStartTime(
  525. /** @type {number} */
  526. (this.initialScanFinished),
  527. false,
  528. )
  529. ) {
  530. process.nextTick(() => {
  531. if (this.closed) return;
  532. watcher.emit("initial-missing", "watch (missing on attach)");
  533. });
  534. }
  535. return watcher;
  536. }
  537. /**
  538. * @param {EventType} eventType event type
  539. * @param {string=} filename filename
  540. */
  541. onWatchEvent(eventType, filename) {
  542. if (this.closed) return;
  543. if (!filename) {
  544. // In some cases no filename is provided
  545. // This seem to happen on windows
  546. // So some event happened but we don't know which file is affected
  547. // We have to do a full scan of the directory
  548. this.doScan(false);
  549. return;
  550. }
  551. const target = path.join(this.path, filename);
  552. if (this.ignored(target)) return;
  553. if (this._activeEvents.get(filename) === undefined) {
  554. this._activeEvents.set(filename, false);
  555. const checkStats = () => {
  556. if (this.closed) return;
  557. this._activeEvents.set(filename, false);
  558. lstatWithRetry(target, this, (err, stats) => {
  559. if (this.closed) return;
  560. if (this._activeEvents.get(filename) === true) {
  561. process.nextTick(checkStats);
  562. return;
  563. }
  564. this._activeEvents.delete(filename);
  565. // ENOENT happens when the file/directory doesn't exist
  566. // EPERM happens when the containing directory doesn't exist
  567. // EBUSY happens when another process has the file locked (e.g.
  568. // Windows AV scanner). lstatWithRetry already retried before
  569. // giving up here.
  570. if (err) {
  571. if (
  572. err.code !== "ENOENT" &&
  573. err.code !== "EPERM" &&
  574. err.code !== "EBUSY"
  575. ) {
  576. this.onStatsError(err);
  577. } else if (
  578. filename === path.basename(this.path) && // This may indicate that the directory itself was removed
  579. !fs.existsSync(this.path)
  580. ) {
  581. this.onDirectoryRemoved("stat failed");
  582. }
  583. }
  584. this.lastWatchEvent = Date.now();
  585. if (!stats) {
  586. // On EBUSY we keep the tracked state: the file is likely still
  587. // there and a later event or scan will reconcile. Emitting a
  588. // remove here would make the watch appear to stop after a
  589. // transient lock (webpack/watchpack#223, #44).
  590. if (err && err.code === "EBUSY" && BUSY_RETRIES > 0) {
  591. return;
  592. }
  593. this.setMissing(target, false, eventType);
  594. } else if (stats.isDirectory()) {
  595. this.setDirectory(target, +stats.birthtime || 1, false, eventType);
  596. } else if (stats.isFile() || stats.isSymbolicLink()) {
  597. if (stats.mtime) {
  598. ensureFsAccuracy(+stats.mtime);
  599. }
  600. this.setFileTime(
  601. target,
  602. +stats.mtime || +stats.ctime || 1,
  603. false,
  604. false,
  605. eventType,
  606. );
  607. }
  608. });
  609. };
  610. process.nextTick(checkStats);
  611. } else {
  612. this._activeEvents.set(filename, true);
  613. }
  614. }
  615. /**
  616. * @param {unknown=} err error
  617. */
  618. onWatcherError(err) {
  619. if (this.closed) return;
  620. if (err) {
  621. if (
  622. /** @type {NodeJS.ErrnoException} */
  623. (err).code !== "EPERM" &&
  624. /** @type {NodeJS.ErrnoException} */
  625. (err).code !== "ENOENT"
  626. ) {
  627. // eslint-disable-next-line no-console
  628. console.error(`Watchpack Error (watcher): ${err}`);
  629. }
  630. this.onDirectoryRemoved("watch error");
  631. }
  632. }
  633. /**
  634. * @param {Error | NodeJS.ErrnoException=} err error
  635. */
  636. onStatsError(err) {
  637. if (err) {
  638. // eslint-disable-next-line no-console
  639. console.error(`Watchpack Error (stats): ${err}`);
  640. }
  641. }
  642. /**
  643. * @param {Error | NodeJS.ErrnoException=} err error
  644. */
  645. onScanError(err) {
  646. if (err) {
  647. // eslint-disable-next-line no-console
  648. console.error(`Watchpack Error (initial scan): ${err}`);
  649. }
  650. this.onScanFinished();
  651. }
  652. onScanFinished() {
  653. if (this.polledWatching) {
  654. this.timeout = setTimeout(() => {
  655. if (this.closed) return;
  656. this.doScan(false);
  657. }, this.polledWatching);
  658. }
  659. }
  660. /**
  661. * @param {string} reason a reason
  662. */
  663. onDirectoryRemoved(reason) {
  664. if (this.watcher) {
  665. this.watcher.close();
  666. this.watcher = null;
  667. }
  668. this.watchInParentDirectory();
  669. const type = /** @type {EventType} */ (`directory-removed (${reason})`);
  670. for (const directory of this.directories.keys()) {
  671. this.setMissing(directory, false, type);
  672. }
  673. for (const file of this.files.keys()) {
  674. this.setMissing(file, false, type);
  675. }
  676. }
  677. watchInParentDirectory() {
  678. if (!this.parentWatcher) {
  679. const parentDir = path.dirname(this.path);
  680. // avoid watching in the root directory
  681. // removing directories in the root directory is not supported
  682. if (path.dirname(parentDir) === parentDir) return;
  683. this.parentWatcher = this.watcherManager.watchFile(this.path, 1);
  684. /** @type {Watcher<FileWatcherEvents>} */
  685. (this.parentWatcher).on("change", (mtime, type) => {
  686. if (this.closed) return;
  687. // On non-osx platforms we don't need this watcher to detect
  688. // directory removal, as an EPERM error indicates that
  689. if ((!IS_OSX || this.polledWatching) && this.parentWatcher) {
  690. this.parentWatcher.close();
  691. this.parentWatcher = null;
  692. }
  693. // Try to create the watcher when parent directory is found
  694. if (!this.watcher) {
  695. this.createWatcher();
  696. this.doScan(false);
  697. // directory was created so we emit an event
  698. this.forEachWatcher(this.path, (w) =>
  699. w.emit("change", this.path, mtime, type, false),
  700. );
  701. }
  702. });
  703. /** @type {Watcher<FileWatcherEvents>} */
  704. (this.parentWatcher).on("remove", () => {
  705. this.onDirectoryRemoved("parent directory removed");
  706. });
  707. }
  708. }
  709. /**
  710. * @param {boolean} initial true when initial, otherwise false
  711. */
  712. doScan(initial) {
  713. if (this.scanning) {
  714. if (this.scanAgain) {
  715. if (!initial) this.scanAgainInitial = false;
  716. } else {
  717. this.scanAgain = true;
  718. this.scanAgainInitial = initial;
  719. }
  720. return;
  721. }
  722. this.scanning = true;
  723. if (this.timeout) {
  724. clearTimeout(this.timeout);
  725. this.timeout = undefined;
  726. }
  727. process.nextTick(() => {
  728. if (this.closed) return;
  729. fs.readdir(this.path, (err, items) => {
  730. if (this.closed) return;
  731. if (err) {
  732. // Mirror the lstat error handling below: treat permission /
  733. // invalid-argument / no-device errors on the directory itself
  734. // as removed rather than logging "Watchpack Error (initial
  735. // scan)". These surface for unreadable mounts (WSL `/mnt/c`,
  736. // fuse mounts), unmounted devices (`/efi`), and libuv's
  737. // post-Node 22.17 `EINVAL` on protected Windows paths
  738. // (see #187).
  739. if (
  740. err.code === "ENOENT" ||
  741. err.code === "EPERM" ||
  742. err.code === "EACCES" ||
  743. err.code === "ENODEV" ||
  744. (err.code === "EINVAL" && IS_WIN)
  745. ) {
  746. this.onDirectoryRemoved("scan readdir failed");
  747. } else {
  748. this.onScanError(err);
  749. }
  750. this.initialScan = false;
  751. this.initialScanFinished = Date.now();
  752. if (initial) {
  753. for (const watchers of this.watchers.values()) {
  754. for (const watcher of watchers) {
  755. if (watcher.checkStartTime(this.initialScanFinished, false)) {
  756. watcher.emit(
  757. "initial-missing",
  758. "scan (parent directory missing in initial scan)",
  759. );
  760. }
  761. }
  762. }
  763. }
  764. if (this.scanAgain) {
  765. this.scanAgain = false;
  766. this.doScan(this.scanAgainInitial);
  767. } else {
  768. this.scanning = false;
  769. }
  770. return;
  771. }
  772. const itemPaths = new Set(
  773. items.map((item) => path.join(this.path, item.normalize("NFC"))),
  774. );
  775. for (const file of this.files.keys()) {
  776. if (!itemPaths.has(file)) {
  777. this.setMissing(file, initial, "scan (missing)");
  778. }
  779. }
  780. for (const directory of this.directories.keys()) {
  781. if (!itemPaths.has(directory)) {
  782. this.setMissing(directory, initial, "scan (missing)");
  783. }
  784. }
  785. if (this.scanAgain) {
  786. // Early repeat of scan
  787. this.scanAgain = false;
  788. this.doScan(initial);
  789. return;
  790. }
  791. const itemFinished = needCalls(itemPaths.size + 1, () => {
  792. if (this.closed) return;
  793. this.initialScan = false;
  794. this.initialScanRemoved = null;
  795. this.initialScanFinished = Date.now();
  796. if (initial) {
  797. const missingWatchers = new Map(this.watchers);
  798. missingWatchers.delete(withoutCase(this.path));
  799. for (const item of itemPaths) {
  800. missingWatchers.delete(withoutCase(item));
  801. }
  802. for (const watchers of missingWatchers.values()) {
  803. for (const watcher of watchers) {
  804. if (watcher.checkStartTime(this.initialScanFinished, false)) {
  805. watcher.emit(
  806. "initial-missing",
  807. "scan (missing in initial scan)",
  808. );
  809. }
  810. }
  811. }
  812. }
  813. if (this.scanAgain) {
  814. this.scanAgain = false;
  815. this.doScan(this.scanAgainInitial);
  816. } else {
  817. this.scanning = false;
  818. this.onScanFinished();
  819. }
  820. });
  821. for (const itemPath of itemPaths) {
  822. lstatWithRetry(itemPath, this, (err2, stats) => {
  823. if (this.closed) return;
  824. if (err2) {
  825. if (
  826. err2.code === "ENOENT" ||
  827. err2.code === "EPERM" ||
  828. err2.code === "EACCES" ||
  829. err2.code === "EBUSY" ||
  830. err2.code === "ENODEV" ||
  831. // TODO https://github.com/libuv/libuv/pull/4566
  832. (err2.code === "EINVAL" && IS_WIN)
  833. ) {
  834. // readdir saw the entry but we can't stat it due to a
  835. // transient lock — keep the previously-known entry instead
  836. // of incorrectly flagging it as missing.
  837. if (
  838. !(
  839. err2.code === "EBUSY" &&
  840. BUSY_RETRIES > 0 &&
  841. this.files.has(itemPath)
  842. )
  843. ) {
  844. this.setMissing(itemPath, initial, `scan (${err2.code})`);
  845. }
  846. } else {
  847. this.onScanError(err2);
  848. }
  849. itemFinished();
  850. return;
  851. }
  852. /**
  853. * @param {string | null} realPath resolved real path for an outside-dir symlink target
  854. */
  855. const apply = (realPath) => {
  856. if (stats.isFile() || stats.isSymbolicLink()) {
  857. if (stats.mtime) ensureFsAccuracy(+stats.mtime);
  858. this.setFileTime(
  859. itemPath,
  860. +stats.mtime || +stats.ctime || 1,
  861. initial,
  862. true,
  863. "scan (file)",
  864. );
  865. if (realPath && !this._symlinkTargetWatchers.has(itemPath)) {
  866. const w = this.watcherManager.watchFile(realPath, Date.now());
  867. if (w) {
  868. w.on("change", (mtime, type, wInitial) => {
  869. if (wInitial) return;
  870. this.setFileTime(itemPath, mtime, false, false, type);
  871. });
  872. this._symlinkTargetWatchers.set(itemPath, w);
  873. }
  874. }
  875. } else if (
  876. stats.isDirectory() &&
  877. (!initial || !this.directories.has(itemPath))
  878. ) {
  879. this.setDirectory(
  880. itemPath,
  881. +stats.birthtime || 1,
  882. initial,
  883. "scan (dir)",
  884. );
  885. }
  886. itemFinished();
  887. };
  888. if (
  889. this.options.followSymlinks &&
  890. this.nestedWatching &&
  891. stats.isSymbolicLink()
  892. ) {
  893. fs.realpath(itemPath, (err3, realPath) => {
  894. if (this.closed) return;
  895. if (
  896. err3 ||
  897. !realPath ||
  898. withoutCase(path.dirname(realPath)) === withoutCase(this.path)
  899. ) {
  900. apply(null);
  901. return;
  902. }
  903. // Cycle protection: when the symlink's target is the symlink
  904. // path itself or one of its ancestors, descending would
  905. // create an unbounded chain of `DirectoryWatcher`s as we walk
  906. // back into territory we are already watching. Treat the
  907. // symlink as a plain entry instead so the symlink itself is
  908. // still tracked but no recursion happens.
  909. const rel = path.relative(realPath, itemPath);
  910. if (!path.isAbsolute(rel) && !rel.startsWith("..")) {
  911. apply(null);
  912. return;
  913. }
  914. fs.stat(realPath, (err4, targetStats) => {
  915. if (this.closed) return;
  916. if (err4 || !targetStats) {
  917. apply(null);
  918. } else if (targetStats.isFile()) {
  919. apply(realPath);
  920. } else if (targetStats.isDirectory()) {
  921. // Treat a symlink whose target is a directory as a
  922. // nested watched directory so files inside the target
  923. // propagate change events to the symlink path.
  924. this.setDirectory(
  925. itemPath,
  926. +targetStats.birthtime || +stats.birthtime || 1,
  927. initial,
  928. "scan (dir)",
  929. );
  930. itemFinished();
  931. } else {
  932. apply(null);
  933. }
  934. });
  935. });
  936. return;
  937. }
  938. apply(null);
  939. });
  940. }
  941. itemFinished();
  942. });
  943. });
  944. }
  945. /**
  946. * @returns {Record<string, number>} times
  947. */
  948. getTimes() {
  949. const obj = Object.create(null);
  950. let safeTime = this.lastWatchEvent;
  951. for (const [file, entry] of this.files) {
  952. fixupEntryAccuracy(entry);
  953. safeTime = Math.max(safeTime, entry.safeTime);
  954. obj[file] = Math.max(entry.safeTime, entry.timestamp);
  955. }
  956. if (this.nestedWatching) {
  957. for (const w of this.directories.values()) {
  958. const times =
  959. /** @type {Watcher<DirectoryWatcherEvents>} */
  960. (w).directoryWatcher.getTimes();
  961. for (const file of Object.keys(times)) {
  962. const time = times[file];
  963. safeTime = Math.max(safeTime, time);
  964. obj[file] = time;
  965. }
  966. }
  967. obj[this.path] = safeTime;
  968. }
  969. if (!this.initialScan) {
  970. for (const watchers of this.watchers.values()) {
  971. for (const watcher of watchers) {
  972. const { path } = watcher;
  973. if (!Object.prototype.hasOwnProperty.call(obj, path)) {
  974. obj[path] = null;
  975. }
  976. }
  977. }
  978. }
  979. return obj;
  980. }
  981. /**
  982. * @param {TimeInfoEntries} fileTimestamps file timestamps
  983. * @param {TimeInfoEntries} directoryTimestamps directory timestamps
  984. * @returns {number} safe time
  985. */
  986. collectTimeInfoEntries(fileTimestamps, directoryTimestamps) {
  987. let safeTime = this.lastWatchEvent;
  988. for (const [file, entry] of this.files) {
  989. fixupEntryAccuracy(entry);
  990. safeTime = Math.max(safeTime, entry.safeTime);
  991. fileTimestamps.set(file, entry);
  992. }
  993. if (this.nestedWatching) {
  994. for (const w of this.directories.values()) {
  995. safeTime = Math.max(
  996. safeTime,
  997. /** @type {Watcher<DirectoryWatcherEvents>} */
  998. (w).directoryWatcher.collectTimeInfoEntries(
  999. fileTimestamps,
  1000. directoryTimestamps,
  1001. ),
  1002. );
  1003. }
  1004. fileTimestamps.set(this.path, EXISTANCE_ONLY_TIME_ENTRY);
  1005. directoryTimestamps.set(this.path, {
  1006. safeTime,
  1007. });
  1008. } else {
  1009. for (const dir of this.directories.keys()) {
  1010. // No additional info about this directory
  1011. // but maybe another DirectoryWatcher has info
  1012. fileTimestamps.set(dir, EXISTANCE_ONLY_TIME_ENTRY);
  1013. if (!directoryTimestamps.has(dir)) {
  1014. directoryTimestamps.set(dir, EXISTANCE_ONLY_TIME_ENTRY);
  1015. }
  1016. }
  1017. fileTimestamps.set(this.path, EXISTANCE_ONLY_TIME_ENTRY);
  1018. directoryTimestamps.set(this.path, EXISTANCE_ONLY_TIME_ENTRY);
  1019. }
  1020. if (!this.initialScan) {
  1021. for (const watchers of this.watchers.values()) {
  1022. for (const watcher of watchers) {
  1023. const { path } = watcher;
  1024. if (!fileTimestamps.has(path)) {
  1025. fileTimestamps.set(path, null);
  1026. }
  1027. }
  1028. }
  1029. }
  1030. return safeTime;
  1031. }
  1032. close() {
  1033. if (this.closed) return;
  1034. this.closed = true;
  1035. this.initialScan = false;
  1036. if (this.watcher) {
  1037. this.watcher.close();
  1038. this.watcher = null;
  1039. }
  1040. if (this.nestedWatching) {
  1041. for (const w of this.directories.values()) {
  1042. /** @type {Watcher<DirectoryWatcherEvents>} */
  1043. (w).close();
  1044. }
  1045. this.directories.clear();
  1046. }
  1047. for (const w of this._symlinkTargetWatchers.values()) {
  1048. w.close();
  1049. }
  1050. this._symlinkTargetWatchers.clear();
  1051. if (this.parentWatcher) {
  1052. this.parentWatcher.close();
  1053. this.parentWatcher = null;
  1054. }
  1055. this.emit("closed");
  1056. }
  1057. }
  1058. module.exports = DirectoryWatcher;
  1059. module.exports.EXISTANCE_ONLY_TIME_ENTRY = EXISTANCE_ONLY_TIME_ENTRY;
  1060. module.exports.Watcher = Watcher;