525 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			525 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| const fs = require('fs');
 | |
| const sysPath = require('path');
 | |
| const { promisify } = require('util');
 | |
| 
 | |
| let fsevents;
 | |
| try {
 | |
|   fsevents = require('fsevents');
 | |
| } catch (error) {
 | |
|   if (process.env.CHOKIDAR_PRINT_FSEVENTS_REQUIRE_ERROR) console.error(error);
 | |
| }
 | |
| 
 | |
| if (fsevents) {
 | |
|   // TODO: real check
 | |
|   const mtch = process.version.match(/v(\d+)\.(\d+)/);
 | |
|   if (mtch && mtch[1] && mtch[2]) {
 | |
|     const maj = Number.parseInt(mtch[1], 10);
 | |
|     const min = Number.parseInt(mtch[2], 10);
 | |
|     if (maj === 8 && min < 16) {
 | |
|       fsevents = undefined;
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| const {
 | |
|   EV_ADD,
 | |
|   EV_CHANGE,
 | |
|   EV_ADD_DIR,
 | |
|   EV_UNLINK,
 | |
|   EV_ERROR,
 | |
|   STR_DATA,
 | |
|   STR_END,
 | |
|   FSEVENT_CREATED,
 | |
|   FSEVENT_MODIFIED,
 | |
|   FSEVENT_DELETED,
 | |
|   FSEVENT_MOVED,
 | |
|   // FSEVENT_CLONED,
 | |
|   FSEVENT_UNKNOWN,
 | |
|   FSEVENT_TYPE_FILE,
 | |
|   FSEVENT_TYPE_DIRECTORY,
 | |
|   FSEVENT_TYPE_SYMLINK,
 | |
| 
 | |
|   ROOT_GLOBSTAR,
 | |
|   DIR_SUFFIX,
 | |
|   DOT_SLASH,
 | |
|   FUNCTION_TYPE,
 | |
|   EMPTY_FN,
 | |
|   IDENTITY_FN
 | |
| } = require('./constants');
 | |
| 
 | |
| const Depth = (value) => isNaN(value) ? {} : {depth: value};
 | |
| 
 | |
| const stat = promisify(fs.stat);
 | |
| const lstat = promisify(fs.lstat);
 | |
| const realpath = promisify(fs.realpath);
 | |
| 
 | |
| const statMethods = { stat, lstat };
 | |
| 
 | |
| /**
 | |
|  * @typedef {String} Path
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * @typedef {Object} FsEventsWatchContainer
 | |
|  * @property {Set<Function>} listeners
 | |
|  * @property {Function} rawEmitter
 | |
|  * @property {{stop: Function}} watcher
 | |
|  */
 | |
| 
 | |
| // fsevents instance helper functions
 | |
| /**
 | |
|  * Object to hold per-process fsevents instances (may be shared across chokidar FSWatcher instances)
 | |
|  * @type {Map<Path,FsEventsWatchContainer>}
 | |
|  */
 | |
| const FSEventsWatchers = new Map();
 | |
| 
 | |
| // Threshold of duplicate path prefixes at which to start
 | |
| // consolidating going forward
 | |
| const consolidateThreshhold = 10;
 | |
| 
 | |
| const wrongEventFlags = new Set([
 | |
|   69888, 70400, 71424, 72704, 73472, 131328, 131840, 262912
 | |
| ]);
 | |
| 
 | |
| /**
 | |
|  * Instantiates the fsevents interface
 | |
|  * @param {Path} path path to be watched
 | |
|  * @param {Function} callback called when fsevents is bound and ready
 | |
|  * @returns {{stop: Function}} new fsevents instance
 | |
|  */
 | |
| const createFSEventsInstance = (path, callback) => {
 | |
|   const stop = fsevents.watch(path, callback);
 | |
|   return {stop};
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Instantiates the fsevents interface or binds listeners to an existing one covering
 | |
|  * the same file tree.
 | |
|  * @param {Path} path           - to be watched
 | |
|  * @param {Path} realPath       - real path for symlinks
 | |
|  * @param {Function} listener   - called when fsevents emits events
 | |
|  * @param {Function} rawEmitter - passes data to listeners of the 'raw' event
 | |
|  * @returns {Function} closer
 | |
|  */
 | |
| function setFSEventsListener(path, realPath, listener, rawEmitter) {
 | |
|   let watchPath = sysPath.extname(realPath) ? sysPath.dirname(realPath) : realPath;
 | |
| 
 | |
|   const parentPath = sysPath.dirname(watchPath);
 | |
|   let cont = FSEventsWatchers.get(watchPath);
 | |
| 
 | |
|   // If we've accumulated a substantial number of paths that
 | |
|   // could have been consolidated by watching one directory
 | |
|   // above the current one, create a watcher on the parent
 | |
|   // path instead, so that we do consolidate going forward.
 | |
|   if (couldConsolidate(parentPath)) {
 | |
|     watchPath = parentPath;
 | |
|   }
 | |
| 
 | |
|   const resolvedPath = sysPath.resolve(path);
 | |
|   const hasSymlink = resolvedPath !== realPath;
 | |
| 
 | |
|   const filteredListener = (fullPath, flags, info) => {
 | |
|     if (hasSymlink) fullPath = fullPath.replace(realPath, resolvedPath);
 | |
|     if (
 | |
|       fullPath === resolvedPath ||
 | |
|       !fullPath.indexOf(resolvedPath + sysPath.sep)
 | |
|     ) listener(fullPath, flags, info);
 | |
|   };
 | |
| 
 | |
|   // check if there is already a watcher on a parent path
 | |
|   // modifies `watchPath` to the parent path when it finds a match
 | |
|   let watchedParent = false;
 | |
|   for (const watchedPath of FSEventsWatchers.keys()) {
 | |
|     if (realPath.indexOf(sysPath.resolve(watchedPath) + sysPath.sep) === 0) {
 | |
|       watchPath = watchedPath;
 | |
|       cont = FSEventsWatchers.get(watchPath);
 | |
|       watchedParent = true;
 | |
|       break;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (cont || watchedParent) {
 | |
|     cont.listeners.add(filteredListener);
 | |
|   } else {
 | |
|     cont = {
 | |
|       listeners: new Set([filteredListener]),
 | |
|       rawEmitter,
 | |
|       watcher: createFSEventsInstance(watchPath, (fullPath, flags) => {
 | |
|         if (!cont.listeners.size) return;
 | |
|         const info = fsevents.getInfo(fullPath, flags);
 | |
|         cont.listeners.forEach(list => {
 | |
|           list(fullPath, flags, info);
 | |
|         });
 | |
| 
 | |
|         cont.rawEmitter(info.event, fullPath, info);
 | |
|       })
 | |
|     };
 | |
|     FSEventsWatchers.set(watchPath, cont);
 | |
|   }
 | |
| 
 | |
|   // removes this instance's listeners and closes the underlying fsevents
 | |
|   // instance if there are no more listeners left
 | |
|   return () => {
 | |
|     const lst = cont.listeners;
 | |
| 
 | |
|     lst.delete(filteredListener);
 | |
|     if (!lst.size) {
 | |
|       FSEventsWatchers.delete(watchPath);
 | |
|       if (cont.watcher) return cont.watcher.stop().then(() => {
 | |
|         cont.rawEmitter = cont.watcher = undefined;
 | |
|         Object.freeze(cont);
 | |
|       });
 | |
|     }
 | |
|   };
 | |
| }
 | |
| 
 | |
| // Decide whether or not we should start a new higher-level
 | |
| // parent watcher
 | |
| const couldConsolidate = (path) => {
 | |
|   let count = 0;
 | |
|   for (const watchPath of FSEventsWatchers.keys()) {
 | |
|     if (watchPath.indexOf(path) === 0) {
 | |
|       count++;
 | |
|       if (count >= consolidateThreshhold) {
 | |
|         return true;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return false;
 | |
| };
 | |
| 
 | |
| // returns boolean indicating whether fsevents can be used
 | |
| const canUse = () => fsevents && FSEventsWatchers.size < 128;
 | |
| 
 | |
| // determines subdirectory traversal levels from root to path
 | |
| const calcDepth = (path, root) => {
 | |
|   let i = 0;
 | |
|   while (!path.indexOf(root) && (path = sysPath.dirname(path)) !== root) i++;
 | |
|   return i;
 | |
| };
 | |
| 
 | |
| // returns boolean indicating whether the fsevents' event info has the same type
 | |
| // as the one returned by fs.stat
 | |
| const sameTypes = (info, stats) => (
 | |
|   info.type === FSEVENT_TYPE_DIRECTORY && stats.isDirectory() ||
 | |
|   info.type === FSEVENT_TYPE_SYMLINK && stats.isSymbolicLink() ||
 | |
|   info.type === FSEVENT_TYPE_FILE && stats.isFile()
 | |
| )
 | |
| 
 | |
| /**
 | |
|  * @mixin
 | |
|  */
 | |
| class FsEventsHandler {
 | |
| 
 | |
| /**
 | |
|  * @param {import('../index').FSWatcher} fsw
 | |
|  */
 | |
| constructor(fsw) {
 | |
|   this.fsw = fsw;
 | |
| }
 | |
| checkIgnored(path, stats) {
 | |
|   const ipaths = this.fsw._ignoredPaths;
 | |
|   if (this.fsw._isIgnored(path, stats)) {
 | |
|     ipaths.add(path);
 | |
|     if (stats && stats.isDirectory()) {
 | |
|       ipaths.add(path + ROOT_GLOBSTAR);
 | |
|     }
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   ipaths.delete(path);
 | |
|   ipaths.delete(path + ROOT_GLOBSTAR);
 | |
| }
 | |
| 
 | |
| addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts) {
 | |
|   const event = watchedDir.has(item) ? EV_CHANGE : EV_ADD;
 | |
|   this.handleEvent(event, path, fullPath, realPath, parent, watchedDir, item, info, opts);
 | |
| }
 | |
| 
 | |
| async checkExists(path, fullPath, realPath, parent, watchedDir, item, info, opts) {
 | |
|   try {
 | |
|     const stats = await stat(path)
 | |
|     if (this.fsw.closed) return;
 | |
|     if (sameTypes(info, stats)) {
 | |
|       this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
 | |
|     } else {
 | |
|       this.handleEvent(EV_UNLINK, path, fullPath, realPath, parent, watchedDir, item, info, opts);
 | |
|     }
 | |
|   } catch (error) {
 | |
|     if (error.code === 'EACCES') {
 | |
|       this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
 | |
|     } else {
 | |
|       this.handleEvent(EV_UNLINK, path, fullPath, realPath, parent, watchedDir, item, info, opts);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| handleEvent(event, path, fullPath, realPath, parent, watchedDir, item, info, opts) {
 | |
|   if (this.fsw.closed || this.checkIgnored(path)) return;
 | |
| 
 | |
|   if (event === EV_UNLINK) {
 | |
|     const isDirectory = info.type === FSEVENT_TYPE_DIRECTORY
 | |
|     // suppress unlink events on never before seen files
 | |
|     if (isDirectory || watchedDir.has(item)) {
 | |
|       this.fsw._remove(parent, item, isDirectory);
 | |
|     }
 | |
|   } else {
 | |
|     if (event === EV_ADD) {
 | |
|       // track new directories
 | |
|       if (info.type === FSEVENT_TYPE_DIRECTORY) this.fsw._getWatchedDir(path);
 | |
| 
 | |
|       if (info.type === FSEVENT_TYPE_SYMLINK && opts.followSymlinks) {
 | |
|         // push symlinks back to the top of the stack to get handled
 | |
|         const curDepth = opts.depth === undefined ?
 | |
|           undefined : calcDepth(fullPath, realPath) + 1;
 | |
|         return this._addToFsEvents(path, false, true, curDepth);
 | |
|       }
 | |
| 
 | |
|       // track new paths
 | |
|       // (other than symlinks being followed, which will be tracked soon)
 | |
|       this.fsw._getWatchedDir(parent).add(item);
 | |
|     }
 | |
|     /**
 | |
|      * @type {'add'|'addDir'|'unlink'|'unlinkDir'}
 | |
|      */
 | |
|     const eventName = info.type === FSEVENT_TYPE_DIRECTORY ? event + DIR_SUFFIX : event;
 | |
|     this.fsw._emit(eventName, path);
 | |
|     if (eventName === EV_ADD_DIR) this._addToFsEvents(path, false, true);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Handle symlinks encountered during directory scan
 | |
|  * @param {String} watchPath  - file/dir path to be watched with fsevents
 | |
|  * @param {String} realPath   - real path (in case of symlinks)
 | |
|  * @param {Function} transform  - path transformer
 | |
|  * @param {Function} globFilter - path filter in case a glob pattern was provided
 | |
|  * @returns {Function} closer for the watcher instance
 | |
| */
 | |
| _watchWithFsEvents(watchPath, realPath, transform, globFilter) {
 | |
|   if (this.fsw.closed || this.fsw._isIgnored(watchPath)) return;
 | |
|   const opts = this.fsw.options;
 | |
|   const watchCallback = async (fullPath, flags, info) => {
 | |
|     if (this.fsw.closed) return;
 | |
|     if (
 | |
|       opts.depth !== undefined &&
 | |
|       calcDepth(fullPath, realPath) > opts.depth
 | |
|     ) return;
 | |
|     const path = transform(sysPath.join(
 | |
|       watchPath, sysPath.relative(watchPath, fullPath)
 | |
|     ));
 | |
|     if (globFilter && !globFilter(path)) return;
 | |
|     // ensure directories are tracked
 | |
|     const parent = sysPath.dirname(path);
 | |
|     const item = sysPath.basename(path);
 | |
|     const watchedDir = this.fsw._getWatchedDir(
 | |
|       info.type === FSEVENT_TYPE_DIRECTORY ? path : parent
 | |
|     );
 | |
| 
 | |
|     // correct for wrong events emitted
 | |
|     if (wrongEventFlags.has(flags) || info.event === FSEVENT_UNKNOWN) {
 | |
|       if (typeof opts.ignored === FUNCTION_TYPE) {
 | |
|         let stats;
 | |
|         try {
 | |
|           stats = await stat(path);
 | |
|         } catch (error) {}
 | |
|         if (this.fsw.closed) return;
 | |
|         if (this.checkIgnored(path, stats)) return;
 | |
|         if (sameTypes(info, stats)) {
 | |
|           this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
 | |
|         } else {
 | |
|           this.handleEvent(EV_UNLINK, path, fullPath, realPath, parent, watchedDir, item, info, opts);
 | |
|         }
 | |
|       } else {
 | |
|         this.checkExists(path, fullPath, realPath, parent, watchedDir, item, info, opts);
 | |
|       }
 | |
|     } else {
 | |
|       switch (info.event) {
 | |
|       case FSEVENT_CREATED:
 | |
|       case FSEVENT_MODIFIED:
 | |
|         return this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
 | |
|       case FSEVENT_DELETED:
 | |
|       case FSEVENT_MOVED:
 | |
|         return this.checkExists(path, fullPath, realPath, parent, watchedDir, item, info, opts);
 | |
|       }
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   const closer = setFSEventsListener(
 | |
|     watchPath,
 | |
|     realPath,
 | |
|     watchCallback,
 | |
|     this.fsw._emitRaw
 | |
|   );
 | |
| 
 | |
|   this.fsw._emitReady();
 | |
|   return closer;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Handle symlinks encountered during directory scan
 | |
|  * @param {String} linkPath path to symlink
 | |
|  * @param {String} fullPath absolute path to the symlink
 | |
|  * @param {Function} transform pre-existing path transformer
 | |
|  * @param {Number} curDepth level of subdirectories traversed to where symlink is
 | |
|  * @returns {Promise<void>}
 | |
|  */
 | |
| async _handleFsEventsSymlink(linkPath, fullPath, transform, curDepth) {
 | |
|   // don't follow the same symlink more than once
 | |
|   if (this.fsw.closed || this.fsw._symlinkPaths.has(fullPath)) return;
 | |
| 
 | |
|   this.fsw._symlinkPaths.set(fullPath, true);
 | |
|   this.fsw._incrReadyCount();
 | |
| 
 | |
|   try {
 | |
|     const linkTarget = await realpath(linkPath);
 | |
|     if (this.fsw.closed) return;
 | |
|     if (this.fsw._isIgnored(linkTarget)) {
 | |
|       return this.fsw._emitReady();
 | |
|     }
 | |
| 
 | |
|     this.fsw._incrReadyCount();
 | |
| 
 | |
|     // add the linkTarget for watching with a wrapper for transform
 | |
|     // that causes emitted paths to incorporate the link's path
 | |
|     this._addToFsEvents(linkTarget || linkPath, (path) => {
 | |
|       let aliasedPath = linkPath;
 | |
|       if (linkTarget && linkTarget !== DOT_SLASH) {
 | |
|         aliasedPath = path.replace(linkTarget, linkPath);
 | |
|       } else if (path !== DOT_SLASH) {
 | |
|         aliasedPath = sysPath.join(linkPath, path);
 | |
|       }
 | |
|       return transform(aliasedPath);
 | |
|     }, false, curDepth);
 | |
|   } catch(error) {
 | |
|     if (this.fsw._handleError(error)) {
 | |
|       return this.fsw._emitReady();
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  *
 | |
|  * @param {Path} newPath
 | |
|  * @param {fs.Stats} stats
 | |
|  */
 | |
| emitAdd(newPath, stats, processPath, opts, forceAdd) {
 | |
|   const pp = processPath(newPath);
 | |
|   const isDir = stats.isDirectory();
 | |
|   const dirObj = this.fsw._getWatchedDir(sysPath.dirname(pp));
 | |
|   const base = sysPath.basename(pp);
 | |
| 
 | |
|   // ensure empty dirs get tracked
 | |
|   if (isDir) this.fsw._getWatchedDir(pp);
 | |
|   if (dirObj.has(base)) return;
 | |
|   dirObj.add(base);
 | |
| 
 | |
|   if (!opts.ignoreInitial || forceAdd === true) {
 | |
|     this.fsw._emit(isDir ? EV_ADD_DIR : EV_ADD, pp, stats);
 | |
|   }
 | |
| }
 | |
| 
 | |
| initWatch(realPath, path, wh, processPath) {
 | |
|   if (this.fsw.closed) return;
 | |
|   const closer = this._watchWithFsEvents(
 | |
|     wh.watchPath,
 | |
|     sysPath.resolve(realPath || wh.watchPath),
 | |
|     processPath,
 | |
|     wh.globFilter
 | |
|   );
 | |
|   this.fsw._addPathCloser(path, closer);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Handle added path with fsevents
 | |
|  * @param {String} path file/dir path or glob pattern
 | |
|  * @param {Function|Boolean=} transform converts working path to what the user expects
 | |
|  * @param {Boolean=} forceAdd ensure add is emitted
 | |
|  * @param {Number=} priorDepth Level of subdirectories already traversed.
 | |
|  * @returns {Promise<void>}
 | |
|  */
 | |
| async _addToFsEvents(path, transform, forceAdd, priorDepth) {
 | |
|   if (this.fsw.closed) {
 | |
|     return;
 | |
|   }
 | |
|   const opts = this.fsw.options;
 | |
|   const processPath = typeof transform === FUNCTION_TYPE ? transform : IDENTITY_FN;
 | |
| 
 | |
|   const wh = this.fsw._getWatchHelpers(path);
 | |
| 
 | |
|   // evaluate what is at the path we're being asked to watch
 | |
|   try {
 | |
|     const stats = await statMethods[wh.statMethod](wh.watchPath);
 | |
|     if (this.fsw.closed) return;
 | |
|     if (this.fsw._isIgnored(wh.watchPath, stats)) {
 | |
|       throw null;
 | |
|     }
 | |
|     if (stats.isDirectory()) {
 | |
|       // emit addDir unless this is a glob parent
 | |
|       if (!wh.globFilter) this.emitAdd(processPath(path), stats, processPath, opts, forceAdd);
 | |
| 
 | |
|       // don't recurse further if it would exceed depth setting
 | |
|       if (priorDepth && priorDepth > opts.depth) return;
 | |
| 
 | |
|       // scan the contents of the dir
 | |
|       this.fsw._readdirp(wh.watchPath, {
 | |
|         fileFilter: entry => wh.filterPath(entry),
 | |
|         directoryFilter: entry => wh.filterDir(entry),
 | |
|         ...Depth(opts.depth - (priorDepth || 0))
 | |
|       }).on(STR_DATA, (entry) => {
 | |
|         // need to check filterPath on dirs b/c filterDir is less restrictive
 | |
|         if (this.fsw.closed) {
 | |
|           return;
 | |
|         }
 | |
|         if (entry.stats.isDirectory() && !wh.filterPath(entry)) return;
 | |
| 
 | |
|         const joinedPath = sysPath.join(wh.watchPath, entry.path);
 | |
|         const {fullPath} = entry;
 | |
| 
 | |
|         if (wh.followSymlinks && entry.stats.isSymbolicLink()) {
 | |
|           // preserve the current depth here since it can't be derived from
 | |
|           // real paths past the symlink
 | |
|           const curDepth = opts.depth === undefined ?
 | |
|             undefined : calcDepth(joinedPath, sysPath.resolve(wh.watchPath)) + 1;
 | |
| 
 | |
|           this._handleFsEventsSymlink(joinedPath, fullPath, processPath, curDepth);
 | |
|         } else {
 | |
|           this.emitAdd(joinedPath, entry.stats, processPath, opts, forceAdd);
 | |
|         }
 | |
|       }).on(EV_ERROR, EMPTY_FN).on(STR_END, () => {
 | |
|         this.fsw._emitReady();
 | |
|       });
 | |
|     } else {
 | |
|       this.emitAdd(wh.watchPath, stats, processPath, opts, forceAdd);
 | |
|       this.fsw._emitReady();
 | |
|     }
 | |
|   } catch (error) {
 | |
|     if (!error || this.fsw._handleError(error)) {
 | |
|       // TODO: Strange thing: "should not choke on an ignored watch path" will be failed without 2 ready calls -__-
 | |
|       this.fsw._emitReady();
 | |
|       this.fsw._emitReady();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (opts.persistent && forceAdd !== true) {
 | |
|     if (typeof transform === FUNCTION_TYPE) {
 | |
|       // realpath has already been resolved
 | |
|       this.initWatch(undefined, path, wh, processPath);
 | |
|     } else {
 | |
|       let realPath;
 | |
|       try {
 | |
|         realPath = await realpath(wh.watchPath);
 | |
|       } catch (e) {}
 | |
|       this.initWatch(realPath, path, wh, processPath);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| }
 | |
| 
 | |
| module.exports = FsEventsHandler;
 | |
| module.exports.canUse = canUse;
 |