Projekt

Obecné

Profil

Stáhnout (13.3 KB) Statistiky
| Větev: | Revize:
1
'use strict';
2

    
3
var fs = require('fs');
4
var sysPath = require('path');
5
var readdirp = require('readdirp');
6
var fsevents;
7
try { fsevents = require('fsevents'); } catch (error) {
8
  if (process.env.CHOKIDAR_PRINT_FSEVENTS_REQUIRE_ERROR) console.error(error)
9
}
10

    
11
// fsevents instance helper functions
12

    
13
// object to hold per-process fsevents instances
14
// (may be shared across chokidar FSWatcher instances)
15
var FSEventsWatchers = Object.create(null);
16

    
17
// Threshold of duplicate path prefixes at which to start
18
// consolidating going forward
19
var consolidateThreshhold = 10;
20

    
21
// Private function: Instantiates the fsevents interface
22

    
23
// * path       - string, path to be watched
24
// * callback   - function, called when fsevents is bound and ready
25

    
26
// Returns new fsevents instance
27
function createFSEventsInstance(path, callback) {
28
  return (new fsevents(path)).on('fsevent', callback).start();
29
}
30

    
31
// Private function: Instantiates the fsevents interface or binds listeners
32
// to an existing one covering the same file tree
33

    
34
// * path       - string, path to be watched
35
// * realPath   - string, real path (in case of symlinks)
36
// * listener   - function, called when fsevents emits events
37
// * rawEmitter - function, passes data to listeners of the 'raw' event
38

    
39
// Returns close function
40
function setFSEventsListener(path, realPath, listener, rawEmitter) {
41
  var watchPath = sysPath.extname(path) ? sysPath.dirname(path) : path;
42
  var watchContainer;
43
  var parentPath = sysPath.dirname(watchPath);
44

    
45
  // If we've accumulated a substantial number of paths that
46
  // could have been consolidated by watching one directory
47
  // above the current one, create a watcher on the parent
48
  // path instead, so that we do consolidate going forward.
49
  if (couldConsolidate(parentPath)) {
50
    watchPath = parentPath;
51
  }
52

    
53
  var resolvedPath = sysPath.resolve(path);
54
  var hasSymlink = resolvedPath !== realPath;
55
  function filteredListener(fullPath, flags, info) {
56
    if (hasSymlink) fullPath = fullPath.replace(realPath, resolvedPath);
57
    if (
58
      fullPath === resolvedPath ||
59
      !fullPath.indexOf(resolvedPath + sysPath.sep)
60
    ) listener(fullPath, flags, info);
61
  }
62

    
63
  // check if there is already a watcher on a parent path
64
  // modifies `watchPath` to the parent path when it finds a match
65
  function watchedParent() {
66
    return Object.keys(FSEventsWatchers).some(function(watchedPath) {
67
      // condition is met when indexOf returns 0
68
      if (!realPath.indexOf(sysPath.resolve(watchedPath) + sysPath.sep)) {
69
        watchPath = watchedPath;
70
        return true;
71
      }
72
    });
73
  }
74

    
75
  if (watchPath in FSEventsWatchers || watchedParent()) {
76
    watchContainer = FSEventsWatchers[watchPath];
77
    watchContainer.listeners.push(filteredListener);
78
  } else {
79
    watchContainer = FSEventsWatchers[watchPath] = {
80
      listeners: [filteredListener],
81
      rawEmitters: [rawEmitter],
82
      watcher: createFSEventsInstance(watchPath, function(fullPath, flags) {
83
        var info = fsevents.getInfo(fullPath, flags);
84
        watchContainer.listeners.forEach(function(listener) {
85
          listener(fullPath, flags, info);
86
        });
87
        watchContainer.rawEmitters.forEach(function(emitter) {
88
          emitter(info.event, fullPath, info);
89
        });
90
      })
91
    };
92
  }
93
  var listenerIndex = watchContainer.listeners.length - 1;
94

    
95
  // removes this instance's listeners and closes the underlying fsevents
96
  // instance if there are no more listeners left
97
  return function close() {
98
    delete watchContainer.listeners[listenerIndex];
99
    delete watchContainer.rawEmitters[listenerIndex];
100
    if (!Object.keys(watchContainer.listeners).length) {
101
      watchContainer.watcher.stop();
102
      delete FSEventsWatchers[watchPath];
103
    }
104
  };
105
}
106

    
107
// Decide whether or not we should start a new higher-level
108
// parent watcher
109
function couldConsolidate(path) {
110
  var keys = Object.keys(FSEventsWatchers);
111
  var count = 0;
112

    
113
  for (var i = 0, len = keys.length; i < len; ++i) {
114
    var watchPath = keys[i];
115
    if (watchPath.indexOf(path) === 0) {
116
      count++;
117
      if (count >= consolidateThreshhold) {
118
        return true;
119
      }
120
    }
121
  }
122

    
123
  return false;
124
}
125

    
126
function isConstructor(obj) {
127
  return obj.prototype !== undefined && obj.prototype.constructor !== undefined;
128
}
129

    
130
// returns boolean indicating whether fsevents can be used
131
function canUse() {
132
  return fsevents && Object.keys(FSEventsWatchers).length < 128 && isConstructor(fsevents);
133
}
134

    
135
// determines subdirectory traversal levels from root to path
136
function depth(path, root) {
137
  var i = 0;
138
  while (!path.indexOf(root) && (path = sysPath.dirname(path)) !== root) i++;
139
  return i;
140
}
141

    
142
// fake constructor for attaching fsevents-specific prototype methods that
143
// will be copied to FSWatcher's prototype
144
function FsEventsHandler() {}
145

    
146
// Private method: Handle symlinks encountered during directory scan
147

    
148
// * watchPath  - string, file/dir path to be watched with fsevents
149
// * realPath   - string, real path (in case of symlinks)
150
// * transform  - function, path transformer
151
// * globFilter - function, path filter in case a glob pattern was provided
152

    
153
// Returns close function for the watcher instance
154
FsEventsHandler.prototype._watchWithFsEvents =
155
function(watchPath, realPath, transform, globFilter) {
156
  if (this._isIgnored(watchPath)) return;
157
  var watchCallback = function(fullPath, flags, info) {
158
    if (
159
      this.options.depth !== undefined &&
160
      depth(fullPath, realPath) > this.options.depth
161
    ) return;
162
    var path = transform(sysPath.join(
163
      watchPath, sysPath.relative(watchPath, fullPath)
164
    ));
165
    if (globFilter && !globFilter(path)) return;
166
    // ensure directories are tracked
167
    var parent = sysPath.dirname(path);
168
    var item = sysPath.basename(path);
169
    var watchedDir = this._getWatchedDir(
170
      info.type === 'directory' ? path : parent
171
    );
172
    var checkIgnored = function(stats) {
173
      if (this._isIgnored(path, stats)) {
174
        this._ignoredPaths[path] = true;
175
        if (stats && stats.isDirectory()) {
176
          this._ignoredPaths[path + '/**/*'] = true;
177
        }
178
        return true;
179
      } else {
180
        delete this._ignoredPaths[path];
181
        delete this._ignoredPaths[path + '/**/*'];
182
      }
183
    }.bind(this);
184

    
185
    var handleEvent = function(event) {
186
      if (checkIgnored()) return;
187

    
188
      if (event === 'unlink') {
189
        // suppress unlink events on never before seen files
190
        if (info.type === 'directory' || watchedDir.has(item)) {
191
          this._remove(parent, item);
192
        }
193
      } else {
194
        if (event === 'add') {
195
          // track new directories
196
          if (info.type === 'directory') this._getWatchedDir(path);
197

    
198
          if (info.type === 'symlink' && this.options.followSymlinks) {
199
            // push symlinks back to the top of the stack to get handled
200
            var curDepth = this.options.depth === undefined ?
201
              undefined : depth(fullPath, realPath) + 1;
202
            return this._addToFsEvents(path, false, true, curDepth);
203
          } else {
204
            // track new paths
205
            // (other than symlinks being followed, which will be tracked soon)
206
            this._getWatchedDir(parent).add(item);
207
          }
208
        }
209
        var eventName = info.type === 'directory' ? event + 'Dir' : event;
210
        this._emit(eventName, path);
211
        if (eventName === 'addDir') this._addToFsEvents(path, false, true);
212
      }
213
    }.bind(this);
214

    
215
    function addOrChange() {
216
      handleEvent(watchedDir.has(item) ? 'change' : 'add');
217
    }
218
    function checkFd() {
219
      fs.open(path, 'r', function(error, fd) {
220
        if (error) {
221
          error.code !== 'EACCES' ?
222
            handleEvent('unlink') : addOrChange();
223
        } else {
224
          fs.close(fd, function(err) {
225
            err && err.code !== 'EACCES' ?
226
              handleEvent('unlink') : addOrChange();
227
          });
228
        }
229
      });
230
    }
231
    // correct for wrong events emitted
232
    var wrongEventFlags = [
233
      69888, 70400, 71424, 72704, 73472, 131328, 131840, 262912
234
    ];
235
    if (wrongEventFlags.indexOf(flags) !== -1 || info.event === 'unknown') {
236
      if (typeof this.options.ignored === 'function') {
237
        fs.stat(path, function(error, stats) {
238
          if (checkIgnored(stats)) return;
239
          stats ? addOrChange() : handleEvent('unlink');
240
        });
241
      } else {
242
        checkFd();
243
      }
244
    } else {
245
      switch (info.event) {
246
      case 'created':
247
      case 'modified':
248
        return addOrChange();
249
      case 'deleted':
250
      case 'moved':
251
        return checkFd();
252
      }
253
    }
254
  }.bind(this);
255

    
256
  var closer = setFSEventsListener(
257
    watchPath,
258
    realPath,
259
    watchCallback,
260
    this.emit.bind(this, 'raw')
261
  );
262

    
263
  this._emitReady();
264
  return closer;
265
};
266

    
267
// Private method: Handle symlinks encountered during directory scan
268

    
269
// * linkPath   - string, path to symlink
270
// * fullPath   - string, absolute path to the symlink
271
// * transform  - function, pre-existing path transformer
272
// * curDepth   - int, level of subdirectories traversed to where symlink is
273

    
274
// Returns nothing
275
FsEventsHandler.prototype._handleFsEventsSymlink =
276
function(linkPath, fullPath, transform, curDepth) {
277
  // don't follow the same symlink more than once
278
  if (this._symlinkPaths[fullPath]) return;
279
  else this._symlinkPaths[fullPath] = true;
280

    
281
  this._readyCount++;
282

    
283
  fs.realpath(linkPath, function(error, linkTarget) {
284
    if (this._handleError(error) || this._isIgnored(linkTarget)) {
285
      return this._emitReady();
286
    }
287

    
288
    this._readyCount++;
289

    
290
    // add the linkTarget for watching with a wrapper for transform
291
    // that causes emitted paths to incorporate the link's path
292
    this._addToFsEvents(linkTarget || linkPath, function(path) {
293
      var dotSlash = '.' + sysPath.sep;
294
      var aliasedPath = linkPath;
295
      if (linkTarget && linkTarget !== dotSlash) {
296
        aliasedPath = path.replace(linkTarget, linkPath);
297
      } else if (path !== dotSlash) {
298
        aliasedPath = sysPath.join(linkPath, path);
299
      }
300
      return transform(aliasedPath);
301
    }, false, curDepth);
302
  }.bind(this));
303
};
304

    
305
// Private method: Handle added path with fsevents
306

    
307
// * path       - string, file/directory path or glob pattern
308
// * transform  - function, converts working path to what the user expects
309
// * forceAdd   - boolean, ensure add is emitted
310
// * priorDepth - int, level of subdirectories already traversed
311

    
312
// Returns nothing
313
FsEventsHandler.prototype._addToFsEvents =
314
function(path, transform, forceAdd, priorDepth) {
315

    
316
  // applies transform if provided, otherwise returns same value
317
  var processPath = typeof transform === 'function' ?
318
    transform : function(val) { return val; };
319

    
320
  var emitAdd = function(newPath, stats) {
321
    var pp = processPath(newPath);
322
    var isDir = stats.isDirectory();
323
    var dirObj = this._getWatchedDir(sysPath.dirname(pp));
324
    var base = sysPath.basename(pp);
325

    
326
    // ensure empty dirs get tracked
327
    if (isDir) this._getWatchedDir(pp);
328

    
329
    if (dirObj.has(base)) return;
330
    dirObj.add(base);
331

    
332
    if (!this.options.ignoreInitial || forceAdd === true) {
333
      this._emit(isDir ? 'addDir' : 'add', pp, stats);
334
    }
335
  }.bind(this);
336

    
337
  var wh = this._getWatchHelpers(path);
338

    
339
  // evaluate what is at the path we're being asked to watch
340
  fs[wh.statMethod](wh.watchPath, function(error, stats) {
341
    if (this._handleError(error) || this._isIgnored(wh.watchPath, stats)) {
342
      this._emitReady();
343
      return this._emitReady();
344
    }
345

    
346
    if (stats.isDirectory()) {
347
      // emit addDir unless this is a glob parent
348
      if (!wh.globFilter) emitAdd(processPath(path), stats);
349

    
350
      // don't recurse further if it would exceed depth setting
351
      if (priorDepth && priorDepth > this.options.depth) return;
352

    
353
      // scan the contents of the dir
354
      readdirp({
355
        root: wh.watchPath,
356
        entryType: 'all',
357
        fileFilter: wh.filterPath,
358
        directoryFilter: wh.filterDir,
359
        lstat: true,
360
        depth: this.options.depth - (priorDepth || 0)
361
      }).on('data', function(entry) {
362
        // need to check filterPath on dirs b/c filterDir is less restrictive
363
        if (entry.stat.isDirectory() && !wh.filterPath(entry)) return;
364

    
365
        var joinedPath = sysPath.join(wh.watchPath, entry.path);
366
        var fullPath = entry.fullPath;
367

    
368
        if (wh.followSymlinks && entry.stat.isSymbolicLink()) {
369
          // preserve the current depth here since it can't be derived from
370
          // real paths past the symlink
371
          var curDepth = this.options.depth === undefined ?
372
            undefined : depth(joinedPath, sysPath.resolve(wh.watchPath)) + 1;
373

    
374
          this._handleFsEventsSymlink(joinedPath, fullPath, processPath, curDepth);
375
        } else {
376
          emitAdd(joinedPath, entry.stat);
377
        }
378
      }.bind(this)).on('error', function() {
379
        // Ignore readdirp errors
380
      }).on('end', this._emitReady);
381
    } else {
382
      emitAdd(wh.watchPath, stats);
383
      this._emitReady();
384
    }
385
  }.bind(this));
386

    
387
  if (this.options.persistent && forceAdd !== true) {
388
    var initWatch = function(error, realPath) {
389
      if (this.closed) return;
390
      var closer = this._watchWithFsEvents(
391
        wh.watchPath,
392
        sysPath.resolve(realPath || wh.watchPath),
393
        processPath,
394
        wh.globFilter
395
      );
396
      if (closer) {
397
        this._closers[path] = this._closers[path] || [];
398
        this._closers[path].push(closer);
399
      }
400
    }.bind(this);
401

    
402
    if (typeof transform === 'function') {
403
      // realpath has already been resolved
404
      initWatch();
405
    } else {
406
      fs.realpath(wh.watchPath, initWatch);
407
    }
408
  }
409
};
410

    
411
module.exports = FsEventsHandler;
412
module.exports.canUse = canUse;
(1-1/2)