source: trip-planner-front/node_modules/chokidar/lib/fsevents-handler.js@ e29cc2e

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

initial commit

  • Property mode set to 100644
File size: 15.9 KB
Line 
1'use strict';
2
3const fs = require('fs');
4const sysPath = require('path');
5const { promisify } = require('util');
6
7let fsevents;
8try {
9 fsevents = require('fsevents');
10} catch (error) {
11 if (process.env.CHOKIDAR_PRINT_FSEVENTS_REQUIRE_ERROR) console.error(error);
12}
13
14if (fsevents) {
15 // TODO: real check
16 const mtch = process.version.match(/v(\d+)\.(\d+)/);
17 if (mtch && mtch[1] && mtch[2]) {
18 const maj = Number.parseInt(mtch[1], 10);
19 const min = Number.parseInt(mtch[2], 10);
20 if (maj === 8 && min < 16) {
21 fsevents = undefined;
22 }
23 }
24}
25
26const {
27 EV_ADD,
28 EV_CHANGE,
29 EV_ADD_DIR,
30 EV_UNLINK,
31 EV_ERROR,
32 STR_DATA,
33 STR_END,
34 FSEVENT_CREATED,
35 FSEVENT_MODIFIED,
36 FSEVENT_DELETED,
37 FSEVENT_MOVED,
38 // FSEVENT_CLONED,
39 FSEVENT_UNKNOWN,
40 FSEVENT_TYPE_FILE,
41 FSEVENT_TYPE_DIRECTORY,
42 FSEVENT_TYPE_SYMLINK,
43
44 ROOT_GLOBSTAR,
45 DIR_SUFFIX,
46 DOT_SLASH,
47 FUNCTION_TYPE,
48 EMPTY_FN,
49 IDENTITY_FN
50} = require('./constants');
51
52const Depth = (value) => isNaN(value) ? {} : {depth: value};
53
54const stat = promisify(fs.stat);
55const lstat = promisify(fs.lstat);
56const realpath = promisify(fs.realpath);
57
58const statMethods = { stat, lstat };
59
60/**
61 * @typedef {String} Path
62 */
63
64/**
65 * @typedef {Object} FsEventsWatchContainer
66 * @property {Set<Function>} listeners
67 * @property {Function} rawEmitter
68 * @property {{stop: Function}} watcher
69 */
70
71// fsevents instance helper functions
72/**
73 * Object to hold per-process fsevents instances (may be shared across chokidar FSWatcher instances)
74 * @type {Map<Path,FsEventsWatchContainer>}
75 */
76const FSEventsWatchers = new Map();
77
78// Threshold of duplicate path prefixes at which to start
79// consolidating going forward
80const consolidateThreshhold = 10;
81
82const wrongEventFlags = new Set([
83 69888, 70400, 71424, 72704, 73472, 131328, 131840, 262912
84]);
85
86/**
87 * Instantiates the fsevents interface
88 * @param {Path} path path to be watched
89 * @param {Function} callback called when fsevents is bound and ready
90 * @returns {{stop: Function}} new fsevents instance
91 */
92const createFSEventsInstance = (path, callback) => {
93 const stop = fsevents.watch(path, callback);
94 return {stop};
95};
96
97/**
98 * Instantiates the fsevents interface or binds listeners to an existing one covering
99 * the same file tree.
100 * @param {Path} path - to be watched
101 * @param {Path} realPath - real path for symlinks
102 * @param {Function} listener - called when fsevents emits events
103 * @param {Function} rawEmitter - passes data to listeners of the 'raw' event
104 * @returns {Function} closer
105 */
106function setFSEventsListener(path, realPath, listener, rawEmitter) {
107 let watchPath = sysPath.extname(realPath) ? sysPath.dirname(realPath) : realPath;
108
109 const parentPath = sysPath.dirname(watchPath);
110 let cont = FSEventsWatchers.get(watchPath);
111
112 // If we've accumulated a substantial number of paths that
113 // could have been consolidated by watching one directory
114 // above the current one, create a watcher on the parent
115 // path instead, so that we do consolidate going forward.
116 if (couldConsolidate(parentPath)) {
117 watchPath = parentPath;
118 }
119
120 const resolvedPath = sysPath.resolve(path);
121 const hasSymlink = resolvedPath !== realPath;
122
123 const filteredListener = (fullPath, flags, info) => {
124 if (hasSymlink) fullPath = fullPath.replace(realPath, resolvedPath);
125 if (
126 fullPath === resolvedPath ||
127 !fullPath.indexOf(resolvedPath + sysPath.sep)
128 ) listener(fullPath, flags, info);
129 };
130
131 // check if there is already a watcher on a parent path
132 // modifies `watchPath` to the parent path when it finds a match
133 let watchedParent = false;
134 for (const watchedPath of FSEventsWatchers.keys()) {
135 if (realPath.indexOf(sysPath.resolve(watchedPath) + sysPath.sep) === 0) {
136 watchPath = watchedPath;
137 cont = FSEventsWatchers.get(watchPath);
138 watchedParent = true;
139 break;
140 }
141 }
142
143 if (cont || watchedParent) {
144 cont.listeners.add(filteredListener);
145 } else {
146 cont = {
147 listeners: new Set([filteredListener]),
148 rawEmitter,
149 watcher: createFSEventsInstance(watchPath, (fullPath, flags) => {
150 if (!cont.listeners.size) return;
151 const info = fsevents.getInfo(fullPath, flags);
152 cont.listeners.forEach(list => {
153 list(fullPath, flags, info);
154 });
155
156 cont.rawEmitter(info.event, fullPath, info);
157 })
158 };
159 FSEventsWatchers.set(watchPath, cont);
160 }
161
162 // removes this instance's listeners and closes the underlying fsevents
163 // instance if there are no more listeners left
164 return () => {
165 const lst = cont.listeners;
166
167 lst.delete(filteredListener);
168 if (!lst.size) {
169 FSEventsWatchers.delete(watchPath);
170 if (cont.watcher) return cont.watcher.stop().then(() => {
171 cont.rawEmitter = cont.watcher = undefined;
172 Object.freeze(cont);
173 });
174 }
175 };
176}
177
178// Decide whether or not we should start a new higher-level
179// parent watcher
180const couldConsolidate = (path) => {
181 let count = 0;
182 for (const watchPath of FSEventsWatchers.keys()) {
183 if (watchPath.indexOf(path) === 0) {
184 count++;
185 if (count >= consolidateThreshhold) {
186 return true;
187 }
188 }
189 }
190
191 return false;
192};
193
194// returns boolean indicating whether fsevents can be used
195const canUse = () => fsevents && FSEventsWatchers.size < 128;
196
197// determines subdirectory traversal levels from root to path
198const calcDepth = (path, root) => {
199 let i = 0;
200 while (!path.indexOf(root) && (path = sysPath.dirname(path)) !== root) i++;
201 return i;
202};
203
204// returns boolean indicating whether the fsevents' event info has the same type
205// as the one returned by fs.stat
206const sameTypes = (info, stats) => (
207 info.type === FSEVENT_TYPE_DIRECTORY && stats.isDirectory() ||
208 info.type === FSEVENT_TYPE_SYMLINK && stats.isSymbolicLink() ||
209 info.type === FSEVENT_TYPE_FILE && stats.isFile()
210)
211
212/**
213 * @mixin
214 */
215class FsEventsHandler {
216
217/**
218 * @param {import('../index').FSWatcher} fsw
219 */
220constructor(fsw) {
221 this.fsw = fsw;
222}
223checkIgnored(path, stats) {
224 const ipaths = this.fsw._ignoredPaths;
225 if (this.fsw._isIgnored(path, stats)) {
226 ipaths.add(path);
227 if (stats && stats.isDirectory()) {
228 ipaths.add(path + ROOT_GLOBSTAR);
229 }
230 return true;
231 }
232
233 ipaths.delete(path);
234 ipaths.delete(path + ROOT_GLOBSTAR);
235}
236
237addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts) {
238 const event = watchedDir.has(item) ? EV_CHANGE : EV_ADD;
239 this.handleEvent(event, path, fullPath, realPath, parent, watchedDir, item, info, opts);
240}
241
242async checkExists(path, fullPath, realPath, parent, watchedDir, item, info, opts) {
243 try {
244 const stats = await stat(path)
245 if (this.fsw.closed) return;
246 if (sameTypes(info, stats)) {
247 this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
248 } else {
249 this.handleEvent(EV_UNLINK, path, fullPath, realPath, parent, watchedDir, item, info, opts);
250 }
251 } catch (error) {
252 if (error.code === 'EACCES') {
253 this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
254 } else {
255 this.handleEvent(EV_UNLINK, path, fullPath, realPath, parent, watchedDir, item, info, opts);
256 }
257 }
258}
259
260handleEvent(event, path, fullPath, realPath, parent, watchedDir, item, info, opts) {
261 if (this.fsw.closed || this.checkIgnored(path)) return;
262
263 if (event === EV_UNLINK) {
264 const isDirectory = info.type === FSEVENT_TYPE_DIRECTORY
265 // suppress unlink events on never before seen files
266 if (isDirectory || watchedDir.has(item)) {
267 this.fsw._remove(parent, item, isDirectory);
268 }
269 } else {
270 if (event === EV_ADD) {
271 // track new directories
272 if (info.type === FSEVENT_TYPE_DIRECTORY) this.fsw._getWatchedDir(path);
273
274 if (info.type === FSEVENT_TYPE_SYMLINK && opts.followSymlinks) {
275 // push symlinks back to the top of the stack to get handled
276 const curDepth = opts.depth === undefined ?
277 undefined : calcDepth(fullPath, realPath) + 1;
278 return this._addToFsEvents(path, false, true, curDepth);
279 }
280
281 // track new paths
282 // (other than symlinks being followed, which will be tracked soon)
283 this.fsw._getWatchedDir(parent).add(item);
284 }
285 /**
286 * @type {'add'|'addDir'|'unlink'|'unlinkDir'}
287 */
288 const eventName = info.type === FSEVENT_TYPE_DIRECTORY ? event + DIR_SUFFIX : event;
289 this.fsw._emit(eventName, path);
290 if (eventName === EV_ADD_DIR) this._addToFsEvents(path, false, true);
291 }
292}
293
294/**
295 * Handle symlinks encountered during directory scan
296 * @param {String} watchPath - file/dir path to be watched with fsevents
297 * @param {String} realPath - real path (in case of symlinks)
298 * @param {Function} transform - path transformer
299 * @param {Function} globFilter - path filter in case a glob pattern was provided
300 * @returns {Function} closer for the watcher instance
301*/
302_watchWithFsEvents(watchPath, realPath, transform, globFilter) {
303 if (this.fsw.closed || this.fsw._isIgnored(watchPath)) return;
304 const opts = this.fsw.options;
305 const watchCallback = async (fullPath, flags, info) => {
306 if (this.fsw.closed) return;
307 if (
308 opts.depth !== undefined &&
309 calcDepth(fullPath, realPath) > opts.depth
310 ) return;
311 const path = transform(sysPath.join(
312 watchPath, sysPath.relative(watchPath, fullPath)
313 ));
314 if (globFilter && !globFilter(path)) return;
315 // ensure directories are tracked
316 const parent = sysPath.dirname(path);
317 const item = sysPath.basename(path);
318 const watchedDir = this.fsw._getWatchedDir(
319 info.type === FSEVENT_TYPE_DIRECTORY ? path : parent
320 );
321
322 // correct for wrong events emitted
323 if (wrongEventFlags.has(flags) || info.event === FSEVENT_UNKNOWN) {
324 if (typeof opts.ignored === FUNCTION_TYPE) {
325 let stats;
326 try {
327 stats = await stat(path);
328 } catch (error) {}
329 if (this.fsw.closed) return;
330 if (this.checkIgnored(path, stats)) return;
331 if (sameTypes(info, stats)) {
332 this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
333 } else {
334 this.handleEvent(EV_UNLINK, path, fullPath, realPath, parent, watchedDir, item, info, opts);
335 }
336 } else {
337 this.checkExists(path, fullPath, realPath, parent, watchedDir, item, info, opts);
338 }
339 } else {
340 switch (info.event) {
341 case FSEVENT_CREATED:
342 case FSEVENT_MODIFIED:
343 return this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
344 case FSEVENT_DELETED:
345 case FSEVENT_MOVED:
346 return this.checkExists(path, fullPath, realPath, parent, watchedDir, item, info, opts);
347 }
348 }
349 };
350
351 const closer = setFSEventsListener(
352 watchPath,
353 realPath,
354 watchCallback,
355 this.fsw._emitRaw
356 );
357
358 this.fsw._emitReady();
359 return closer;
360}
361
362/**
363 * Handle symlinks encountered during directory scan
364 * @param {String} linkPath path to symlink
365 * @param {String} fullPath absolute path to the symlink
366 * @param {Function} transform pre-existing path transformer
367 * @param {Number} curDepth level of subdirectories traversed to where symlink is
368 * @returns {Promise<void>}
369 */
370async _handleFsEventsSymlink(linkPath, fullPath, transform, curDepth) {
371 // don't follow the same symlink more than once
372 if (this.fsw.closed || this.fsw._symlinkPaths.has(fullPath)) return;
373
374 this.fsw._symlinkPaths.set(fullPath, true);
375 this.fsw._incrReadyCount();
376
377 try {
378 const linkTarget = await realpath(linkPath);
379 if (this.fsw.closed) return;
380 if (this.fsw._isIgnored(linkTarget)) {
381 return this.fsw._emitReady();
382 }
383
384 this.fsw._incrReadyCount();
385
386 // add the linkTarget for watching with a wrapper for transform
387 // that causes emitted paths to incorporate the link's path
388 this._addToFsEvents(linkTarget || linkPath, (path) => {
389 let aliasedPath = linkPath;
390 if (linkTarget && linkTarget !== DOT_SLASH) {
391 aliasedPath = path.replace(linkTarget, linkPath);
392 } else if (path !== DOT_SLASH) {
393 aliasedPath = sysPath.join(linkPath, path);
394 }
395 return transform(aliasedPath);
396 }, false, curDepth);
397 } catch(error) {
398 if (this.fsw._handleError(error)) {
399 return this.fsw._emitReady();
400 }
401 }
402}
403
404/**
405 *
406 * @param {Path} newPath
407 * @param {fs.Stats} stats
408 */
409emitAdd(newPath, stats, processPath, opts, forceAdd) {
410 const pp = processPath(newPath);
411 const isDir = stats.isDirectory();
412 const dirObj = this.fsw._getWatchedDir(sysPath.dirname(pp));
413 const base = sysPath.basename(pp);
414
415 // ensure empty dirs get tracked
416 if (isDir) this.fsw._getWatchedDir(pp);
417 if (dirObj.has(base)) return;
418 dirObj.add(base);
419
420 if (!opts.ignoreInitial || forceAdd === true) {
421 this.fsw._emit(isDir ? EV_ADD_DIR : EV_ADD, pp, stats);
422 }
423}
424
425initWatch(realPath, path, wh, processPath) {
426 if (this.fsw.closed) return;
427 const closer = this._watchWithFsEvents(
428 wh.watchPath,
429 sysPath.resolve(realPath || wh.watchPath),
430 processPath,
431 wh.globFilter
432 );
433 this.fsw._addPathCloser(path, closer);
434}
435
436/**
437 * Handle added path with fsevents
438 * @param {String} path file/dir path or glob pattern
439 * @param {Function|Boolean=} transform converts working path to what the user expects
440 * @param {Boolean=} forceAdd ensure add is emitted
441 * @param {Number=} priorDepth Level of subdirectories already traversed.
442 * @returns {Promise<void>}
443 */
444async _addToFsEvents(path, transform, forceAdd, priorDepth) {
445 if (this.fsw.closed) {
446 return;
447 }
448 const opts = this.fsw.options;
449 const processPath = typeof transform === FUNCTION_TYPE ? transform : IDENTITY_FN;
450
451 const wh = this.fsw._getWatchHelpers(path);
452
453 // evaluate what is at the path we're being asked to watch
454 try {
455 const stats = await statMethods[wh.statMethod](wh.watchPath);
456 if (this.fsw.closed) return;
457 if (this.fsw._isIgnored(wh.watchPath, stats)) {
458 throw null;
459 }
460 if (stats.isDirectory()) {
461 // emit addDir unless this is a glob parent
462 if (!wh.globFilter) this.emitAdd(processPath(path), stats, processPath, opts, forceAdd);
463
464 // don't recurse further if it would exceed depth setting
465 if (priorDepth && priorDepth > opts.depth) return;
466
467 // scan the contents of the dir
468 this.fsw._readdirp(wh.watchPath, {
469 fileFilter: entry => wh.filterPath(entry),
470 directoryFilter: entry => wh.filterDir(entry),
471 ...Depth(opts.depth - (priorDepth || 0))
472 }).on(STR_DATA, (entry) => {
473 // need to check filterPath on dirs b/c filterDir is less restrictive
474 if (this.fsw.closed) {
475 return;
476 }
477 if (entry.stats.isDirectory() && !wh.filterPath(entry)) return;
478
479 const joinedPath = sysPath.join(wh.watchPath, entry.path);
480 const {fullPath} = entry;
481
482 if (wh.followSymlinks && entry.stats.isSymbolicLink()) {
483 // preserve the current depth here since it can't be derived from
484 // real paths past the symlink
485 const curDepth = opts.depth === undefined ?
486 undefined : calcDepth(joinedPath, sysPath.resolve(wh.watchPath)) + 1;
487
488 this._handleFsEventsSymlink(joinedPath, fullPath, processPath, curDepth);
489 } else {
490 this.emitAdd(joinedPath, entry.stats, processPath, opts, forceAdd);
491 }
492 }).on(EV_ERROR, EMPTY_FN).on(STR_END, () => {
493 this.fsw._emitReady();
494 });
495 } else {
496 this.emitAdd(wh.watchPath, stats, processPath, opts, forceAdd);
497 this.fsw._emitReady();
498 }
499 } catch (error) {
500 if (!error || this.fsw._handleError(error)) {
501 // TODO: Strange thing: "should not choke on an ignored watch path" will be failed without 2 ready calls -__-
502 this.fsw._emitReady();
503 this.fsw._emitReady();
504 }
505 }
506
507 if (opts.persistent && forceAdd !== true) {
508 if (typeof transform === FUNCTION_TYPE) {
509 // realpath has already been resolved
510 this.initWatch(undefined, path, wh, processPath);
511 } else {
512 let realPath;
513 try {
514 realPath = await realpath(wh.watchPath);
515 } catch (e) {}
516 this.initWatch(realPath, path, wh, processPath);
517 }
518 }
519}
520
521}
522
523module.exports = FsEventsHandler;
524module.exports.canUse = canUse;
Note: See TracBrowser for help on using the repository browser.