123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330 |
- 'use strict';
- var debug = require('debug')('wt');
- var path = require('path');
- var fs = require('fs');
- var Base = require('sdk-base');
- var util = require('util');
- var ndir = require('ndir');
- module.exports = Watcher;
- module.exports.Watcher = Watcher;
- /**
- * Watcher
- *
- * @param {Object} options
- * - {Boolean} [ignoreNodeModules] ignore node_modules or not, default is `true`
- * - {Boolean} [ignoreHidden] ignore hidden file or not, default is `true`
- * - {Number} [rewatchInterval] auto rewatch root dir interval,
- * default is `0`, don't rewatch.
- * @param {Function} [done], watch all dirs done callback.
- */
- function Watcher(options) {
- Base.call(this);
- // http://nodejs.org/dist/v0.11.12/docs/api/fs.html#fs_caveats
- // The recursive option is currently supported on OS X.
- // Only FSEvents supports this type of file watching
- // so it is unlikely any additional platforms will be added soon.
- options = options || {};
- if (options.ignoreHidden === undefined || options.ignoreHidden === null) {
- options.ignoreHidden = true;
- }
- if (options.ignoreNodeModules === undefined || options.ignoreNodeModules === null){
- options.ignoreNodeModules = true;
- }
- this._ignoreHidden = !!options.ignoreHidden;
- this._ignoreNodeModules = !!options.ignoreNodeModules;
- this._rewatchInterval = options.rewatchInterval;
- this.watchOptions = {
- persistent: true,
- recursive: false, // so we dont use this features
- };
- this._watchers = {};
- this._rootDirs = [];
- this._rewatchTimer = null;
- if (this._rewatchInterval && typeof this._rewatchInterval === 'number') {
- this._rewatchTimer = setInterval(this._rewatchRootDirs.bind(this), this._rewatchInterval);
- debug('start rewatch timer every %dms', this._rewatchInterval);
- }
- }
- Watcher.watch = function (dirs, options, done) {
- // watch(dirs, done);
- if (typeof options === 'function') {
- done = options;
- options = null;
- }
- var watcher = new Watcher(options).watch(dirs);
- if (typeof done === 'function') {
- var count = Array.isArray(dirs) ? dirs.length : 1;
- watcher.on('watch', function () {
- count--;
- if (count === 0) {
- done();
- }
- });
- }
- return watcher;
- };
- util.inherits(Watcher, Base);
- var proto = Watcher.prototype;
- proto.isWatching = function (dir) {
- return !!this._watchers[dir];
- };
- /**
- * Start watch dir(s)
- *
- * @param {Array|String} dirs: dir path or path list
- * @return {this}
- */
- proto.watch = function (dirs) {
- if (!Array.isArray(dirs)) {
- dirs = [dirs];
- }
- debug('watch(%j)', dirs);
- for (var i = 0; i < dirs.length; i++) {
- var dir = dirs[i];
- this._watchDir(dir);
- if (this._rootDirs.indexOf(dir) === -1) {
- this._rootDirs.push(dir);
- }
- }
- return this;
- };
- proto.unwatch = function (dirs) {
- if (!Array.isArray(dirs)) {
- dirs = [dirs];
- }
- for (var i = 0; i < dirs.length; i++) {
- var dir = dirs[i];
- this._unwatchDir(dir);
- if (this._rootDirs.indexOf(dir) !== -1) {
- // remove from root dirs
- this._rootDirs.splice(this._rootDirs.indexOf(dir), 1);
- }
- }
- return this;
- };
- proto.close = function () {
- for (var k in this._watchers) {
- this._watchers[k].close();
- this._watchers[k].removeAllListeners();
- this._watchers[k] = null;
- }
- this._watchers = {};
- // 等待一个事件循环后再移除所有时间,确保 watcher 的 error 事件还能被捕获
- setImmediate(function () {
- this.removeAllListeners();
- }.bind(this));
- if (this._rewatchTimer) {
- clearInterval(this._rewatchTimer);
- this._rewatchTimer = null;
- }
- };
- proto._rewatchRootDirs = function () {
- for (var i = 0; i < this._rootDirs.length; i++) {
- var dir = this._rootDirs[i];
- // watcher missing, meaning dir was deleted
- // try to rewatch again
- this._watchDirIfExists(dir);
- }
- };
- proto._watchDirIfExists = function (dir) {
- var that = this;
- fs.stat(dir, function (err, stat) {
- debug('[watchDirIfExists] %s, error: %s, exists: %s', dir, err, !!stat);
- if (stat && stat.isDirectory()) {
- if (!that._watchers[dir]) {
- // watch again!
- that._watchDir(dir);
- }
- } else if (!stat) {
- // not exists, close watcher
- that._unwatchDir(dir);
- }
- });
- };
- proto._watchDir = function (dir) {
- var watchers = this._watchers;
- var that = this;
- debug('walking %s...', dir);
- ndir.walk(dir).on('dir', function (dirpath) {
- if (path.basename(dirpath)[0] === '.' && that._ignoreHidden) {
- debug('ignore hidden dir: %s', dirpath);
- return;
- }
- if (path.basename(dirpath) === 'node_modules' && that._ignoreNodeModules){
- debug('ignore node_modules dir: %s', dirpath);
- return;
- }
- if (watchers[dirpath]) {
- debug('%s exists', dirpath);
- return;
- }
- debug('fs.watch(%s) start...', dirpath);
- var watcher;
- try {
- watcher = fs.watch(dirpath, that.watchOptions, that._handle.bind(that, dirpath));
- } catch (err) {
- err.dir = dirpath;
- that.emit('error', err);
- return;
- }
- watchers[dirpath] = watcher;
- watcher.once('error', that._onWatcherError.bind(that, dirpath));
- }).on('error', function (err) {
- err.dir = dir;
- that.emit('error', err);
- }).on('end', function () {
- debug('_watchDir(%s) done', dir);
- // debug('now watching %s', Object.keys(that._watchers));
- that.emit('watch', dir);
- });
- return this;
- };
- proto._unwatchDir = function (dir) {
- var watcher = this._watchers[dir];
- debug('unwatch %s, watcher exists: %s', dir, !!watcher);
- if (watcher) {
- watcher.close();
- watcher.removeAllListeners();
- delete this._watchers[dir];
- this.emit('unwatch', dir);
- }
- // should close all subdir watchers too
- var subdirs = Object.keys(this._watchers);
- for (var i = 0; i < subdirs.length; i++) {
- var subdir = subdirs[i];
- if (subdir.indexOf(dir + '/') === 0) {
- // close subdir watcher
- watcher = this._watchers[subdir];
- watcher.close();
- watcher.removeAllListeners();
- delete this._watchers[subdir];
- debug('close subdir %s watcher by %s', subdir, dir);
- }
- }
- };
- proto._onWatcherError = function (dir, err) {
- this._unwatchDir(dir);
- err.dir = dir;
- this.emit('error', err);
- };
- proto._handle = function (root, event, name) {
- var that = this;
- if (!name) {
- debug('[WARNING] event:%j, name:%j not exists on %s', root);
- return;
- }
- if (name[0] === '.' && this._ignoreHidden) {
- debug('ignore %s on %s/%s', event, root, name);
- return;
- }
- if (name === 'node_modules' && this._ignoreNodeModules) {
- debug('ignore %s on %s/%s', event, root, name);
- return;
- }
- // check root stat
- fs.exists(root, function (exists) {
- if (!exists) {
- debug('[handle] %s %s on %s, root not exists', event, name, root);
- that._handleChange({
- event: event,
- path: path.join(root, name),
- stat: null,
- remove: true,
- isDirectory: false,
- isFile: false,
- });
- // linux event, dir self remove, will fire `rename with dir name itself`
- if (that._watchers[root]) {
- debug('[handle] fire root:%s %s by %s', root, event, name);
- that._handleChange({
- event: event,
- path: root,
- stat: null,
- remove: true,
- isDirectory: true,
- isFile: false,
- });
- }
- return;
- }
- // children change
- debug('[handle] %s %s on %s, root exists', event, name, root);
- var fullpath = path.join(root, name);
- fs.stat(fullpath, function (err, stat) {
- var info = {
- event: event,
- path: fullpath,
- stat: stat,
- remove: false,
- isDirectory: stat && stat.isDirectory() || false,
- isFile: stat && stat.isFile() || false,
- };
- if (err) {
- if (err.code === 'ENOENT') {
- info.remove = true;
- }
- }
- if (event === 'change' && info.remove) {
- // this should be a fs.watch bug
- debug('[WARNING] %s on %s, but file not exists, ignore this', event, fullpath);
- return;
- }
- that._handleChange(info);
- });
- });
- };
- proto._handleChange = function (info) {
- debug('_handleChange(%j)', info);
- var that = this;
- if (info.remove) {
- var watcher = that._watchers[info.path];
- if (watcher) {
- // close the exists watcher
- info.isDirectory = true;
- that._unwatchDir(info.path);
- }
- } else if (info.isDirectory) {
- var watcher = that._watchers[info.path];
- if (!watcher) {
- // add new watcher
- that._watchDir(info.path);
- }
- }
- that.emit('all', info);
- if (info.remove) {
- debug('[remove event] %s, isDirectory: %s', info.path, info.isDirectory);
- that.emit('remove', info);
- } else if (info.isFile) {
- debug('[file change event] %s', info.path);
- that.emit('file', info);
- } else if (info.isDirectory) {
- debug('[dir change envet] %s', info.path);
- that.emit('dir', info);
- }
- };
|