diff --git a/lib/gaze.js b/lib/gaze.js index 59e53c4..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: 500, - // Use a desired watch method over the other - forceWatchMethod: false + debounceDelay: 500 }); // Default done callback @@ -94,9 +92,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 +156,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 +315,26 @@ 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); } - }); - } - 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(); - } - } - }); - } + }, delay + 100); + + + }); } catch (err) { if (err.code === 'EMFILE') { return this.emit('error', new Error('EMFILE: Too many opened files.')); @@ -356,61 +344,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 new file mode 100644 index 0000000..9200f11 --- /dev/null +++ b/test/safewrite_test.js @@ -0,0 +1,61 @@ +'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, 1000); + } else { + this.close(); + test.done(); + } + }); + + setTimeout(function() { + simSafewrite(); + }, 1000); + + }); + } +}; diff --git a/test/watch_test.js b/test/watch_test.js index f068318..0c7ebaa 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) { @@ -193,27 +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(); - }); - fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'two.js'), 'var two = true;'); - }); - }, - 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(); - }); - fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'two.js'), 'var two = true;'); - }); } };