123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526 |
- '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_FLAG_MUST_SCAN_SUBDIRS,
- 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;
- if (flags & FSEVENT_FLAG_MUST_SCAN_SUBDIRS) 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;
|