Layout von Websiten mit Bootstrap und Foundation
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.

fsevents-handler.js 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. 'use strict';
  2. const fs = require('fs');
  3. const sysPath = require('path');
  4. const { promisify } = require('util');
  5. let fsevents;
  6. try {
  7. fsevents = require('fsevents');
  8. } catch (error) {
  9. if (process.env.CHOKIDAR_PRINT_FSEVENTS_REQUIRE_ERROR) console.error(error);
  10. }
  11. if (fsevents) {
  12. // TODO: real check
  13. const mtch = process.version.match(/v(\d+)\.(\d+)/);
  14. if (mtch && mtch[1] && mtch[2]) {
  15. const maj = Number.parseInt(mtch[1], 10);
  16. const min = Number.parseInt(mtch[2], 10);
  17. if (maj === 8 && min < 16) {
  18. fsevents = undefined;
  19. }
  20. }
  21. }
  22. const {
  23. EV_ADD,
  24. EV_CHANGE,
  25. EV_ADD_DIR,
  26. EV_UNLINK,
  27. EV_ERROR,
  28. STR_DATA,
  29. STR_END,
  30. FSEVENT_CREATED,
  31. FSEVENT_MODIFIED,
  32. FSEVENT_DELETED,
  33. FSEVENT_MOVED,
  34. // FSEVENT_CLONED,
  35. FSEVENT_UNKNOWN,
  36. FSEVENT_TYPE_FILE,
  37. FSEVENT_TYPE_DIRECTORY,
  38. FSEVENT_TYPE_SYMLINK,
  39. ROOT_GLOBSTAR,
  40. DIR_SUFFIX,
  41. DOT_SLASH,
  42. FUNCTION_TYPE,
  43. EMPTY_FN,
  44. IDENTITY_FN
  45. } = require('./constants');
  46. const Depth = (value) => isNaN(value) ? {} : {depth: value};
  47. const stat = promisify(fs.stat);
  48. const lstat = promisify(fs.lstat);
  49. const realpath = promisify(fs.realpath);
  50. const statMethods = { stat, lstat };
  51. /**
  52. * @typedef {String} Path
  53. */
  54. /**
  55. * @typedef {Object} FsEventsWatchContainer
  56. * @property {Set<Function>} listeners
  57. * @property {Function} rawEmitter
  58. * @property {{stop: Function}} watcher
  59. */
  60. // fsevents instance helper functions
  61. /**
  62. * Object to hold per-process fsevents instances (may be shared across chokidar FSWatcher instances)
  63. * @type {Map<Path,FsEventsWatchContainer>}
  64. */
  65. const FSEventsWatchers = new Map();
  66. // Threshold of duplicate path prefixes at which to start
  67. // consolidating going forward
  68. const consolidateThreshhold = 10;
  69. const wrongEventFlags = new Set([
  70. 69888, 70400, 71424, 72704, 73472, 131328, 131840, 262912
  71. ]);
  72. /**
  73. * Instantiates the fsevents interface
  74. * @param {Path} path path to be watched
  75. * @param {Function} callback called when fsevents is bound and ready
  76. * @returns {{stop: Function}} new fsevents instance
  77. */
  78. const createFSEventsInstance = (path, callback) => {
  79. const stop = fsevents.watch(path, callback);
  80. return {stop};
  81. };
  82. /**
  83. * Instantiates the fsevents interface or binds listeners to an existing one covering
  84. * the same file tree.
  85. * @param {Path} path - to be watched
  86. * @param {Path} realPath - real path for symlinks
  87. * @param {Function} listener - called when fsevents emits events
  88. * @param {Function} rawEmitter - passes data to listeners of the 'raw' event
  89. * @returns {Function} closer
  90. */
  91. function setFSEventsListener(path, realPath, listener, rawEmitter, fsw) {
  92. let watchPath = sysPath.extname(path) ? sysPath.dirname(path) : path;
  93. const parentPath = sysPath.dirname(watchPath);
  94. let cont = FSEventsWatchers.get(watchPath);
  95. // If we've accumulated a substantial number of paths that
  96. // could have been consolidated by watching one directory
  97. // above the current one, create a watcher on the parent
  98. // path instead, so that we do consolidate going forward.
  99. if (couldConsolidate(parentPath)) {
  100. watchPath = parentPath;
  101. }
  102. const resolvedPath = sysPath.resolve(path);
  103. const hasSymlink = resolvedPath !== realPath;
  104. const filteredListener = (fullPath, flags, info) => {
  105. if (hasSymlink) fullPath = fullPath.replace(realPath, resolvedPath);
  106. if (
  107. fullPath === resolvedPath ||
  108. !fullPath.indexOf(resolvedPath + sysPath.sep)
  109. ) listener(fullPath, flags, info);
  110. };
  111. // check if there is already a watcher on a parent path
  112. // modifies `watchPath` to the parent path when it finds a match
  113. let watchedParent = false;
  114. for (const watchedPath of FSEventsWatchers.keys()) {
  115. if (realPath.indexOf(sysPath.resolve(watchedPath) + sysPath.sep) === 0) {
  116. watchPath = watchedPath;
  117. cont = FSEventsWatchers.get(watchPath);
  118. watchedParent = true;
  119. break;
  120. }
  121. }
  122. if (cont || watchedParent) {
  123. cont.listeners.add(filteredListener);
  124. } else {
  125. cont = {
  126. listeners: new Set([filteredListener]),
  127. rawEmitter,
  128. watcher: createFSEventsInstance(watchPath, (fullPath, flags) => {
  129. if (fsw.closed) return;
  130. const info = fsevents.getInfo(fullPath, flags);
  131. cont.listeners.forEach(list => {
  132. list(fullPath, flags, info);
  133. });
  134. cont.rawEmitter(info.event, fullPath, info);
  135. })
  136. };
  137. FSEventsWatchers.set(watchPath, cont);
  138. }
  139. // removes this instance's listeners and closes the underlying fsevents
  140. // instance if there are no more listeners left
  141. return () => {
  142. const lst = cont.listeners;
  143. lst.delete(filteredListener);
  144. if (!lst.size) {
  145. FSEventsWatchers.delete(watchPath);
  146. if (cont.watcher) return cont.watcher.stop().then(() => {
  147. cont.rawEmitter = cont.watcher = undefined;
  148. Object.freeze(cont);
  149. });
  150. }
  151. };
  152. }
  153. // Decide whether or not we should start a new higher-level
  154. // parent watcher
  155. const couldConsolidate = (path) => {
  156. let count = 0;
  157. for (const watchPath of FSEventsWatchers.keys()) {
  158. if (watchPath.indexOf(path) === 0) {
  159. count++;
  160. if (count >= consolidateThreshhold) {
  161. return true;
  162. }
  163. }
  164. }
  165. return false;
  166. };
  167. // returns boolean indicating whether fsevents can be used
  168. const canUse = () => fsevents && FSEventsWatchers.size < 128;
  169. // determines subdirectory traversal levels from root to path
  170. const calcDepth = (path, root) => {
  171. let i = 0;
  172. while (!path.indexOf(root) && (path = sysPath.dirname(path)) !== root) i++;
  173. return i;
  174. };
  175. // returns boolean indicating whether the fsevents' event info has the same type
  176. // as the one returned by fs.stat
  177. const sameTypes = (info, stats) => (
  178. info.type === FSEVENT_TYPE_DIRECTORY && stats.isDirectory() ||
  179. info.type === FSEVENT_TYPE_SYMLINK && stats.isSymbolicLink() ||
  180. info.type === FSEVENT_TYPE_FILE && stats.isFile()
  181. )
  182. /**
  183. * @mixin
  184. */
  185. class FsEventsHandler {
  186. /**
  187. * @param {import('../index').FSWatcher} fsw
  188. */
  189. constructor(fsw) {
  190. this.fsw = fsw;
  191. }
  192. checkIgnored(path, stats) {
  193. const ipaths = this.fsw._ignoredPaths;
  194. if (this.fsw._isIgnored(path, stats)) {
  195. ipaths.add(path);
  196. if (stats && stats.isDirectory()) {
  197. ipaths.add(path + ROOT_GLOBSTAR);
  198. }
  199. return true;
  200. }
  201. ipaths.delete(path);
  202. ipaths.delete(path + ROOT_GLOBSTAR);
  203. }
  204. addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts) {
  205. const event = watchedDir.has(item) ? EV_CHANGE : EV_ADD;
  206. this.handleEvent(event, path, fullPath, realPath, parent, watchedDir, item, info, opts);
  207. }
  208. async checkExists(path, fullPath, realPath, parent, watchedDir, item, info, opts) {
  209. try {
  210. const stats = await stat(path)
  211. if (this.fsw.closed) return;
  212. if (this.fsw.closed) return;
  213. if (sameTypes(info, stats)) {
  214. this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
  215. } else {
  216. this.handleEvent(EV_UNLINK, path, fullPath, realPath, parent, watchedDir, item, info, opts);
  217. }
  218. } catch (error) {
  219. if (error.code === 'EACCES') {
  220. this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
  221. } else {
  222. this.handleEvent(EV_UNLINK, path, fullPath, realPath, parent, watchedDir, item, info, opts);
  223. }
  224. }
  225. }
  226. handleEvent(event, path, fullPath, realPath, parent, watchedDir, item, info, opts) {
  227. if (this.fsw.closed || this.checkIgnored(path)) return;
  228. if (event === EV_UNLINK) {
  229. const isDirectory = info.type === FSEVENT_TYPE_DIRECTORY
  230. // suppress unlink events on never before seen files
  231. if (isDirectory || watchedDir.has(item)) {
  232. this.fsw._remove(parent, item, isDirectory);
  233. }
  234. } else {
  235. if (event === EV_ADD) {
  236. // track new directories
  237. if (info.type === FSEVENT_TYPE_DIRECTORY) this.fsw._getWatchedDir(path);
  238. if (info.type === FSEVENT_TYPE_SYMLINK && opts.followSymlinks) {
  239. // push symlinks back to the top of the stack to get handled
  240. const curDepth = opts.depth === undefined ?
  241. undefined : calcDepth(fullPath, realPath) + 1;
  242. return this._addToFsEvents(path, false, true, curDepth);
  243. }
  244. // track new paths
  245. // (other than symlinks being followed, which will be tracked soon)
  246. this.fsw._getWatchedDir(parent).add(item);
  247. }
  248. /**
  249. * @type {'add'|'addDir'|'unlink'|'unlinkDir'}
  250. */
  251. const eventName = info.type === FSEVENT_TYPE_DIRECTORY ? event + DIR_SUFFIX : event;
  252. this.fsw._emit(eventName, path);
  253. if (eventName === EV_ADD_DIR) this._addToFsEvents(path, false, true);
  254. }
  255. }
  256. /**
  257. * Handle symlinks encountered during directory scan
  258. * @param {String} watchPath - file/dir path to be watched with fsevents
  259. * @param {String} realPath - real path (in case of symlinks)
  260. * @param {Function} transform - path transformer
  261. * @param {Function} globFilter - path filter in case a glob pattern was provided
  262. * @returns {Function} closer for the watcher instance
  263. */
  264. _watchWithFsEvents(watchPath, realPath, transform, globFilter) {
  265. if (this.fsw.closed) return;
  266. if (this.fsw._isIgnored(watchPath)) return;
  267. const opts = this.fsw.options;
  268. const watchCallback = async (fullPath, flags, info) => {
  269. if (this.fsw.closed) return;
  270. if (
  271. opts.depth !== undefined &&
  272. calcDepth(fullPath, realPath) > opts.depth
  273. ) return;
  274. const path = transform(sysPath.join(
  275. watchPath, sysPath.relative(watchPath, fullPath)
  276. ));
  277. if (globFilter && !globFilter(path)) return;
  278. // ensure directories are tracked
  279. const parent = sysPath.dirname(path);
  280. const item = sysPath.basename(path);
  281. const watchedDir = this.fsw._getWatchedDir(
  282. info.type === FSEVENT_TYPE_DIRECTORY ? path : parent
  283. );
  284. // correct for wrong events emitted
  285. if (wrongEventFlags.has(flags) || info.event === FSEVENT_UNKNOWN) {
  286. if (typeof opts.ignored === FUNCTION_TYPE) {
  287. let stats;
  288. try {
  289. stats = await stat(path);
  290. } catch (error) {}
  291. if (this.fsw.closed) return;
  292. if (this.checkIgnored(path, stats)) return;
  293. if (sameTypes(info, stats)) {
  294. this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
  295. } else {
  296. this.handleEvent(EV_UNLINK, path, fullPath, realPath, parent, watchedDir, item, info, opts);
  297. }
  298. } else {
  299. this.checkExists(path, fullPath, realPath, parent, watchedDir, item, info, opts);
  300. }
  301. } else {
  302. switch (info.event) {
  303. case FSEVENT_CREATED:
  304. case FSEVENT_MODIFIED:
  305. return this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
  306. case FSEVENT_DELETED:
  307. case FSEVENT_MOVED:
  308. return this.checkExists(path, fullPath, realPath, parent, watchedDir, item, info, opts);
  309. }
  310. }
  311. };
  312. const closer = setFSEventsListener(
  313. watchPath,
  314. realPath,
  315. watchCallback,
  316. this.fsw._emitRaw,
  317. this.fsw
  318. );
  319. this.fsw._emitReady();
  320. return closer;
  321. }
  322. /**
  323. * Handle symlinks encountered during directory scan
  324. * @param {String} linkPath path to symlink
  325. * @param {String} fullPath absolute path to the symlink
  326. * @param {Function} transform pre-existing path transformer
  327. * @param {Number} curDepth level of subdirectories traversed to where symlink is
  328. * @returns {Promise<void>}
  329. */
  330. async _handleFsEventsSymlink(linkPath, fullPath, transform, curDepth) {
  331. // don't follow the same symlink more than once
  332. if (this.fsw.closed || this.fsw._symlinkPaths.has(fullPath)) return;
  333. this.fsw._symlinkPaths.set(fullPath, true);
  334. this.fsw._incrReadyCount();
  335. try {
  336. const linkTarget = await realpath(linkPath);
  337. if (this.fsw.closed) return;
  338. if (this.fsw._isIgnored(linkTarget)) {
  339. return this.fsw._emitReady();
  340. }
  341. this.fsw._incrReadyCount();
  342. // add the linkTarget for watching with a wrapper for transform
  343. // that causes emitted paths to incorporate the link's path
  344. this._addToFsEvents(linkTarget || linkPath, (path) => {
  345. let aliasedPath = linkPath;
  346. if (linkTarget && linkTarget !== DOT_SLASH) {
  347. aliasedPath = path.replace(linkTarget, linkPath);
  348. } else if (path !== DOT_SLASH) {
  349. aliasedPath = sysPath.join(linkPath, path);
  350. }
  351. return transform(aliasedPath);
  352. }, false, curDepth);
  353. } catch(error) {
  354. if (this.fsw._handleError(error)) {
  355. return this.fsw._emitReady();
  356. }
  357. }
  358. }
  359. /**
  360. *
  361. * @param {Path} newPath
  362. * @param {fs.Stats} stats
  363. */
  364. emitAdd(newPath, stats, processPath, opts, forceAdd) {
  365. const pp = processPath(newPath);
  366. const isDir = stats.isDirectory();
  367. const dirObj = this.fsw._getWatchedDir(sysPath.dirname(pp));
  368. const base = sysPath.basename(pp);
  369. // ensure empty dirs get tracked
  370. if (isDir) this.fsw._getWatchedDir(pp);
  371. if (dirObj.has(base)) return;
  372. dirObj.add(base);
  373. if (!opts.ignoreInitial || forceAdd === true) {
  374. this.fsw._emit(isDir ? EV_ADD_DIR : EV_ADD, pp, stats);
  375. }
  376. }
  377. initWatch(realPath, path, wh, processPath) {
  378. if (this.fsw.closed) return;
  379. const closer = this._watchWithFsEvents(
  380. wh.watchPath,
  381. sysPath.resolve(realPath || wh.watchPath),
  382. processPath,
  383. wh.globFilter
  384. );
  385. this.fsw._addPathCloser(path, closer);
  386. }
  387. /**
  388. * Handle added path with fsevents
  389. * @param {String} path file/dir path or glob pattern
  390. * @param {Function|Boolean=} transform converts working path to what the user expects
  391. * @param {Boolean=} forceAdd ensure add is emitted
  392. * @param {Number=} priorDepth Level of subdirectories already traversed.
  393. * @returns {Promise<void>}
  394. */
  395. async _addToFsEvents(path, transform, forceAdd, priorDepth) {
  396. if (this.fsw.closed) {
  397. return;
  398. }
  399. const opts = this.fsw.options;
  400. const processPath = typeof transform === FUNCTION_TYPE ? transform : IDENTITY_FN;
  401. const wh = this.fsw._getWatchHelpers(path);
  402. // evaluate what is at the path we're being asked to watch
  403. try {
  404. const stats = await statMethods[wh.statMethod](wh.watchPath);
  405. if (this.fsw.closed) return;
  406. if (this.fsw._isIgnored(wh.watchPath, stats)) {
  407. throw null;
  408. }
  409. if (stats.isDirectory()) {
  410. // emit addDir unless this is a glob parent
  411. if (!wh.globFilter) this.emitAdd(processPath(path), stats, processPath, opts, forceAdd);
  412. // don't recurse further if it would exceed depth setting
  413. if (priorDepth && priorDepth > opts.depth) return;
  414. // scan the contents of the dir
  415. this.fsw._readdirp(wh.watchPath, {
  416. fileFilter: entry => wh.filterPath(entry),
  417. directoryFilter: entry => wh.filterDir(entry),
  418. ...Depth(opts.depth - (priorDepth || 0))
  419. }).on(STR_DATA, (entry) => {
  420. // need to check filterPath on dirs b/c filterDir is less restrictive
  421. if (this.fsw.closed) {
  422. return;
  423. }
  424. if (entry.stats.isDirectory() && !wh.filterPath(entry)) return;
  425. const joinedPath = sysPath.join(wh.watchPath, entry.path);
  426. const {fullPath} = entry;
  427. if (wh.followSymlinks && entry.stats.isSymbolicLink()) {
  428. // preserve the current depth here since it can't be derived from
  429. // real paths past the symlink
  430. const curDepth = opts.depth === undefined ?
  431. undefined : calcDepth(joinedPath, sysPath.resolve(wh.watchPath)) + 1;
  432. this._handleFsEventsSymlink(joinedPath, fullPath, processPath, curDepth);
  433. } else {
  434. this.emitAdd(joinedPath, entry.stats, processPath, opts, forceAdd);
  435. }
  436. }).on(EV_ERROR, EMPTY_FN).on(STR_END, () => {
  437. this.fsw._emitReady();
  438. });
  439. } else {
  440. this.emitAdd(wh.watchPath, stats, processPath, opts, forceAdd);
  441. this.fsw._emitReady();
  442. }
  443. } catch (error) {
  444. if (!error || this.fsw._handleError(error)) {
  445. // TODO: Strange thing: "should not choke on an ignored watch path" will be failed without 2 ready calls -__-
  446. this.fsw._emitReady();
  447. this.fsw._emitReady();
  448. }
  449. }
  450. if (opts.persistent && forceAdd !== true) {
  451. if (typeof transform === FUNCTION_TYPE) {
  452. // realpath has already been resolved
  453. this.initWatch(undefined, path, wh, processPath);
  454. } else {
  455. let realPath;
  456. try {
  457. realPath = await realpath(wh.watchPath);
  458. } catch (e) {}
  459. this.initWatch(realPath, path, wh, processPath);
  460. }
  461. }
  462. }
  463. }
  464. module.exports = FsEventsHandler;
  465. module.exports.canUse = canUse;