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

main
Last change on this file was 79a0317, checked in by stefan toskovski <stefantoska84@…>, 4 days ago

F4 Finalna Verzija

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