Permalink
Cannot retrieve contributors at this time
524 lines (462 sloc)
15.9 KB
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
codeql-action/node_modules/chokidar/lib/fsevents-handler.js
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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; |