fsevents-handler.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. 'use strict';
  2. var fs = require('fs');
  3. var sysPath = require('path');
  4. var readdirp = require('readdirp');
  5. var fsevents;
  6. try { fsevents = require('fsevents'); } catch (error) {
  7. if (process.env.CHOKIDAR_PRINT_FSEVENTS_REQUIRE_ERROR) console.error(error)
  8. }
  9. // fsevents instance helper functions
  10. // object to hold per-process fsevents instances
  11. // (may be shared across chokidar FSWatcher instances)
  12. var FSEventsWatchers = Object.create(null);
  13. // Threshold of duplicate path prefixes at which to start
  14. // consolidating going forward
  15. var consolidateThreshhold = 10;
  16. // Private function: Instantiates the fsevents interface
  17. // * path - string, path to be watched
  18. // * callback - function, called when fsevents is bound and ready
  19. // Returns new fsevents instance
  20. function createFSEventsInstance(path, callback) {
  21. return (new fsevents(path)).on('fsevent', callback).start();
  22. }
  23. // Private function: Instantiates the fsevents interface or binds listeners
  24. // to an existing one covering the same file tree
  25. // * path - string, path to be watched
  26. // * realPath - string, real path (in case of symlinks)
  27. // * listener - function, called when fsevents emits events
  28. // * rawEmitter - function, passes data to listeners of the 'raw' event
  29. // Returns close function
  30. function setFSEventsListener(path, realPath, listener, rawEmitter) {
  31. var watchPath = sysPath.extname(path) ? sysPath.dirname(path) : path;
  32. var watchContainer;
  33. var parentPath = sysPath.dirname(watchPath);
  34. // If we've accumulated a substantial number of paths that
  35. // could have been consolidated by watching one directory
  36. // above the current one, create a watcher on the parent
  37. // path instead, so that we do consolidate going forward.
  38. if (couldConsolidate(parentPath)) {
  39. watchPath = parentPath;
  40. }
  41. var resolvedPath = sysPath.resolve(path);
  42. var hasSymlink = resolvedPath !== realPath;
  43. function filteredListener(fullPath, flags, info) {
  44. if (hasSymlink) fullPath = fullPath.replace(realPath, resolvedPath);
  45. if (
  46. fullPath === resolvedPath ||
  47. !fullPath.indexOf(resolvedPath + sysPath.sep)
  48. ) listener(fullPath, flags, info);
  49. }
  50. // check if there is already a watcher on a parent path
  51. // modifies `watchPath` to the parent path when it finds a match
  52. function watchedParent() {
  53. return Object.keys(FSEventsWatchers).some(function(watchedPath) {
  54. // condition is met when indexOf returns 0
  55. if (!realPath.indexOf(sysPath.resolve(watchedPath) + sysPath.sep)) {
  56. watchPath = watchedPath;
  57. return true;
  58. }
  59. });
  60. }
  61. if (watchPath in FSEventsWatchers || watchedParent()) {
  62. watchContainer = FSEventsWatchers[watchPath];
  63. watchContainer.listeners.push(filteredListener);
  64. } else {
  65. watchContainer = FSEventsWatchers[watchPath] = {
  66. listeners: [filteredListener],
  67. rawEmitters: [rawEmitter],
  68. watcher: createFSEventsInstance(watchPath, function(fullPath, flags) {
  69. var info = fsevents.getInfo(fullPath, flags);
  70. watchContainer.listeners.forEach(function(listener) {
  71. listener(fullPath, flags, info);
  72. });
  73. watchContainer.rawEmitters.forEach(function(emitter) {
  74. emitter(info.event, fullPath, info);
  75. });
  76. })
  77. };
  78. }
  79. var listenerIndex = watchContainer.listeners.length - 1;
  80. // removes this instance's listeners and closes the underlying fsevents
  81. // instance if there are no more listeners left
  82. return function close() {
  83. delete watchContainer.listeners[listenerIndex];
  84. delete watchContainer.rawEmitters[listenerIndex];
  85. if (!Object.keys(watchContainer.listeners).length) {
  86. watchContainer.watcher.stop();
  87. delete FSEventsWatchers[watchPath];
  88. }
  89. };
  90. }
  91. // Decide whether or not we should start a new higher-level
  92. // parent watcher
  93. function couldConsolidate(path) {
  94. var keys = Object.keys(FSEventsWatchers);
  95. var count = 0;
  96. for (var i = 0, len = keys.length; i < len; ++i) {
  97. var watchPath = keys[i];
  98. if (watchPath.indexOf(path) === 0) {
  99. count++;
  100. if (count >= consolidateThreshhold) {
  101. return true;
  102. }
  103. }
  104. }
  105. return false;
  106. }
  107. function isConstructor(obj) {
  108. return obj.prototype !== undefined && obj.prototype.constructor !== undefined;
  109. }
  110. // returns boolean indicating whether fsevents can be used
  111. function canUse() {
  112. return fsevents && Object.keys(FSEventsWatchers).length < 128 && isConstructor(fsevents);
  113. }
  114. // determines subdirectory traversal levels from root to path
  115. function depth(path, root) {
  116. var i = 0;
  117. while (!path.indexOf(root) && (path = sysPath.dirname(path)) !== root) i++;
  118. return i;
  119. }
  120. // fake constructor for attaching fsevents-specific prototype methods that
  121. // will be copied to FSWatcher's prototype
  122. function FsEventsHandler() {}
  123. // Private method: Handle symlinks encountered during directory scan
  124. // * watchPath - string, file/dir path to be watched with fsevents
  125. // * realPath - string, real path (in case of symlinks)
  126. // * transform - function, path transformer
  127. // * globFilter - function, path filter in case a glob pattern was provided
  128. // Returns close function for the watcher instance
  129. FsEventsHandler.prototype._watchWithFsEvents =
  130. function(watchPath, realPath, transform, globFilter) {
  131. if (this._isIgnored(watchPath)) return;
  132. var watchCallback = function(fullPath, flags, info) {
  133. if (
  134. this.options.depth !== undefined &&
  135. depth(fullPath, realPath) > this.options.depth
  136. ) return;
  137. var path = transform(sysPath.join(
  138. watchPath, sysPath.relative(watchPath, fullPath)
  139. ));
  140. if (globFilter && !globFilter(path)) return;
  141. // ensure directories are tracked
  142. var parent = sysPath.dirname(path);
  143. var item = sysPath.basename(path);
  144. var watchedDir = this._getWatchedDir(
  145. info.type === 'directory' ? path : parent
  146. );
  147. var checkIgnored = function(stats) {
  148. if (this._isIgnored(path, stats)) {
  149. this._ignoredPaths[path] = true;
  150. if (stats && stats.isDirectory()) {
  151. this._ignoredPaths[path + '/**/*'] = true;
  152. }
  153. return true;
  154. } else {
  155. delete this._ignoredPaths[path];
  156. delete this._ignoredPaths[path + '/**/*'];
  157. }
  158. }.bind(this);
  159. var handleEvent = function(event) {
  160. if (checkIgnored()) return;
  161. if (event === 'unlink') {
  162. // suppress unlink events on never before seen files
  163. if (info.type === 'directory' || watchedDir.has(item)) {
  164. this._remove(parent, item);
  165. }
  166. } else {
  167. if (event === 'add') {
  168. // track new directories
  169. if (info.type === 'directory') this._getWatchedDir(path);
  170. if (info.type === 'symlink' && this.options.followSymlinks) {
  171. // push symlinks back to the top of the stack to get handled
  172. var curDepth = this.options.depth === undefined ?
  173. undefined : depth(fullPath, realPath) + 1;
  174. return this._addToFsEvents(path, false, true, curDepth);
  175. } else {
  176. // track new paths
  177. // (other than symlinks being followed, which will be tracked soon)
  178. this._getWatchedDir(parent).add(item);
  179. }
  180. }
  181. var eventName = info.type === 'directory' ? event + 'Dir' : event;
  182. this._emit(eventName, path);
  183. if (eventName === 'addDir') this._addToFsEvents(path, false, true);
  184. }
  185. }.bind(this);
  186. function addOrChange() {
  187. handleEvent(watchedDir.has(item) ? 'change' : 'add');
  188. }
  189. function checkFd() {
  190. fs.open(path, 'r', function(error, fd) {
  191. if (error) {
  192. error.code !== 'EACCES' ?
  193. handleEvent('unlink') : addOrChange();
  194. } else {
  195. fs.close(fd, function(err) {
  196. err && err.code !== 'EACCES' ?
  197. handleEvent('unlink') : addOrChange();
  198. });
  199. }
  200. });
  201. }
  202. // correct for wrong events emitted
  203. var wrongEventFlags = [
  204. 69888, 70400, 71424, 72704, 73472, 131328, 131840, 262912
  205. ];
  206. if (wrongEventFlags.indexOf(flags) !== -1 || info.event === 'unknown') {
  207. if (typeof this.options.ignored === 'function') {
  208. fs.stat(path, function(error, stats) {
  209. if (checkIgnored(stats)) return;
  210. stats ? addOrChange() : handleEvent('unlink');
  211. });
  212. } else {
  213. checkFd();
  214. }
  215. } else {
  216. switch (info.event) {
  217. case 'created':
  218. case 'modified':
  219. return addOrChange();
  220. case 'deleted':
  221. case 'moved':
  222. return checkFd();
  223. }
  224. }
  225. }.bind(this);
  226. var closer = setFSEventsListener(
  227. watchPath,
  228. realPath,
  229. watchCallback,
  230. this.emit.bind(this, 'raw')
  231. );
  232. this._emitReady();
  233. return closer;
  234. };
  235. // Private method: Handle symlinks encountered during directory scan
  236. // * linkPath - string, path to symlink
  237. // * fullPath - string, absolute path to the symlink
  238. // * transform - function, pre-existing path transformer
  239. // * curDepth - int, level of subdirectories traversed to where symlink is
  240. // Returns nothing
  241. FsEventsHandler.prototype._handleFsEventsSymlink =
  242. function(linkPath, fullPath, transform, curDepth) {
  243. // don't follow the same symlink more than once
  244. if (this._symlinkPaths[fullPath]) return;
  245. else this._symlinkPaths[fullPath] = true;
  246. this._readyCount++;
  247. fs.realpath(linkPath, function(error, linkTarget) {
  248. if (this._handleError(error) || this._isIgnored(linkTarget)) {
  249. return this._emitReady();
  250. }
  251. this._readyCount++;
  252. // add the linkTarget for watching with a wrapper for transform
  253. // that causes emitted paths to incorporate the link's path
  254. this._addToFsEvents(linkTarget || linkPath, function(path) {
  255. var dotSlash = '.' + sysPath.sep;
  256. var aliasedPath = linkPath;
  257. if (linkTarget && linkTarget !== dotSlash) {
  258. aliasedPath = path.replace(linkTarget, linkPath);
  259. } else if (path !== dotSlash) {
  260. aliasedPath = sysPath.join(linkPath, path);
  261. }
  262. return transform(aliasedPath);
  263. }, false, curDepth);
  264. }.bind(this));
  265. };
  266. // Private method: Handle added path with fsevents
  267. // * path - string, file/directory path or glob pattern
  268. // * transform - function, converts working path to what the user expects
  269. // * forceAdd - boolean, ensure add is emitted
  270. // * priorDepth - int, level of subdirectories already traversed
  271. // Returns nothing
  272. FsEventsHandler.prototype._addToFsEvents =
  273. function(path, transform, forceAdd, priorDepth) {
  274. // applies transform if provided, otherwise returns same value
  275. var processPath = typeof transform === 'function' ?
  276. transform : function(val) { return val; };
  277. var emitAdd = function(newPath, stats) {
  278. var pp = processPath(newPath);
  279. var isDir = stats.isDirectory();
  280. var dirObj = this._getWatchedDir(sysPath.dirname(pp));
  281. var base = sysPath.basename(pp);
  282. // ensure empty dirs get tracked
  283. if (isDir) this._getWatchedDir(pp);
  284. if (dirObj.has(base)) return;
  285. dirObj.add(base);
  286. if (!this.options.ignoreInitial || forceAdd === true) {
  287. this._emit(isDir ? 'addDir' : 'add', pp, stats);
  288. }
  289. }.bind(this);
  290. var wh = this._getWatchHelpers(path);
  291. // evaluate what is at the path we're being asked to watch
  292. fs[wh.statMethod](wh.watchPath, function(error, stats) {
  293. if (this._handleError(error) || this._isIgnored(wh.watchPath, stats)) {
  294. this._emitReady();
  295. return this._emitReady();
  296. }
  297. if (stats.isDirectory()) {
  298. // emit addDir unless this is a glob parent
  299. if (!wh.globFilter) emitAdd(processPath(path), stats);
  300. // don't recurse further if it would exceed depth setting
  301. if (priorDepth && priorDepth > this.options.depth) return;
  302. // scan the contents of the dir
  303. readdirp({
  304. root: wh.watchPath,
  305. entryType: 'all',
  306. fileFilter: wh.filterPath,
  307. directoryFilter: wh.filterDir,
  308. lstat: true,
  309. depth: this.options.depth - (priorDepth || 0)
  310. }).on('data', function(entry) {
  311. // need to check filterPath on dirs b/c filterDir is less restrictive
  312. if (entry.stat.isDirectory() && !wh.filterPath(entry)) return;
  313. var joinedPath = sysPath.join(wh.watchPath, entry.path);
  314. var fullPath = entry.fullPath;
  315. if (wh.followSymlinks && entry.stat.isSymbolicLink()) {
  316. // preserve the current depth here since it can't be derived from
  317. // real paths past the symlink
  318. var curDepth = this.options.depth === undefined ?
  319. undefined : depth(joinedPath, sysPath.resolve(wh.watchPath)) + 1;
  320. this._handleFsEventsSymlink(joinedPath, fullPath, processPath, curDepth);
  321. } else {
  322. emitAdd(joinedPath, entry.stat);
  323. }
  324. }.bind(this)).on('error', function() {
  325. // Ignore readdirp errors
  326. }).on('end', this._emitReady);
  327. } else {
  328. emitAdd(wh.watchPath, stats);
  329. this._emitReady();
  330. }
  331. }.bind(this));
  332. if (this.options.persistent && forceAdd !== true) {
  333. var initWatch = function(error, realPath) {
  334. if (this.closed) return;
  335. var closer = this._watchWithFsEvents(
  336. wh.watchPath,
  337. sysPath.resolve(realPath || wh.watchPath),
  338. processPath,
  339. wh.globFilter
  340. );
  341. if (closer) {
  342. this._closers[path] = this._closers[path] || [];
  343. this._closers[path].push(closer);
  344. }
  345. }.bind(this);
  346. if (typeof transform === 'function') {
  347. // realpath has already been resolved
  348. initWatch();
  349. } else {
  350. fs.realpath(wh.watchPath, initWatch);
  351. }
  352. }
  353. };
  354. module.exports = FsEventsHandler;
  355. module.exports.canUse = canUse;