Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Safewrite Option2 #9

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 83 additions & 59 deletions lib/gaze.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.'));
Expand All @@ -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();
Expand Down
61 changes: 61 additions & 0 deletions test/safewrite_test.js
Original file line number Diff line number Diff line change
@@ -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);

});
}
};
28 changes: 5 additions & 23 deletions test/watch_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;');
});
}
};