diff --git a/package.json b/package.json index 4d55267b..bce39d68 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "c9", "description": "New Cloud9 Client", - "version": "3.0.2445", + "version": "3.0.2465", "author": "Ajax.org B.V. ", "private": true, "main": "bin/c9", @@ -63,7 +63,7 @@ "c9.ide.language.javascript": "#8479d0a9c1", "c9.ide.language.javascript.immediate": "#0535804ada", "c9.ide.language.javascript.eslint": "#08e0af061f", - "c9.ide.language.javascript.tern": "#2f81b66c94", + "c9.ide.language.javascript.tern": "#cdad7aa7d4", "c9.ide.language.javascript.infer": "#4c9a4282ff", "c9.ide.language.jsonalyzer": "#349da3b388", "c9.ide.collab": "#b49eda3791", @@ -102,7 +102,7 @@ "c9.ide.recentfiles": "#7c099abf40", "c9.ide.remote": "#301d2ab519", "c9.ide.processlist": "#bc11818bb5", - "c9.ide.run": "#acc8f1aa81", + "c9.ide.run": "#dc74ede441", "c9.ide.run.build": "#ad45874c88", "c9.ide.run.debug.xdebug": "#3b1520f83d", "c9.ide.save": "#cc613b6ead", diff --git a/plugins/c9.ide.plugins/loader.js b/plugins/c9.ide.plugins/loader.js index 6f51dbef..e891c837 100644 --- a/plugins/c9.ide.plugins/loader.js +++ b/plugins/c9.ide.plugins/loader.js @@ -16,6 +16,9 @@ define(function(require, exports, module) { var dirname = require("path").dirname; var join = require("path").join; + var async = require("async"); + var _ = require("lodash"); + var architect; /***** Initialization *****/ @@ -23,18 +26,15 @@ define(function(require, exports, module) { var plugin = new Plugin("Ajax.org", main.consumes); // var emit = plugin.getEmitter(); - var ENABLED = c9.location.indexOf("plugins=0") == -1; - var HASSDK = c9.location.indexOf("sdk=0") === -1; + var ENABLED = (c9.location.indexOf("plugins=0") === -1); + var HASSDK = (c9.location.indexOf("sdk=0") === -1); var plugins = options.plugins; var loadFromDisk = options.loadFromDisk + var names = []; - var loaded = false; function load() { - if (loaded) return false; - loaded = true; - if (!HASSDK) return; if (!ENABLED) return; @@ -42,96 +42,263 @@ define(function(require, exports, module) { } /***** Methods *****/ - - function loadPlugins(config){ + + /** + * List all packages on disk by scanning `~/.c9` and resolve the + * detected packages by order of override priority: + * + * - Developer plugins in `~/.c9/dev/plugins` have the highest + * priority to allow local development of new functionality without + * the risk of having your changes overwritten by any update + * mechanism. + * + * - Managed plugins in `~/.c9/managed/plugins` are pre-installed and + * updated by the Cloud9 system and have a higher priority that + * possibly outdated or unknown packages installed by the user. + * + * - User-installed in `~/.c9/plugins` plugins installed plugins are + * the default priority have the lowest priority. + * + * @param {Function} callback + * @param {Error=} callback.err + */ + function listAllPackages(callback) { + async.parallel({ + "plugins" : async.apply(listPackages, "~/.c9/plugins"), + "managed" : async.apply(listPackages, "~/.c9/managed/plugins"), + "dev" : async.apply(listPackages, "~/.c9/dev/plugins"), + }, function(err, packages) { + if (err && err.code === "EDISCONNECT") { + c9.once("connect", function() { + loadPluginsFromDisk(callback); + }); + return; + } + + if (err) return callback(err); + + var resolved = {}; + + // default: ~/.c9/plugins + packages.plugins.forEach(function(config) { + if (!config) return; + resolved[config.name] = config; + }); + + // high: ~/.c9/managed/plugins + packages.managed.forEach(function(config) { + if (!config) return; + resolved[config.name] = config; + }); + + // higher: ~/.c9/dev/plugins + packages.dev.forEach(function(config) { + if (!config) return; + resolved[config.name] = config; + }); + + callback(null, _.values(resolved)); + }); + } + + /** + * List packages in the given directory + * + * @param {String} dirPath Path to the directory to scan + * + * @param {Function} callback + * @param {Error=} callback.err + * @param {Object[]} callback.packages + */ + function listPackages(dirPath, callback) { + fs.readdir(dirPath, function(err, stats) { + if (err && err.code === "ENOENT") + return callback(null, []); + + if (err) + return callback(err); + + async.map(stats, function(stat, done) { + // check for folder or symlink with folder target + + if (stat.mime !== "inode/directory" + && (stat.mime === "inode/symlink" && stat.linkStat.mime !== "inode/directory") + ) { + return done(); + } + + // check folers not prefixed with [._] + + if (stat.name[0] === "." || stat.name[0] === "_") { + return done(); + } + + // check and load package.json + + var config = { + name: stat.name, + path: absolutePath([ dirPath, stat.name ].join("/")), + packagePath: [ "plugins", stat.name ].join("/"), + staticPrefix: stat.href.replace(/\/$/, ""), + }; + + loadPackageMetadata(config, function(err, metadata) { + if (err) return done(err); + config.metadata = metadata; + done(null, config); + }); + }, callback); + }); + } + + /** + * Load a the `package.json` metadata of the given package + * + * @param {Object} config The package configuration + * + * @param {Function} callback + * @param {Error=} callback.err + * @param {Object} callback.metadata + */ + function loadPackageMetadata(config, callback) { + var paths = {}; + paths[config.packagePath] = config.staticPrefix; + + requirejs.config({ paths: paths }); + requirejs.undef([config.packagePath, "package.json"].join("/")); + + require([("text!" + [config.packagePath, "package.json" ].join("/"))], function(metadataStr) { + var metadata; + + try { + metadata = JSON.parse(metadataStr); + } catch (e) { + return callback(e); + } + + callback(null, metadata); + }, function(err) { + callback(err); + }); + } + + /** + * Load a the `__installed__.js` definition of the given package + * + * @param {Object} config The package configuration + * + * @param {Function} callback + * @param {Error=} callback.err + * @param {Array} callback.installed + */ + function loadPackageInstalledJs(config, callback) { + var paths = {}; + paths[config.packagePath] = config.staticPrefix; + + requirejs.config({ paths: paths }); + requirejs.undef([config.packagePath, "__installed__.js"].join("/")); + + require([[config.packagePath, "__installed__" ].join("/")], function(installed) { + callback(null, installed); + }, function(err) { + callback(err); + }); + } + + /** + * Load the given package by checking its `__installed__.js` file or + * its `package.json#plugins`, then call `Architect#loadAdditionalPlugins()`. + * + * @param {Object} config The package configuration + * + * @param {Function} callback + * @param {Error=} callback.err + */ + function loadPackage(config, callback) { + loadPackageInstalledJs(config, function(err, installed) { + var plugins; + + if (err) { + plugins = _.map(config.metadata.plugins, function(value, key) { + return [ "plugins", config.name, key ].join("/"); + }); + } else { + plugins = installed; + } + + var architectConfig = installed.map(function(plugin) { + if (typeof plugin == "string") + plugin = { packagePath: plugin }; + + plugin.staticPrefix = config.staticPrefix; + + plugin.packageName = config.name; + plugin.packageMetadata = config.metadata; + plugin.packageDir = config.path; + + plugin.apiKey = null; // FIXME + + return plugin; + }); + + architect.loadAdditionalPlugins(architectConfig, function(err) { + callback(err); + }); + }); + } + + function loadPlugins(loaderConfig){ if (!vfs.connected) { - vfs.once("connect", loadPlugins.bind(this, config)); + vfs.once("connect", loadPlugins.bind(this, loaderConfig)); return; } - - var wait = 0; - var host = vfs.baseUrl + "/"; - var base = join(String(c9.projectId), "plugins", auth.accessToken); - var install = []; - - if (loadFromDisk) { - fs.readdir("~/.c9/plugins", function handle(err, files){ - if (err) { - if (err.code == "EDISCONNECT") { - c9.once("connect", function(){ - fs.readdir("~/.c9/plugins", handle); - }); + + listAllPackages(function(err, resolved) { + if (!loadFromDisk) { + // filter packages by config instead of loading + // everything from disk + + resolved = resolved.filter(function(config) { + var extraConfig = _.find(loaderConfig, { packagePath: config.packagePath }); + + if (!extraConfig) { + console.warn("[c9.ide.loader] Not loading package " + + config.path + " because it is not installed, " + + "according to the database"); + + return false; } - console.error(err); - return; - } - - files.forEach(function(f) { - if (!/^[_.]/.test(f.name)) { - fs.exists("~/.c9/plugins/" + f.name + "/__installed__.js", function(exists) { - if (exists) loadOne({packageName: f.name}, false); - }); - } - }); - }); - } - - var packages = {}; - config.forEach(function(options){ - var name = options.packagePath.replace(/^plugins\/([^\/]*?)\/.*$/, "$1"); - if (!packages[name]) { - packages[name] = { - packageName: name, - apiKey: options.apiKey - }; - } - names.push(name); - }); - - Object.keys(packages).forEach(function(key) { - loadOne(packages[key], false); - }); - - function loadOne(packageConfig, forceInstall) { - wait++; - - var packageName = packageConfig.packageName; - var root = "plugins/" + packageName; - var paths = {}; - paths[root] = host + base + "/" + packageName; - requirejs.config({paths: paths}); - requirejs.undef(root + "/__installed__.js"); - require([root + "/__installed__"], function(plugins) { - var config = plugins.map(function(p) { - if (typeof p == "string") - p = { packagePath: p }; - p.staticPrefix = paths[root]; - return p; - }); - - architect.loadAdditionalPlugins(config, function(err){ - if (err) console.error(err); - }); - - done(); - }, function(err) { - if (err && forceInstall) { - install.push(packageName); - } - done(); - }); - } - - function done(){ - if (!--wait) return; - if (install.length) { - installer.installPlugins(install, function(err){ - if (err) console.error(err); + + config.apiKey = extraConfig.apiKey; + + return true; }); } - } + + loaderConfig.forEach(function(extraConfig) { + // warn about missing packages which are supposed to be installed + + if (!_.find(resolved, { packagePath: extraConfig.packagePath })) { + console.warn("[c9.ide.loader] Package " + + extraConfig.packagePath + " should be installed, according " + + "to the database, but was not found on the filesystem. " + + "Try reinstalling it."); + } + }); + + names = _.pluck(resolved, "name"); + + async.each(resolved, loadPackage, function(err) { + if (err) console.error(err); + }); + }); } - + + function absolutePath(fromPath) { + var toPath = fromPath.replace(/^~/, c9.home); + return toPath; + } + /***** Lifecycle *****/ plugin.on("load", function() { diff --git a/plugins/c9.ide.plugins/updater-npm.js b/plugins/c9.ide.plugins/updater-npm.js index 670695a4..2f46dd23 100644 --- a/plugins/c9.ide.plugins/updater-npm.js +++ b/plugins/c9.ide.plugins/updater-npm.js @@ -23,7 +23,6 @@ define(function(require, exports, module) { /***** Initialization *****/ var npmBin = options.npmBin || "/home/ubuntu/.nvm/nvm-exec"; - var pluginsPath = options.pluginsPath || "/home/ubuntu/.c9/plugins"; var managedPath = options.managedPath || "/home/ubuntu/.c9/managed"; var managedNpmPath = [managedPath, "npm"].join("/"); @@ -45,7 +44,7 @@ define(function(require, exports, module) { // TODO: DRY error handling - fsMkdirs([ managedPath, managedEtcPath, managedModulesPath, pluginsPath ], function(err) { + fsMkdirs([ managedPath, managedEtcPath, managedModulesPath, managedPluginsPath ], function(err) { if (err) { console.error("[plugin.updater.npm]", err); showErrorDialog(err); @@ -269,16 +268,16 @@ define(function(require, exports, module) { } /** - * Removes symbolic links from the `~/.c9/plugins` folder. + * Removes symbolic links from the `~/.c9/managed/plugins` folder. */ function fsRmLinks(callback) { - debug("find", { args: [ pluginsPath, "-maxdepth", "1", "-type", "l", "-exec", "rm", "{}", ";" ] }); + debug("find", { args: [ managedPluginsPath, "-maxdepth", "1", "-type", "l", "-exec", "rm", "{}", ";" ] }); // find . -maxdepth 1 -type l -exec rm {} \; proc.execFile("find", { args: [ - pluginsPath, + managedPluginsPath, "-maxdepth", "1", "-type", "l", "-exec", "rm", "{}", ";" @@ -296,13 +295,13 @@ define(function(require, exports, module) { * @param {String} pkgPath Path to the source package folder */ function fsLink(pkgPath, callback) { - debug("ls", { args: [ "-s", "-f", pkgPath, [ pluginsPath, "." ].join("/") ]}); + debug("ls", { args: [ "-s", "-f", pkgPath, [ managedPluginsPath, "." ].join("/") ]}); proc.execFile("ln", { args: [ "-s", "-f", pkgPath, - [ pluginsPath, "." ].join("/"), + [ managedPluginsPath, "." ].join("/"), ], }, function(err, stdout, stderr) { debug([err, stdout, stderr]); @@ -324,7 +323,7 @@ define(function(require, exports, module) { args: [ "-rf", basename, ], - cwd: pluginsPath, + cwd: managedPluginsPath, }, function(err, stdout, stderr) { debug([err, stdout, stderr]);