source: trip-planner-front/node_modules/watchpack/lib/DirectoryWatcher.js@ ceaed42

Last change on this file since ceaed42 was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

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