source: trip-planner-front/node_modules/chokidar/lib/nodefs-handler.js@ 6a3a178

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

initial commit

  • Property mode set to 100644
File size: 19.5 KB
Line 
1'use strict';
2
3const fs = require('fs');
4const sysPath = require('path');
5const { promisify } = require('util');
6const isBinaryPath = require('is-binary-path');
7const {
8 isWindows,
9 isLinux,
10 EMPTY_FN,
11 EMPTY_STR,
12 KEY_LISTENERS,
13 KEY_ERR,
14 KEY_RAW,
15 HANDLER_KEYS,
16 EV_CHANGE,
17 EV_ADD,
18 EV_ADD_DIR,
19 EV_ERROR,
20 STR_DATA,
21 STR_END,
22 BRACE_START,
23 STAR
24} = require('./constants');
25
26const THROTTLE_MODE_WATCH = 'watch';
27
28const open = promisify(fs.open);
29const stat = promisify(fs.stat);
30const lstat = promisify(fs.lstat);
31const close = promisify(fs.close);
32const fsrealpath = promisify(fs.realpath);
33
34const statMethods = { lstat, stat };
35
36// TODO: emit errors properly. Example: EMFILE on Macos.
37const foreach = (val, fn) => {
38 if (val instanceof Set) {
39 val.forEach(fn);
40 } else {
41 fn(val);
42 }
43};
44
45const addAndConvert = (main, prop, item) => {
46 let container = main[prop];
47 if (!(container instanceof Set)) {
48 main[prop] = container = new Set([container]);
49 }
50 container.add(item);
51};
52
53const clearItem = cont => key => {
54 const set = cont[key];
55 if (set instanceof Set) {
56 set.clear();
57 } else {
58 delete cont[key];
59 }
60};
61
62const delFromSet = (main, prop, item) => {
63 const container = main[prop];
64 if (container instanceof Set) {
65 container.delete(item);
66 } else if (container === item) {
67 delete main[prop];
68 }
69};
70
71const isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
72
73/**
74 * @typedef {String} Path
75 */
76
77// fs_watch helpers
78
79// object to hold per-process fs_watch instances
80// (may be shared across chokidar FSWatcher instances)
81
82/**
83 * @typedef {Object} FsWatchContainer
84 * @property {Set} listeners
85 * @property {Set} errHandlers
86 * @property {Set} rawEmitters
87 * @property {fs.FSWatcher=} watcher
88 * @property {Boolean=} watcherUnusable
89 */
90
91/**
92 * @type {Map<String,FsWatchContainer>}
93 */
94const FsWatchInstances = new Map();
95
96/**
97 * Instantiates the fs_watch interface
98 * @param {String} path to be watched
99 * @param {Object} options to be passed to fs_watch
100 * @param {Function} listener main event handler
101 * @param {Function} errHandler emits info about errors
102 * @param {Function} emitRaw emits raw event data
103 * @returns {fs.FSWatcher} new fsevents instance
104 */
105function createFsWatchInstance(path, options, listener, errHandler, emitRaw) {
106 const handleEvent = (rawEvent, evPath) => {
107 listener(path);
108 emitRaw(rawEvent, evPath, {watchedPath: path});
109
110 // emit based on events occurring for files from a directory's watcher in
111 // case the file's watcher misses it (and rely on throttling to de-dupe)
112 if (evPath && path !== evPath) {
113 fsWatchBroadcast(
114 sysPath.resolve(path, evPath), KEY_LISTENERS, sysPath.join(path, evPath)
115 );
116 }
117 };
118 try {
119 return fs.watch(path, options, handleEvent);
120 } catch (error) {
121 errHandler(error);
122 }
123}
124
125/**
126 * Helper for passing fs_watch event data to a collection of listeners
127 * @param {Path} fullPath absolute path bound to fs_watch instance
128 * @param {String} type listener type
129 * @param {*=} val1 arguments to be passed to listeners
130 * @param {*=} val2
131 * @param {*=} val3
132 */
133const fsWatchBroadcast = (fullPath, type, val1, val2, val3) => {
134 const cont = FsWatchInstances.get(fullPath);
135 if (!cont) return;
136 foreach(cont[type], (listener) => {
137 listener(val1, val2, val3);
138 });
139};
140
141/**
142 * Instantiates the fs_watch interface or binds listeners
143 * to an existing one covering the same file system entry
144 * @param {String} path
145 * @param {String} fullPath absolute path
146 * @param {Object} options to be passed to fs_watch
147 * @param {Object} handlers container for event listener functions
148 */
149const setFsWatchListener = (path, fullPath, options, handlers) => {
150 const {listener, errHandler, rawEmitter} = handlers;
151 let cont = FsWatchInstances.get(fullPath);
152
153 /** @type {fs.FSWatcher=} */
154 let watcher;
155 if (!options.persistent) {
156 watcher = createFsWatchInstance(
157 path, options, listener, errHandler, rawEmitter
158 );
159 return watcher.close.bind(watcher);
160 }
161 if (cont) {
162 addAndConvert(cont, KEY_LISTENERS, listener);
163 addAndConvert(cont, KEY_ERR, errHandler);
164 addAndConvert(cont, KEY_RAW, rawEmitter);
165 } else {
166 watcher = createFsWatchInstance(
167 path,
168 options,
169 fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
170 errHandler, // no need to use broadcast here
171 fsWatchBroadcast.bind(null, fullPath, KEY_RAW)
172 );
173 if (!watcher) return;
174 watcher.on(EV_ERROR, async (error) => {
175 const broadcastErr = fsWatchBroadcast.bind(null, fullPath, KEY_ERR);
176 cont.watcherUnusable = true; // documented since Node 10.4.1
177 // Workaround for https://github.com/joyent/node/issues/4337
178 if (isWindows && error.code === 'EPERM') {
179 try {
180 const fd = await open(path, 'r');
181 await close(fd);
182 broadcastErr(error);
183 } catch (err) {}
184 } else {
185 broadcastErr(error);
186 }
187 });
188 cont = {
189 listeners: listener,
190 errHandlers: errHandler,
191 rawEmitters: rawEmitter,
192 watcher
193 };
194 FsWatchInstances.set(fullPath, cont);
195 }
196 // const index = cont.listeners.indexOf(listener);
197
198 // removes this instance's listeners and closes the underlying fs_watch
199 // instance if there are no more listeners left
200 return () => {
201 delFromSet(cont, KEY_LISTENERS, listener);
202 delFromSet(cont, KEY_ERR, errHandler);
203 delFromSet(cont, KEY_RAW, rawEmitter);
204 if (isEmptySet(cont.listeners)) {
205 // Check to protect against issue gh-730.
206 // if (cont.watcherUnusable) {
207 cont.watcher.close();
208 // }
209 FsWatchInstances.delete(fullPath);
210 HANDLER_KEYS.forEach(clearItem(cont));
211 cont.watcher = undefined;
212 Object.freeze(cont);
213 }
214 };
215};
216
217// fs_watchFile helpers
218
219// object to hold per-process fs_watchFile instances
220// (may be shared across chokidar FSWatcher instances)
221const FsWatchFileInstances = new Map();
222
223/**
224 * Instantiates the fs_watchFile interface or binds listeners
225 * to an existing one covering the same file system entry
226 * @param {String} path to be watched
227 * @param {String} fullPath absolute path
228 * @param {Object} options options to be passed to fs_watchFile
229 * @param {Object} handlers container for event listener functions
230 * @returns {Function} closer
231 */
232const setFsWatchFileListener = (path, fullPath, options, handlers) => {
233 const {listener, rawEmitter} = handlers;
234 let cont = FsWatchFileInstances.get(fullPath);
235
236 /* eslint-disable no-unused-vars, prefer-destructuring */
237 let listeners = new Set();
238 let rawEmitters = new Set();
239
240 const copts = cont && cont.options;
241 if (copts && (copts.persistent < options.persistent || copts.interval > options.interval)) {
242 // "Upgrade" the watcher to persistence or a quicker interval.
243 // This creates some unlikely edge case issues if the user mixes
244 // settings in a very weird way, but solving for those cases
245 // doesn't seem worthwhile for the added complexity.
246 listeners = cont.listeners;
247 rawEmitters = cont.rawEmitters;
248 fs.unwatchFile(fullPath);
249 cont = undefined;
250 }
251
252 /* eslint-enable no-unused-vars, prefer-destructuring */
253
254 if (cont) {
255 addAndConvert(cont, KEY_LISTENERS, listener);
256 addAndConvert(cont, KEY_RAW, rawEmitter);
257 } else {
258 // TODO
259 // listeners.add(listener);
260 // rawEmitters.add(rawEmitter);
261 cont = {
262 listeners: listener,
263 rawEmitters: rawEmitter,
264 options,
265 watcher: fs.watchFile(fullPath, options, (curr, prev) => {
266 foreach(cont.rawEmitters, (rawEmitter) => {
267 rawEmitter(EV_CHANGE, fullPath, {curr, prev});
268 });
269 const currmtime = curr.mtimeMs;
270 if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
271 foreach(cont.listeners, (listener) => listener(path, curr));
272 }
273 })
274 };
275 FsWatchFileInstances.set(fullPath, cont);
276 }
277 // const index = cont.listeners.indexOf(listener);
278
279 // Removes this instance's listeners and closes the underlying fs_watchFile
280 // instance if there are no more listeners left.
281 return () => {
282 delFromSet(cont, KEY_LISTENERS, listener);
283 delFromSet(cont, KEY_RAW, rawEmitter);
284 if (isEmptySet(cont.listeners)) {
285 FsWatchFileInstances.delete(fullPath);
286 fs.unwatchFile(fullPath);
287 cont.options = cont.watcher = undefined;
288 Object.freeze(cont);
289 }
290 };
291};
292
293/**
294 * @mixin
295 */
296class NodeFsHandler {
297
298/**
299 * @param {import("../index").FSWatcher} fsW
300 */
301constructor(fsW) {
302 this.fsw = fsW;
303 this._boundHandleError = (error) => fsW._handleError(error);
304}
305
306/**
307 * Watch file for changes with fs_watchFile or fs_watch.
308 * @param {String} path to file or dir
309 * @param {Function} listener on fs change
310 * @returns {Function} closer for the watcher instance
311 */
312_watchWithNodeFs(path, listener) {
313 const opts = this.fsw.options;
314 const directory = sysPath.dirname(path);
315 const basename = sysPath.basename(path);
316 const parent = this.fsw._getWatchedDir(directory);
317 parent.add(basename);
318 const absolutePath = sysPath.resolve(path);
319 const options = {persistent: opts.persistent};
320 if (!listener) listener = EMPTY_FN;
321
322 let closer;
323 if (opts.usePolling) {
324 options.interval = opts.enableBinaryInterval && isBinaryPath(basename) ?
325 opts.binaryInterval : opts.interval;
326 closer = setFsWatchFileListener(path, absolutePath, options, {
327 listener,
328 rawEmitter: this.fsw._emitRaw
329 });
330 } else {
331 closer = setFsWatchListener(path, absolutePath, options, {
332 listener,
333 errHandler: this._boundHandleError,
334 rawEmitter: this.fsw._emitRaw
335 });
336 }
337 return closer;
338}
339
340/**
341 * Watch a file and emit add event if warranted.
342 * @param {Path} file Path
343 * @param {fs.Stats} stats result of fs_stat
344 * @param {Boolean} initialAdd was the file added at watch instantiation?
345 * @returns {Function} closer for the watcher instance
346 */
347_handleFile(file, stats, initialAdd) {
348 if (this.fsw.closed) {
349 return;
350 }
351 const dirname = sysPath.dirname(file);
352 const basename = sysPath.basename(file);
353 const parent = this.fsw._getWatchedDir(dirname);
354 // stats is always present
355 let prevStats = stats;
356
357 // if the file is already being watched, do nothing
358 if (parent.has(basename)) return;
359
360 const listener = async (path, newStats) => {
361 if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5)) return;
362 if (!newStats || newStats.mtimeMs === 0) {
363 try {
364 const newStats = await stat(file);
365 if (this.fsw.closed) return;
366 // Check that change event was not fired because of changed only accessTime.
367 const at = newStats.atimeMs;
368 const mt = newStats.mtimeMs;
369 if (!at || at <= mt || mt !== prevStats.mtimeMs) {
370 this.fsw._emit(EV_CHANGE, file, newStats);
371 }
372 if (isLinux && prevStats.ino !== newStats.ino) {
373 this.fsw._closeFile(path)
374 prevStats = newStats;
375 this.fsw._addPathCloser(path, this._watchWithNodeFs(file, listener));
376 } else {
377 prevStats = newStats;
378 }
379 } catch (error) {
380 // Fix issues where mtime is null but file is still present
381 this.fsw._remove(dirname, basename);
382 }
383 // add is about to be emitted if file not already tracked in parent
384 } else if (parent.has(basename)) {
385 // Check that change event was not fired because of changed only accessTime.
386 const at = newStats.atimeMs;
387 const mt = newStats.mtimeMs;
388 if (!at || at <= mt || mt !== prevStats.mtimeMs) {
389 this.fsw._emit(EV_CHANGE, file, newStats);
390 }
391 prevStats = newStats;
392 }
393 }
394 // kick off the watcher
395 const closer = this._watchWithNodeFs(file, listener);
396
397 // emit an add event if we're supposed to
398 if (!(initialAdd && this.fsw.options.ignoreInitial) && this.fsw._isntIgnored(file)) {
399 if (!this.fsw._throttle(EV_ADD, file, 0)) return;
400 this.fsw._emit(EV_ADD, file, stats);
401 }
402
403 return closer;
404}
405
406/**
407 * Handle symlinks encountered while reading a dir.
408 * @param {Object} entry returned by readdirp
409 * @param {String} directory path of dir being read
410 * @param {String} path of this item
411 * @param {String} item basename of this item
412 * @returns {Promise<Boolean>} true if no more processing is needed for this entry.
413 */
414async _handleSymlink(entry, directory, path, item) {
415 if (this.fsw.closed) {
416 return;
417 }
418 const full = entry.fullPath;
419 const dir = this.fsw._getWatchedDir(directory);
420
421 if (!this.fsw.options.followSymlinks) {
422 // watch symlink directly (don't follow) and detect changes
423 this.fsw._incrReadyCount();
424 const linkPath = await fsrealpath(path);
425 if (this.fsw.closed) return;
426 if (dir.has(item)) {
427 if (this.fsw._symlinkPaths.get(full) !== linkPath) {
428 this.fsw._symlinkPaths.set(full, linkPath);
429 this.fsw._emit(EV_CHANGE, path, entry.stats);
430 }
431 } else {
432 dir.add(item);
433 this.fsw._symlinkPaths.set(full, linkPath);
434 this.fsw._emit(EV_ADD, path, entry.stats);
435 }
436 this.fsw._emitReady();
437 return true;
438 }
439
440 // don't follow the same symlink more than once
441 if (this.fsw._symlinkPaths.has(full)) {
442 return true;
443 }
444
445 this.fsw._symlinkPaths.set(full, true);
446}
447
448_handleRead(directory, initialAdd, wh, target, dir, depth, throttler) {
449 // Normalize the directory name on Windows
450 directory = sysPath.join(directory, EMPTY_STR);
451
452 if (!wh.hasGlob) {
453 throttler = this.fsw._throttle('readdir', directory, 1000);
454 if (!throttler) return;
455 }
456
457 const previous = this.fsw._getWatchedDir(wh.path);
458 const current = new Set();
459
460 let stream = this.fsw._readdirp(directory, {
461 fileFilter: entry => wh.filterPath(entry),
462 directoryFilter: entry => wh.filterDir(entry),
463 depth: 0
464 }).on(STR_DATA, async (entry) => {
465 if (this.fsw.closed) {
466 stream = undefined;
467 return;
468 }
469 const item = entry.path;
470 let path = sysPath.join(directory, item);
471 current.add(item);
472
473 if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path, item)) {
474 return;
475 }
476
477 if (this.fsw.closed) {
478 stream = undefined;
479 return;
480 }
481 // Files that present in current directory snapshot
482 // but absent in previous are added to watch list and
483 // emit `add` event.
484 if (item === target || !target && !previous.has(item)) {
485 this.fsw._incrReadyCount();
486
487 // ensure relativeness of path is preserved in case of watcher reuse
488 path = sysPath.join(dir, sysPath.relative(dir, path));
489
490 this._addToNodeFs(path, initialAdd, wh, depth + 1);
491 }
492 }).on(EV_ERROR, this._boundHandleError);
493
494 return new Promise(resolve =>
495 stream.once(STR_END, () => {
496 if (this.fsw.closed) {
497 stream = undefined;
498 return;
499 }
500 const wasThrottled = throttler ? throttler.clear() : false;
501
502 resolve();
503
504 // Files that absent in current directory snapshot
505 // but present in previous emit `remove` event
506 // and are removed from @watched[directory].
507 previous.getChildren().filter((item) => {
508 return item !== directory &&
509 !current.has(item) &&
510 // in case of intersecting globs;
511 // a path may have been filtered out of this readdir, but
512 // shouldn't be removed because it matches a different glob
513 (!wh.hasGlob || wh.filterPath({
514 fullPath: sysPath.resolve(directory, item)
515 }));
516 }).forEach((item) => {
517 this.fsw._remove(directory, item);
518 });
519
520 stream = undefined;
521
522 // one more time for any missed in case changes came in extremely quickly
523 if (wasThrottled) this._handleRead(directory, false, wh, target, dir, depth, throttler);
524 })
525 );
526}
527
528/**
529 * Read directory to add / remove files from `@watched` list and re-read it on change.
530 * @param {String} dir fs path
531 * @param {fs.Stats} stats
532 * @param {Boolean} initialAdd
533 * @param {Number} depth relative to user-supplied path
534 * @param {String} target child path targeted for watch
535 * @param {Object} wh Common watch helpers for this path
536 * @param {String} realpath
537 * @returns {Promise<Function>} closer for the watcher instance.
538 */
539async _handleDir(dir, stats, initialAdd, depth, target, wh, realpath) {
540 const parentDir = this.fsw._getWatchedDir(sysPath.dirname(dir));
541 const tracked = parentDir.has(sysPath.basename(dir));
542 if (!(initialAdd && this.fsw.options.ignoreInitial) && !target && !tracked) {
543 if (!wh.hasGlob || wh.globFilter(dir)) this.fsw._emit(EV_ADD_DIR, dir, stats);
544 }
545
546 // ensure dir is tracked (harmless if redundant)
547 parentDir.add(sysPath.basename(dir));
548 this.fsw._getWatchedDir(dir);
549 let throttler;
550 let closer;
551
552 const oDepth = this.fsw.options.depth;
553 if ((oDepth == null || depth <= oDepth) && !this.fsw._symlinkPaths.has(realpath)) {
554 if (!target) {
555 await this._handleRead(dir, initialAdd, wh, target, dir, depth, throttler);
556 if (this.fsw.closed) return;
557 }
558
559 closer = this._watchWithNodeFs(dir, (dirPath, stats) => {
560 // if current directory is removed, do nothing
561 if (stats && stats.mtimeMs === 0) return;
562
563 this._handleRead(dirPath, false, wh, target, dir, depth, throttler);
564 });
565 }
566 return closer;
567}
568
569/**
570 * Handle added file, directory, or glob pattern.
571 * Delegates call to _handleFile / _handleDir after checks.
572 * @param {String} path to file or ir
573 * @param {Boolean} initialAdd was the file added at watch instantiation?
574 * @param {Object} priorWh depth relative to user-supplied path
575 * @param {Number} depth Child path actually targeted for watch
576 * @param {String=} target Child path actually targeted for watch
577 * @returns {Promise}
578 */
579async _addToNodeFs(path, initialAdd, priorWh, depth, target) {
580 const ready = this.fsw._emitReady;
581 if (this.fsw._isIgnored(path) || this.fsw.closed) {
582 ready();
583 return false;
584 }
585
586 const wh = this.fsw._getWatchHelpers(path, depth);
587 if (!wh.hasGlob && priorWh) {
588 wh.hasGlob = priorWh.hasGlob;
589 wh.globFilter = priorWh.globFilter;
590 wh.filterPath = entry => priorWh.filterPath(entry);
591 wh.filterDir = entry => priorWh.filterDir(entry);
592 }
593
594 // evaluate what is at the path we're being asked to watch
595 try {
596 const stats = await statMethods[wh.statMethod](wh.watchPath);
597 if (this.fsw.closed) return;
598 if (this.fsw._isIgnored(wh.watchPath, stats)) {
599 ready();
600 return false;
601 }
602
603 const follow = this.fsw.options.followSymlinks && !path.includes(STAR) && !path.includes(BRACE_START);
604 let closer;
605 if (stats.isDirectory()) {
606 const absPath = sysPath.resolve(path);
607 const targetPath = follow ? await fsrealpath(path) : path;
608 if (this.fsw.closed) return;
609 closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
610 if (this.fsw.closed) return;
611 // preserve this symlink's target path
612 if (absPath !== targetPath && targetPath !== undefined) {
613 this.fsw._symlinkPaths.set(absPath, targetPath);
614 }
615 } else if (stats.isSymbolicLink()) {
616 const targetPath = follow ? await fsrealpath(path) : path;
617 if (this.fsw.closed) return;
618 const parent = sysPath.dirname(wh.watchPath);
619 this.fsw._getWatchedDir(parent).add(wh.watchPath);
620 this.fsw._emit(EV_ADD, wh.watchPath, stats);
621 closer = await this._handleDir(parent, stats, initialAdd, depth, path, wh, targetPath);
622 if (this.fsw.closed) return;
623
624 // preserve this symlink's target path
625 if (targetPath !== undefined) {
626 this.fsw._symlinkPaths.set(sysPath.resolve(path), targetPath);
627 }
628 } else {
629 closer = this._handleFile(wh.watchPath, stats, initialAdd);
630 }
631 ready();
632
633 this.fsw._addPathCloser(path, closer);
634 return false;
635
636 } catch (error) {
637 if (this.fsw._handleError(error)) {
638 ready();
639 return path;
640 }
641 }
642}
643
644}
645
646module.exports = NodeFsHandler;
Note: See TracBrowser for help on using the repository browser.