You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

nodefs-handler.js 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. 'use strict';
  2. var fs = require('fs');
  3. var sysPath = require('path');
  4. var readdirp = require('readdirp');
  5. var isBinaryPath = require('is-binary-path');
  6. // fs.watch helpers
  7. // object to hold per-process fs.watch instances
  8. // (may be shared across chokidar FSWatcher instances)
  9. var FsWatchInstances = Object.create(null);
  10. // Private function: Instantiates the fs.watch interface
  11. // * path - string, path to be watched
  12. // * options - object, options to be passed to fs.watch
  13. // * listener - function, main event handler
  14. // * errHandler - function, handler which emits info about errors
  15. // * emitRaw - function, handler which emits raw event data
  16. // Returns new fsevents instance
  17. function createFsWatchInstance(path, options, listener, errHandler, emitRaw) {
  18. var handleEvent = function(rawEvent, evPath) {
  19. listener(path);
  20. emitRaw(rawEvent, evPath, {watchedPath: path});
  21. // emit based on events occurring for files from a directory's watcher in
  22. // case the file's watcher misses it (and rely on throttling to de-dupe)
  23. if (evPath && path !== evPath) {
  24. fsWatchBroadcast(
  25. sysPath.resolve(path, evPath), 'listeners', sysPath.join(path, evPath)
  26. );
  27. }
  28. };
  29. try {
  30. return fs.watch(path, options, handleEvent);
  31. } catch (error) {
  32. errHandler(error);
  33. }
  34. }
  35. // Private function: Helper for passing fs.watch event data to a
  36. // collection of listeners
  37. // * fullPath - string, absolute path bound to the fs.watch instance
  38. // * type - string, listener type
  39. // * val[1..3] - arguments to be passed to listeners
  40. // Returns nothing
  41. function fsWatchBroadcast(fullPath, type, val1, val2, val3) {
  42. if (!FsWatchInstances[fullPath]) return;
  43. FsWatchInstances[fullPath][type].forEach(function(listener) {
  44. listener(val1, val2, val3);
  45. });
  46. }
  47. // Private function: Instantiates the fs.watch interface or binds listeners
  48. // to an existing one covering the same file system entry
  49. // * path - string, path to be watched
  50. // * fullPath - string, absolute path
  51. // * options - object, options to be passed to fs.watch
  52. // * handlers - object, container for event listener functions
  53. // Returns close function
  54. function setFsWatchListener(path, fullPath, options, handlers) {
  55. var listener = handlers.listener;
  56. var errHandler = handlers.errHandler;
  57. var rawEmitter = handlers.rawEmitter;
  58. var container = FsWatchInstances[fullPath];
  59. var watcher;
  60. if (!options.persistent) {
  61. watcher = createFsWatchInstance(
  62. path, options, listener, errHandler, rawEmitter
  63. );
  64. return watcher.close.bind(watcher);
  65. }
  66. if (!container) {
  67. watcher = createFsWatchInstance(
  68. path,
  69. options,
  70. fsWatchBroadcast.bind(null, fullPath, 'listeners'),
  71. errHandler, // no need to use broadcast here
  72. fsWatchBroadcast.bind(null, fullPath, 'rawEmitters')
  73. );
  74. if (!watcher) return;
  75. var broadcastErr = fsWatchBroadcast.bind(null, fullPath, 'errHandlers');
  76. watcher.on('error', function(error) {
  77. container.watcherUnusable = true; // documented since Node 10.4.1
  78. // Workaround for https://github.com/joyent/node/issues/4337
  79. if (process.platform === 'win32' && error.code === 'EPERM') {
  80. fs.open(path, 'r', function(err, fd) {
  81. if (!err) fs.close(fd, function(err) {
  82. if (!err) broadcastErr(error);
  83. });
  84. });
  85. } else {
  86. broadcastErr(error);
  87. }
  88. });
  89. container = FsWatchInstances[fullPath] = {
  90. listeners: [listener],
  91. errHandlers: [errHandler],
  92. rawEmitters: [rawEmitter],
  93. watcher: watcher
  94. };
  95. } else {
  96. container.listeners.push(listener);
  97. container.errHandlers.push(errHandler);
  98. container.rawEmitters.push(rawEmitter);
  99. }
  100. var listenerIndex = container.listeners.length - 1;
  101. // removes this instance's listeners and closes the underlying fs.watch
  102. // instance if there are no more listeners left
  103. return function close() {
  104. delete container.listeners[listenerIndex];
  105. delete container.errHandlers[listenerIndex];
  106. delete container.rawEmitters[listenerIndex];
  107. if (!Object.keys(container.listeners).length) {
  108. if (!container.watcherUnusable) { // check to protect against issue #730
  109. container.watcher.close();
  110. }
  111. delete FsWatchInstances[fullPath];
  112. }
  113. };
  114. }
  115. // fs.watchFile helpers
  116. // object to hold per-process fs.watchFile instances
  117. // (may be shared across chokidar FSWatcher instances)
  118. var FsWatchFileInstances = Object.create(null);
  119. // Private function: Instantiates the fs.watchFile interface or binds listeners
  120. // to an existing one covering the same file system entry
  121. // * path - string, path to be watched
  122. // * fullPath - string, absolute path
  123. // * options - object, options to be passed to fs.watchFile
  124. // * handlers - object, container for event listener functions
  125. // Returns close function
  126. function setFsWatchFileListener(path, fullPath, options, handlers) {
  127. var listener = handlers.listener;
  128. var rawEmitter = handlers.rawEmitter;
  129. var container = FsWatchFileInstances[fullPath];
  130. var listeners = [];
  131. var rawEmitters = [];
  132. if (
  133. container && (
  134. container.options.persistent < options.persistent ||
  135. container.options.interval > options.interval
  136. )
  137. ) {
  138. // "Upgrade" the watcher to persistence or a quicker interval.
  139. // This creates some unlikely edge case issues if the user mixes
  140. // settings in a very weird way, but solving for those cases
  141. // doesn't seem worthwhile for the added complexity.
  142. listeners = container.listeners;
  143. rawEmitters = container.rawEmitters;
  144. fs.unwatchFile(fullPath);
  145. container = false;
  146. }
  147. if (!container) {
  148. listeners.push(listener);
  149. rawEmitters.push(rawEmitter);
  150. container = FsWatchFileInstances[fullPath] = {
  151. listeners: listeners,
  152. rawEmitters: rawEmitters,
  153. options: options,
  154. watcher: fs.watchFile(fullPath, options, function(curr, prev) {
  155. container.rawEmitters.forEach(function(rawEmitter) {
  156. rawEmitter('change', fullPath, {curr: curr, prev: prev});
  157. });
  158. var currmtime = curr.mtime.getTime();
  159. if (curr.size !== prev.size || currmtime > prev.mtime.getTime() || currmtime === 0) {
  160. container.listeners.forEach(function(listener) {
  161. listener(path, curr);
  162. });
  163. }
  164. })
  165. };
  166. } else {
  167. container.listeners.push(listener);
  168. container.rawEmitters.push(rawEmitter);
  169. }
  170. var listenerIndex = container.listeners.length - 1;
  171. // removes this instance's listeners and closes the underlying fs.watchFile
  172. // instance if there are no more listeners left
  173. return function close() {
  174. delete container.listeners[listenerIndex];
  175. delete container.rawEmitters[listenerIndex];
  176. if (!Object.keys(container.listeners).length) {
  177. fs.unwatchFile(fullPath);
  178. delete FsWatchFileInstances[fullPath];
  179. }
  180. };
  181. }
  182. // fake constructor for attaching nodefs-specific prototype methods that
  183. // will be copied to FSWatcher's prototype
  184. function NodeFsHandler() {}
  185. // Private method: Watch file for changes with fs.watchFile or fs.watch.
  186. // * path - string, path to file or directory.
  187. // * listener - function, to be executed on fs change.
  188. // Returns close function for the watcher instance
  189. NodeFsHandler.prototype._watchWithNodeFs =
  190. function(path, listener) {
  191. var directory = sysPath.dirname(path);
  192. var basename = sysPath.basename(path);
  193. var parent = this._getWatchedDir(directory);
  194. parent.add(basename);
  195. var absolutePath = sysPath.resolve(path);
  196. var options = {persistent: this.options.persistent};
  197. if (!listener) listener = Function.prototype; // empty function
  198. var closer;
  199. if (this.options.usePolling) {
  200. options.interval = this.enableBinaryInterval && isBinaryPath(basename) ?
  201. this.options.binaryInterval : this.options.interval;
  202. closer = setFsWatchFileListener(path, absolutePath, options, {
  203. listener: listener,
  204. rawEmitter: this.emit.bind(this, 'raw')
  205. });
  206. } else {
  207. closer = setFsWatchListener(path, absolutePath, options, {
  208. listener: listener,
  209. errHandler: this._handleError.bind(this),
  210. rawEmitter: this.emit.bind(this, 'raw')
  211. });
  212. }
  213. return closer;
  214. };
  215. // Private method: Watch a file and emit add event if warranted
  216. // * file - string, the file's path
  217. // * stats - object, result of fs.stat
  218. // * initialAdd - boolean, was the file added at watch instantiation?
  219. // * callback - function, called when done processing as a newly seen file
  220. // Returns close function for the watcher instance
  221. NodeFsHandler.prototype._handleFile =
  222. function(file, stats, initialAdd, callback) {
  223. var dirname = sysPath.dirname(file);
  224. var basename = sysPath.basename(file);
  225. var parent = this._getWatchedDir(dirname);
  226. // stats is always present
  227. var prevStats = stats;
  228. // if the file is already being watched, do nothing
  229. if (parent.has(basename)) return callback();
  230. // kick off the watcher
  231. var closer = this._watchWithNodeFs(file, function(path, newStats) {
  232. if (!this._throttle('watch', file, 5)) return;
  233. if (!newStats || newStats && newStats.mtime.getTime() === 0) {
  234. fs.stat(file, function(error, newStats) {
  235. // Fix issues where mtime is null but file is still present
  236. if (error) {
  237. this._remove(dirname, basename);
  238. } else {
  239. // Check that change event was not fired because of changed only accessTime.
  240. var at = newStats.atime.getTime();
  241. var mt = newStats.mtime.getTime();
  242. if (!at || at <= mt || mt !== prevStats.mtime.getTime()) {
  243. this._emit('change', file, newStats);
  244. }
  245. prevStats = newStats;
  246. }
  247. }.bind(this));
  248. // add is about to be emitted if file not already tracked in parent
  249. } else if (parent.has(basename)) {
  250. // Check that change event was not fired because of changed only accessTime.
  251. var at = newStats.atime.getTime();
  252. var mt = newStats.mtime.getTime();
  253. if (!at || at <= mt || mt !== prevStats.mtime.getTime()) {
  254. this._emit('change', file, newStats);
  255. }
  256. prevStats = newStats;
  257. }
  258. }.bind(this));
  259. // emit an add event if we're supposed to
  260. if (!(initialAdd && this.options.ignoreInitial)) {
  261. if (!this._throttle('add', file, 0)) return;
  262. this._emit('add', file, stats);
  263. }
  264. if (callback) callback();
  265. return closer;
  266. };
  267. // Private method: Handle symlinks encountered while reading a dir
  268. // * entry - object, entry object returned by readdirp
  269. // * directory - string, path of the directory being read
  270. // * path - string, path of this item
  271. // * item - string, basename of this item
  272. // Returns true if no more processing is needed for this entry.
  273. NodeFsHandler.prototype._handleSymlink =
  274. function(entry, directory, path, item) {
  275. var full = entry.fullPath;
  276. var dir = this._getWatchedDir(directory);
  277. if (!this.options.followSymlinks) {
  278. // watch symlink directly (don't follow) and detect changes
  279. this._readyCount++;
  280. fs.realpath(path, function(error, linkPath) {
  281. if (dir.has(item)) {
  282. if (this._symlinkPaths[full] !== linkPath) {
  283. this._symlinkPaths[full] = linkPath;
  284. this._emit('change', path, entry.stat);
  285. }
  286. } else {
  287. dir.add(item);
  288. this._symlinkPaths[full] = linkPath;
  289. this._emit('add', path, entry.stat);
  290. }
  291. this._emitReady();
  292. }.bind(this));
  293. return true;
  294. }
  295. // don't follow the same symlink more than once
  296. if (this._symlinkPaths[full]) return true;
  297. else this._symlinkPaths[full] = true;
  298. };
  299. // Private method: Read directory to add / remove files from `@watched` list
  300. // and re-read it on change.
  301. // * dir - string, fs path.
  302. // * stats - object, result of fs.stat
  303. // * initialAdd - boolean, was the file added at watch instantiation?
  304. // * depth - int, depth relative to user-supplied path
  305. // * target - string, child path actually targeted for watch
  306. // * wh - object, common watch helpers for this path
  307. // * callback - function, called when dir scan is complete
  308. // Returns close function for the watcher instance
  309. NodeFsHandler.prototype._handleDir =
  310. function(dir, stats, initialAdd, depth, target, wh, callback) {
  311. var parentDir = this._getWatchedDir(sysPath.dirname(dir));
  312. var tracked = parentDir.has(sysPath.basename(dir));
  313. if (!(initialAdd && this.options.ignoreInitial) && !target && !tracked) {
  314. if (!wh.hasGlob || wh.globFilter(dir)) this._emit('addDir', dir, stats);
  315. }
  316. // ensure dir is tracked (harmless if redundant)
  317. parentDir.add(sysPath.basename(dir));
  318. this._getWatchedDir(dir);
  319. var read = function(directory, initialAdd, done) {
  320. // Normalize the directory name on Windows
  321. directory = sysPath.join(directory, '');
  322. if (!wh.hasGlob) {
  323. var throttler = this._throttle('readdir', directory, 1000);
  324. if (!throttler) return;
  325. }
  326. var previous = this._getWatchedDir(wh.path);
  327. var current = [];
  328. readdirp({
  329. root: directory,
  330. entryType: 'all',
  331. fileFilter: wh.filterPath,
  332. directoryFilter: wh.filterDir,
  333. depth: 0,
  334. lstat: true
  335. }).on('data', function(entry) {
  336. var item = entry.path;
  337. var path = sysPath.join(directory, item);
  338. current.push(item);
  339. if (entry.stat.isSymbolicLink() &&
  340. this._handleSymlink(entry, directory, path, item)) return;
  341. // Files that present in current directory snapshot
  342. // but absent in previous are added to watch list and
  343. // emit `add` event.
  344. if (item === target || !target && !previous.has(item)) {
  345. this._readyCount++;
  346. // ensure relativeness of path is preserved in case of watcher reuse
  347. path = sysPath.join(dir, sysPath.relative(dir, path));
  348. this._addToNodeFs(path, initialAdd, wh, depth + 1);
  349. }
  350. }.bind(this)).on('end', function() {
  351. var wasThrottled = throttler ? throttler.clear() : false;
  352. if (done) done();
  353. // Files that absent in current directory snapshot
  354. // but present in previous emit `remove` event
  355. // and are removed from @watched[directory].
  356. previous.children().filter(function(item) {
  357. return item !== directory &&
  358. current.indexOf(item) === -1 &&
  359. // in case of intersecting globs;
  360. // a path may have been filtered out of this readdir, but
  361. // shouldn't be removed because it matches a different glob
  362. (!wh.hasGlob || wh.filterPath({
  363. fullPath: sysPath.resolve(directory, item)
  364. }));
  365. }).forEach(function(item) {
  366. this._remove(directory, item);
  367. }, this);
  368. // one more time for any missed in case changes came in extremely quickly
  369. if (wasThrottled) read(directory, false);
  370. }.bind(this)).on('error', this._handleError.bind(this));
  371. }.bind(this);
  372. var closer;
  373. if (this.options.depth == null || depth <= this.options.depth) {
  374. if (!target) read(dir, initialAdd, callback);
  375. closer = this._watchWithNodeFs(dir, function(dirPath, stats) {
  376. // if current directory is removed, do nothing
  377. if (stats && stats.mtime.getTime() === 0) return;
  378. read(dirPath, false);
  379. });
  380. } else {
  381. callback();
  382. }
  383. return closer;
  384. };
  385. // Private method: Handle added file, directory, or glob pattern.
  386. // Delegates call to _handleFile / _handleDir after checks.
  387. // * path - string, path to file or directory.
  388. // * initialAdd - boolean, was the file added at watch instantiation?
  389. // * depth - int, depth relative to user-supplied path
  390. // * target - string, child path actually targeted for watch
  391. // * callback - function, indicates whether the path was found or not
  392. // Returns nothing
  393. NodeFsHandler.prototype._addToNodeFs =
  394. function(path, initialAdd, priorWh, depth, target, callback) {
  395. if (!callback) callback = Function.prototype;
  396. var ready = this._emitReady;
  397. if (this._isIgnored(path) || this.closed) {
  398. ready();
  399. return callback(null, false);
  400. }
  401. var wh = this._getWatchHelpers(path, depth);
  402. if (!wh.hasGlob && priorWh) {
  403. wh.hasGlob = priorWh.hasGlob;
  404. wh.globFilter = priorWh.globFilter;
  405. wh.filterPath = priorWh.filterPath;
  406. wh.filterDir = priorWh.filterDir;
  407. }
  408. // evaluate what is at the path we're being asked to watch
  409. fs[wh.statMethod](wh.watchPath, function(error, stats) {
  410. if (this._handleError(error)) return callback(null, path);
  411. if (this._isIgnored(wh.watchPath, stats)) {
  412. ready();
  413. return callback(null, false);
  414. }
  415. var initDir = function(dir, target) {
  416. return this._handleDir(dir, stats, initialAdd, depth, target, wh, ready);
  417. }.bind(this);
  418. var closer;
  419. if (stats.isDirectory()) {
  420. closer = initDir(wh.watchPath, target);
  421. } else if (stats.isSymbolicLink()) {
  422. var parent = sysPath.dirname(wh.watchPath);
  423. this._getWatchedDir(parent).add(wh.watchPath);
  424. this._emit('add', wh.watchPath, stats);
  425. closer = initDir(parent, path);
  426. // preserve this symlink's target path
  427. fs.realpath(path, function(error, targetPath) {
  428. this._symlinkPaths[sysPath.resolve(path)] = targetPath;
  429. ready();
  430. }.bind(this));
  431. } else {
  432. closer = this._handleFile(wh.watchPath, stats, initialAdd, ready);
  433. }
  434. if (closer) {
  435. this._closers[path] = this._closers[path] || [];
  436. this._closers[path].push(closer);
  437. }
  438. callback(null, false);
  439. }.bind(this));
  440. };
  441. module.exports = NodeFsHandler;