watcher.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. var assert = require("assert");
  2. var path = require("path");
  3. var fs = require("graceful-fs");
  4. var spawn = require("child_process").spawn;
  5. var Q = require("q");
  6. var EventEmitter = require("events").EventEmitter;
  7. var ReadFileCache = require("./cache").ReadFileCache;
  8. var util = require("./util");
  9. var hasOwn = Object.prototype.hasOwnProperty;
  10. function Watcher(readFileCache, persistent) {
  11. assert.ok(this instanceof Watcher);
  12. assert.ok(this instanceof EventEmitter);
  13. assert.ok(readFileCache instanceof ReadFileCache);
  14. // During tests (and only during tests), persistent === false so that
  15. // the test suite can actually finish and exit.
  16. if (typeof persistent === "undefined") {
  17. persistent = true;
  18. }
  19. EventEmitter.call(this);
  20. var self = this;
  21. var sourceDir = readFileCache.sourceDir;
  22. var dirWatcher = new DirWatcher(sourceDir, persistent);
  23. Object.defineProperties(self, {
  24. sourceDir: { value: sourceDir },
  25. readFileCache: { value: readFileCache },
  26. dirWatcher: { value: dirWatcher }
  27. });
  28. // Watch everything the readFileCache already knows about, and any new
  29. // files added in the future.
  30. readFileCache.subscribe(function(relativePath) {
  31. self.watch(relativePath);
  32. });
  33. readFileCache.on("changed", function(relativePath) {
  34. self.emit("changed", relativePath);
  35. });
  36. function handleDirEvent(event, relativePath) {
  37. if (self.dirWatcher.ready) {
  38. self.getFileHandler(relativePath)(event);
  39. }
  40. }
  41. dirWatcher.on("added", function(relativePath) {
  42. handleDirEvent("added", relativePath);
  43. }).on("deleted", function(relativePath) {
  44. handleDirEvent("deleted", relativePath);
  45. }).on("changed", function(relativePath) {
  46. handleDirEvent("changed", relativePath);
  47. });
  48. }
  49. util.inherits(Watcher, EventEmitter);
  50. var Wp = Watcher.prototype;
  51. Wp.watch = function(relativePath) {
  52. this.dirWatcher.add(path.dirname(path.join(
  53. this.sourceDir, relativePath)));
  54. };
  55. Wp.readFileP = function(relativePath) {
  56. return this.readFileCache.readFileP(relativePath);
  57. };
  58. Wp.noCacheReadFileP = function(relativePath) {
  59. return this.readFileCache.noCacheReadFileP(relativePath);
  60. };
  61. Wp.getFileHandler = util.cachedMethod(function(relativePath) {
  62. var self = this;
  63. return function handler(event) {
  64. self.readFileCache.reportPossiblyChanged(relativePath);
  65. };
  66. });
  67. function orNull(err) {
  68. return null;
  69. }
  70. Wp.close = function() {
  71. this.dirWatcher.close();
  72. };
  73. /**
  74. * DirWatcher code adapted from Jeffrey Lin's original implementation:
  75. * https://github.com/jeffreylin/jsx_transformer_fun/blob/master/dirWatcher.js
  76. *
  77. * Invariant: this only watches the dir inode, not the actual path.
  78. * That means the dir can't be renamed and swapped with another dir.
  79. */
  80. function DirWatcher(inputPath, persistent) {
  81. assert.ok(this instanceof DirWatcher);
  82. var self = this;
  83. var absPath = path.resolve(inputPath);
  84. if (!fs.statSync(absPath).isDirectory()) {
  85. throw new Error(inputPath + "is not a directory!");
  86. }
  87. EventEmitter.call(self);
  88. self.ready = false;
  89. self.on("ready", function(){
  90. self.ready = true;
  91. });
  92. Object.defineProperties(self, {
  93. // Map of absDirPaths to fs.FSWatcher objects from fs.watch().
  94. watchers: { value: {} },
  95. dirContents: { value: {} },
  96. rootPath: { value: absPath },
  97. persistent: { value: !!persistent }
  98. });
  99. process.nextTick(function() {
  100. self.add(absPath);
  101. self.emit("ready");
  102. });
  103. }
  104. util.inherits(DirWatcher, EventEmitter);
  105. var DWp = DirWatcher.prototype;
  106. DWp.add = function(absDirPath) {
  107. var self = this;
  108. if (hasOwn.call(self.watchers, absDirPath)) {
  109. return;
  110. }
  111. self.watchers[absDirPath] = fs.watch(absDirPath, {
  112. persistent: this.persistent
  113. }).on("change", function(event, filename) {
  114. self.updateDirContents(absDirPath, event, filename);
  115. });
  116. // Update internal dir contents.
  117. self.updateDirContents(absDirPath);
  118. // Since we've never seen this path before, recursively add child
  119. // directories of this path. TODO: Don't do fs.readdirSync on the
  120. // same dir twice in a row. We already do an fs.statSync in
  121. // this.updateDirContents() and we're just going to do another one
  122. // here...
  123. fs.readdirSync(absDirPath).forEach(function(filename) {
  124. var filepath = path.join(absDirPath, filename);
  125. // Look for directories.
  126. if (fs.statSync(filepath).isDirectory()) {
  127. self.add(filepath);
  128. }
  129. });
  130. };
  131. DWp.updateDirContents = function(absDirPath, event, fsWatchReportedFilename) {
  132. var self = this;
  133. if (!hasOwn.call(self.dirContents, absDirPath)) {
  134. self.dirContents[absDirPath] = [];
  135. }
  136. var oldContents = self.dirContents[absDirPath];
  137. var newContents = fs.readdirSync(absDirPath);
  138. var deleted = {};
  139. var added = {};
  140. oldContents.forEach(function(filename) {
  141. deleted[filename] = true;
  142. });
  143. newContents.forEach(function(filename) {
  144. if (hasOwn.call(deleted, filename)) {
  145. delete deleted[filename];
  146. } else {
  147. added[filename] = true;
  148. }
  149. });
  150. var deletedNames = Object.keys(deleted);
  151. deletedNames.forEach(function(filename) {
  152. self.emit(
  153. "deleted",
  154. path.relative(
  155. self.rootPath,
  156. path.join(absDirPath, filename)
  157. )
  158. );
  159. });
  160. var addedNames = Object.keys(added);
  161. addedNames.forEach(function(filename) {
  162. self.emit(
  163. "added",
  164. path.relative(
  165. self.rootPath,
  166. path.join(absDirPath, filename)
  167. )
  168. );
  169. });
  170. // So changed is not deleted or added?
  171. if (fsWatchReportedFilename &&
  172. !hasOwn.call(deleted, fsWatchReportedFilename) &&
  173. !hasOwn.call(added, fsWatchReportedFilename))
  174. {
  175. self.emit(
  176. "changed",
  177. path.relative(
  178. self.rootPath,
  179. path.join(absDirPath, fsWatchReportedFilename)
  180. )
  181. );
  182. }
  183. // If any of the things removed were directories, remove their watchers.
  184. // If a dir was moved, hopefully two changed events fired?
  185. // 1) event in dir where it was removed
  186. // 2) event in dir where it was moved to (added)
  187. deletedNames.forEach(function(filename) {
  188. var filepath = path.join(absDirPath, filename);
  189. delete self.dirContents[filepath];
  190. delete self.watchers[filepath];
  191. });
  192. // if any of the things added were directories, recursively deal with them
  193. addedNames.forEach(function(filename) {
  194. var filepath = path.join(absDirPath, filename);
  195. if (fs.existsSync(filepath) &&
  196. fs.statSync(filepath).isDirectory())
  197. {
  198. self.add(filepath);
  199. // mighttttttt need a self.updateDirContents() here in case
  200. // we're somehow adding a path that replaces another one...?
  201. }
  202. });
  203. // Update state of internal dir contents.
  204. self.dirContents[absDirPath] = newContents;
  205. };
  206. DWp.close = function() {
  207. var watchers = this.watchers;
  208. Object.keys(watchers).forEach(function(filename) {
  209. watchers[filename].close();
  210. });
  211. };
  212. exports.Watcher = Watcher;