3 var fs = require('fs');
4 var sysPath = require('path');
5 var readdirp = require('readdirp');
6 var isBinaryPath = require('is-binary-path');
10 // object to hold per-process fs.watch instances
11 // (may be shared across chokidar FSWatcher instances)
12 var FsWatchInstances = Object.create(null);
14 // Private function: Instantiates the fs.watch interface
16 // * path - string, path to be watched
17 // * options - object, options to be passed to fs.watch
18 // * listener - function, main event handler
19 // * errHandler - function, handler which emits info about errors
20 // * emitRaw - function, handler which emits raw event data
22 // Returns new fsevents instance
23 function createFsWatchInstance(path, options, listener, errHandler, emitRaw) {
24 var handleEvent = function(rawEvent, evPath) {
26 emitRaw(rawEvent, evPath, {watchedPath: path});
28 // emit based on events occuring for files from a directory's watcher in
29 // case the file's watcher misses it (and rely on throttling to de-dupe)
30 if (evPath && path !== evPath) {
32 sysPath.resolve(path, evPath), 'listeners', sysPath.join(path, evPath)
37 return fs.watch(path, options, handleEvent);
43 // Private function: Helper for passing fs.watch event data to a
44 // collection of listeners
46 // * fullPath - string, absolute path bound to the fs.watch instance
47 // * type - string, listener type
48 // * val[1..3] - arguments to be passed to listeners
51 function fsWatchBroadcast(fullPath, type, val1, val2, val3) {
52 if (!FsWatchInstances[fullPath]) return;
53 FsWatchInstances[fullPath][type].forEach(function(listener) {
54 listener(val1, val2, val3);
58 // Private function: Instantiates the fs.watch interface or binds listeners
59 // to an existing one covering the same file system entry
61 // * path - string, path to be watched
62 // * fullPath - string, absolute path
63 // * options - object, options to be passed to fs.watch
64 // * handlers - object, container for event listener functions
66 // Returns close function
67 function setFsWatchListener(path, fullPath, options, handlers) {
68 var listener = handlers.listener;
69 var errHandler = handlers.errHandler;
70 var rawEmitter = handlers.rawEmitter;
71 var container = FsWatchInstances[fullPath];
73 if (!options.persistent) {
74 watcher = createFsWatchInstance(
75 path, options, listener, errHandler, rawEmitter
77 return watcher.close.bind(watcher);
80 watcher = createFsWatchInstance(
83 fsWatchBroadcast.bind(null, fullPath, 'listeners'),
84 errHandler, // no need to use broadcast here
85 fsWatchBroadcast.bind(null, fullPath, 'rawEmitters')
88 var broadcastErr = fsWatchBroadcast.bind(null, fullPath, 'errHandlers');
89 watcher.on('error', function(error) {
90 // Workaround for https://github.com/joyent/node/issues/4337
91 if (process.platform === 'win32' && error.code === 'EPERM') {
92 fs.open(path, 'r', function(err, fd) {
94 if (!err) broadcastErr(error);
100 container = FsWatchInstances[fullPath] = {
101 listeners: [listener],
102 errHandlers: [errHandler],
103 rawEmitters: [rawEmitter],
107 container.listeners.push(listener);
108 container.errHandlers.push(errHandler);
109 container.rawEmitters.push(rawEmitter);
111 var listenerIndex = container.listeners.length - 1;
113 // removes this instance's listeners and closes the underlying fs.watch
114 // instance if there are no more listeners left
115 return function close() {
116 delete container.listeners[listenerIndex];
117 delete container.errHandlers[listenerIndex];
118 delete container.rawEmitters[listenerIndex];
119 if (!Object.keys(container.listeners).length) {
120 container.watcher.close();
121 delete FsWatchInstances[fullPath];
126 // fs.watchFile helpers
128 // object to hold per-process fs.watchFile instances
129 // (may be shared across chokidar FSWatcher instances)
130 var FsWatchFileInstances = Object.create(null);
132 // Private function: Instantiates the fs.watchFile interface or binds listeners
133 // to an existing one covering the same file system entry
135 // * path - string, path to be watched
136 // * fullPath - string, absolute path
137 // * options - object, options to be passed to fs.watchFile
138 // * handlers - object, container for event listener functions
140 // Returns close function
141 function setFsWatchFileListener(path, fullPath, options, handlers) {
142 var listener = handlers.listener;
143 var rawEmitter = handlers.rawEmitter;
144 var container = FsWatchFileInstances[fullPath];
146 var rawEmitters = [];
149 container.options.persistent < options.persistent ||
150 container.options.interval > options.interval
153 // "Upgrade" the watcher to persistence or a quicker interval.
154 // This creates some unlikely edge case issues if the user mixes
155 // settings in a very weird way, but solving for those cases
156 // doesn't seem worthwhile for the added complexity.
157 listeners = container.listeners;
158 rawEmitters = container.rawEmitters;
159 fs.unwatchFile(fullPath);
163 listeners.push(listener);
164 rawEmitters.push(rawEmitter);
165 container = FsWatchFileInstances[fullPath] = {
166 listeners: listeners,
167 rawEmitters: rawEmitters,
169 watcher: fs.watchFile(fullPath, options, function(curr, prev) {
170 container.rawEmitters.forEach(function(rawEmitter) {
171 rawEmitter('change', fullPath, {curr: curr, prev: prev});
173 var currmtime = curr.mtime.getTime();
174 if (curr.size !== prev.size || currmtime > prev.mtime.getTime() || currmtime === 0) {
175 container.listeners.forEach(function(listener) {
176 listener(path, curr);
182 container.listeners.push(listener);
183 container.rawEmitters.push(rawEmitter);
185 var listenerIndex = container.listeners.length - 1;
187 // removes this instance's listeners and closes the underlying fs.watchFile
188 // instance if there are no more listeners left
189 return function close() {
190 delete container.listeners[listenerIndex];
191 delete container.rawEmitters[listenerIndex];
192 if (!Object.keys(container.listeners).length) {
193 fs.unwatchFile(fullPath);
194 delete FsWatchFileInstances[fullPath];
199 // fake constructor for attaching nodefs-specific prototype methods that
200 // will be copied to FSWatcher's prototype
201 function NodeFsHandler() {}
203 // Private method: Watch file for changes with fs.watchFile or fs.watch.
205 // * path - string, path to file or directory.
206 // * listener - function, to be executed on fs change.
208 // Returns close function for the watcher instance
209 NodeFsHandler.prototype._watchWithNodeFs =
210 function(path, listener) {
211 var directory = sysPath.dirname(path);
212 var basename = sysPath.basename(path);
213 var parent = this._getWatchedDir(directory);
214 parent.add(basename);
215 var absolutePath = sysPath.resolve(path);
216 var options = {persistent: this.options.persistent};
217 if (!listener) listener = Function.prototype; // empty function
220 if (this.options.usePolling) {
221 options.interval = this.enableBinaryInterval && isBinaryPath(basename) ?
222 this.options.binaryInterval : this.options.interval;
223 closer = setFsWatchFileListener(path, absolutePath, options, {
225 rawEmitter: this.emit.bind(this, 'raw')
228 closer = setFsWatchListener(path, absolutePath, options, {
230 errHandler: this._handleError.bind(this),
231 rawEmitter: this.emit.bind(this, 'raw')
237 // Private method: Watch a file and emit add event if warranted
239 // * file - string, the file's path
240 // * stats - object, result of fs.stat
241 // * initialAdd - boolean, was the file added at watch instantiation?
242 // * callback - function, called when done processing as a newly seen file
244 // Returns close function for the watcher instance
245 NodeFsHandler.prototype._handleFile =
246 function(file, stats, initialAdd, callback) {
247 var dirname = sysPath.dirname(file);
248 var basename = sysPath.basename(file);
249 var parent = this._getWatchedDir(dirname);
251 // if the file is already being watched, do nothing
252 if (parent.has(basename)) return callback();
254 // kick off the watcher
255 var closer = this._watchWithNodeFs(file, function(path, newStats) {
256 if (!this._throttle('watch', file, 5)) return;
257 if (!newStats || newStats && newStats.mtime.getTime() === 0) {
258 fs.stat(file, function(error, newStats) {
259 // Fix issues where mtime is null but file is still present
261 this._remove(dirname, basename);
263 this._emit('change', file, newStats);
266 // add is about to be emitted if file not already tracked in parent
267 } else if (parent.has(basename)) {
268 this._emit('change', file, newStats);
272 // emit an add event if we're supposed to
273 if (!(initialAdd && this.options.ignoreInitial)) {
274 if (!this._throttle('add', file, 0)) return;
275 this._emit('add', file, stats);
278 if (callback) callback();
282 // Private method: Handle symlinks encountered while reading a dir
284 // * entry - object, entry object returned by readdirp
285 // * directory - string, path of the directory being read
286 // * path - string, path of this item
287 // * item - string, basename of this item
289 // Returns true if no more processing is needed for this entry.
290 NodeFsHandler.prototype._handleSymlink =
291 function(entry, directory, path, item) {
292 var full = entry.fullPath;
293 var dir = this._getWatchedDir(directory);
295 if (!this.options.followSymlinks) {
296 // watch symlink directly (don't follow) and detect changes
298 fs.realpath(path, function(error, linkPath) {
300 if (this._symlinkPaths[full] !== linkPath) {
301 this._symlinkPaths[full] = linkPath;
302 this._emit('change', path, entry.stat);
306 this._symlinkPaths[full] = linkPath;
307 this._emit('add', path, entry.stat);
314 // don't follow the same symlink more than once
315 if (this._symlinkPaths[full]) return true;
316 else this._symlinkPaths[full] = true;
319 // Private method: Read directory to add / remove files from `@watched` list
320 // and re-read it on change.
322 // * dir - string, fs path.
323 // * stats - object, result of fs.stat
324 // * initialAdd - boolean, was the file added at watch instantiation?
325 // * depth - int, depth relative to user-supplied path
326 // * target - string, child path actually targeted for watch
327 // * wh - object, common watch helpers for this path
328 // * callback - function, called when dir scan is complete
330 // Returns close function for the watcher instance
331 NodeFsHandler.prototype._handleDir =
332 function(dir, stats, initialAdd, depth, target, wh, callback) {
333 var parentDir = this._getWatchedDir(sysPath.dirname(dir));
334 var tracked = parentDir.has(sysPath.basename(dir));
335 if (!(initialAdd && this.options.ignoreInitial) && !target && !tracked) {
336 if (!wh.hasGlob || wh.globFilter(dir)) this._emit('addDir', dir, stats);
339 // ensure dir is tracked (harmless if redundant)
340 parentDir.add(sysPath.basename(dir));
341 this._getWatchedDir(dir);
343 var read = function(directory, initialAdd, done) {
344 // Normalize the directory name on Windows
345 directory = sysPath.join(directory, '');
348 var throttler = this._throttle('readdir', directory, 1000);
349 if (!throttler) return;
352 var previous = this._getWatchedDir(wh.path);
358 fileFilter: wh.filterPath,
359 directoryFilter: wh.filterDir,
362 }).on('data', function(entry) {
363 var item = entry.path;
364 var path = sysPath.join(directory, item);
367 if (entry.stat.isSymbolicLink() &&
368 this._handleSymlink(entry, directory, path, item)) return;
370 // Files that present in current directory snapshot
371 // but absent in previous are added to watch list and
373 if (item === target || !target && !previous.has(item)) {
376 // ensure relativeness of path is preserved in case of watcher reuse
377 path = sysPath.join(dir, sysPath.relative(dir, path));
379 this._addToNodeFs(path, initialAdd, wh, depth + 1);
381 }.bind(this)).on('end', function() {
382 if (throttler) throttler.clear();
385 // Files that absent in current directory snapshot
386 // but present in previous emit `remove` event
387 // and are removed from @watched[directory].
388 previous.children().filter(function(item) {
389 return item !== directory &&
390 current.indexOf(item) === -1 &&
391 // in case of intersecting globs;
392 // a path may have been filtered out of this readdir, but
393 // shouldn't be removed because it matches a different glob
394 (!wh.hasGlob || wh.filterPath({
395 fullPath: sysPath.resolve(directory, item)
397 }).forEach(function(item) {
398 this._remove(directory, item);
400 }.bind(this)).on('error', this._handleError.bind(this));
405 if (this.options.depth == null || depth <= this.options.depth) {
406 if (!target) read(dir, initialAdd, callback);
407 closer = this._watchWithNodeFs(dir, function(dirPath, stats) {
408 // if current directory is removed, do nothing
409 if (stats && stats.mtime.getTime() === 0) return;
411 read(dirPath, false);
419 // Private method: Handle added file, directory, or glob pattern.
420 // Delegates call to _handleFile / _handleDir after checks.
422 // * path - string, path to file or directory.
423 // * initialAdd - boolean, was the file added at watch instantiation?
424 // * depth - int, depth relative to user-supplied path
425 // * target - string, child path actually targeted for watch
426 // * callback - function, indicates whether the path was found or not
429 NodeFsHandler.prototype._addToNodeFs =
430 function(path, initialAdd, priorWh, depth, target, callback) {
431 if (!callback) callback = Function.prototype;
432 var ready = this._emitReady;
433 if (this._isIgnored(path) || this.closed) {
435 return callback(null, false);
438 var wh = this._getWatchHelpers(path, depth);
439 if (!wh.hasGlob && priorWh) {
440 wh.hasGlob = priorWh.hasGlob;
441 wh.globFilter = priorWh.globFilter;
442 wh.filterPath = priorWh.filterPath;
443 wh.filterDir = priorWh.filterDir;
446 // evaluate what is at the path we're being asked to watch
447 fs[wh.statMethod](wh.watchPath, function(error, stats) {
448 if (this._handleError(error)) return callback(null, path);
449 if (this._isIgnored(wh.watchPath, stats)) {
451 return callback(null, false);
454 var initDir = function(dir, target) {
455 return this._handleDir(dir, stats, initialAdd, depth, target, wh, ready);
459 if (stats.isDirectory()) {
460 closer = initDir(wh.watchPath, target);
461 } else if (stats.isSymbolicLink()) {
462 var parent = sysPath.dirname(wh.watchPath);
463 this._getWatchedDir(parent).add(wh.watchPath);
464 this._emit('add', wh.watchPath, stats);
465 closer = initDir(parent, path);
467 // preserve this symlink's target path
468 fs.realpath(path, function(error, targetPath) {
469 this._symlinkPaths[sysPath.resolve(path)] = targetPath;
473 closer = this._handleFile(wh.watchPath, stats, initialAdd, ready);
476 if (closer) this._closers[path] = closer;
477 callback(null, false);
481 module.exports = NodeFsHandler;