DirectoryWatcher.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965
  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 } = 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. /**
  31. * @param {string} str string
  32. * @returns {string} lower cased string
  33. */
  34. function withoutCase(str) {
  35. return str.toLowerCase();
  36. }
  37. /**
  38. * @param {number} times times
  39. * @param {() => void} callback callback
  40. * @returns {() => void} result
  41. */
  42. function needCalls(times, callback) {
  43. return function needCallsCallback() {
  44. if (--times === 0) {
  45. return callback();
  46. }
  47. };
  48. }
  49. /**
  50. * @param {Entry} entry entry
  51. */
  52. function fixupEntryAccuracy(entry) {
  53. if (entry.accuracy > FS_ACCURACY) {
  54. entry.safeTime = entry.safeTime - entry.accuracy + FS_ACCURACY;
  55. entry.accuracy = FS_ACCURACY;
  56. }
  57. }
  58. /**
  59. * @param {number=} mtime mtime
  60. */
  61. function ensureFsAccuracy(mtime) {
  62. if (!mtime) return;
  63. if (FS_ACCURACY > 1 && mtime % 1 !== 0) FS_ACCURACY = 1;
  64. else if (FS_ACCURACY > 10 && mtime % 10 !== 0) FS_ACCURACY = 10;
  65. else if (FS_ACCURACY > 100 && mtime % 100 !== 0) FS_ACCURACY = 100;
  66. else if (FS_ACCURACY > 1000 && mtime % 1000 !== 0) FS_ACCURACY = 1000;
  67. }
  68. /**
  69. * @typedef {object} FileWatcherEvents
  70. * @property {(type: EventType) => void} initial-missing initial missing event
  71. * @property {(mtime: number, type: EventType, initial: boolean) => void} change change event
  72. * @property {(type: EventType) => void} remove remove event
  73. * @property {() => void} closed closed event
  74. */
  75. /**
  76. * @typedef {object} DirectoryWatcherEvents
  77. * @property {(type: EventType) => void} initial-missing initial missing event
  78. * @property {((file: string, mtime: number, type: EventType, initial: boolean) => void)} change change event
  79. * @property {(type: EventType) => void} remove remove event
  80. * @property {() => void} closed closed event
  81. */
  82. /**
  83. * @template {EventMap} T
  84. * @extends {EventEmitter<{ [K in keyof T]: Parameters<T[K]> }>}
  85. */
  86. class Watcher extends EventEmitter {
  87. /**
  88. * @param {DirectoryWatcher} directoryWatcher a directory watcher
  89. * @param {string} target a target to watch
  90. * @param {number=} startTime start time
  91. */
  92. constructor(directoryWatcher, target, startTime) {
  93. super();
  94. this.directoryWatcher = directoryWatcher;
  95. this.path = target;
  96. this.startTime = startTime && +startTime;
  97. }
  98. /**
  99. * @param {number} mtime mtime
  100. * @param {boolean} initial true when initial, otherwise false
  101. * @returns {boolean} true of start time less than mtile, otherwise false
  102. */
  103. checkStartTime(mtime, initial) {
  104. const { startTime } = this;
  105. if (typeof startTime !== "number") return !initial;
  106. return startTime <= mtime;
  107. }
  108. close() {
  109. // @ts-expect-error bad typing in EventEmitter
  110. this.emit("closed");
  111. }
  112. }
  113. /** @typedef {Set<string>} InitialScanRemoved */
  114. /**
  115. * @typedef {object} WatchpackEvents
  116. * @property {(target: string, mtime: string, type: EventType, initial: boolean) => void} change change event
  117. * @property {() => void} closed closed event
  118. */
  119. /**
  120. * @typedef {object} DirectoryWatcherOptions
  121. * @property {boolean=} followSymlinks true when need to resolve symlinks and watch symlink and real file, otherwise false
  122. * @property {IgnoredFunction=} ignored ignore some files from watching (glob pattern or regexp)
  123. * @property {number | boolean=} poll true when need to enable polling mode for watching, otherwise false
  124. */
  125. /**
  126. * @extends {EventEmitter<{ [K in keyof WatchpackEvents]: Parameters<WatchpackEvents[K]> }>}
  127. */
  128. class DirectoryWatcher extends EventEmitter {
  129. /**
  130. * @param {WatcherManager} watcherManager a watcher manager
  131. * @param {string} directoryPath directory path
  132. * @param {DirectoryWatcherOptions=} options options
  133. */
  134. constructor(watcherManager, directoryPath, options = {}) {
  135. super();
  136. if (FORCE_POLLING) {
  137. options.poll = FORCE_POLLING;
  138. }
  139. this.watcherManager = watcherManager;
  140. this.options = options;
  141. this.path = directoryPath;
  142. // safeTime is the point in time after which reading is safe to be unchanged
  143. // timestamp is a value that should be compared with another timestamp (mtime)
  144. /** @type {Map<string, Entry>} */
  145. this.files = new Map();
  146. /** @type {Map<string, number>} */
  147. this.filesWithoutCase = new Map();
  148. /** @type {Map<string, Watcher<DirectoryWatcherEvents> | boolean>} */
  149. this.directories = new Map();
  150. this.lastWatchEvent = 0;
  151. this.initialScan = true;
  152. this.ignored = options.ignored || (() => false);
  153. this.nestedWatching = false;
  154. /** @type {number | false} */
  155. this.polledWatching =
  156. typeof options.poll === "number"
  157. ? options.poll
  158. : options.poll
  159. ? 5007
  160. : false;
  161. /** @type {undefined | NodeJS.Timeout} */
  162. this.timeout = undefined;
  163. /** @type {null | InitialScanRemoved} */
  164. this.initialScanRemoved = new Set();
  165. /** @type {undefined | number} */
  166. this.initialScanFinished = undefined;
  167. /** @type {Map<string, Set<Watcher<DirectoryWatcherEvents> | Watcher<FileWatcherEvents>>>} */
  168. this.watchers = new Map();
  169. /** @type {Watcher<FileWatcherEvents> | null} */
  170. this.parentWatcher = null;
  171. this.refs = 0;
  172. /** @type {Map<string, boolean>} */
  173. this._activeEvents = new Map();
  174. this.closed = false;
  175. this.scanning = false;
  176. this.scanAgain = false;
  177. this.scanAgainInitial = false;
  178. this.createWatcher();
  179. this.doScan(true);
  180. }
  181. createWatcher() {
  182. try {
  183. if (this.polledWatching) {
  184. /** @type {EventSourceWatcher} */
  185. (this.watcher) = /** @type {EventSourceWatcher} */ ({
  186. close: () => {
  187. if (this.timeout) {
  188. clearTimeout(this.timeout);
  189. this.timeout = undefined;
  190. }
  191. },
  192. });
  193. } else {
  194. if (IS_OSX) {
  195. this.watchInParentDirectory();
  196. }
  197. this.watcher =
  198. /** @type {EventSourceWatcher} */
  199. (watchEventSource.watch(this.path));
  200. this.watcher.on("change", this.onWatchEvent.bind(this));
  201. this.watcher.on("error", this.onWatcherError.bind(this));
  202. }
  203. } catch (err) {
  204. this.onWatcherError(err);
  205. }
  206. }
  207. /**
  208. * @template {(watcher: Watcher<EventMap>) => void} T
  209. * @param {string} path path
  210. * @param {T} fn function
  211. */
  212. forEachWatcher(path, fn) {
  213. const watchers = this.watchers.get(withoutCase(path));
  214. if (watchers !== undefined) {
  215. for (const w of watchers) {
  216. fn(w);
  217. }
  218. }
  219. }
  220. /**
  221. * @param {string} itemPath an item path
  222. * @param {boolean} initial true when initial, otherwise false
  223. * @param {EventType} type even type
  224. */
  225. setMissing(itemPath, initial, type) {
  226. if (this.initialScan) {
  227. /** @type {InitialScanRemoved} */
  228. (this.initialScanRemoved).add(itemPath);
  229. }
  230. const oldDirectory = this.directories.get(itemPath);
  231. if (oldDirectory) {
  232. if (this.nestedWatching) {
  233. /** @type {Watcher<DirectoryWatcherEvents>} */
  234. (oldDirectory).close();
  235. }
  236. this.directories.delete(itemPath);
  237. this.forEachWatcher(itemPath, (w) => w.emit("remove", type));
  238. if (!initial) {
  239. this.forEachWatcher(this.path, (w) =>
  240. w.emit("change", itemPath, null, type, initial),
  241. );
  242. }
  243. }
  244. const oldFile = this.files.get(itemPath);
  245. if (oldFile) {
  246. this.files.delete(itemPath);
  247. const key = withoutCase(itemPath);
  248. const count = /** @type {number} */ (this.filesWithoutCase.get(key)) - 1;
  249. if (count <= 0) {
  250. this.filesWithoutCase.delete(key);
  251. this.forEachWatcher(itemPath, (w) => w.emit("remove", type));
  252. } else {
  253. this.filesWithoutCase.set(key, count);
  254. }
  255. if (!initial) {
  256. this.forEachWatcher(this.path, (w) =>
  257. w.emit("change", itemPath, null, type, initial),
  258. );
  259. }
  260. }
  261. }
  262. /**
  263. * @param {string} target a target to set file time
  264. * @param {number} mtime mtime
  265. * @param {boolean} initial true when initial, otherwise false
  266. * @param {boolean} ignoreWhenEqual true to ignore when equal, otherwise false
  267. * @param {EventType} type type
  268. */
  269. setFileTime(target, mtime, initial, ignoreWhenEqual, type) {
  270. const now = Date.now();
  271. if (this.ignored(target)) return;
  272. const old = this.files.get(target);
  273. let safeTime;
  274. let accuracy;
  275. if (initial) {
  276. safeTime = Math.min(now, mtime) + FS_ACCURACY;
  277. accuracy = FS_ACCURACY;
  278. } else {
  279. safeTime = now;
  280. accuracy = 0;
  281. if (old && old.timestamp === mtime && mtime + FS_ACCURACY < now) {
  282. // We are sure that mtime is untouched
  283. // This can be caused by some file attribute change
  284. // e. g. when access time has been changed
  285. // but the file content is untouched
  286. return;
  287. }
  288. }
  289. if (ignoreWhenEqual && old && old.timestamp === mtime) return;
  290. this.files.set(target, {
  291. safeTime,
  292. accuracy,
  293. timestamp: mtime,
  294. });
  295. if (!old) {
  296. const key = withoutCase(target);
  297. const count = this.filesWithoutCase.get(key);
  298. this.filesWithoutCase.set(key, (count || 0) + 1);
  299. if (count !== undefined) {
  300. // There is already a file with case-insensitive-equal name
  301. // On a case-insensitive filesystem we may miss the renaming
  302. // when only casing is changed.
  303. // To be sure that our information is correct
  304. // we trigger a rescan here
  305. this.doScan(false);
  306. }
  307. this.forEachWatcher(target, (w) => {
  308. if (!initial || w.checkStartTime(safeTime, initial)) {
  309. w.emit("change", mtime, type);
  310. }
  311. });
  312. } else if (!initial) {
  313. this.forEachWatcher(target, (w) => w.emit("change", mtime, type));
  314. }
  315. this.forEachWatcher(this.path, (w) => {
  316. if (!initial || w.checkStartTime(safeTime, initial)) {
  317. w.emit("change", target, safeTime, type, initial);
  318. }
  319. });
  320. }
  321. /**
  322. * @param {string} directoryPath directory path
  323. * @param {number} birthtime birthtime
  324. * @param {boolean} initial true when initial, otherwise false
  325. * @param {EventType} type even type
  326. */
  327. setDirectory(directoryPath, birthtime, initial, type) {
  328. if (this.ignored(directoryPath)) return;
  329. if (directoryPath === this.path) {
  330. if (!initial) {
  331. this.forEachWatcher(this.path, (w) =>
  332. w.emit("change", directoryPath, birthtime, type, initial),
  333. );
  334. }
  335. } else {
  336. const old = this.directories.get(directoryPath);
  337. if (!old) {
  338. const now = Date.now();
  339. if (this.nestedWatching) {
  340. this.createNestedWatcher(directoryPath);
  341. } else {
  342. this.directories.set(directoryPath, true);
  343. }
  344. const safeTime = initial ? Math.min(now, birthtime) + FS_ACCURACY : now;
  345. this.forEachWatcher(directoryPath, (w) => {
  346. if (!initial || w.checkStartTime(safeTime, false)) {
  347. w.emit("change", birthtime, type);
  348. }
  349. });
  350. this.forEachWatcher(this.path, (w) => {
  351. if (!initial || w.checkStartTime(safeTime, initial)) {
  352. w.emit("change", directoryPath, safeTime, type, initial);
  353. }
  354. });
  355. }
  356. }
  357. }
  358. /**
  359. * @param {string} directoryPath directory path
  360. */
  361. createNestedWatcher(directoryPath) {
  362. const watcher = this.watcherManager.watchDirectory(directoryPath, 1);
  363. watcher.on("change", (target, mtime, type, initial) => {
  364. this.forEachWatcher(this.path, (w) => {
  365. if (!initial || w.checkStartTime(mtime, initial)) {
  366. w.emit("change", target, mtime, type, initial);
  367. }
  368. });
  369. });
  370. this.directories.set(directoryPath, watcher);
  371. }
  372. /**
  373. * @param {boolean} flag true when nested, otherwise false
  374. */
  375. setNestedWatching(flag) {
  376. if (this.nestedWatching !== Boolean(flag)) {
  377. this.nestedWatching = Boolean(flag);
  378. if (this.nestedWatching) {
  379. for (const directory of this.directories.keys()) {
  380. this.createNestedWatcher(directory);
  381. }
  382. } else {
  383. for (const [directory, watcher] of this.directories) {
  384. /** @type {Watcher<DirectoryWatcherEvents>} */
  385. (watcher).close();
  386. this.directories.set(directory, true);
  387. }
  388. }
  389. }
  390. }
  391. /**
  392. * @param {string} target a target to watch
  393. * @param {number=} startTime start time
  394. * @returns {Watcher<DirectoryWatcherEvents> | Watcher<FileWatcherEvents>} watcher
  395. */
  396. watch(target, startTime) {
  397. const key = withoutCase(target);
  398. let watchers = this.watchers.get(key);
  399. if (watchers === undefined) {
  400. watchers = new Set();
  401. this.watchers.set(key, watchers);
  402. }
  403. this.refs++;
  404. const watcher =
  405. /** @type {Watcher<DirectoryWatcherEvents> | Watcher<FileWatcherEvents>} */
  406. (new Watcher(this, target, startTime));
  407. watcher.on("closed", () => {
  408. if (--this.refs <= 0) {
  409. this.close();
  410. return;
  411. }
  412. watchers.delete(watcher);
  413. if (watchers.size === 0) {
  414. this.watchers.delete(key);
  415. if (this.path === target) this.setNestedWatching(false);
  416. }
  417. });
  418. watchers.add(watcher);
  419. let safeTime;
  420. if (target === this.path) {
  421. this.setNestedWatching(true);
  422. safeTime = this.lastWatchEvent;
  423. for (const entry of this.files.values()) {
  424. fixupEntryAccuracy(entry);
  425. safeTime = Math.max(safeTime, entry.safeTime);
  426. }
  427. } else {
  428. const entry = this.files.get(target);
  429. if (entry) {
  430. fixupEntryAccuracy(entry);
  431. safeTime = entry.safeTime;
  432. } else {
  433. safeTime = 0;
  434. }
  435. }
  436. if (safeTime) {
  437. if (startTime && safeTime >= startTime) {
  438. process.nextTick(() => {
  439. if (this.closed) return;
  440. if (target === this.path) {
  441. /** @type {Watcher<DirectoryWatcherEvents>} */
  442. (watcher).emit(
  443. "change",
  444. target,
  445. safeTime,
  446. "watch (outdated on attach)",
  447. true,
  448. );
  449. } else {
  450. /** @type {Watcher<FileWatcherEvents>} */
  451. (watcher).emit(
  452. "change",
  453. safeTime,
  454. "watch (outdated on attach)",
  455. true,
  456. );
  457. }
  458. });
  459. }
  460. } else if (this.initialScan) {
  461. if (
  462. /** @type {InitialScanRemoved} */
  463. (this.initialScanRemoved).has(target)
  464. ) {
  465. process.nextTick(() => {
  466. if (this.closed) return;
  467. watcher.emit("remove");
  468. });
  469. }
  470. } else if (
  471. target !== this.path &&
  472. !this.directories.has(target) &&
  473. watcher.checkStartTime(
  474. /** @type {number} */
  475. (this.initialScanFinished),
  476. false,
  477. )
  478. ) {
  479. process.nextTick(() => {
  480. if (this.closed) return;
  481. watcher.emit("initial-missing", "watch (missing on attach)");
  482. });
  483. }
  484. return watcher;
  485. }
  486. /**
  487. * @param {EventType} eventType event type
  488. * @param {string=} filename filename
  489. */
  490. onWatchEvent(eventType, filename) {
  491. if (this.closed) return;
  492. if (!filename) {
  493. // In some cases no filename is provided
  494. // This seem to happen on windows
  495. // So some event happened but we don't know which file is affected
  496. // We have to do a full scan of the directory
  497. this.doScan(false);
  498. return;
  499. }
  500. const target = path.join(this.path, filename);
  501. if (this.ignored(target)) return;
  502. if (this._activeEvents.get(filename) === undefined) {
  503. this._activeEvents.set(filename, false);
  504. const checkStats = () => {
  505. if (this.closed) return;
  506. this._activeEvents.set(filename, false);
  507. fs.lstat(target, (err, stats) => {
  508. if (this.closed) return;
  509. if (this._activeEvents.get(filename) === true) {
  510. process.nextTick(checkStats);
  511. return;
  512. }
  513. this._activeEvents.delete(filename);
  514. // ENOENT happens when the file/directory doesn't exist
  515. // EPERM happens when the containing directory doesn't exist
  516. if (err) {
  517. if (
  518. err.code !== "ENOENT" &&
  519. err.code !== "EPERM" &&
  520. err.code !== "EBUSY"
  521. ) {
  522. this.onStatsError(err);
  523. } else if (
  524. filename === path.basename(this.path) && // This may indicate that the directory itself was removed
  525. !fs.existsSync(this.path)
  526. ) {
  527. this.onDirectoryRemoved("stat failed");
  528. }
  529. }
  530. this.lastWatchEvent = Date.now();
  531. if (!stats) {
  532. this.setMissing(target, false, eventType);
  533. } else if (stats.isDirectory()) {
  534. this.setDirectory(target, +stats.birthtime || 1, false, eventType);
  535. } else if (stats.isFile() || stats.isSymbolicLink()) {
  536. if (stats.mtime) {
  537. ensureFsAccuracy(+stats.mtime);
  538. }
  539. this.setFileTime(
  540. target,
  541. +stats.mtime || +stats.ctime || 1,
  542. false,
  543. false,
  544. eventType,
  545. );
  546. }
  547. });
  548. };
  549. process.nextTick(checkStats);
  550. } else {
  551. this._activeEvents.set(filename, true);
  552. }
  553. }
  554. /**
  555. * @param {unknown=} err error
  556. */
  557. onWatcherError(err) {
  558. if (this.closed) return;
  559. if (err) {
  560. if (
  561. /** @type {NodeJS.ErrnoException} */
  562. (err).code !== "EPERM" &&
  563. /** @type {NodeJS.ErrnoException} */
  564. (err).code !== "ENOENT"
  565. ) {
  566. // eslint-disable-next-line no-console
  567. console.error(`Watchpack Error (watcher): ${err}`);
  568. }
  569. this.onDirectoryRemoved("watch error");
  570. }
  571. }
  572. /**
  573. * @param {Error | NodeJS.ErrnoException=} err error
  574. */
  575. onStatsError(err) {
  576. if (err) {
  577. // eslint-disable-next-line no-console
  578. console.error(`Watchpack Error (stats): ${err}`);
  579. }
  580. }
  581. /**
  582. * @param {Error | NodeJS.ErrnoException=} err error
  583. */
  584. onScanError(err) {
  585. if (err) {
  586. // eslint-disable-next-line no-console
  587. console.error(`Watchpack Error (initial scan): ${err}`);
  588. }
  589. this.onScanFinished();
  590. }
  591. onScanFinished() {
  592. if (this.polledWatching) {
  593. this.timeout = setTimeout(() => {
  594. if (this.closed) return;
  595. this.doScan(false);
  596. }, this.polledWatching);
  597. }
  598. }
  599. /**
  600. * @param {string} reason a reason
  601. */
  602. onDirectoryRemoved(reason) {
  603. if (this.watcher) {
  604. this.watcher.close();
  605. this.watcher = null;
  606. }
  607. this.watchInParentDirectory();
  608. const type = /** @type {EventType} */ (`directory-removed (${reason})`);
  609. for (const directory of this.directories.keys()) {
  610. this.setMissing(directory, false, type);
  611. }
  612. for (const file of this.files.keys()) {
  613. this.setMissing(file, false, type);
  614. }
  615. }
  616. watchInParentDirectory() {
  617. if (!this.parentWatcher) {
  618. const parentDir = path.dirname(this.path);
  619. // avoid watching in the root directory
  620. // removing directories in the root directory is not supported
  621. if (path.dirname(parentDir) === parentDir) return;
  622. this.parentWatcher = this.watcherManager.watchFile(this.path, 1);
  623. /** @type {Watcher<FileWatcherEvents>} */
  624. (this.parentWatcher).on("change", (mtime, type) => {
  625. if (this.closed) return;
  626. // On non-osx platforms we don't need this watcher to detect
  627. // directory removal, as an EPERM error indicates that
  628. if ((!IS_OSX || this.polledWatching) && this.parentWatcher) {
  629. this.parentWatcher.close();
  630. this.parentWatcher = null;
  631. }
  632. // Try to create the watcher when parent directory is found
  633. if (!this.watcher) {
  634. this.createWatcher();
  635. this.doScan(false);
  636. // directory was created so we emit an event
  637. this.forEachWatcher(this.path, (w) =>
  638. w.emit("change", this.path, mtime, type, false),
  639. );
  640. }
  641. });
  642. /** @type {Watcher<FileWatcherEvents>} */
  643. (this.parentWatcher).on("remove", () => {
  644. this.onDirectoryRemoved("parent directory removed");
  645. });
  646. }
  647. }
  648. /**
  649. * @param {boolean} initial true when initial, otherwise false
  650. */
  651. doScan(initial) {
  652. if (this.scanning) {
  653. if (this.scanAgain) {
  654. if (!initial) this.scanAgainInitial = false;
  655. } else {
  656. this.scanAgain = true;
  657. this.scanAgainInitial = initial;
  658. }
  659. return;
  660. }
  661. this.scanning = true;
  662. if (this.timeout) {
  663. clearTimeout(this.timeout);
  664. this.timeout = undefined;
  665. }
  666. process.nextTick(() => {
  667. if (this.closed) return;
  668. fs.readdir(this.path, (err, items) => {
  669. if (this.closed) return;
  670. if (err) {
  671. if (err.code === "ENOENT" || err.code === "EPERM") {
  672. this.onDirectoryRemoved("scan readdir failed");
  673. } else {
  674. this.onScanError(err);
  675. }
  676. this.initialScan = false;
  677. this.initialScanFinished = Date.now();
  678. if (initial) {
  679. for (const watchers of this.watchers.values()) {
  680. for (const watcher of watchers) {
  681. if (watcher.checkStartTime(this.initialScanFinished, false)) {
  682. watcher.emit(
  683. "initial-missing",
  684. "scan (parent directory missing in initial scan)",
  685. );
  686. }
  687. }
  688. }
  689. }
  690. if (this.scanAgain) {
  691. this.scanAgain = false;
  692. this.doScan(this.scanAgainInitial);
  693. } else {
  694. this.scanning = false;
  695. }
  696. return;
  697. }
  698. const itemPaths = new Set(
  699. items.map((item) => path.join(this.path, item.normalize("NFC"))),
  700. );
  701. for (const file of this.files.keys()) {
  702. if (!itemPaths.has(file)) {
  703. this.setMissing(file, initial, "scan (missing)");
  704. }
  705. }
  706. for (const directory of this.directories.keys()) {
  707. if (!itemPaths.has(directory)) {
  708. this.setMissing(directory, initial, "scan (missing)");
  709. }
  710. }
  711. if (this.scanAgain) {
  712. // Early repeat of scan
  713. this.scanAgain = false;
  714. this.doScan(initial);
  715. return;
  716. }
  717. const itemFinished = needCalls(itemPaths.size + 1, () => {
  718. if (this.closed) return;
  719. this.initialScan = false;
  720. this.initialScanRemoved = null;
  721. this.initialScanFinished = Date.now();
  722. if (initial) {
  723. const missingWatchers = new Map(this.watchers);
  724. missingWatchers.delete(withoutCase(this.path));
  725. for (const item of itemPaths) {
  726. missingWatchers.delete(withoutCase(item));
  727. }
  728. for (const watchers of missingWatchers.values()) {
  729. for (const watcher of watchers) {
  730. if (watcher.checkStartTime(this.initialScanFinished, false)) {
  731. watcher.emit(
  732. "initial-missing",
  733. "scan (missing in initial scan)",
  734. );
  735. }
  736. }
  737. }
  738. }
  739. if (this.scanAgain) {
  740. this.scanAgain = false;
  741. this.doScan(this.scanAgainInitial);
  742. } else {
  743. this.scanning = false;
  744. this.onScanFinished();
  745. }
  746. });
  747. for (const itemPath of itemPaths) {
  748. fs.lstat(itemPath, (err2, stats) => {
  749. if (this.closed) return;
  750. if (err2) {
  751. if (
  752. err2.code === "ENOENT" ||
  753. err2.code === "EPERM" ||
  754. err2.code === "EACCES" ||
  755. err2.code === "EBUSY" ||
  756. // TODO https://github.com/libuv/libuv/pull/4566
  757. (err2.code === "EINVAL" && IS_WIN)
  758. ) {
  759. this.setMissing(itemPath, initial, `scan (${err2.code})`);
  760. } else {
  761. this.onScanError(err2);
  762. }
  763. itemFinished();
  764. return;
  765. }
  766. if (stats.isFile() || stats.isSymbolicLink()) {
  767. if (stats.mtime) {
  768. ensureFsAccuracy(+stats.mtime);
  769. }
  770. this.setFileTime(
  771. itemPath,
  772. +stats.mtime || +stats.ctime || 1,
  773. initial,
  774. true,
  775. "scan (file)",
  776. );
  777. } else if (
  778. stats.isDirectory() &&
  779. (!initial || !this.directories.has(itemPath))
  780. ) {
  781. this.setDirectory(
  782. itemPath,
  783. +stats.birthtime || 1,
  784. initial,
  785. "scan (dir)",
  786. );
  787. }
  788. itemFinished();
  789. });
  790. }
  791. itemFinished();
  792. });
  793. });
  794. }
  795. /**
  796. * @returns {Record<string, number>} times
  797. */
  798. getTimes() {
  799. const obj = Object.create(null);
  800. let safeTime = this.lastWatchEvent;
  801. for (const [file, entry] of this.files) {
  802. fixupEntryAccuracy(entry);
  803. safeTime = Math.max(safeTime, entry.safeTime);
  804. obj[file] = Math.max(entry.safeTime, entry.timestamp);
  805. }
  806. if (this.nestedWatching) {
  807. for (const w of this.directories.values()) {
  808. const times =
  809. /** @type {Watcher<DirectoryWatcherEvents>} */
  810. (w).directoryWatcher.getTimes();
  811. for (const file of Object.keys(times)) {
  812. const time = times[file];
  813. safeTime = Math.max(safeTime, time);
  814. obj[file] = time;
  815. }
  816. }
  817. obj[this.path] = safeTime;
  818. }
  819. if (!this.initialScan) {
  820. for (const watchers of this.watchers.values()) {
  821. for (const watcher of watchers) {
  822. const { path } = watcher;
  823. if (!Object.prototype.hasOwnProperty.call(obj, path)) {
  824. obj[path] = null;
  825. }
  826. }
  827. }
  828. }
  829. return obj;
  830. }
  831. /**
  832. * @param {TimeInfoEntries} fileTimestamps file timestamps
  833. * @param {TimeInfoEntries} directoryTimestamps directory timestamps
  834. * @returns {number} safe time
  835. */
  836. collectTimeInfoEntries(fileTimestamps, directoryTimestamps) {
  837. let safeTime = this.lastWatchEvent;
  838. for (const [file, entry] of this.files) {
  839. fixupEntryAccuracy(entry);
  840. safeTime = Math.max(safeTime, entry.safeTime);
  841. fileTimestamps.set(file, entry);
  842. }
  843. if (this.nestedWatching) {
  844. for (const w of this.directories.values()) {
  845. safeTime = Math.max(
  846. safeTime,
  847. /** @type {Watcher<DirectoryWatcherEvents>} */
  848. (w).directoryWatcher.collectTimeInfoEntries(
  849. fileTimestamps,
  850. directoryTimestamps,
  851. ),
  852. );
  853. }
  854. fileTimestamps.set(this.path, EXISTANCE_ONLY_TIME_ENTRY);
  855. directoryTimestamps.set(this.path, {
  856. safeTime,
  857. });
  858. } else {
  859. for (const dir of this.directories.keys()) {
  860. // No additional info about this directory
  861. // but maybe another DirectoryWatcher has info
  862. fileTimestamps.set(dir, EXISTANCE_ONLY_TIME_ENTRY);
  863. if (!directoryTimestamps.has(dir)) {
  864. directoryTimestamps.set(dir, EXISTANCE_ONLY_TIME_ENTRY);
  865. }
  866. }
  867. fileTimestamps.set(this.path, EXISTANCE_ONLY_TIME_ENTRY);
  868. directoryTimestamps.set(this.path, EXISTANCE_ONLY_TIME_ENTRY);
  869. }
  870. if (!this.initialScan) {
  871. for (const watchers of this.watchers.values()) {
  872. for (const watcher of watchers) {
  873. const { path } = watcher;
  874. if (!fileTimestamps.has(path)) {
  875. fileTimestamps.set(path, null);
  876. }
  877. }
  878. }
  879. }
  880. return safeTime;
  881. }
  882. close() {
  883. this.closed = true;
  884. this.initialScan = false;
  885. if (this.watcher) {
  886. this.watcher.close();
  887. this.watcher = null;
  888. }
  889. if (this.nestedWatching) {
  890. for (const w of this.directories.values()) {
  891. /** @type {Watcher<DirectoryWatcherEvents>} */
  892. (w).close();
  893. }
  894. this.directories.clear();
  895. }
  896. if (this.parentWatcher) {
  897. this.parentWatcher.close();
  898. this.parentWatcher = null;
  899. }
  900. this.emit("closed");
  901. }
  902. }
  903. module.exports = DirectoryWatcher;
  904. module.exports.EXISTANCE_ONLY_TIME_ENTRY = EXISTANCE_ONLY_TIME_ENTRY;
  905. module.exports.Watcher = Watcher;