|
|
- 'use strict';
-
- var fs = require('fs');
- var sysPath = require('path');
- var readdirp = require('readdirp');
- var isBinaryPath = require('is-binary-path');
-
- // fs.watch helpers
-
- // object to hold per-process fs.watch instances
- // (may be shared across chokidar FSWatcher instances)
- var FsWatchInstances = Object.create(null);
-
-
- // Private function: Instantiates the fs.watch interface
-
- // * path - string, path to be watched
- // * options - object, options to be passed to fs.watch
- // * listener - function, main event handler
- // * errHandler - function, handler which emits info about errors
- // * emitRaw - function, handler which emits raw event data
-
- // Returns new fsevents instance
- function createFsWatchInstance(path, options, listener, errHandler, emitRaw) {
- var handleEvent = function(rawEvent, evPath) {
- listener(path);
- emitRaw(rawEvent, evPath, {watchedPath: path});
-
- // emit based on events occurring for files from a directory's watcher in
- // case the file's watcher misses it (and rely on throttling to de-dupe)
- if (evPath && path !== evPath) {
- fsWatchBroadcast(
- sysPath.resolve(path, evPath), 'listeners', sysPath.join(path, evPath)
- );
- }
- };
- try {
- return fs.watch(path, options, handleEvent);
- } catch (error) {
- errHandler(error);
- }
- }
-
- // Private function: Helper for passing fs.watch event data to a
- // collection of listeners
-
- // * fullPath - string, absolute path bound to the fs.watch instance
- // * type - string, listener type
- // * val[1..3] - arguments to be passed to listeners
-
- // Returns nothing
- function fsWatchBroadcast(fullPath, type, val1, val2, val3) {
- if (!FsWatchInstances[fullPath]) return;
- FsWatchInstances[fullPath][type].forEach(function(listener) {
- listener(val1, val2, val3);
- });
- }
-
- // Private function: Instantiates the fs.watch interface or binds listeners
- // to an existing one covering the same file system entry
-
- // * path - string, path to be watched
- // * fullPath - string, absolute path
- // * options - object, options to be passed to fs.watch
- // * handlers - object, container for event listener functions
-
- // Returns close function
- function setFsWatchListener(path, fullPath, options, handlers) {
- var listener = handlers.listener;
- var errHandler = handlers.errHandler;
- var rawEmitter = handlers.rawEmitter;
- var container = FsWatchInstances[fullPath];
- var watcher;
- if (!options.persistent) {
- watcher = createFsWatchInstance(
- path, options, listener, errHandler, rawEmitter
- );
- return watcher.close.bind(watcher);
- }
- if (!container) {
- watcher = createFsWatchInstance(
- path,
- options,
- fsWatchBroadcast.bind(null, fullPath, 'listeners'),
- errHandler, // no need to use broadcast here
- fsWatchBroadcast.bind(null, fullPath, 'rawEmitters')
- );
- if (!watcher) return;
- var broadcastErr = fsWatchBroadcast.bind(null, fullPath, 'errHandlers');
- watcher.on('error', function(error) {
- container.watcherUnusable = true; // documented since Node 10.4.1
- // Workaround for https://github.com/joyent/node/issues/4337
- if (process.platform === 'win32' && error.code === 'EPERM') {
- fs.open(path, 'r', function(err, fd) {
- if (!err) fs.close(fd, function(err) {
- if (!err) broadcastErr(error);
- });
- });
- } else {
- broadcastErr(error);
- }
- });
- container = FsWatchInstances[fullPath] = {
- listeners: [listener],
- errHandlers: [errHandler],
- rawEmitters: [rawEmitter],
- watcher: watcher
- };
- } else {
- container.listeners.push(listener);
- container.errHandlers.push(errHandler);
- container.rawEmitters.push(rawEmitter);
- }
- var listenerIndex = container.listeners.length - 1;
-
- // removes this instance's listeners and closes the underlying fs.watch
- // instance if there are no more listeners left
- return function close() {
- delete container.listeners[listenerIndex];
- delete container.errHandlers[listenerIndex];
- delete container.rawEmitters[listenerIndex];
- if (!Object.keys(container.listeners).length) {
- if (!container.watcherUnusable) { // check to protect against issue #730
- container.watcher.close();
- }
- delete FsWatchInstances[fullPath];
- }
- };
- }
-
- // fs.watchFile helpers
-
- // object to hold per-process fs.watchFile instances
- // (may be shared across chokidar FSWatcher instances)
- var FsWatchFileInstances = Object.create(null);
-
- // Private function: Instantiates the fs.watchFile interface or binds listeners
- // to an existing one covering the same file system entry
-
- // * path - string, path to be watched
- // * fullPath - string, absolute path
- // * options - object, options to be passed to fs.watchFile
- // * handlers - object, container for event listener functions
-
- // Returns close function
- function setFsWatchFileListener(path, fullPath, options, handlers) {
- var listener = handlers.listener;
- var rawEmitter = handlers.rawEmitter;
- var container = FsWatchFileInstances[fullPath];
- var listeners = [];
- var rawEmitters = [];
- if (
- container && (
- container.options.persistent < options.persistent ||
- container.options.interval > options.interval
- )
- ) {
- // "Upgrade" the watcher to persistence or a quicker interval.
- // This creates some unlikely edge case issues if the user mixes
- // settings in a very weird way, but solving for those cases
- // doesn't seem worthwhile for the added complexity.
- listeners = container.listeners;
- rawEmitters = container.rawEmitters;
- fs.unwatchFile(fullPath);
- container = false;
- }
- if (!container) {
- listeners.push(listener);
- rawEmitters.push(rawEmitter);
- container = FsWatchFileInstances[fullPath] = {
- listeners: listeners,
- rawEmitters: rawEmitters,
- options: options,
- watcher: fs.watchFile(fullPath, options, function(curr, prev) {
- container.rawEmitters.forEach(function(rawEmitter) {
- rawEmitter('change', fullPath, {curr: curr, prev: prev});
- });
- var currmtime = curr.mtime.getTime();
- if (curr.size !== prev.size || currmtime > prev.mtime.getTime() || currmtime === 0) {
- container.listeners.forEach(function(listener) {
- listener(path, curr);
- });
- }
- })
- };
- } else {
- container.listeners.push(listener);
- container.rawEmitters.push(rawEmitter);
- }
- var listenerIndex = container.listeners.length - 1;
-
- // removes this instance's listeners and closes the underlying fs.watchFile
- // instance if there are no more listeners left
- return function close() {
- delete container.listeners[listenerIndex];
- delete container.rawEmitters[listenerIndex];
- if (!Object.keys(container.listeners).length) {
- fs.unwatchFile(fullPath);
- delete FsWatchFileInstances[fullPath];
- }
- };
- }
-
- // fake constructor for attaching nodefs-specific prototype methods that
- // will be copied to FSWatcher's prototype
- function NodeFsHandler() {}
-
- // Private method: Watch file for changes with fs.watchFile or fs.watch.
-
- // * path - string, path to file or directory.
- // * listener - function, to be executed on fs change.
-
- // Returns close function for the watcher instance
- NodeFsHandler.prototype._watchWithNodeFs =
- function(path, listener) {
- var directory = sysPath.dirname(path);
- var basename = sysPath.basename(path);
- var parent = this._getWatchedDir(directory);
- parent.add(basename);
- var absolutePath = sysPath.resolve(path);
- var options = {persistent: this.options.persistent};
- if (!listener) listener = Function.prototype; // empty function
-
- var closer;
- if (this.options.usePolling) {
- options.interval = this.enableBinaryInterval && isBinaryPath(basename) ?
- this.options.binaryInterval : this.options.interval;
- closer = setFsWatchFileListener(path, absolutePath, options, {
- listener: listener,
- rawEmitter: this.emit.bind(this, 'raw')
- });
- } else {
- closer = setFsWatchListener(path, absolutePath, options, {
- listener: listener,
- errHandler: this._handleError.bind(this),
- rawEmitter: this.emit.bind(this, 'raw')
- });
- }
- return closer;
- };
-
- // Private method: Watch a file and emit add event if warranted
-
- // * file - string, the file's path
- // * stats - object, result of fs.stat
- // * initialAdd - boolean, was the file added at watch instantiation?
- // * callback - function, called when done processing as a newly seen file
-
- // Returns close function for the watcher instance
- NodeFsHandler.prototype._handleFile =
- function(file, stats, initialAdd, callback) {
- var dirname = sysPath.dirname(file);
- var basename = sysPath.basename(file);
- var parent = this._getWatchedDir(dirname);
- // stats is always present
- var prevStats = stats;
-
- // if the file is already being watched, do nothing
- if (parent.has(basename)) return callback();
-
- // kick off the watcher
- var closer = this._watchWithNodeFs(file, function(path, newStats) {
- if (!this._throttle('watch', file, 5)) return;
- if (!newStats || newStats && newStats.mtime.getTime() === 0) {
- fs.stat(file, function(error, newStats) {
- // Fix issues where mtime is null but file is still present
- if (error) {
- this._remove(dirname, basename);
- } else {
- // Check that change event was not fired because of changed only accessTime.
- var at = newStats.atime.getTime();
- var mt = newStats.mtime.getTime();
- if (!at || at <= mt || mt !== prevStats.mtime.getTime()) {
- this._emit('change', file, newStats);
- }
- prevStats = newStats;
- }
- }.bind(this));
- // add is about to be emitted if file not already tracked in parent
- } else if (parent.has(basename)) {
- // Check that change event was not fired because of changed only accessTime.
- var at = newStats.atime.getTime();
- var mt = newStats.mtime.getTime();
- if (!at || at <= mt || mt !== prevStats.mtime.getTime()) {
- this._emit('change', file, newStats);
- }
- prevStats = newStats;
- }
- }.bind(this));
-
- // emit an add event if we're supposed to
- if (!(initialAdd && this.options.ignoreInitial)) {
- if (!this._throttle('add', file, 0)) return;
- this._emit('add', file, stats);
- }
-
- if (callback) callback();
- return closer;
- };
-
- // Private method: Handle symlinks encountered while reading a dir
-
- // * entry - object, entry object returned by readdirp
- // * directory - string, path of the directory being read
- // * path - string, path of this item
- // * item - string, basename of this item
-
- // Returns true if no more processing is needed for this entry.
- NodeFsHandler.prototype._handleSymlink =
- function(entry, directory, path, item) {
- var full = entry.fullPath;
- var dir = this._getWatchedDir(directory);
-
- if (!this.options.followSymlinks) {
- // watch symlink directly (don't follow) and detect changes
- this._readyCount++;
- fs.realpath(path, function(error, linkPath) {
- if (dir.has(item)) {
- if (this._symlinkPaths[full] !== linkPath) {
- this._symlinkPaths[full] = linkPath;
- this._emit('change', path, entry.stat);
- }
- } else {
- dir.add(item);
- this._symlinkPaths[full] = linkPath;
- this._emit('add', path, entry.stat);
- }
- this._emitReady();
- }.bind(this));
- return true;
- }
-
- // don't follow the same symlink more than once
- if (this._symlinkPaths[full]) return true;
- else this._symlinkPaths[full] = true;
- };
-
- // Private method: Read directory to add / remove files from `@watched` list
- // and re-read it on change.
-
- // * dir - string, fs path.
- // * stats - object, result of fs.stat
- // * initialAdd - boolean, was the file added at watch instantiation?
- // * depth - int, depth relative to user-supplied path
- // * target - string, child path actually targeted for watch
- // * wh - object, common watch helpers for this path
- // * callback - function, called when dir scan is complete
-
- // Returns close function for the watcher instance
- NodeFsHandler.prototype._handleDir =
- function(dir, stats, initialAdd, depth, target, wh, callback) {
- var parentDir = this._getWatchedDir(sysPath.dirname(dir));
- var tracked = parentDir.has(sysPath.basename(dir));
- if (!(initialAdd && this.options.ignoreInitial) && !target && !tracked) {
- if (!wh.hasGlob || wh.globFilter(dir)) this._emit('addDir', dir, stats);
- }
-
- // ensure dir is tracked (harmless if redundant)
- parentDir.add(sysPath.basename(dir));
- this._getWatchedDir(dir);
-
- var read = function(directory, initialAdd, done) {
- // Normalize the directory name on Windows
- directory = sysPath.join(directory, '');
-
- if (!wh.hasGlob) {
- var throttler = this._throttle('readdir', directory, 1000);
- if (!throttler) return;
- }
-
- var previous = this._getWatchedDir(wh.path);
- var current = [];
-
- readdirp({
- root: directory,
- entryType: 'all',
- fileFilter: wh.filterPath,
- directoryFilter: wh.filterDir,
- depth: 0,
- lstat: true
- }).on('data', function(entry) {
- var item = entry.path;
- var path = sysPath.join(directory, item);
- current.push(item);
-
- if (entry.stat.isSymbolicLink() &&
- this._handleSymlink(entry, directory, path, item)) return;
-
- // Files that present in current directory snapshot
- // but absent in previous are added to watch list and
- // emit `add` event.
- if (item === target || !target && !previous.has(item)) {
- this._readyCount++;
-
- // ensure relativeness of path is preserved in case of watcher reuse
- path = sysPath.join(dir, sysPath.relative(dir, path));
-
- this._addToNodeFs(path, initialAdd, wh, depth + 1);
- }
- }.bind(this)).on('end', function() {
- var wasThrottled = throttler ? throttler.clear() : false;
- if (done) done();
-
- // Files that absent in current directory snapshot
- // but present in previous emit `remove` event
- // and are removed from @watched[directory].
- previous.children().filter(function(item) {
- return item !== directory &&
- current.indexOf(item) === -1 &&
- // in case of intersecting globs;
- // a path may have been filtered out of this readdir, but
- // shouldn't be removed because it matches a different glob
- (!wh.hasGlob || wh.filterPath({
- fullPath: sysPath.resolve(directory, item)
- }));
- }).forEach(function(item) {
- this._remove(directory, item);
- }, this);
-
- // one more time for any missed in case changes came in extremely quickly
- if (wasThrottled) read(directory, false);
- }.bind(this)).on('error', this._handleError.bind(this));
- }.bind(this);
-
- var closer;
-
- if (this.options.depth == null || depth <= this.options.depth) {
- if (!target) read(dir, initialAdd, callback);
- closer = this._watchWithNodeFs(dir, function(dirPath, stats) {
- // if current directory is removed, do nothing
- if (stats && stats.mtime.getTime() === 0) return;
-
- read(dirPath, false);
- });
- } else {
- callback();
- }
- return closer;
- };
-
- // Private method: Handle added file, directory, or glob pattern.
- // Delegates call to _handleFile / _handleDir after checks.
-
- // * path - string, path to file or directory.
- // * initialAdd - boolean, was the file added at watch instantiation?
- // * depth - int, depth relative to user-supplied path
- // * target - string, child path actually targeted for watch
- // * callback - function, indicates whether the path was found or not
-
- // Returns nothing
- NodeFsHandler.prototype._addToNodeFs =
- function(path, initialAdd, priorWh, depth, target, callback) {
- if (!callback) callback = Function.prototype;
- var ready = this._emitReady;
- if (this._isIgnored(path) || this.closed) {
- ready();
- return callback(null, false);
- }
-
- var wh = this._getWatchHelpers(path, depth);
- if (!wh.hasGlob && priorWh) {
- wh.hasGlob = priorWh.hasGlob;
- wh.globFilter = priorWh.globFilter;
- wh.filterPath = priorWh.filterPath;
- wh.filterDir = priorWh.filterDir;
- }
-
- // evaluate what is at the path we're being asked to watch
- fs[wh.statMethod](wh.watchPath, function(error, stats) {
- if (this._handleError(error)) return callback(null, path);
- if (this._isIgnored(wh.watchPath, stats)) {
- ready();
- return callback(null, false);
- }
-
- var initDir = function(dir, target) {
- return this._handleDir(dir, stats, initialAdd, depth, target, wh, ready);
- }.bind(this);
-
- var closer;
- if (stats.isDirectory()) {
- closer = initDir(wh.watchPath, target);
- } else if (stats.isSymbolicLink()) {
- var parent = sysPath.dirname(wh.watchPath);
- this._getWatchedDir(parent).add(wh.watchPath);
- this._emit('add', wh.watchPath, stats);
- closer = initDir(parent, path);
-
- // preserve this symlink's target path
- fs.realpath(path, function(error, targetPath) {
- this._symlinkPaths[sysPath.resolve(path)] = targetPath;
- ready();
- }.bind(this));
- } else {
- closer = this._handleFile(wh.watchPath, stats, initialAdd, ready);
- }
-
- if (closer) {
- this._closers[path] = this._closers[path] || [];
- this._closers[path].push(closer);
- }
- callback(null, false);
- }.bind(this));
- };
-
- module.exports = NodeFsHandler;
|