Skip to content

Commit

Permalink
add polling
Browse files Browse the repository at this point in the history
  • Loading branch information
sokra committed Jan 7, 2019
1 parent 54c5b24 commit fb16799
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 55 deletions.
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ node_js:
- "8"
- "6"

env:
- NORMAL=1
- WATCHPACK_POLLING=200

script:
- yarn cover --report lcovonly

Expand Down
147 changes: 125 additions & 22 deletions lib/DirectoryWatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const watcherManager = require("./watcherManager");
let FS_ACCURACY = 1000;

const IS_OSX = require("os").platform() === "darwin";
const FORCE_POLLING = process.env.WATCHPACK_POLLING ? +process.env.WATCHPACK_POLLING || true : false;

function withoutCase(str) {
return str.toLowerCase();
Expand Down Expand Up @@ -41,6 +42,9 @@ class Watcher extends EventEmitter {
class DirectoryWatcher extends EventEmitter {
constructor(directoryPath, options) {
super();
if(FORCE_POLLING) {
options.poll = FORCE_POLLING;
}
this.options = options;
this.path = directoryPath;
// safeTime is the point in time after which reading is safe to be unchanged
Expand All @@ -51,34 +55,50 @@ class DirectoryWatcher extends EventEmitter {
this.lastWatchEvent = 0;
this.initialScan = true;
this.nestedWatching = false;
this.polledWatching = typeof options.poll === "number" ? options.poll : options.poll ? 5007 : false;
this.filesWatchListener = new Map();
this.initialScanRemoved = new Set();
this.watchers = new Map();
this.parentWatcher = null;
this.refs = 0;
this._activeEvents = new Map();
this.closed = false;
this.scanning = false;
this.scanAgain = false;

this.createWatcher();
this.doScan(true);
}

createWatcher() {
if(IS_OSX) {
this.watchInParentDirectory();
}
try {
const options = this.options;
const interval = typeof options.poll === "number" ? options.poll : undefined;
// TODO options.poll
// TODO options.ignored
this.watcher = fs.watch(this.path);
this.watcher.on("change", this.onWatchEvent.bind(this));
//this.watcher.on("add", this.onFileAdded.bind(this));
//this.watcher.on("addDir", this.onDirectoryAdded.bind(this));
//this.watcher.on("change", this.onChange.bind(this));
//this.watcher.on("unlink", this.onFileUnlinked.bind(this));
//this.watcher.on("unlinkDir", this.onDirectoryUnlinked.bind(this));
this.watcher.on("error", this.onWatcherError.bind(this));
if(this.polledWatching) {
const listener = this.onWatchFileEvent.bind(this);
fs.watchFile(this.path, { persistent: true, interval: this.polledWatching }, listener);
// Fallback to caputure too late attached watchers
const timeout = setTimeout(() => {
fs.stat(this.path, (err, stats) => {
if(!err) {
listener(stats);
}
})
}, 1000);
this.watcher = {
close: () => {
clearTimeout(timeout);
fs.unwatchFile(this.path, listener);
}
};
} else {
if(IS_OSX) {
this.watchInParentDirectory();
}
this.watcher = fs.watch(this.path);
this.watcher.on("change", this.onWatchEvent.bind(this));
this.watcher.on("error", this.onWatcherError.bind(this));
}
} catch(err) {
this.onWatcherError(err);
}
Expand Down Expand Up @@ -119,9 +139,17 @@ class DirectoryWatcher extends EventEmitter {
this.forEachWatcher(this.path, w => w.emit("change", itemPath, null, type));
}
}

if(this.polledWatching) {
const oldListener = this.filesWatchListener.get(itemPath);
if(oldListener) {
this.filesWatchListener.delete(itemPath);
fs.unwatchFile(itemPath, oldListener);
}
}
}

setFileTime(filePath, mtime, initial, type) {
setFileTime(filePath, mtime, initial, ignoreWhenEqual, type) {
const now = Date.now();
const old = this.files.get(filePath);

Expand All @@ -133,12 +161,19 @@ class DirectoryWatcher extends EventEmitter {
safeTime = now;
accuracy = 0;
}

if(ignoreWhenEqual && old && old.timestamp === mtime) return;

this.files.set(filePath, {
safeTime,
accuracy,
timestamp: mtime
});

if(this.polledWatching) {
this._ensureFilePolling(filePath);
}

if(!old) {
this.forEachWatcher(filePath, w => {
if(!initial || w.checkStartTime(safeTime, initial)) {
Expand All @@ -164,6 +199,11 @@ class DirectoryWatcher extends EventEmitter {
const old = this.directories.get(directoryPath);
if(!old) {
const now = Date.now();

if(this.polledWatching) {
this._ensureFilePolling(directoryPath);
}

if(this.nestedWatching) {
this.createNestedWatcher(directoryPath);
} else {
Expand Down Expand Up @@ -203,6 +243,23 @@ class DirectoryWatcher extends EventEmitter {
this.directories.set(directoryPath, watcher);
}

_ensureFilePolling(path) {
if(!this.filesWatchListener.has(path)) {
const listener = this.onWatchFileChildEvent.bind(this, path);
this.filesWatchListener.set(path, listener);
fs.watchFile(path, { persistent: true, interval: this.polledWatching }, listener);
// It's possible that we miss the first event
// so we have a fallback after 1s
setTimeout(() => {
fs.stat(path, (err, stats) => {
if(!err) {
listener(stats);
}
})
}, 1000);
}
}

setNestedWatching(flag) {
if(this.nestedWatching !== !!flag) {
this.nestedWatching = !!flag;
Expand Down Expand Up @@ -307,7 +364,7 @@ class DirectoryWatcher extends EventEmitter {
if(stats.mtime) {
ensureFsAccuracy(stats.mtime);
}
this.setFileTime(filePath, +stats.mtime || +stats.ctime || 1, false, eventType);
this.setFileTime(filePath, +stats.mtime || +stats.ctime || 1, false, false, eventType);
}
});
};
Expand All @@ -317,7 +374,33 @@ class DirectoryWatcher extends EventEmitter {
}
}

onWatchFileEvent(current) {
if(this.closed) return;
if(current.isDirectory()) {
this.lastWatchEvent = Date.now();
this.doScan(false);
} else {
this.onDirectoryRemoved();
}
}

onWatchFileChildEvent(path, stats) {
if(this.closed) return;
this.lastWatchEvent = Date.now();
if(stats.isFile()) {
if(stats.mtime) {
ensureFsAccuracy(stats.mtime);
}
this.setFileTime(path, +stats.mtime || +stats.ctime || 1, false, true, "poll");
} else if(stats.isDirectory()) {
this.setDirectory(path, +stats.mtime || +stats.ctime || 1, false, "poll");
} else {
this.setMissing(path, false, "poll");
}
}

onWatcherError(err) {
if(this.closed) return;
if(err) {
if(err.code !== "EPERM" && err.code !== "ENOENT") {
console.error("Watchpack Error (watcher): " + err);
Expand Down Expand Up @@ -385,6 +468,11 @@ class DirectoryWatcher extends EventEmitter {
}

doScan(initial) {
if(this.scanning) {
this.scanAgain = true;
return;
}
this.scanning = true;
fs.readdir(this.path, (err, items) => {
if(this.closed) return;
if(err) {
Expand All @@ -396,8 +484,13 @@ class DirectoryWatcher extends EventEmitter {
this.initialScan = false;
return;
}
async.forEach(items, (item, callback) => {
const itemPath = path.join(this.path, item);
const itemPaths = new Set(items.map(item => path.join(this.path, item)));
for(const file of this.files) {
if(!itemPaths.has(file)) {
this.setMissing(file, initial, "scan (missing)");
}
}
async.forEach(itemPaths, (itemPath, callback) => {
fs.stat(itemPath, (err2, stats) => {
if(this.closed) return;
if(err2) {
Expand All @@ -413,17 +506,23 @@ class DirectoryWatcher extends EventEmitter {
if(stats.mtime) {
ensureFsAccuracy(stats.mtime);
}
if(!initial || !this.files.has(itemPath))
this.setFileTime(itemPath, +stats.mtime || +stats.ctime || 1, initial, "scan (file)");
this.setFileTime(itemPath, +stats.mtime || +stats.ctime || 1, initial, true, "scan (file)");
} else if(stats.isDirectory()) {
if(!initial || !this.directories.has(itemPath))
this.setDirectory(itemPath, +stats.mtime || +stats.ctime || 1, initial, "scan (dir)");
}
callback();
});
}, () => {
if(this.closed) return;
this.initialScan = false;
this.initialScanRemoved = null;
if(this.scanAgain) {
this.scanAgain = false;
this.doScan(false);
} else {
this.scanning = false;
}
});
});
}
Expand Down Expand Up @@ -471,13 +570,12 @@ class DirectoryWatcher extends EventEmitter {
if(this.nestedWatching) {
for(const w of this.directories.values()) {
const timeInfoEntries = w.directoryWatcher.getTimeInfoEntries();
Object.keys(timeInfoEntries).forEach(function(file) {
const entry = timeInfoEntries[file];
for(const [file, entry] of timeInfoEntries) {
if(entry) {
safeTime = Math.max(safeTime, entry.safeTime);
}
map.set(file, entry);
});
}
}
map.set(this.path, {
safeTime
Expand All @@ -487,6 +585,7 @@ class DirectoryWatcher extends EventEmitter {
// No additional info about this directory
map.set(dir, {});
}
map.set(this.path, {});
}
if(!this.initialScan) {
for(const watchers of this.watchers.values()) {
Expand Down Expand Up @@ -514,6 +613,10 @@ class DirectoryWatcher extends EventEmitter {
}
this.directories.clear();
}
for(const [file, listener] of this.filesWatchListener) {
fs.unwatchFile(file, listener);
}
this.filesWatchListener.clear();
if(this.parentWatcher) {
this.parentWatcher.close();
this.parentWatcher = null;
Expand Down
51 changes: 27 additions & 24 deletions test/DirectoryWatcher.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,16 @@ describe("DirectoryWatcher", function() {
it("should detect a file change", function(done) {
var d = new DirectoryWatcher(fixtures, {});
testHelper.file("a");
var a = d.watch(path.join(fixtures, "a"));
a.on("change", function(mtime) {
mtime.should.be.type("number");
a.close();
done();
});
testHelper.tick(function() {
testHelper.file("a");
var a = d.watch(path.join(fixtures, "a"));
a.on("change", function(mtime) {
mtime.should.be.type("number");
a.close();
done();
});
testHelper.tick(function() {
testHelper.file("a");
});
});
});

Expand Down Expand Up @@ -159,22 +161,23 @@ describe("DirectoryWatcher", function() {
});
});

it("should log errors emitted from chokidar to stderr", function(done) {
var error_logged = false;
var old_stderr = process.stderr.write
process.stderr.write = function(a){
error_logged = true;
}
var d = new DirectoryWatcher(fixtures, {});
var a = d.watch(path.join(fixtures, "a"));
d.watcher.emit("error", "error_message");

testHelper.tick(function(){
a.close();
process.stderr.write = old_stderr;
error_logged.should.be.true();
done();
})
if(!process.env.WATCHPACK_POLLING) {
it("should log errors emitted from watcher to stderr", function(done) {
var error_logged = false;
var old_stderr = process.stderr.write
process.stderr.write = function(a){
error_logged = true;
}
var d = new DirectoryWatcher(fixtures, {});
var a = d.watch(path.join(fixtures, "a"));
d.watcher.emit("error", "error_message");

})
testHelper.tick(function(){
a.close();
process.stderr.write = old_stderr;
error_logged.should.be.true();
done();
});
});
}
});
Loading

0 comments on commit fb16799

Please sign in to comment.