Projekt

Obecné

Profil

Stáhnout (17.1 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 isBinaryPath = require('is-binary-path');
7

    
8
// fs.watch helpers
9

    
10
// object to hold per-process fs.watch instances
11
// (may be shared across chokidar FSWatcher instances)
12
var FsWatchInstances = Object.create(null);
13

    
14

    
15
// Private function: Instantiates the fs.watch interface
16

    
17
// * path       - string, path to be watched
18
// * options    - object, options to be passed to fs.watch
19
// * listener   - function, main event handler
20
// * errHandler - function, handler which emits info about errors
21
// * emitRaw    - function, handler which emits raw event data
22

    
23
// Returns new fsevents instance
24
function createFsWatchInstance(path, options, listener, errHandler, emitRaw) {
25
  var handleEvent = function(rawEvent, evPath) {
26
    listener(path);
27
    emitRaw(rawEvent, evPath, {watchedPath: path});
28

    
29
    // emit based on events occurring for files from a directory's watcher in
30
    // case the file's watcher misses it (and rely on throttling to de-dupe)
31
    if (evPath && path !== evPath) {
32
      fsWatchBroadcast(
33
        sysPath.resolve(path, evPath), 'listeners', sysPath.join(path, evPath)
34
      );
35
    }
36
  };
37
  try {
38
    return fs.watch(path, options, handleEvent);
39
  } catch (error) {
40
    errHandler(error);
41
  }
42
}
43

    
44
// Private function: Helper for passing fs.watch event data to a
45
// collection of listeners
46

    
47
// * fullPath   - string, absolute path bound to the fs.watch instance
48
// * type       - string, listener type
49
// * val[1..3]  - arguments to be passed to listeners
50

    
51
// Returns nothing
52
function fsWatchBroadcast(fullPath, type, val1, val2, val3) {
53
  if (!FsWatchInstances[fullPath]) return;
54
  FsWatchInstances[fullPath][type].forEach(function(listener) {
55
    listener(val1, val2, val3);
56
  });
57
}
58

    
59
// Private function: Instantiates the fs.watch interface or binds listeners
60
// to an existing one covering the same file system entry
61

    
62
// * path       - string, path to be watched
63
// * fullPath   - string, absolute path
64
// * options    - object, options to be passed to fs.watch
65
// * handlers   - object, container for event listener functions
66

    
67
// Returns close function
68
function setFsWatchListener(path, fullPath, options, handlers) {
69
  var listener = handlers.listener;
70
  var errHandler = handlers.errHandler;
71
  var rawEmitter = handlers.rawEmitter;
72
  var container = FsWatchInstances[fullPath];
73
  var watcher;
74
  if (!options.persistent) {
75
    watcher = createFsWatchInstance(
76
      path, options, listener, errHandler, rawEmitter
77
    );
78
    return watcher.close.bind(watcher);
79
  }
80
  if (!container) {
81
    watcher = createFsWatchInstance(
82
      path,
83
      options,
84
      fsWatchBroadcast.bind(null, fullPath, 'listeners'),
85
      errHandler, // no need to use broadcast here
86
      fsWatchBroadcast.bind(null, fullPath, 'rawEmitters')
87
    );
88
    if (!watcher) return;
89
    var broadcastErr = fsWatchBroadcast.bind(null, fullPath, 'errHandlers');
90
    watcher.on('error', function(error) {
91
      container.watcherUnusable = true; // documented since Node 10.4.1
92
      // Workaround for https://github.com/joyent/node/issues/4337
93
      if (process.platform === 'win32' && error.code === 'EPERM') {
94
        fs.open(path, 'r', function(err, fd) {
95
          if (!err) fs.close(fd, function(err) {
96
            if (!err) broadcastErr(error);
97
          });
98
        });
99
      } else {
100
        broadcastErr(error);
101
      }
102
    });
103
    container = FsWatchInstances[fullPath] = {
104
      listeners: [listener],
105
      errHandlers: [errHandler],
106
      rawEmitters: [rawEmitter],
107
      watcher: watcher
108
    };
109
  } else {
110
    container.listeners.push(listener);
111
    container.errHandlers.push(errHandler);
112
    container.rawEmitters.push(rawEmitter);
113
  }
114
  var listenerIndex = container.listeners.length - 1;
115

    
116
  // removes this instance's listeners and closes the underlying fs.watch
117
  // instance if there are no more listeners left
118
  return function close() {
119
    delete container.listeners[listenerIndex];
120
    delete container.errHandlers[listenerIndex];
121
    delete container.rawEmitters[listenerIndex];
122
    if (!Object.keys(container.listeners).length) {
123
      if (!container.watcherUnusable) { // check to protect against issue #730
124
        container.watcher.close();
125
      }
126
      delete FsWatchInstances[fullPath];
127
    }
128
  };
129
}
130

    
131
// fs.watchFile helpers
132

    
133
// object to hold per-process fs.watchFile instances
134
// (may be shared across chokidar FSWatcher instances)
135
var FsWatchFileInstances = Object.create(null);
136

    
137
// Private function: Instantiates the fs.watchFile interface or binds listeners
138
// to an existing one covering the same file system entry
139

    
140
// * path       - string, path to be watched
141
// * fullPath   - string, absolute path
142
// * options    - object, options to be passed to fs.watchFile
143
// * handlers   - object, container for event listener functions
144

    
145
// Returns close function
146
function setFsWatchFileListener(path, fullPath, options, handlers) {
147
  var listener = handlers.listener;
148
  var rawEmitter = handlers.rawEmitter;
149
  var container = FsWatchFileInstances[fullPath];
150
  var listeners = [];
151
  var rawEmitters = [];
152
  if (
153
    container && (
154
      container.options.persistent < options.persistent ||
155
      container.options.interval > options.interval
156
    )
157
  ) {
158
    // "Upgrade" the watcher to persistence or a quicker interval.
159
    // This creates some unlikely edge case issues if the user mixes
160
    // settings in a very weird way, but solving for those cases
161
    // doesn't seem worthwhile for the added complexity.
162
    listeners = container.listeners;
163
    rawEmitters = container.rawEmitters;
164
    fs.unwatchFile(fullPath);
165
    container = false;
166
  }
167
  if (!container) {
168
    listeners.push(listener);
169
    rawEmitters.push(rawEmitter);
170
    container = FsWatchFileInstances[fullPath] = {
171
      listeners: listeners,
172
      rawEmitters: rawEmitters,
173
      options: options,
174
      watcher: fs.watchFile(fullPath, options, function(curr, prev) {
175
        container.rawEmitters.forEach(function(rawEmitter) {
176
          rawEmitter('change', fullPath, {curr: curr, prev: prev});
177
        });
178
        var currmtime = curr.mtime.getTime();
179
        if (curr.size !== prev.size || currmtime > prev.mtime.getTime() || currmtime === 0) {
180
          container.listeners.forEach(function(listener) {
181
            listener(path, curr);
182
          });
183
        }
184
      })
185
    };
186
  } else {
187
    container.listeners.push(listener);
188
    container.rawEmitters.push(rawEmitter);
189
  }
190
  var listenerIndex = container.listeners.length - 1;
191

    
192
  // removes this instance's listeners and closes the underlying fs.watchFile
193
  // instance if there are no more listeners left
194
  return function close() {
195
    delete container.listeners[listenerIndex];
196
    delete container.rawEmitters[listenerIndex];
197
    if (!Object.keys(container.listeners).length) {
198
      fs.unwatchFile(fullPath);
199
      delete FsWatchFileInstances[fullPath];
200
    }
201
  };
202
}
203

    
204
// fake constructor for attaching nodefs-specific prototype methods that
205
// will be copied to FSWatcher's prototype
206
function NodeFsHandler() {}
207

    
208
// Private method: Watch file for changes with fs.watchFile or fs.watch.
209

    
210
// * path     - string, path to file or directory.
211
// * listener - function, to be executed on fs change.
212

    
213
// Returns close function for the watcher instance
214
NodeFsHandler.prototype._watchWithNodeFs =
215
function(path, listener) {
216
  var directory = sysPath.dirname(path);
217
  var basename = sysPath.basename(path);
218
  var parent = this._getWatchedDir(directory);
219
  parent.add(basename);
220
  var absolutePath = sysPath.resolve(path);
221
  var options = {persistent: this.options.persistent};
222
  if (!listener) listener = Function.prototype; // empty function
223

    
224
  var closer;
225
  if (this.options.usePolling) {
226
    options.interval = this.enableBinaryInterval && isBinaryPath(basename) ?
227
      this.options.binaryInterval : this.options.interval;
228
    closer = setFsWatchFileListener(path, absolutePath, options, {
229
      listener: listener,
230
      rawEmitter: this.emit.bind(this, 'raw')
231
    });
232
  } else {
233
    closer = setFsWatchListener(path, absolutePath, options, {
234
      listener: listener,
235
      errHandler: this._handleError.bind(this),
236
      rawEmitter: this.emit.bind(this, 'raw')
237
    });
238
  }
239
  return closer;
240
};
241

    
242
// Private method: Watch a file and emit add event if warranted
243

    
244
// * file       - string, the file's path
245
// * stats      - object, result of fs.stat
246
// * initialAdd - boolean, was the file added at watch instantiation?
247
// * callback   - function, called when done processing as a newly seen file
248

    
249
// Returns close function for the watcher instance
250
NodeFsHandler.prototype._handleFile =
251
function(file, stats, initialAdd, callback) {
252
  var dirname = sysPath.dirname(file);
253
  var basename = sysPath.basename(file);
254
  var parent = this._getWatchedDir(dirname);
255
  // stats is always present
256
  var prevStats = stats;
257

    
258
  // if the file is already being watched, do nothing
259
  if (parent.has(basename)) return callback();
260

    
261
  // kick off the watcher
262
  var closer = this._watchWithNodeFs(file, function(path, newStats) {
263
    if (!this._throttle('watch', file, 5)) return;
264
    if (!newStats || newStats && newStats.mtime.getTime() === 0) {
265
      fs.stat(file, function(error, newStats) {
266
        // Fix issues where mtime is null but file is still present
267
        if (error) {
268
          this._remove(dirname, basename);
269
        } else {
270
          // Check that change event was not fired because of changed only accessTime.
271
          var at = newStats.atime.getTime();
272
          var mt = newStats.mtime.getTime();
273
          if (!at || at <= mt || mt !== prevStats.mtime.getTime()) {
274
            this._emit('change', file, newStats);
275
          }
276
          prevStats = newStats;
277
        }
278
      }.bind(this));
279
    // add is about to be emitted if file not already tracked in parent
280
    } else if (parent.has(basename)) {
281
      // Check that change event was not fired because of changed only accessTime.
282
      var at = newStats.atime.getTime();
283
      var mt = newStats.mtime.getTime();
284
      if (!at || at <= mt ||  mt !== prevStats.mtime.getTime()) {
285
        this._emit('change', file, newStats);
286
      }
287
      prevStats = newStats;
288
    }
289
  }.bind(this));
290

    
291
  // emit an add event if we're supposed to
292
  if (!(initialAdd && this.options.ignoreInitial)) {
293
    if (!this._throttle('add', file, 0)) return;
294
    this._emit('add', file, stats);
295
  }
296

    
297
  if (callback) callback();
298
  return closer;
299
};
300

    
301
// Private method: Handle symlinks encountered while reading a dir
302

    
303
// * entry      - object, entry object returned by readdirp
304
// * directory  - string, path of the directory being read
305
// * path       - string, path of this item
306
// * item       - string, basename of this item
307

    
308
// Returns true if no more processing is needed for this entry.
309
NodeFsHandler.prototype._handleSymlink =
310
function(entry, directory, path, item) {
311
  var full = entry.fullPath;
312
  var dir = this._getWatchedDir(directory);
313

    
314
  if (!this.options.followSymlinks) {
315
    // watch symlink directly (don't follow) and detect changes
316
    this._readyCount++;
317
    fs.realpath(path, function(error, linkPath) {
318
      if (dir.has(item)) {
319
        if (this._symlinkPaths[full] !== linkPath) {
320
          this._symlinkPaths[full] = linkPath;
321
          this._emit('change', path, entry.stat);
322
        }
323
      } else {
324
        dir.add(item);
325
        this._symlinkPaths[full] = linkPath;
326
        this._emit('add', path, entry.stat);
327
      }
328
      this._emitReady();
329
    }.bind(this));
330
    return true;
331
  }
332

    
333
  // don't follow the same symlink more than once
334
  if (this._symlinkPaths[full]) return true;
335
  else this._symlinkPaths[full] = true;
336
};
337

    
338
// Private method: Read directory to add / remove files from `@watched` list
339
// and re-read it on change.
340

    
341
// * dir        - string, fs path.
342
// * stats      - object, result of fs.stat
343
// * initialAdd - boolean, was the file added at watch instantiation?
344
// * depth      - int, depth relative to user-supplied path
345
// * target     - string, child path actually targeted for watch
346
// * wh         - object, common watch helpers for this path
347
// * callback   - function, called when dir scan is complete
348

    
349
// Returns close function for the watcher instance
350
NodeFsHandler.prototype._handleDir =
351
function(dir, stats, initialAdd, depth, target, wh, callback) {
352
  var parentDir = this._getWatchedDir(sysPath.dirname(dir));
353
  var tracked = parentDir.has(sysPath.basename(dir));
354
  if (!(initialAdd && this.options.ignoreInitial) && !target && !tracked) {
355
    if (!wh.hasGlob || wh.globFilter(dir)) this._emit('addDir', dir, stats);
356
  }
357

    
358
  // ensure dir is tracked (harmless if redundant)
359
  parentDir.add(sysPath.basename(dir));
360
  this._getWatchedDir(dir);
361

    
362
  var read = function(directory, initialAdd, done) {
363
    // Normalize the directory name on Windows
364
    directory = sysPath.join(directory, '');
365

    
366
    if (!wh.hasGlob) {
367
      var throttler = this._throttle('readdir', directory, 1000);
368
      if (!throttler) return;
369
    }
370

    
371
    var previous = this._getWatchedDir(wh.path);
372
    var current = [];
373

    
374
    readdirp({
375
      root: directory,
376
      entryType: 'all',
377
      fileFilter: wh.filterPath,
378
      directoryFilter: wh.filterDir,
379
      depth: 0,
380
      lstat: true
381
    }).on('data', function(entry) {
382
      var item = entry.path;
383
      var path = sysPath.join(directory, item);
384
      current.push(item);
385

    
386
      if (entry.stat.isSymbolicLink() &&
387
        this._handleSymlink(entry, directory, path, item)) return;
388

    
389
      // Files that present in current directory snapshot
390
      // but absent in previous are added to watch list and
391
      // emit `add` event.
392
      if (item === target || !target && !previous.has(item)) {
393
        this._readyCount++;
394

    
395
        // ensure relativeness of path is preserved in case of watcher reuse
396
        path = sysPath.join(dir, sysPath.relative(dir, path));
397

    
398
        this._addToNodeFs(path, initialAdd, wh, depth + 1);
399
      }
400
    }.bind(this)).on('end', function() {
401
      var wasThrottled = throttler ? throttler.clear() : false;
402
      if (done) done();
403

    
404
      // Files that absent in current directory snapshot
405
      // but present in previous emit `remove` event
406
      // and are removed from @watched[directory].
407
      previous.children().filter(function(item) {
408
        return item !== directory &&
409
          current.indexOf(item) === -1 &&
410
          // in case of intersecting globs;
411
          // a path may have been filtered out of this readdir, but
412
          // shouldn't be removed because it matches a different glob
413
          (!wh.hasGlob || wh.filterPath({
414
            fullPath: sysPath.resolve(directory, item)
415
          }));
416
      }).forEach(function(item) {
417
        this._remove(directory, item);
418
      }, this);
419

    
420
      // one more time for any missed in case changes came in extremely quickly
421
      if (wasThrottled) read(directory, false);
422
    }.bind(this)).on('error', this._handleError.bind(this));
423
  }.bind(this);
424

    
425
  var closer;
426

    
427
  if (this.options.depth == null || depth <= this.options.depth) {
428
    if (!target) read(dir, initialAdd, callback);
429
    closer = this._watchWithNodeFs(dir, function(dirPath, stats) {
430
      // if current directory is removed, do nothing
431
      if (stats && stats.mtime.getTime() === 0) return;
432

    
433
      read(dirPath, false);
434
    });
435
  } else {
436
    callback();
437
  }
438
  return closer;
439
};
440

    
441
// Private method: Handle added file, directory, or glob pattern.
442
// Delegates call to _handleFile / _handleDir after checks.
443

    
444
// * path       - string, path to file or directory.
445
// * initialAdd - boolean, was the file added at watch instantiation?
446
// * depth      - int, depth relative to user-supplied path
447
// * target     - string, child path actually targeted for watch
448
// * callback   - function, indicates whether the path was found or not
449

    
450
// Returns nothing
451
NodeFsHandler.prototype._addToNodeFs =
452
function(path, initialAdd, priorWh, depth, target, callback) {
453
  if (!callback) callback = Function.prototype;
454
  var ready = this._emitReady;
455
  if (this._isIgnored(path) || this.closed) {
456
    ready();
457
    return callback(null, false);
458
  }
459

    
460
  var wh = this._getWatchHelpers(path, depth);
461
  if (!wh.hasGlob && priorWh) {
462
    wh.hasGlob = priorWh.hasGlob;
463
    wh.globFilter = priorWh.globFilter;
464
    wh.filterPath = priorWh.filterPath;
465
    wh.filterDir = priorWh.filterDir;
466
  }
467

    
468
  // evaluate what is at the path we're being asked to watch
469
  fs[wh.statMethod](wh.watchPath, function(error, stats) {
470
    if (this._handleError(error)) return callback(null, path);
471
    if (this._isIgnored(wh.watchPath, stats)) {
472
      ready();
473
      return callback(null, false);
474
    }
475

    
476
    var initDir = function(dir, target) {
477
      return this._handleDir(dir, stats, initialAdd, depth, target, wh, ready);
478
    }.bind(this);
479

    
480
    var closer;
481
    if (stats.isDirectory()) {
482
      closer = initDir(wh.watchPath, target);
483
    } else if (stats.isSymbolicLink()) {
484
      var parent = sysPath.dirname(wh.watchPath);
485
      this._getWatchedDir(parent).add(wh.watchPath);
486
      this._emit('add', wh.watchPath, stats);
487
      closer = initDir(parent, path);
488

    
489
      // preserve this symlink's target path
490
      fs.realpath(path, function(error, targetPath) {
491
        this._symlinkPaths[sysPath.resolve(path)] = targetPath;
492
        ready();
493
      }.bind(this));
494
    } else {
495
      closer = this._handleFile(wh.watchPath, stats, initialAdd, ready);
496
    }
497

    
498
    if (closer) {
499
      this._closers[path] = this._closers[path] || [];
500
      this._closers[path].push(closer);
501
    }
502
    callback(null, false);
503
  }.bind(this));
504
};
505

    
506
module.exports = NodeFsHandler;
(2-2/2)