Skip to content

Commit

Permalink
Fix race condition with recursive directories in NodeWatcher (#95)
Browse files Browse the repository at this point in the history
* Add unit test for multi-level subdirectory creation

This problem occurs when you use `mkdir -p`, or make two folders at once.
Nothing below the second level is watched properly with polling or
"normal" mode.

It works as expected when using watchman.

* Fix race condition with recursive directories in NodeWatcher

- add two tests to demonstrate problem
- modify node_watcher to recursively register new directories
  • Loading branch information
marcello3d authored and amasad committed May 22, 2017
1 parent dde845f commit 6dc7022
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 36 deletions.
32 changes: 28 additions & 4 deletions src/node_watcher.js
Expand Up @@ -328,11 +328,35 @@ NodeWatcher.prototype.emitEvent = function(type, file, stat) {
clearTimeout(this.changeTimers[key]);
this.changeTimers[key] = setTimeout(function() {
delete this.changeTimers[key];
this.emit(type, file, this.root, stat);
this.emit(ALL_EVENT, type, file, this.root, stat);
if (type === ADD_EVENT && stat.isDirectory()) {
// Recursively emit add events and watch for sub-files/folders
recReaddir(
path.resolve(this.root, file),
function emitAddDir (dir, stats) {
this.watchdir(dir);
this.rawEmitEvent(ADD_EVENT, path.relative(this.root, dir), stats);
}.bind(this),
function emitAddFile (file, stats) {
this.register(file);
this.rawEmitEvent(ADD_EVENT, path.relative(this.root, file), stats);
}.bind(this),
function endCallback () {},
this.ignored
);
} else {
this.rawEmitEvent(type, file, stat);
}
}.bind(this), DEFAULT_DELAY);
};

/**
* Actually emit the events
*/
NodeWatcher.prototype.rawEmitEvent = function (type, file, stat) {
this.emit(type, file, this.root, stat);
this.emit(ALL_EVENT, type, file, this.root, stat);
};

/**
* Traverse a directory recursively calling `callback` on every directory.
*
Expand Down Expand Up @@ -370,7 +394,7 @@ function recReaddir(dir, dirCallback, fileCallback, endCallback, ignored) {
*/

function normalizeProxy(callback) {
return function(filepath) {
return callback(path.normalize(filepath));
return function(filepath, stats) {
return callback(path.normalize(filepath), stats);
};
}
95 changes: 63 additions & 32 deletions test/test.js
Expand Up @@ -139,39 +139,68 @@ function harness(mode) {
});
});

it('emits events for subdir/subdir files', function(done) {
var subdir1 = 'subsub_1';
var subdir2 = 'subsub_2';
var filename = 'file_1';
var testfile = jo(testdir, subdir1, subdir2, filename);
var addedSubdir1 = false;
var addedSubdir2 = false;
var addedFile = false;
this.watcher.on('add', function(filepath, dir) {
if (filepath === subdir1) {
assert.equal(addedSubdir1, false);
addedSubdir1 = true;
} else if (filepath === jo(subdir1, subdir2)) {
assert.equal(addedSubdir2, false);
addedSubdir2 = true;
} else if (filepath === jo(subdir1, subdir2, filename)) {
assert.equal(addedFile, false);
addedFile = true;
}
if (addedSubdir1 && addedSubdir2 && addedFile) {
done();
}
if (!mode.poll) {
it('emits events for subdir/subdir files', function(done) {
var subdir1 = 'subsub_1';
var subdir2 = 'subsub_2';
var filename = 'file_1';
var testfile = jo(testdir, subdir1, subdir2, filename);
var addedSubdir1 = false;
var addedSubdir2 = false;
var addedFile = false;
this.watcher.on('add', function(filepath) {
if (filepath === subdir1) {
assert.equal(addedSubdir1, false);
addedSubdir1 = true;
} else if (filepath === jo(subdir1, subdir2)) {
assert.equal(addedSubdir2, false);
addedSubdir2 = true;
} else if (filepath === jo(subdir1, subdir2, filename)) {
assert.equal(addedFile, false);
addedFile = true;
}
if (addedSubdir1 && addedSubdir2 && addedFile) {
done();
}
});
this.watcher.on('ready', function() {
fs.mkdirSync(jo(testdir, subdir1));
fs.mkdirSync(jo(testdir, subdir1, subdir2));
fs.writeFileSync(testfile, 'wow');
});
});
this.watcher.on('ready', function() {
fs.mkdirSync(jo(testdir, subdir1));
defer(function() {
it('emits events for subdir/subdir files 2', function (done) {
var subdir1 = 'subsub_1b';
var subdir2 = 'subsub_2b';
var filename = 'file_1b';
var testfile = jo(testdir, subdir1, subdir2, filename);
var addedSubdir1 = false;
var addedSubdir2 = false;
var addedFile = false;
this.watcher.on('add', function (filepath) {
if (filepath === subdir1) {
assert.equal(addedSubdir1, false);
addedSubdir1 = true;
} else if (filepath === jo(subdir1, subdir2)) {
assert.equal(addedSubdir2, false);
addedSubdir2 = true;
} else if (filepath === jo(subdir1, subdir2, filename)) {
assert.equal(addedFile, false);
addedFile = true;
}
if (addedSubdir1 && addedSubdir2 && addedFile) {
done();
}
});
this.watcher.on('ready', function () {
fs.mkdirSync(jo(testdir, subdir1));
fs.mkdirSync(jo(testdir, subdir1, subdir2));
defer(function() {
setTimeout(function () {
fs.writeFileSync(testfile, 'wow');
});
}, 500);
});
});
});
}

it('adding a file will trigger an add event', function(done) {
var testfile = jo(testdir, 'file_x' + Math.floor(Math.random() * 10000));
Expand All @@ -182,8 +211,8 @@ function harness(mode) {
done();
});
this.watcher.on('change', function(filepath, dir, stat) {
done(new Error('Should not emit change on add'))
})
done(new Error('Should not emit change on add'));
});
this.watcher.on('ready', function() {
fs.writeFileSync(testfile, 'wow');
});
Expand All @@ -201,7 +230,7 @@ function harness(mode) {
});
});

it('chaging, removing, deleting should emit the "all" event', function(done) {
it('changing, removing, deleting should emit the "all" event', function(done) {
var toChange = jo(testdir, 'file_4');
var toDelete = jo(testdir, 'file_5');
var toAdd = jo(testdir, 'file_x' + Math.floor(Math.random() * 10000));
Expand Down Expand Up @@ -440,7 +469,9 @@ function harness(mode) {
this.watcher.on('change', function(filepath, dir) {
assert.ok(filepath.match(/file_(1|2)/), 'only file_1 and file_2');
assert.equal(dir, testdir);
if (++i == 2) done();
if (++i === 2) {
done();
}
});
this.watcher.on('ready', function() {
fs.writeFileSync(jo(testdir, 'file_1'), 'wow');
Expand Down

0 comments on commit 6dc7022

Please sign in to comment.