source: frontend/node_modules/watchpack/lib/DirectoryWatcher.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 11 days ago

Fix frontend appearance

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