c9-core/plugins/c9.ide.watcher/watcher.js

376 wiersze
14 KiB
JavaScript

/**
* Watcher
* Watches files and folders for changes and notifies the rest of the system when they happen.
**/
define(function(require, exports, module) {
main.consumes = ["c9", "Plugin", "fs"];
main.provides = ["watcher"];
return main;
function main(options, imports, register) {
var Plugin = imports.Plugin;
var fs = imports.fs;
var c9 = imports.c9;
var basename = require("path").basename;
var dirname = require("path").dirname;
/***** Initialization *****/
var plugin = new Plugin("Ajax.org", main.consumes);
var emit = plugin.getEmitter();
var WAIT_FOR_SIGNAL = false; //c9.platform == "darwin";
var IGNORE_TIMEOUT = WAIT_FOR_SIGNAL ? 60000 : 1000;
var ignored = {};
var handlers = {};
var loaded = false;
var cached;
function load() {
if (loaded) return false;
loaded = true;
function ignoreHandler(e) {
if (!options || !options.testing)
ignore(e.path, 60000);
}
function doneHandler(e) {
if (!options || !options.testing) {
clearTimeout(ignored[e.path]);
ignored[e.path] = setTimeout(function() {
unignore(e.path);
}, IGNORE_TIMEOUT);
}
}
function unwatchChildren(e) {
e.watchers = [];
Object.keys(handlers).forEach(function(path) {
if (path == e.path || path.startsWith(e.path + "/")) {
if (unwatch(path))
e.watchers.push(path.slice(e.path.length));
}
});
ignoreHandler(e);
}
function rewatchChildren(e) {
doneHandler(e);
var toPath = e.result[0] ? e.path : e.args[1];
e.watchers.forEach(function(path) {
watch(toPath + path);
});
}
fs.on("beforeWriteFile", ignoreHandler, plugin);
fs.on("afterWriteFile", doneHandler, plugin);
fs.on("beforeRename", unwatchChildren, plugin);
fs.on("afterRename", rewatchChildren, plugin);
fs.on("beforeMkdir", ignoreHandler, plugin);
fs.on("afterMkdir", doneHandler, plugin);
fs.on("beforeMkdirP", ignoreHandler, plugin);
fs.on("afterMkdirP", doneHandler, plugin);
fs.on("beforeUnlink", ignoreHandler, plugin);
fs.on("afterUnlink", doneHandler, plugin);
fs.on("beforeRmfile", ignoreHandler, plugin);
fs.on("afterRmfile", doneHandler, plugin);
fs.on("beforeRmdir", ignoreHandler, plugin);
fs.on("afterRmdir", doneHandler, plugin);
c9.on("disconnect", function() {
if (!cached)
cached = Object.keys(handlers);
handlers = {};
});
c9.on("connect", function() {
if (cached) {
handlers = {};
cached.forEach(function(path) {
watch(path);
});
cached = null;
}
});
}
/***** Methods *****/
function handleError(err, path) {
if (err.code === "EDISCONNECT")
return false;
// console.error("[watchers] error watching '" + path + "': " + JSON.stringify(err));
if (typeof handlers[path] === "function")
fs.unwatch(path, handlers[path]);
delete handlers[path];
emit("failed", { path: path });
return false;
}
function ignore(path, timeout, skipParent) {
if (!timeout)
timeout = IGNORE_TIMEOUT;
if (ignored[path])
clearTimeout(ignored[path]);
ignored[path] = setTimeout(function() {
unignore(path);
}, timeout);
if (skipParent)
return;
var parent = dirname(path);
if (parent != path)
ignore(parent, timeout, true);
}
function unignore(path) {
var parent = dirname(path);
clearTimeout(ignored[path]);
clearTimeout(ignored[parent]);
delete ignored[path];
delete ignored[parent];
}
function watch(path, $refresh, $force) {
// If we've started setting a watcher within the last minute, abort
var handler = handlers[path];
if (!$force && handler && (!(handler instanceof Date)
|| handler.getTime() + 60000 > Date.now())) {
// We already have a watcher for this file, or it's still initializing
// let's just make sure no one tries to unwatch it right now
handler.unwatchScheduled = false;
return;
}
// console.log("[watchers] watching", path);
if (!$force) {
// Make sure a possible retry on this path is no longer used
if (handler)
clearTimeout(handler.timeout);
handlers[path] = handler = new Date();
handlers[path].retries = 0;
}
fs.watch(path, function handler(err, event, filename, stat, files) {
if (err) {
if (err.code == "ENOENT" && handlers[path] && handlers[path].retries < 1) {
handlers[path].retries++;
handlers[path].timeout =
setTimeout(function() {
watch(path, $refresh, true);
}, 1000);
return false;
}
return handleError(err, path);
}
// OSX sometimes doesn't know the filename
if (!filename)
filename = basename(path);
if (event == "init") {
if (handler.unwatchScheduled) {
fs.unwatch(path, handler);
emit("unwatch", { path: path });
}
else {
handlers[path] = handler;
}
if ($refresh)
fs.readdir(path, function() {});
return;
}
if (ignored[path] || (!options.testing && stat.vfsWrite)) {
if (WAIT_FOR_SIGNAL)
ignore(path, 200);
// console.warn("[watchers] ignored event for", path, stat.vfsWrite ? "(was vfsWrite)" : "(was on ignore list)");
fireWatcherEvent(".all");
} else {
fireWatcherEvent("");
fireWatcherEvent(".all");
}
function fireWatcherEvent(eventSuffix) {
if (event == "error") {
if (eventSuffix != ".all") return;
// console.error("[watchers] received error for", path, err, stat);
}
else if (event == "delete") {
fs.unwatch(path, handler);
delete handlers[path];
// console.log("[watchers] received", event, "event for", path, stat);
emit("delete" + eventSuffix, { path: path });
}
else if (event == "directory") {
emit("directory" + eventSuffix, {
path: path,
files: files,
stat: stat
});
}
else {
// console.log("[watchers] received", event, "event for", path, stat);
emit("change" + eventSuffix, {
type: event, // change || rename
filename: filename,
path: path,
stat: stat
});
}
}
});
}
function unwatch(path) {
if (handlers[path]) {
// console.log("[watchers] stopped watching", path);
if (typeof handlers[path] == "function") {
fs.unwatch(path, handlers[path]);
emit("unwatch", { path: path });
delete handlers[path];
} else {
handlers[path].unwatchScheduled = true;
}
}
}
function check(path, timestamp) {
fs.stat(path, function(err, stat) {
if (err) {
if (err.code == "EDISCONNECT")
return;
if (err.code == "ENOENT" || err.code == "EBADF") {
console.log("[watcher] sending artificial check event for", path, "(deleted)", stat);
emit("delete", { path: path });
}
else {
// console.error("[watchers] watcher.check received unknown error ", err);
}
}
else if (timestamp < stat.mtime) {
console.log("[watcher] sending artificial check event for", path,
"(expected mtime " + timestamp + ")", stat);
emit("change", {
type: "change",
filename: basename(path),
path: path,
stat: stat
});
}
});
}
/***** Lifecycle *****/
plugin.on("load", function() {
load();
});
plugin.on("enable", function() {
});
plugin.on("disable", function() {
});
plugin.on("unload", function() {
loaded = false;
});
/***** Register and define API *****/
/**
* Watches files and directories for changes
* @singleton
**/
plugin.freezePublicAPI({
_events: [
/**
* @event change Fires when a file is changed
* @param {Object} e
* @param {String} e.path the path of the changed file
* @param {String} e.type can take the value "change" or "rename".
* @param {String} e.filename the filename of the file that is
* changed (especially interesting when watcher a directory)
* @param {Object} e.stat object containing information
* about the path. See {@link fs#stat}.
*/
"change",
/**
* @event delete Fires when a file is removed
* @param {Object} e
* @param {String} e.path the path of the removed file
* @param {Object} e.stat object containing information about
* the path. See {@link fs#stat}.
*/
"delete",
/**
* @event directory Fires when a directory contents is changed
* @param {Object} e
* @param {String} e.path the path of the removed file
* @param {Array} e.files list of stat objects representing
* the new file listing of the directory. See {@link fs#stat}.
* @param {Object} e.stat object containing information
* about the path. See {@link fs#stat}.
*/
"directory",
/**
* @event failed Fires when a watcher fails to be set
* @param {Object} e
* @param {String} e.path the path
*/
"failed",
/**
* @event unwatch A watcher got removed for a path
* @param {Object} e
* @param {String} e.path the path
*/
"unwatch",
],
/**
* Set a watcher for a path
* @param {String} path the path of the file or directory to watch
*/
watch: watch,
/**
* Remove a watcher for a path
* @param {String} path the path of the file or directory to unwatch
*/
unwatch: unwatch,
/**
* Checks whether a file has changed or is deleted
* @param {String} path the path of the file to check
*/
check: check,
/**
* Ignore watching a path for [timeout] milliseconds. This is useful
* when the file operation occurs on this client.
* @param {String} path the path of the file or directory to ignore
* @param {Number} timeout the amount of milliseconds to ignore the path
* @private
*/
ignore: ignore
});
register(null, {
watcher: plugin
});
}
});