1 | 'use strict';
|
2 |
|
3 | const fs = require('fs');
|
4 | const sysPath = require('path');
|
5 | const { promisify } = require('util');
|
6 |
|
7 | let fsevents;
|
8 | try {
|
9 | fsevents = require('fsevents');
|
10 | } catch (error) {
|
11 | if (process.env.CHOKIDAR_PRINT_FSEVENTS_REQUIRE_ERROR) console.error(error);
|
12 | }
|
13 |
|
14 | if (fsevents) {
|
15 |
|
16 | const mtch = process.version.match(/v(\d+)\.(\d+)/);
|
17 | if (mtch && mtch[1] && mtch[2]) {
|
18 | const maj = Number.parseInt(mtch[1], 10);
|
19 | const min = Number.parseInt(mtch[2], 10);
|
20 | if (maj === 8 && min < 16) {
|
21 | fsevents = undefined;
|
22 | }
|
23 | }
|
24 | }
|
25 |
|
26 | const {
|
27 | EV_ADD,
|
28 | EV_CHANGE,
|
29 | EV_ADD_DIR,
|
30 | EV_UNLINK,
|
31 | EV_ERROR,
|
32 | STR_DATA,
|
33 | STR_END,
|
34 | FSEVENT_CREATED,
|
35 | FSEVENT_MODIFIED,
|
36 | FSEVENT_DELETED,
|
37 | FSEVENT_MOVED,
|
38 |
|
39 | FSEVENT_UNKNOWN,
|
40 | FSEVENT_TYPE_FILE,
|
41 | FSEVENT_TYPE_DIRECTORY,
|
42 | FSEVENT_TYPE_SYMLINK,
|
43 |
|
44 | ROOT_GLOBSTAR,
|
45 | DIR_SUFFIX,
|
46 | DOT_SLASH,
|
47 | FUNCTION_TYPE,
|
48 | EMPTY_FN,
|
49 | IDENTITY_FN
|
50 | } = require('./constants');
|
51 |
|
52 | const Depth = (value) => isNaN(value) ? {} : {depth: value};
|
53 |
|
54 | const stat = promisify(fs.stat);
|
55 | const lstat = promisify(fs.lstat);
|
56 | const realpath = promisify(fs.realpath);
|
57 |
|
58 | const statMethods = { stat, lstat };
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 | const FSEventsWatchers = new Map();
|
77 |
|
78 |
|
79 |
|
80 | const consolidateThreshhold = 10;
|
81 |
|
82 | const wrongEventFlags = new Set([
|
83 | 69888, 70400, 71424, 72704, 73472, 131328, 131840, 262912
|
84 | ]);
|
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 | const createFSEventsInstance = (path, callback) => {
|
93 | const stop = fsevents.watch(path, callback);
|
94 | return {stop};
|
95 | };
|
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 | function setFSEventsListener(path, realPath, listener, rawEmitter) {
|
107 | let watchPath = sysPath.extname(realPath) ? sysPath.dirname(realPath) : realPath;
|
108 |
|
109 | const parentPath = sysPath.dirname(watchPath);
|
110 | let cont = FSEventsWatchers.get(watchPath);
|
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 | if (couldConsolidate(parentPath)) {
|
117 | watchPath = parentPath;
|
118 | }
|
119 |
|
120 | const resolvedPath = sysPath.resolve(path);
|
121 | const hasSymlink = resolvedPath !== realPath;
|
122 |
|
123 | const filteredListener = (fullPath, flags, info) => {
|
124 | if (hasSymlink) fullPath = fullPath.replace(realPath, resolvedPath);
|
125 | if (
|
126 | fullPath === resolvedPath ||
|
127 | !fullPath.indexOf(resolvedPath + sysPath.sep)
|
128 | ) listener(fullPath, flags, info);
|
129 | };
|
130 |
|
131 |
|
132 |
|
133 | let watchedParent = false;
|
134 | for (const watchedPath of FSEventsWatchers.keys()) {
|
135 | if (realPath.indexOf(sysPath.resolve(watchedPath) + sysPath.sep) === 0) {
|
136 | watchPath = watchedPath;
|
137 | cont = FSEventsWatchers.get(watchPath);
|
138 | watchedParent = true;
|
139 | break;
|
140 | }
|
141 | }
|
142 |
|
143 | if (cont || watchedParent) {
|
144 | cont.listeners.add(filteredListener);
|
145 | } else {
|
146 | cont = {
|
147 | listeners: new Set([filteredListener]),
|
148 | rawEmitter,
|
149 | watcher: createFSEventsInstance(watchPath, (fullPath, flags) => {
|
150 | if (!cont.listeners.size) return;
|
151 | const info = fsevents.getInfo(fullPath, flags);
|
152 | cont.listeners.forEach(list => {
|
153 | list(fullPath, flags, info);
|
154 | });
|
155 |
|
156 | cont.rawEmitter(info.event, fullPath, info);
|
157 | })
|
158 | };
|
159 | FSEventsWatchers.set(watchPath, cont);
|
160 | }
|
161 |
|
162 |
|
163 |
|
164 | return () => {
|
165 | const lst = cont.listeners;
|
166 |
|
167 | lst.delete(filteredListener);
|
168 | if (!lst.size) {
|
169 | FSEventsWatchers.delete(watchPath);
|
170 | if (cont.watcher) return cont.watcher.stop().then(() => {
|
171 | cont.rawEmitter = cont.watcher = undefined;
|
172 | Object.freeze(cont);
|
173 | });
|
174 | }
|
175 | };
|
176 | }
|
177 |
|
178 |
|
179 |
|
180 | const couldConsolidate = (path) => {
|
181 | let count = 0;
|
182 | for (const watchPath of FSEventsWatchers.keys()) {
|
183 | if (watchPath.indexOf(path) === 0) {
|
184 | count++;
|
185 | if (count >= consolidateThreshhold) {
|
186 | return true;
|
187 | }
|
188 | }
|
189 | }
|
190 |
|
191 | return false;
|
192 | };
|
193 |
|
194 |
|
195 | const canUse = () => fsevents && FSEventsWatchers.size < 128;
|
196 |
|
197 |
|
198 | const calcDepth = (path, root) => {
|
199 | let i = 0;
|
200 | while (!path.indexOf(root) && (path = sysPath.dirname(path)) !== root) i++;
|
201 | return i;
|
202 | };
|
203 |
|
204 |
|
205 |
|
206 | const sameTypes = (info, stats) => (
|
207 | info.type === FSEVENT_TYPE_DIRECTORY && stats.isDirectory() ||
|
208 | info.type === FSEVENT_TYPE_SYMLINK && stats.isSymbolicLink() ||
|
209 | info.type === FSEVENT_TYPE_FILE && stats.isFile()
|
210 | )
|
211 |
|
212 |
|
213 |
|
214 |
|
215 | class FsEventsHandler {
|
216 |
|
217 |
|
218 |
|
219 |
|
220 | constructor(fsw) {
|
221 | this.fsw = fsw;
|
222 | }
|
223 | checkIgnored(path, stats) {
|
224 | const ipaths = this.fsw._ignoredPaths;
|
225 | if (this.fsw._isIgnored(path, stats)) {
|
226 | ipaths.add(path);
|
227 | if (stats && stats.isDirectory()) {
|
228 | ipaths.add(path + ROOT_GLOBSTAR);
|
229 | }
|
230 | return true;
|
231 | }
|
232 |
|
233 | ipaths.delete(path);
|
234 | ipaths.delete(path + ROOT_GLOBSTAR);
|
235 | }
|
236 |
|
237 | addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts) {
|
238 | const event = watchedDir.has(item) ? EV_CHANGE : EV_ADD;
|
239 | this.handleEvent(event, path, fullPath, realPath, parent, watchedDir, item, info, opts);
|
240 | }
|
241 |
|
242 | async checkExists(path, fullPath, realPath, parent, watchedDir, item, info, opts) {
|
243 | try {
|
244 | const stats = await stat(path)
|
245 | if (this.fsw.closed) return;
|
246 | if (sameTypes(info, stats)) {
|
247 | this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
|
248 | } else {
|
249 | this.handleEvent(EV_UNLINK, path, fullPath, realPath, parent, watchedDir, item, info, opts);
|
250 | }
|
251 | } catch (error) {
|
252 | if (error.code === 'EACCES') {
|
253 | this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
|
254 | } else {
|
255 | this.handleEvent(EV_UNLINK, path, fullPath, realPath, parent, watchedDir, item, info, opts);
|
256 | }
|
257 | }
|
258 | }
|
259 |
|
260 | handleEvent(event, path, fullPath, realPath, parent, watchedDir, item, info, opts) {
|
261 | if (this.fsw.closed || this.checkIgnored(path)) return;
|
262 |
|
263 | if (event === EV_UNLINK) {
|
264 | const isDirectory = info.type === FSEVENT_TYPE_DIRECTORY
|
265 |
|
266 | if (isDirectory || watchedDir.has(item)) {
|
267 | this.fsw._remove(parent, item, isDirectory);
|
268 | }
|
269 | } else {
|
270 | if (event === EV_ADD) {
|
271 |
|
272 | if (info.type === FSEVENT_TYPE_DIRECTORY) this.fsw._getWatchedDir(path);
|
273 |
|
274 | if (info.type === FSEVENT_TYPE_SYMLINK && opts.followSymlinks) {
|
275 |
|
276 | const curDepth = opts.depth === undefined ?
|
277 | undefined : calcDepth(fullPath, realPath) + 1;
|
278 | return this._addToFsEvents(path, false, true, curDepth);
|
279 | }
|
280 |
|
281 |
|
282 |
|
283 | this.fsw._getWatchedDir(parent).add(item);
|
284 | }
|
285 | |
286 |
|
287 |
|
288 | const eventName = info.type === FSEVENT_TYPE_DIRECTORY ? event + DIR_SUFFIX : event;
|
289 | this.fsw._emit(eventName, path);
|
290 | if (eventName === EV_ADD_DIR) this._addToFsEvents(path, false, true);
|
291 | }
|
292 | }
|
293 |
|
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 |
|
300 |
|
301 |
|
302 | _watchWithFsEvents(watchPath, realPath, transform, globFilter) {
|
303 | if (this.fsw.closed || this.fsw._isIgnored(watchPath)) return;
|
304 | const opts = this.fsw.options;
|
305 | const watchCallback = async (fullPath, flags, info) => {
|
306 | if (this.fsw.closed) return;
|
307 | if (
|
308 | opts.depth !== undefined &&
|
309 | calcDepth(fullPath, realPath) > opts.depth
|
310 | ) return;
|
311 | const path = transform(sysPath.join(
|
312 | watchPath, sysPath.relative(watchPath, fullPath)
|
313 | ));
|
314 | if (globFilter && !globFilter(path)) return;
|
315 |
|
316 | const parent = sysPath.dirname(path);
|
317 | const item = sysPath.basename(path);
|
318 | const watchedDir = this.fsw._getWatchedDir(
|
319 | info.type === FSEVENT_TYPE_DIRECTORY ? path : parent
|
320 | );
|
321 |
|
322 |
|
323 | if (wrongEventFlags.has(flags) || info.event === FSEVENT_UNKNOWN) {
|
324 | if (typeof opts.ignored === FUNCTION_TYPE) {
|
325 | let stats;
|
326 | try {
|
327 | stats = await stat(path);
|
328 | } catch (error) {}
|
329 | if (this.fsw.closed) return;
|
330 | if (this.checkIgnored(path, stats)) return;
|
331 | if (sameTypes(info, stats)) {
|
332 | this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
|
333 | } else {
|
334 | this.handleEvent(EV_UNLINK, path, fullPath, realPath, parent, watchedDir, item, info, opts);
|
335 | }
|
336 | } else {
|
337 | this.checkExists(path, fullPath, realPath, parent, watchedDir, item, info, opts);
|
338 | }
|
339 | } else {
|
340 | switch (info.event) {
|
341 | case FSEVENT_CREATED:
|
342 | case FSEVENT_MODIFIED:
|
343 | return this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
|
344 | case FSEVENT_DELETED:
|
345 | case FSEVENT_MOVED:
|
346 | return this.checkExists(path, fullPath, realPath, parent, watchedDir, item, info, opts);
|
347 | }
|
348 | }
|
349 | };
|
350 |
|
351 | const closer = setFSEventsListener(
|
352 | watchPath,
|
353 | realPath,
|
354 | watchCallback,
|
355 | this.fsw._emitRaw
|
356 | );
|
357 |
|
358 | this.fsw._emitReady();
|
359 | return closer;
|
360 | }
|
361 |
|
362 |
|
363 |
|
364 |
|
365 |
|
366 |
|
367 |
|
368 |
|
369 |
|
370 | async _handleFsEventsSymlink(linkPath, fullPath, transform, curDepth) {
|
371 |
|
372 | if (this.fsw.closed || this.fsw._symlinkPaths.has(fullPath)) return;
|
373 |
|
374 | this.fsw._symlinkPaths.set(fullPath, true);
|
375 | this.fsw._incrReadyCount();
|
376 |
|
377 | try {
|
378 | const linkTarget = await realpath(linkPath);
|
379 | if (this.fsw.closed) return;
|
380 | if (this.fsw._isIgnored(linkTarget)) {
|
381 | return this.fsw._emitReady();
|
382 | }
|
383 |
|
384 | this.fsw._incrReadyCount();
|
385 |
|
386 |
|
387 |
|
388 | this._addToFsEvents(linkTarget || linkPath, (path) => {
|
389 | let aliasedPath = linkPath;
|
390 | if (linkTarget && linkTarget !== DOT_SLASH) {
|
391 | aliasedPath = path.replace(linkTarget, linkPath);
|
392 | } else if (path !== DOT_SLASH) {
|
393 | aliasedPath = sysPath.join(linkPath, path);
|
394 | }
|
395 | return transform(aliasedPath);
|
396 | }, false, curDepth);
|
397 | } catch(error) {
|
398 | if (this.fsw._handleError(error)) {
|
399 | return this.fsw._emitReady();
|
400 | }
|
401 | }
|
402 | }
|
403 |
|
404 |
|
405 |
|
406 |
|
407 |
|
408 |
|
409 | emitAdd(newPath, stats, processPath, opts, forceAdd) {
|
410 | const pp = processPath(newPath);
|
411 | const isDir = stats.isDirectory();
|
412 | const dirObj = this.fsw._getWatchedDir(sysPath.dirname(pp));
|
413 | const base = sysPath.basename(pp);
|
414 |
|
415 |
|
416 | if (isDir) this.fsw._getWatchedDir(pp);
|
417 | if (dirObj.has(base)) return;
|
418 | dirObj.add(base);
|
419 |
|
420 | if (!opts.ignoreInitial || forceAdd === true) {
|
421 | this.fsw._emit(isDir ? EV_ADD_DIR : EV_ADD, pp, stats);
|
422 | }
|
423 | }
|
424 |
|
425 | initWatch(realPath, path, wh, processPath) {
|
426 | if (this.fsw.closed) return;
|
427 | const closer = this._watchWithFsEvents(
|
428 | wh.watchPath,
|
429 | sysPath.resolve(realPath || wh.watchPath),
|
430 | processPath,
|
431 | wh.globFilter
|
432 | );
|
433 | this.fsw._addPathCloser(path, closer);
|
434 | }
|
435 |
|
436 |
|
437 |
|
438 |
|
439 |
|
440 |
|
441 |
|
442 |
|
443 |
|
444 | async _addToFsEvents(path, transform, forceAdd, priorDepth) {
|
445 | if (this.fsw.closed) {
|
446 | return;
|
447 | }
|
448 | const opts = this.fsw.options;
|
449 | const processPath = typeof transform === FUNCTION_TYPE ? transform : IDENTITY_FN;
|
450 |
|
451 | const wh = this.fsw._getWatchHelpers(path);
|
452 |
|
453 |
|
454 | try {
|
455 | const stats = await statMethods[wh.statMethod](wh.watchPath);
|
456 | if (this.fsw.closed) return;
|
457 | if (this.fsw._isIgnored(wh.watchPath, stats)) {
|
458 | throw null;
|
459 | }
|
460 | if (stats.isDirectory()) {
|
461 |
|
462 | if (!wh.globFilter) this.emitAdd(processPath(path), stats, processPath, opts, forceAdd);
|
463 |
|
464 |
|
465 | if (priorDepth && priorDepth > opts.depth) return;
|
466 |
|
467 |
|
468 | this.fsw._readdirp(wh.watchPath, {
|
469 | fileFilter: entry => wh.filterPath(entry),
|
470 | directoryFilter: entry => wh.filterDir(entry),
|
471 | ...Depth(opts.depth - (priorDepth || 0))
|
472 | }).on(STR_DATA, (entry) => {
|
473 |
|
474 | if (this.fsw.closed) {
|
475 | return;
|
476 | }
|
477 | if (entry.stats.isDirectory() && !wh.filterPath(entry)) return;
|
478 |
|
479 | const joinedPath = sysPath.join(wh.watchPath, entry.path);
|
480 | const {fullPath} = entry;
|
481 |
|
482 | if (wh.followSymlinks && entry.stats.isSymbolicLink()) {
|
483 |
|
484 |
|
485 | const curDepth = opts.depth === undefined ?
|
486 | undefined : calcDepth(joinedPath, sysPath.resolve(wh.watchPath)) + 1;
|
487 |
|
488 | this._handleFsEventsSymlink(joinedPath, fullPath, processPath, curDepth);
|
489 | } else {
|
490 | this.emitAdd(joinedPath, entry.stats, processPath, opts, forceAdd);
|
491 | }
|
492 | }).on(EV_ERROR, EMPTY_FN).on(STR_END, () => {
|
493 | this.fsw._emitReady();
|
494 | });
|
495 | } else {
|
496 | this.emitAdd(wh.watchPath, stats, processPath, opts, forceAdd);
|
497 | this.fsw._emitReady();
|
498 | }
|
499 | } catch (error) {
|
500 | if (!error || this.fsw._handleError(error)) {
|
501 |
|
502 | this.fsw._emitReady();
|
503 | this.fsw._emitReady();
|
504 | }
|
505 | }
|
506 |
|
507 | if (opts.persistent && forceAdd !== true) {
|
508 | if (typeof transform === FUNCTION_TYPE) {
|
509 |
|
510 | this.initWatch(undefined, path, wh, processPath);
|
511 | } else {
|
512 | let realPath;
|
513 | try {
|
514 | realPath = await realpath(wh.watchPath);
|
515 | } catch (e) {}
|
516 | this.initWatch(realPath, path, wh, processPath);
|
517 | }
|
518 | }
|
519 | }
|
520 |
|
521 | }
|
522 |
|
523 | module.exports = FsEventsHandler;
|
524 | module.exports.canUse = canUse;
|