From 0658655c20c90b9b2c91788069a1d5141f660c17 Mon Sep 17 00:00:00 2001 From: Kyle Robinson Young Date: Mon, 10 Dec 2012 17:34:36 -0800 Subject: [PATCH 1/3] Test to simulate a safewrite event --- test/safewrite_test.js | 58 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 test/safewrite_test.js diff --git a/test/safewrite_test.js b/test/safewrite_test.js new file mode 100644 index 0000000..788d6d5 --- /dev/null +++ b/test/safewrite_test.js @@ -0,0 +1,58 @@ +'use strict'; + +var gaze = require('../lib/gaze.js'); +var path = require('path'); +var fs = require('fs'); + +// Node v0.6 compat +fs.existsSync = fs.existsSync || path.existsSync; + +// Clean up helper to call in setUp and tearDown +function cleanUp(done) { + [ + 'safewrite.js' + ].forEach(function(d) { + var p = path.resolve(__dirname, 'fixtures', d); + if (fs.existsSync(p)) { fs.unlinkSync(p); } + }); + done(); +} + +exports.safewrite = { + setUp: function(done) { + process.chdir(path.resolve(__dirname, 'fixtures')); + cleanUp(done); + }, + tearDown: cleanUp, + safewrite: function(test) { + test.expect(4); + + var times = 0; + var file = path.resolve(__dirname, 'fixtures', 'safewrite.js'); + var backup = path.resolve(__dirname, 'fixtures', 'safewrite.ext~'); + fs.writeFileSync(file, 'var safe = true;'); + + function simSafewrite() { + fs.writeFileSync(backup, fs.readFileSync(file)); + fs.unlinkSync(file); + fs.renameSync(backup, file); + times++; + } + + gaze('**/*', function() { + this.on('all', function(action, filepath) { + test.equal(action, 'changed'); + test.equal(path.basename(filepath), 'safewrite.js'); + + if (times < 2) { + setTimeout(simSafewrite, 500); + } else { + this.close(); + test.done(); + } + }); + + simSafewrite(); + }); + } +}; From 1ab9b8dffa084ef9e549da39e38675df9f40c955 Mon Sep 17 00:00:00 2001 From: rgaskill Date: Thu, 13 Dec 2012 15:12:13 -0600 Subject: [PATCH 2/3] dir fs.watch with file fs.watchFile --- lib/gaze.js | 170 ++++++++++++++++++++++++++++------------- test/safewrite_test.js | 7 +- test/watch_test.js | 18 ++++- 3 files changed, 136 insertions(+), 59 deletions(-) diff --git a/lib/gaze.js b/lib/gaze.js index 59e53c4..b964859 100644 --- a/lib/gaze.js +++ b/lib/gaze.js @@ -66,7 +66,7 @@ Gaze = gaze.Gaze = __extends(function(files, opts, done) { // Interval to pass to fs.watchFile interval: 100, // Delay for events called in succession for the same file/event - debounceDelay: 500, + debounceDelay: 1000, // Use a desired watch method over the other forceWatchMethod: false }); @@ -94,9 +94,11 @@ Gaze = gaze.Gaze = __extends(function(files, opts, done) { } // Initialize the watch on files - this.add(files, function() { - _this._initWatched(done); - }); + this.add(files, done); + //this.add(files, function() { + // this was already called in the add function. + // _this._initWatched(done); + //}); return this; }, events.EventEmitter); @@ -156,7 +158,9 @@ Gaze.prototype.close = function(_reset) { this._watchers = Object.create(null); Object.keys(this._watched).forEach(function(dir) { fs.unwatchFile(dir); - _this._watched[dir].forEach(fs.unwatchFile); + _this._watched[dir].forEach(function(uFile) { + fs.unwatchFile(uFile); + }); }); if (_reset) { this._watched = Object.create(null); @@ -313,40 +317,62 @@ Gaze.prototype._objectPush = function(obj, key, val) { Gaze.prototype._isMatch = function(file) { var matched = false; this._patterns.forEach(function(pattern) { - if (matched = minimatch(file, pattern)) { + if (matched || (matched = minimatch(file, pattern)) ) { return false; } }); return matched; }; -// Wrapper for fs.watch/fs.watchFile -Gaze.prototype._watchFile = function(file, done) { +Gaze.prototype._watchDir = function(dir, done) { var _this = this; - var opts = Object.create(this.options); - // TODO: Optimize this. Should kill the other func per event - // then unwatch files to not even bother using the method - var watchOne = function() { done(null, file); }; - var watchTwo = function() { done(null, file); }; try { - if (_this.options.forceWatchMethod === false || _this.options.forceWatchMethod === 'new') { - _this._watchers[file] = fs.watch(file, opts, function(event) { - if (typeof watchOne === 'function') { - watchTwo = null; - watchOne(); + _this._watchers[dir] = fs.watch(dir, function(event) { + //race condition. Let's give the fs a little time to settle down. so we don't fire events on non existent files. + setTimeout(function() { + if ( fs.existsSync(dir) ){ + done(null, dir); } - }); + }, delay + 100); + + + }); + } catch (err) { + if (err.code === 'EMFILE') { + return this.emit('error', new Error('EMFILE: Too many opened files.')); } - if (_this.options.forceWatchMethod === false || _this.options.forceWatchMethod === 'old') { - fs.watchFile(file, opts, function(curr, prev) { - if (curr.mtime.getTime() !== prev.mtime.getTime()) { - if (typeof watchTwo === 'function') { - watchOne = null; - watchTwo(); + return this.emit('error', err); + } + return this; +}; + +Gaze.prototype._watchFile = function(file, done) { + var _this = this; + try { + _this._watchers[file] = fs.watch(file,function listener(event) { + + //race condition. Let's give the fs a little time to settle down. so we don't fire events on non existent files. + setTimeout(function() { + + if ( fs.existsSync(file) ){ + + var watcher; + if ( event === 'rename') { + + watcher = _this._watchers[file]; + if ( watcher ){ + watcher.close(); + _this._watchers[file] = fs.watch(file, listener); + } + } + done(null, file); + } - }); - } + + }, delay + 100); + + }); } catch (err) { if (err.code === 'EMFILE') { return this.emit('error', new Error('EMFILE: Too many opened files.')); @@ -356,61 +382,97 @@ Gaze.prototype._watchFile = function(file, done) { return this; }; +Gaze.prototype._pollFile = function(file, done) { + var _this = this; + var opts = { persistent: true, interval: _this.options.interval }; + try { + fs.watchFile(file, opts, function(curr, prev) { + done(null, file); + }); + } catch (err) { + if (err.code === 'EMFILE') { + return this.emit('error', new Error('EMFILE: Too many opened files.')); + } + return this.emit('error', err); + } + return this; +}; + // Initialize the actual watch on `watched` files Gaze.prototype._initWatched = function(done) { var _this = this; var cwd = this.options.cwd || process.cwd(); - async.forEachSeries(Object.keys(_this._watched), function(dir, next) { + var curWatched = Object.keys(_this._watched); + async.forEachSeries(curWatched, function(dir, next) { var files = _this._watched[dir]; - // Triggered when a watched dir has an event - _this._watchFile(dir, function(event, dirpath) { + _this._watchDir(dir, function(event, dirpath) { var relDir = cwd === dir ? '.' : path.relative(cwd, dir); - return fs.readdir(dirpath, function(err, current) { + + fs.readdir(dirpath, function(err, current) { if (err) { return _this.emit('error', err); } if (!current) { return; } + try { + //append path.sep to directories so they match previous. + current = current.map(function(curPath) { + if ( fs.existsSync(curPath) && fs.statSync(path.join(dir, curPath)).isDirectory() ){ + return curPath + path.sep; + } else { + return curPath; + } + }); + } catch (err) { + //race condition-- sometimes the file no longer exists + } + // Get watched files for this dir var previous = _this.relative(relDir); + // If file was deleted _.filter(previous, function(file) { return _.indexOf(current, file) < 0; }).forEach(function(file) { - var filepath = path.join(dir, file); - if (!_this._isDir(file)) { - _this.remove(filepath); - _this.emit('deleted', filepath); - } - }); + var filepath = path.join(dir, file); + if (/*fs.existsSync(filepath) &&*/ !_this._isDir(file)) { //race condition + _this.remove(filepath); + _this.emit('deleted', filepath); + } + }); // If file was added _.filter(current, function(file) { return _.indexOf(previous, file) < 0; }).forEach(function(file) { - // Is it a matching pattern? - if (_this._isMatch(file)) { - var filepath = path.join(dir, file); - // Add to watch then emit event - _this.add(file, function() { - _this.emit('added', filepath); + // Is it a matching pattern? + if (_this._isMatch(file)) { + var filepath = path.join(dir, file); + // Add to watch then emit event + _this.add(file, function() { + _this.emit('added', filepath); + }); + } }); - } - }); }); + + }); // Watch for change/rename events on files files.forEach(function(file) { - _this._watchFile(file, function(err, filepath) { - // Only emit changed if the file still exists - // Prevents changed/deleted duplicate events - // TODO: This ignores changed events on folders, maybe support this? - // When a file is added, a folder changed event emits first - if (fs.existsSync(filepath) && !_this._isDir(file)) { - _this.emit('changed', filepath); - } - }); + if (!_this._isDir(file)){ + _this._pollFile(file, function(err, filepath) { + // Only emit changed if the file still exists + // Prevents changed/deleted duplicate events + // TODO: This ignores changed events on folders, maybe support this? + // When a file is added, a folder changed event emits first + if (fs.existsSync(filepath) ) { + _this.emit('changed', filepath); + } + }); + } + }); next(); diff --git a/test/safewrite_test.js b/test/safewrite_test.js index 788d6d5..9200f11 100644 --- a/test/safewrite_test.js +++ b/test/safewrite_test.js @@ -45,14 +45,17 @@ exports.safewrite = { test.equal(path.basename(filepath), 'safewrite.js'); if (times < 2) { - setTimeout(simSafewrite, 500); + setTimeout(simSafewrite, 1000); } else { this.close(); test.done(); } }); - simSafewrite(); + setTimeout(function() { + simSafewrite(); + }, 1000); + }); } }; diff --git a/test/watch_test.js b/test/watch_test.js index f068318..f4253ee 100644 --- a/test/watch_test.js +++ b/test/watch_test.js @@ -124,7 +124,11 @@ exports.watch = { test.done(); }, 5000); }); - fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'one.js'), 'var one = true;'); + + setTimeout(function() { + fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'one.js'), 'var one = true;'); + }, 1000); + }); }, emitTwice: function(test) { @@ -202,7 +206,11 @@ exports.watch = { watcher.close(); test.done(); }); - fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'two.js'), 'var two = true;'); + + setTimeout(function() { + fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'two.js'), 'var two = true;'); + }, 1000); + }); }, forceWatchMethodNew: function(test) { @@ -213,7 +221,11 @@ exports.watch = { watcher.close(); test.done(); }); - fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'two.js'), 'var two = true;'); + + setTimeout(function() { + fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'two.js'), 'var two = true;'); + }, 1000); + }); } }; From 23d92f3f0d0e0cce51cca2291c555de26343e199 Mon Sep 17 00:00:00 2001 From: rgaskill Date: Fri, 14 Dec 2012 06:26:33 -0600 Subject: [PATCH 3/3] A little code cleanup. --- lib/gaze.js | 40 +--------------------------------------- test/watch_test.js | 30 ------------------------------ 2 files changed, 1 insertion(+), 69 deletions(-) diff --git a/lib/gaze.js b/lib/gaze.js index b964859..86f58b0 100644 --- a/lib/gaze.js +++ b/lib/gaze.js @@ -66,9 +66,7 @@ Gaze = gaze.Gaze = __extends(function(files, opts, done) { // Interval to pass to fs.watchFile interval: 100, // Delay for events called in succession for the same file/event - debounceDelay: 1000, - // Use a desired watch method over the other - forceWatchMethod: false + debounceDelay: 500 }); // Default done callback @@ -346,42 +344,6 @@ Gaze.prototype._watchDir = function(dir, done) { return this; }; -Gaze.prototype._watchFile = function(file, done) { - var _this = this; - try { - _this._watchers[file] = fs.watch(file,function listener(event) { - - //race condition. Let's give the fs a little time to settle down. so we don't fire events on non existent files. - setTimeout(function() { - - if ( fs.existsSync(file) ){ - - var watcher; - if ( event === 'rename') { - - watcher = _this._watchers[file]; - if ( watcher ){ - watcher.close(); - _this._watchers[file] = fs.watch(file, listener); - } - - } - done(null, file); - - } - - }, delay + 100); - - }); - } catch (err) { - if (err.code === 'EMFILE') { - return this.emit('error', new Error('EMFILE: Too many opened files.')); - } - return this.emit('error', err); - } - return this; -}; - Gaze.prototype._pollFile = function(file, done) { var _this = this; var opts = { persistent: true, interval: _this.options.interval }; diff --git a/test/watch_test.js b/test/watch_test.js index f4253ee..0c7ebaa 100644 --- a/test/watch_test.js +++ b/test/watch_test.js @@ -197,35 +197,5 @@ exports.watch = { }); createFile(); }); - }, - forceWatchMethodOld: function(test) { - test.expect(1); - gaze('**/*', {forceWatchMethod:'old'}, function(err, watcher) { - watcher.on('all', function(e, filepath) { - test.ok(true); - watcher.close(); - test.done(); - }); - - setTimeout(function() { - fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'two.js'), 'var two = true;'); - }, 1000); - - }); - }, - forceWatchMethodNew: function(test) { - test.expect(1); - gaze('**/*', {forceWatchMethod:'new'}, function(err, watcher) { - watcher.on('all', function(e, filepath) { - test.ok(true); - watcher.close(); - test.done(); - }); - - setTimeout(function() { - fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'two.js'), 'var two = true;'); - }, 1000); - - }); } };