index.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. 'use strict';
  2. var debug = require('debug')('wt');
  3. var path = require('path');
  4. var fs = require('fs');
  5. var Base = require('sdk-base');
  6. var util = require('util');
  7. var ndir = require('ndir');
  8. module.exports = Watcher;
  9. module.exports.Watcher = Watcher;
  10. /**
  11. * Watcher
  12. *
  13. * @param {Object} options
  14. * - {Boolean} [ignoreNodeModules] ignore node_modules or not, default is `true`
  15. * - {Boolean} [ignoreHidden] ignore hidden file or not, default is `true`
  16. * - {Number} [rewatchInterval] auto rewatch root dir interval,
  17. * default is `0`, don't rewatch.
  18. * @param {Function} [done], watch all dirs done callback.
  19. */
  20. function Watcher(options) {
  21. Base.call(this);
  22. // http://nodejs.org/dist/v0.11.12/docs/api/fs.html#fs_caveats
  23. // The recursive option is currently supported on OS X.
  24. // Only FSEvents supports this type of file watching
  25. // so it is unlikely any additional platforms will be added soon.
  26. options = options || {};
  27. if (options.ignoreHidden === undefined || options.ignoreHidden === null) {
  28. options.ignoreHidden = true;
  29. }
  30. if (options.ignoreNodeModules === undefined || options.ignoreNodeModules === null){
  31. options.ignoreNodeModules = true;
  32. }
  33. this._ignoreHidden = !!options.ignoreHidden;
  34. this._ignoreNodeModules = !!options.ignoreNodeModules;
  35. this._rewatchInterval = options.rewatchInterval;
  36. this.watchOptions = {
  37. persistent: true,
  38. recursive: false, // so we dont use this features
  39. };
  40. this._watchers = {};
  41. this._rootDirs = [];
  42. this._rewatchTimer = null;
  43. if (this._rewatchInterval && typeof this._rewatchInterval === 'number') {
  44. this._rewatchTimer = setInterval(this._rewatchRootDirs.bind(this), this._rewatchInterval);
  45. debug('start rewatch timer every %dms', this._rewatchInterval);
  46. }
  47. }
  48. Watcher.watch = function (dirs, options, done) {
  49. // watch(dirs, done);
  50. if (typeof options === 'function') {
  51. done = options;
  52. options = null;
  53. }
  54. var watcher = new Watcher(options).watch(dirs);
  55. if (typeof done === 'function') {
  56. var count = Array.isArray(dirs) ? dirs.length : 1;
  57. watcher.on('watch', function () {
  58. count--;
  59. if (count === 0) {
  60. done();
  61. }
  62. });
  63. }
  64. return watcher;
  65. };
  66. util.inherits(Watcher, Base);
  67. var proto = Watcher.prototype;
  68. proto.isWatching = function (dir) {
  69. return !!this._watchers[dir];
  70. };
  71. /**
  72. * Start watch dir(s)
  73. *
  74. * @param {Array|String} dirs: dir path or path list
  75. * @return {this}
  76. */
  77. proto.watch = function (dirs) {
  78. if (!Array.isArray(dirs)) {
  79. dirs = [dirs];
  80. }
  81. debug('watch(%j)', dirs);
  82. for (var i = 0; i < dirs.length; i++) {
  83. var dir = dirs[i];
  84. this._watchDir(dir);
  85. if (this._rootDirs.indexOf(dir) === -1) {
  86. this._rootDirs.push(dir);
  87. }
  88. }
  89. return this;
  90. };
  91. proto.unwatch = function (dirs) {
  92. if (!Array.isArray(dirs)) {
  93. dirs = [dirs];
  94. }
  95. for (var i = 0; i < dirs.length; i++) {
  96. var dir = dirs[i];
  97. this._unwatchDir(dir);
  98. if (this._rootDirs.indexOf(dir) !== -1) {
  99. // remove from root dirs
  100. this._rootDirs.splice(this._rootDirs.indexOf(dir), 1);
  101. }
  102. }
  103. return this;
  104. };
  105. proto.close = function () {
  106. for (var k in this._watchers) {
  107. this._watchers[k].close();
  108. this._watchers[k].removeAllListeners();
  109. this._watchers[k] = null;
  110. }
  111. this._watchers = {};
  112. // 等待一个事件循环后再移除所有时间,确保 watcher 的 error 事件还能被捕获
  113. setImmediate(function () {
  114. this.removeAllListeners();
  115. }.bind(this));
  116. if (this._rewatchTimer) {
  117. clearInterval(this._rewatchTimer);
  118. this._rewatchTimer = null;
  119. }
  120. };
  121. proto._rewatchRootDirs = function () {
  122. for (var i = 0; i < this._rootDirs.length; i++) {
  123. var dir = this._rootDirs[i];
  124. // watcher missing, meaning dir was deleted
  125. // try to rewatch again
  126. this._watchDirIfExists(dir);
  127. }
  128. };
  129. proto._watchDirIfExists = function (dir) {
  130. var that = this;
  131. fs.stat(dir, function (err, stat) {
  132. debug('[watchDirIfExists] %s, error: %s, exists: %s', dir, err, !!stat);
  133. if (stat && stat.isDirectory()) {
  134. if (!that._watchers[dir]) {
  135. // watch again!
  136. that._watchDir(dir);
  137. }
  138. } else if (!stat) {
  139. // not exists, close watcher
  140. that._unwatchDir(dir);
  141. }
  142. });
  143. };
  144. proto._watchDir = function (dir) {
  145. var watchers = this._watchers;
  146. var that = this;
  147. debug('walking %s...', dir);
  148. ndir.walk(dir).on('dir', function (dirpath) {
  149. if (path.basename(dirpath)[0] === '.' && that._ignoreHidden) {
  150. debug('ignore hidden dir: %s', dirpath);
  151. return;
  152. }
  153. if (path.basename(dirpath) === 'node_modules' && that._ignoreNodeModules){
  154. debug('ignore node_modules dir: %s', dirpath);
  155. return;
  156. }
  157. if (watchers[dirpath]) {
  158. debug('%s exists', dirpath);
  159. return;
  160. }
  161. debug('fs.watch(%s) start...', dirpath);
  162. var watcher;
  163. try {
  164. watcher = fs.watch(dirpath, that.watchOptions, that._handle.bind(that, dirpath));
  165. } catch (err) {
  166. err.dir = dirpath;
  167. that.emit('error', err);
  168. return;
  169. }
  170. watchers[dirpath] = watcher;
  171. watcher.once('error', that._onWatcherError.bind(that, dirpath));
  172. }).on('error', function (err) {
  173. err.dir = dir;
  174. that.emit('error', err);
  175. }).on('end', function () {
  176. debug('_watchDir(%s) done', dir);
  177. // debug('now watching %s', Object.keys(that._watchers));
  178. that.emit('watch', dir);
  179. });
  180. return this;
  181. };
  182. proto._unwatchDir = function (dir) {
  183. var watcher = this._watchers[dir];
  184. debug('unwatch %s, watcher exists: %s', dir, !!watcher);
  185. if (watcher) {
  186. watcher.close();
  187. watcher.removeAllListeners();
  188. delete this._watchers[dir];
  189. this.emit('unwatch', dir);
  190. }
  191. // should close all subdir watchers too
  192. var subdirs = Object.keys(this._watchers);
  193. for (var i = 0; i < subdirs.length; i++) {
  194. var subdir = subdirs[i];
  195. if (subdir.indexOf(dir + '/') === 0) {
  196. // close subdir watcher
  197. watcher = this._watchers[subdir];
  198. watcher.close();
  199. watcher.removeAllListeners();
  200. delete this._watchers[subdir];
  201. debug('close subdir %s watcher by %s', subdir, dir);
  202. }
  203. }
  204. };
  205. proto._onWatcherError = function (dir, err) {
  206. this._unwatchDir(dir);
  207. err.dir = dir;
  208. this.emit('error', err);
  209. };
  210. proto._handle = function (root, event, name) {
  211. var that = this;
  212. if (!name) {
  213. debug('[WARNING] event:%j, name:%j not exists on %s', root);
  214. return;
  215. }
  216. if (name[0] === '.' && this._ignoreHidden) {
  217. debug('ignore %s on %s/%s', event, root, name);
  218. return;
  219. }
  220. if (name === 'node_modules' && this._ignoreNodeModules) {
  221. debug('ignore %s on %s/%s', event, root, name);
  222. return;
  223. }
  224. // check root stat
  225. fs.exists(root, function (exists) {
  226. if (!exists) {
  227. debug('[handle] %s %s on %s, root not exists', event, name, root);
  228. that._handleChange({
  229. event: event,
  230. path: path.join(root, name),
  231. stat: null,
  232. remove: true,
  233. isDirectory: false,
  234. isFile: false,
  235. });
  236. // linux event, dir self remove, will fire `rename with dir name itself`
  237. if (that._watchers[root]) {
  238. debug('[handle] fire root:%s %s by %s', root, event, name);
  239. that._handleChange({
  240. event: event,
  241. path: root,
  242. stat: null,
  243. remove: true,
  244. isDirectory: true,
  245. isFile: false,
  246. });
  247. }
  248. return;
  249. }
  250. // children change
  251. debug('[handle] %s %s on %s, root exists', event, name, root);
  252. var fullpath = path.join(root, name);
  253. fs.stat(fullpath, function (err, stat) {
  254. var info = {
  255. event: event,
  256. path: fullpath,
  257. stat: stat,
  258. remove: false,
  259. isDirectory: stat && stat.isDirectory() || false,
  260. isFile: stat && stat.isFile() || false,
  261. };
  262. if (err) {
  263. if (err.code === 'ENOENT') {
  264. info.remove = true;
  265. }
  266. }
  267. if (event === 'change' && info.remove) {
  268. // this should be a fs.watch bug
  269. debug('[WARNING] %s on %s, but file not exists, ignore this', event, fullpath);
  270. return;
  271. }
  272. that._handleChange(info);
  273. });
  274. });
  275. };
  276. proto._handleChange = function (info) {
  277. debug('_handleChange(%j)', info);
  278. var that = this;
  279. if (info.remove) {
  280. var watcher = that._watchers[info.path];
  281. if (watcher) {
  282. // close the exists watcher
  283. info.isDirectory = true;
  284. that._unwatchDir(info.path);
  285. }
  286. } else if (info.isDirectory) {
  287. var watcher = that._watchers[info.path];
  288. if (!watcher) {
  289. // add new watcher
  290. that._watchDir(info.path);
  291. }
  292. }
  293. that.emit('all', info);
  294. if (info.remove) {
  295. debug('[remove event] %s, isDirectory: %s', info.path, info.isDirectory);
  296. that.emit('remove', info);
  297. } else if (info.isFile) {
  298. debug('[file change event] %s', info.path);
  299. that.emit('file', info);
  300. } else if (info.isDirectory) {
  301. debug('[dir change envet] %s', info.path);
  302. that.emit('dir', info);
  303. }
  304. };