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

801 wiersze
30 KiB
JavaScript

/*global requirejs*/
define(function(require, exports, module) {
main.consumes = [
"app", "ext", "c9", "Plugin", "proc", "fs", "vfs", "dialog.error",
"util"
];
main.provides = ["pluginManager", "plugin.manager", "plugin.debug"];
return main;
function main(options, imports, register) {
var c9 = imports.c9;
var fs = imports.fs;
var ext = imports.ext;
var proc = imports.proc;
var Plugin = imports.Plugin;
var util = imports.util;
var vfs = imports.vfs;
var showError = imports["dialog.error"].show;
var architectApp = imports.app;
var join = require("path").join;
var basename = require("path").basename;
var dirname = require("path").dirname;
var async = require("async");
var staticPrefix = options.staticPrefix;
var TEMPLATES = {
"plugin.simple": "Empty Plugin",
"plugin.default": "Full Plugin",
"plugin.installer": "Installer Plugin",
"plugin.bundle": "Cloud9 Bundle"
};
/***** Initialization *****/
var plugin = new Plugin();
var emit = plugin.getEmitter();
var disabledPlugins = Object.create(null);
var packages = Object.create(null);
function load() {
function loadDefaultPlugins() {
// do not allow errors here to interfer with connect event
setTimeout(function() {
loadPackage(options.loadFromDisk, function(err) {
if (err) return showError(err);
});
});
}
if (options.loadFromDisk) {
if (vfs.connected) loadDefaultPlugins();
else vfs.once("connect", loadDefaultPlugins);
}
}
/***** Methods *****/
function readAvailablePlugins(callback) {
fs.readdir("~/.c9/plugins", function(err, list) {
if (err) return callback && callback(err);
var available = [];
list.forEach(function(stat) {
var name = stat.name;
if (!/(directory|folder)$/.test(stat.mime)) return;
if (!/[._]/.test(name[0])) available.push(name);
});
callback && callback(null, available);
});
}
function createNewPlugin(template) {
if (!template)
template = "c9.ide.default";
var tarSourcePath = join("templates", template + ".tar.gz");
var url = staticPrefix + "/" + tarSourcePath;
if (!url.match(/^http/))
url = location.origin + url;
function getPath(callback, i) {
i = i || 0;
var path = join("~", ".c9/plugins/", template + (i ? "." + i : ""));
fs.exists(path, function(exists) {
if (exists) return getPath(callback, i + 1);
callback(null, path);
});
}
function handleError(err) {
showError("Could not create plugin.");
console.error(err);
}
getPath(function(err, path) {
if (err)
return handleError(err);
var pluginsDir = join("~", ".c9/plugins/_/");
var pluginsDirAbsolute = pluginsDir.replace(/^~/, c9.home);
var tarPath = join(pluginsDir, template + ".tar.gz");
var tarPathAbsolute = tarPath.replace(/^~/, c9.home);
// Download tar file with template for plugin
proc.execFile("bash", {
args: ["-c", [
// using mkdirp since "--create-dirs" is broken on windows
"mkdir", "-p", util.escapeShell(dirname(tarPathAbsolute)), ";",
].concat(
c9.sourceDir
? [ "cp", util.escapeShell(c9.sourceDir + "/plugins/c9.ide.plugins/" + tarSourcePath),
util.escapeShell(tarPathAbsolute) ]
: [ "curl", "-L",
(architectApp.services.onlinedev_helper ? "-k" : ""), // ignore certificate errors in dev mode
util.escapeShell(url),
"-o", util.escapeShell(tarPathAbsolute)
].filter(Boolean)
).join(" ")]
}, function(err, stderr, stdout) {
if (err)
return handleError(err);
// Untar tar file
proc.execFile("bash", {
args: ["-c", ["tar", "-zxvf", util.escapeShell(tarPath), "-C", util.escapeShell(pluginsDirAbsolute)].join(" ")]
}, function(err, stderr, stdout) {
if (err)
return handleError(err);
// Move template to the right folder
var dirPath = join(dirname(tarPath), template);
fs.rename(dirPath, path, function(err) {
if (err)
return handleError(err);
// Remove .tar.gz
fs.unlink(tarPath, function() {
// using this to allow reloading the tree
var tree = architectApp.services["tree"];
var favs = architectApp.services["tree.favorites"];
// Add plugin to favorites
favs.addFavorite(dirname(pluginsDir), "plugins");
// Select and expand the folder of the plugin
tree.expandAndSelect(path);
readAvailablePlugins(function(e) {
emit("change");
});
});
});
});
});
});
}
function reload(nodes, mode) {
var reloadLast = [];
nodes.forEach(function(node) {
var id;
if (node.packageConfig) {
var config = node.packageConfig;
if (!mode)
unloadPackage(config.name);
if (mode != false)
loadPackage(config.filePath || config.url);
id = config.name;
}
else {
if (!mode)
unloadPlugins({ path: node.path });
if (mode != false)
loadPlugins({ path: node.path });
id = node.path;
}
reloadLast.push(id);
if (mode == false)
disabledPlugins[id] = true;
else
delete disabledPlugins[id];
});
return reloadLast;
}
function checkPluginsWithMissingDependencies() {
var services = architectApp.services;
var plugins = [];
getAllPlugins().forEach(function(p) {
if (!p.provides.length) return;
var packagePath = p.packagePath;
var isDisabled = p.provides.every(function(name) {
if (!services[name]) return true;
if (!services[name].loaded && typeof services[name].load == "function")
return true;
});
if (isDisabled && packagePath) {
var packageName = packagePath.split("/")[1];
if (disabledPlugins[packageName]) return;
while (packagePath && !disabledPlugins[packagePath]) {
var i = packagePath.lastIndexOf("/");
packagePath = packagePath.slice(0, i > 0 ? i : 0);
}
if (!packagePath) plugins.push(p);
}
});
if (!plugins.length) return;
architectApp.loadAdditionalPlugins(plugins, function(err) {
if (err) return showError(err);
});
}
function getAllPlugins(includeDisabled) {
var config = architectApp.config;
Object.keys(packages).forEach(function(n) {
if (packages[n] && packages[n].c9 && packages[n].c9.plugins)
config = config.concat(packages[n].c9.plugins);
});
return includeDisabled ? config : config.filter(function(x) {
return x.consumes && x.provides;
});
}
function addAllDependents(plugins) {
var config = getAllPlugins();
var level = 0;
do {
level++;
var changed = false;
var packageNames = Object.keys(plugins);
config.forEach(function(x) {
packageNames.forEach(function(p) {
if (x.consumes.indexOf(p) != -1) {
x.provides.forEach(function(name) {
if (plugins[name] == null) {
changed = true;
plugins[name] = level;
}
});
}
});
});
} while (changed);
return plugins;
}
function addAllProviders(plugins) {
var serviceToPlugin = architectApp.serviceToPlugin;
var level = 0;
do {
level--;
var changed = false;
var packageNames = Object.keys(plugins);
packageNames.forEach(function(p) {
var service = serviceToPlugin[p];
if (service && service.consumes) {
service.consumes.forEach(function(name) {
if (plugins[name] == null) {
changed = true;
plugins[name] = level;
}
});
}
});
} while (changed);
return plugins;
}
function unloadPackage(options, callback) {
if (Array.isArray(options))
return async.map(options, unloadPackage, callback || function() {});
var name = typeof options == "object" ? options.name : options;
if (packages[name]) {
packages[name].enabled = false;
unloadPlugins(packages[name].path);
emit("disablePackage");
}
}
function loadPackage(options, callback) {
if (Array.isArray(options))
return async.map(options, loadPackage, callback || function() {});
if (typeof options == "string") {
if (/^https?:/.test(options)) {
options = { url: options };
} else if (/^[~\/]/.test(options)) {
options = { path: options };
} else if (/^[~\/]/.test(options)) {
options = { url: require.toUrl(options) };
}
}
var url = options.url;
if (!options.url && options.path) {
if (!vfs.connected) {
// wait until vfs.url is available
return vfs.once("connect", function() {
loadPackage(options, callback);
});
}
options.url = vfs.url(options.path);
}
var parts = options.url.split("/");
var root = parts.pop();
options.url = parts.join("/");
if (!options.name) {
// try to find the name from file name
options.name = /^package\.(.*)\.js$|$/.exec(root)[1];
// try folder name
if (!options.name || options.name == "json")
options.name = parts[parts.length - 1];
// try parent folder name
if (/^(.?build|master|c9build)/.test(options.name))
options.name = parts[parts.length - 2];
// remove version from the name
options.name = options.name.replace(/@.*$/, "");
}
if (!options.packageName)
options.packageName = root.replace(/\.js$/, "");
if (!options.rootDir)
options.rootDir = "plugins";
var name = options.name;
var id = options.rootDir + "/" + name;
var pathMappings = {};
if (packages[name]) packages[name].loading = true;
unloadPlugins("plugins/" + options.name);
pathMappings[id] = options.url;
requirejs.config({ paths: pathMappings });
requirejs.undef(id + "/", true);
if (/\.js$/.test(root)) {
require([options.url + "/" + root], function(json) {
json = json || require(id + "/" + options.packageName);
if (!json) {
var err = new Error("Didn't provide " + id + "/" + options.packageName);
return addError("Error loading plugin", err);
}
if (Array.isArray(json))
json = { plugins: json };
if (json.name && json.name != name)
name = json.name;
getPluginsFromPackage(json, callback);
}, function(err) {
addError("Error loading plugin", err);
});
}
else if (options.path && /\.json$/.test(root)) {
fs.readFile(options.path, function(err, value) {
if (err) return addError("Error reading " + options.path, err);
try {
var json = JSON.parse(value);
} catch (e) {
return addError("Error parsing package.json", e);
}
json.fromVfs = true;
// handle the old format
if (!json.c9) loadBundleFiles(json, options);
getPluginsFromPackage(json, callback);
});
}
else if (options.url && /\.json$/.test(root)) {
require(["text!" + options.url + "/" + root], function(value) {
try {
var json = JSON.parse(value);
} catch (e) {
return addError("Error parsing package.json", e);
}
getPluginsFromPackage(json, callback);
}, function(err) {
addError("Error loading plugin", err);
});
}
else {
callback && callback(new Error("Missing path and url"));
}
function addError(message, err) {
if (!packages[name])
packages[name] = {};
packages[name].filePath = options.path;
packages[name].url = url;
packages[name].__error = new Error(message + "\n" + err.message);
packages[name].loading = false;
emit("change");
callback && callback(err);
}
function getPluginsFromPackage(json, callback) {
var plugins = [];
if (json.name != name)
json.name = name;
var unhandledPlugins = json.c9 && json.c9.plugins || json.plugins;
if (unhandledPlugins) {
Object.keys(unhandledPlugins).forEach(function(name) {
var plugin = unhandledPlugins[name];
if (typeof plugin == "string")
plugin = { packagePath: plugin };
if (!plugin.packagePath)
plugin.packagePath = id + "/" + name;
plugin.staticPrefix = options.url;
plugins.push(plugin);
});
}
packages[json.name] = json;
json.filePath = options.path;
json.url = url;
if (!json.c9)
json.c9 = {};
json.c9.plugins = plugins;
json.enabled = true;
json.path = id;
emit("enablePackage", json);
loadPlugins(plugins, function(err, result) {
if (err) return addError("Error loading plugins", err);
if (packages[name])
packages[name].loading = false;
emit("change");
callback && callback(err, result);
});
}
function loadBundleFiles(json, options, callback) {
var cwd = dirname(options.path);
var resourceHolder = new Plugin();
fs.readdir(cwd, function(err, files) {
if (err) return callback && callback(err);
function forEachFile(dir, fn) {
fs.readdir(dir, function(err, files) {
if (err) return callback && callback(err);
files.forEach(function(stat) {
fs.readFile(dir + "/" + stat.name, function(err, value) {
if (err) return callback && callback(err);
fn(stat.name, value);
});
});
});
}
function parseHeader(data, filename) {
var firstLine = data.split("\n", 1)[0].replace(/\/\*|\*\//g, "").trim();
var info = {};
firstLine.split(";").forEach(function(n) {
var key = n.split(":");
if (key.length != 2)
return console.error("Ignoring invalid key " + n + " in " + filename);
info[key[0].trim()] = key[1].trim();
});
info.data = firstLine;
return info;
}
function addResource(type) {
forEachFile(cwd + "/" + type, function(filename, data) {
addStaticPlugin(type, options.name, filename, data, plugin);
});
}
function addMode(type) {
forEachFile(cwd + "/modes", function(filename, data) {
if (/(?:_highlight_rules|_test|_worker|_fold|_behaviou?r)\.js$/.test(filename))
return;
if (!/\.js$/.test(filename))
return;
var info = parseHeader(data, cwd + "/modes/" + filename);
if (!info.caption) info.caption = filename;
info.type = "modes";
info.filename = filename;
addStaticPlugin(type, options.name, filename, data, plugin);
});
}
var handlers = {
templates: addResource,
snippets: addResource,
builders: addResource,
keymaps: addResource,
outline: addResource,
runners: addResource,
themes: addResource,
modes: addMode,
};
files.forEach(function(stat) {
var type = stat.name;
if (handlers.hasOwnProperty(type))
handlers[type](type);
});
var name = options.name + ".bundle";
var bundle = {
packagePath: id + "/" + name,
consumes: [],
provides: [name],
setup: function(imports, options, register) {
var ret = {};
ret[name] = resourceHolder;
register(null, ret);
}
};
json.c9.plugins.push(bundle);
architectApp.loadAdditionalPlugins([bundle], function() {});
});
}
}
function loadPlugins(plugins, callback) {
if (!Array.isArray(plugins)) {
options = plugins;
plugins = [];
Object.keys(getServiceNamesByPath(options)).forEach(function(n) {
var plugin = architectApp.serviceToPlugin[n];
if (plugin && plugin.packagePath) {
unloadPluginConfig(plugin);
plugins.push(plugin);
}
});
}
architectApp.loadAdditionalPlugins(plugins, function(err) {
setTimeout(checkPluginsWithMissingDependencies);
callback && callback && callback(err);
});
}
function getServiceNamesByPath(options) {
var toUnload = Object.create(null);
var config = getAllPlugins();
function addPath(path) {
config.forEach(function(p) {
if (!p.packagePath) return;
if (p.packagePath.startsWith(path)) {
p.provides.forEach(function(name) {
toUnload[name] = 0;
});
}
});
}
if (!options) {
return toUnload;
}
if (typeof options == "string") {
addPath(options);
}
if (options.path) {
addPath(options.path);
}
if (options.paths) {
options.path.forEach(addPath);
}
if (options.services) {
options.services.forEach(function(name) {
toUnload[name] = 0;
});
}
return toUnload;
}
function unloadPlugins(options, callback) {
var toUnload = getServiceNamesByPath(options);
addAllDependents(toUnload);
var services = architectApp.services;
var serviceToPlugin = architectApp.serviceToPlugin;
Object.keys(toUnload).forEach(function(name) {
recurUnload(name);
});
function recurUnload(name) {
var service = services[name];
if (!service || !service.loaded)
return;
// Find all the dependencies
var deps = ext.getDependents(service.name);
// Unload all the dependencies (and their deps)
deps.forEach(function(name) {
recurUnload(name);
});
console.log(name);
// Unload plugin
service.unload();
var pluginConfig = serviceToPlugin[name];
if (pluginConfig && toUnload[name] == 0)
pluginConfig.__userDisabled = true;
}
emit("change");
}
function unloadPluginConfig(plugin) {
var url = requirejs.toUrl(plugin.packagePath, ".js");
if (!define.fetchedUrls[url]) return false;
delete plugin.provides;
delete plugin.consumes;
delete plugin.setup;
requirejs.undef(plugin.packagePath);
return true;
}
// TODO optimize this
function addStaticPlugin(type, pluginName, filename, data, plugin) {
var services = architectApp.services;
var path = "plugins/" + pluginName + "/"
+ (type == "installer" ? "" : type + "/")
+ filename.replace(/\.js$/, "");
switch (type) {
case "builders":
data = util.safeParseJson(data, function() {});
if (!data) return;
if (!services.build.addBuilder) return;
services.build.addBuilder(filename, data, plugin);
break;
case "keymaps":
data = util.safeParseJson(data, function() {});
if (!data) return;
if (!services["preferences.keybindings"].addCustomKeymap) return;
services["preferences.keybindings"].addCustomKeymap(filename, data, plugin);
break;
case "modes":
if (!services.ace) return;
var mode = {};
var firstLine = data.split("\n", 1)[0].replace(/\/\*|\*\//g, "").trim();
firstLine.split(";").forEach(function(n) {
var info = n.split(":");
if (info.length != 2) return;
mode[info[0].trim()] = info[1].trim();
});
services.ace.defineSyntax({
name: path,
caption: mode.caption || filename,
extensions: (mode.extensions || "").trim()
.replace(/\s*,\s*/g, "|").replace(/(^|\|)\./g, "$1")
});
break;
case "outline":
if (!data) return;
if (!services.outline.addOutlinePlugin) return;
services.outline.addOutlinePlugin(path, data, plugin);
break;
case "runners":
data = util.safeParseJson(data, function() {});
if (!data) return;
services.run.addRunner(data.caption || filename, data, plugin);
break;
case "snippets":
services["language.complete"].addSnippet(data, plugin);
break;
case "themes":
services.ace.addTheme(data, plugin);
break;
case "templates":
services.newresource.addFileTemplate(data, plugin);
break;
case "installer":
if (data) {
services.installer.createSession(pluginName, data, function(v, o) {
require([path], function(fn) {
fn(v, o);
});
});
}
else {
require([path], function(fn) {
services.installer.createSession(pluginName, fn.version, function(v, o) {
fn(v, o);
});
});
}
default:
console.error("Unsupported type", type);
}
}
/***** Lifecycle *****/
plugin.on("load", function() {
load();
});
plugin.on("unload", function() {
disabledPlugins = null;
plugin = null;
});
/***** Register and define API *****/
/**
*
**/
plugin.freezePublicAPI({
/**
*
*/
readAvailablePlugins: readAvailablePlugins,
/**
*
*/
createNewPlugin: createNewPlugin,
/**
*
*/
getAllPlugins: getAllPlugins,
/**
*
*/
addAllDependents: addAllDependents,
/**
*
*/
addAllProviders: addAllProviders,
/**
*
*/
unloadPackage: unloadPackage,
/**
*
*/
loadPackage: loadPackage,
/**
*
*/
loadPlugins: loadPlugins,
/**
*
*/
reload: reload,
/**
*
*/
getServiceNamesByPath: getServiceNamesByPath,
/**
*
*/
unloadPlugins: unloadPlugins,
/**
*
*/
unloadPluginConfig: unloadPluginConfig,
/**
*
*/
addStaticPlugin: addStaticPlugin,
get packages() { return packages; },
get disabledPlugins() { return disabledPlugins; },
});
var shim = new Plugin();
shim.addStaticPlugin = addStaticPlugin;
register(null, {
"pluginManager": plugin,
"plugin.manager": shim,
"plugin.debug": shim,
});
}
});