From fb00fef0d58768fe77b360f9368871a850609058 Mon Sep 17 00:00:00 2001 From: Alex Brausewetter Date: Wed, 12 Aug 2015 13:30:35 -0700 Subject: [PATCH 1/3] Add npm update mechanism for SFDC plugins --- package.json | 2 +- plugins/c9.nodeapi/semver-compare.js | 39 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 plugins/c9.nodeapi/semver-compare.js diff --git a/package.json b/package.json index ee1afae3..bddcf541 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "c9", "description": "New Cloud9 Client", - "version": "3.0.1", + "version": "3.0.2317", "author": "Ajax.org B.V. ", "private": true, "main": "bin/c9", diff --git a/plugins/c9.nodeapi/semver-compare.js b/plugins/c9.nodeapi/semver-compare.js new file mode 100644 index 00000000..96bfa665 --- /dev/null +++ b/plugins/c9.nodeapi/semver-compare.js @@ -0,0 +1,39 @@ +// semver-compare (1.0.0) +// https://github.com/substack/semver-compare +// +// This software is released under the MIT license: +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +define(function(require, exports, module) { + +module.exports = function cmp (a, b) { + var pa = a.split('.'); + var pb = b.split('.'); + for (var i = 0; i < 3; i++) { + var na = Number(pa[i]); + var nb = Number(pb[i]); + if (na > nb) return 1; + if (nb > na) return -1; + if (!isNaN(na) && isNaN(nb)) return 1; + if (isNaN(na) && !isNaN(nb)) return -1; + } + return 0; +}; + +}); From 79855339e1d4f0739cd0c9a2a9a68f51c9146109 Mon Sep 17 00:00:00 2001 From: Alex Brausewetter Date: Sun, 16 Aug 2015 22:43:02 -0700 Subject: [PATCH 2/3] Rename to plugin.updater.npm --- plugins/c9.ide.plugins/updater-npm.js | 440 ++++++++++++++++++++++++++ 1 file changed, 440 insertions(+) create mode 100644 plugins/c9.ide.plugins/updater-npm.js diff --git a/plugins/c9.ide.plugins/updater-npm.js b/plugins/c9.ide.plugins/updater-npm.js new file mode 100644 index 00000000..47dc9617 --- /dev/null +++ b/plugins/c9.ide.plugins/updater-npm.js @@ -0,0 +1,440 @@ +define(function(require, exports, module) { + "use strict"; + + main.consumes = [ + "Plugin", + "c9", "proc", "Dialog", + ]; + main.provides = ["plugin.updater.npm"]; + return main; + + function main(options, imports, register) { + var Plugin = imports["Plugin"]; + var c9 = imports["c9"]; + var proc = imports["proc"]; + var Dialog = imports["Dialog"]; + + var async = require("async"); + var path = require("path"); + var semverCompare = require("semver-compare"); + + var NPM_MIN_VERSION = "2.6.0"; + + /***** Initialization *****/ + + var pluginsPath = "/home/ubuntu/.c9/plugins"; + var managedPath = "/home/ubuntu/.c9/managed"; + + var managedNpmPath = [managedPath, "npm"].join("/"); + var managedEtcPath = [managedNpmPath, "etc"].join("/"); + var managedRcPath = [managedEtcPath, "npmrc"].join("/"); + var managedCachePath = [managedPath, "npm", "cache"].join("/"); + var managedPluginsPath = [managedPath, "plugins"].join("/"); + var managedModulesPath = [managedPath, "node_modules"].join("/"); + + var plugin = new Plugin("Ajax.org", main.consumes); + + function load() { + var pkgs = options.packages; + + if (!pkgs) { + console.warn("[plugin.updater.npm] no managed packages configured, not loading."); + return; + } + + // TODO: DRY error handling + + fsMkdirs([ managedPath, managedEtcPath, managedModulesPath, pluginsPath ], function(err) { + if (err) { + console.error("[plugin.updater.npm]", err); + showErrorDialog(err); + return; + } + + fsWriteNpmrc(function(err) { + if (err) { + console.error("[plugin.updater.npm]", err); + showErrorDialog(err); + return; + } + + npmCheckVersion(function(err) { + if (err) { + console.error("[plugin.updater.npm]", err); + showErrorDialog(err); + return; + } + + // TODO: clean up the flow for detecting and installing missing + // packages. the nested functions are messy. + + async.filter(pkgs, function(pkg, done) { + npmExplorePath(pkg, function(err, path) { + var isMissing = !!err; + + if (isMissing) + debug("missing package:", pkg, err); + + done(isMissing); + }); + }, function(missing) { + if (missing.length) { + console.info("[plugin.updater.npm] Installing missing plugins:", missing); + showUpdateDialog(); + + npmInstall(missing, function(err) { + if (err) { + console.error("[plugin.updater.npm]", err); + showErrorDialog(err); + return; + } + + buildLinks(pkgs, function(err) { + if (err) { + console.error("[plugin.updater.npm]", err); + showErrorDialog(err); + return; + } + + // reload browser + window.location.reload(); + }); + }); + } + else { + npmOutdated(pkgs, function(err, outdated) { + if (err) { + console.error("[plugin.updater.npm]", err); + showErrorDialog(err); + return; + } + + if (!outdated) { + debug("Plugins up-to-date."); + return; + } + + console.info("[plugin.updater.npm] Updating outdated plugins:", outdated); + showUpdateDialog(); + + buildLinks(pkgs, function(err) { + if (err) { + console.error("[plugin.updater.npm]", err); + showErrorDialog(err); + return; + } + + npmUpdate(outdated, function(err) { + if (err) { + console.error("[plugin.updater.npm]", err); + alert("[plugin.updater.npm] Error: " + err.message); + } + + // reload browser + window.location.reload(); + }); + }); + }); + } + }); + }); + }); + }); + } + + function unload() { + } + + /* -- Helper Functions ----- */ + + function debug(format) { + if (c9.debug) { + var util = require("util"); + console.info(util.format.apply(null, + Array.prototype.slice.call(arguments) + )); + } + } + + function trimRight(str) { + return str.replace(/\s+$/, ""); + } + + /* -- npm Management ----- */ + + /** + * Check that the installed npm version matches the NPM_MIN_VERSION + * required + * + * @param {Function} callback + * @param {Error=} callback.err An error if the version is lower than required + */ + function npmCheckVersion(callback) { + npmExec("npm", ["-v"], function(err, stdout, stderr) { + if (err) return callback(err, stdout, stderr); + + var version = stdout; + + debug("npm version", version); + + if (semverCompare(version, NPM_MIN_VERSION) === -1) { + var error = new Error("npm version " + NPM_MIN_VERSION + " or greater required"); + return callback(error); + } + + callback(); + }); + } + + function npmExec(command, args, callback) { + var npmBin = "/home/ubuntu/.nvm/nvm-exec"; + + debug(npmBin, { args: [ "npm", command ].concat(args) }); + + proc.execFile(npmBin, { + args: [ "npm", command ].concat(args), + cwd: managedPath, + env: { + "npm_config_production": "true", + "npm_config_depth": 0, + "npm_config_link": "true", + "npm_config_userconfig": "/dev/null", + "npm_config_prefix": managedNpmPath, + "npm_config_cache": managedCachePath, + }, + }, function(err, stdout, stderr) { + debug([err, stdout, stderr]); + + if (err) return callback(err, stdout, err.message); + + stdout = trimRight(stdout); + stderr = trimRight(stderr); + + callback(null, stdout, stderr); + }); + } + + /* -- Package Management ----- */ + + function npmOutdated(pkgs, callback) { + npmExec("outdated", ["--json"], function(err, stdout, stderr) { + if (err) + return callback(err, null, stdout, stderr); + + if (!stdout) + return callback(null, null, stdout, stderr); + + var outdated = JSON.parse(stdout); + outdated = Object.keys(outdated); + + callback(null, outdated, stdout, stderr); + }); + } + + function npmInstall(pkgs, callback) { + npmExec("install", ["--"].concat(pkgs), function(err, stdout, stderr) { + callback(err, stdout, stderr); + }); + } + + function npmUpdate(pkgs, callback) { + npmExec("update", ["--"].concat(pkgs), function(err, stdout, stderr) { + callback(err, stdout, stderr); + }); + } + + function npmExplorePath(pkg, callback) { + npmExec("explore", [pkg, "--", "pwd"], function(err, stdout, stderr) { + if (err) + return callback(err, null, stderr); + + callback(null, stdout, stderr); + }); + } + + /** + * Build the symbolic links from `~/.c9/plugins` to the managed plugins. + * + * @param {String[]} pkgs A list of package names to link. + * + * @param {Function} callback + * @param {Error=} callback.err + */ + function buildLinks(pkgs, callback) { + async.each(pkgs, function(pkg, done) { + npmExplorePath(pkg, function(err, pkgPath) { + if (err) return done(err); + fsForceLink(pkgPath, done); + }); + }, callback); + } + + /** + * Removes symbolic links from the `~/.c9/plugins` folder. + */ + function fsRmLinks(callback) { + debug("find", { args: [ pluginsPath, "-maxdepth", "1", "-type", "l", "-exec", "rm", "{}", ";" ] }); + + // find . -maxdepth 1 -type l -exec rm {} \; + + proc.execFile("find", { + args: [ + pluginsPath, + "-maxdepth", "1", + "-type", "l", + "-exec", "rm", "{}", ";" + ], + }, function(err, stdout, stderr) { + debug([err, stdout, stderr]); + callback(err, stdout, stderr); + }); + } + + /** + * Create a symbolic link in `~/.c9/plugins` pointing to the given + * plugin path. + * + * @param {String} pkgPath Path to the source package folder + */ + function fsLink(pkgPath, callback) { + debug("ls", { args: [ "-s", "-f", pkgPath, [ pluginsPath, "." ].join("/") ]}); + + proc.execFile("ln", { + args: [ + "-s", "-f", + pkgPath, + [ pluginsPath, "." ].join("/"), + ], + }, function(err, stdout, stderr) { + debug([err, stdout, stderr]); + callback(err, stdout, stderr); + }); + } + + /** + * Forcefully delete an existing plugin folder and change it to + * a symbolic link in `~/.c9/plugins` pointing to the given plugin + * path. + * + * @param {String} pkgPath Path to the source package folder + */ + function fsForceLink(pkgPath, callback) { + var basename = path.basename(pkgPath); + + proc.execFile("rm", { + args: [ + "-rf", basename, + ], + cwd: pluginsPath, + }, function(err, stdout, stderr) { + debug([err, stdout, stderr]); + + if (err) + return callback(err, stdout, stderr); + + fsLink(pkgPath, callback); + }); + } + + function fsMkdirs(dirPaths, callback) { + debug("mkdir", { args: [ "-p", "--" ].concat(dirPaths) }); + + proc.execFile("mkdir", { + args: [ "-p", "--" ].concat(dirPaths), + }, function(err, stdout, stderr) { + callback(err, stdout, stderr); + }); + } + + function fsWriteNpmrc(callback) { + var config = [ + "//registry.npmjs.org/:_authToken = a7c61f6e-5b10-41db-947f-8bc8f1f9468b", + ]; + + // + // HACK: - fs.writeFile() does not always work? we are using echo + // instead + // + // - config is not escaped + // + + debug("sh", { args: [ "-c", "echo '" + config.join("\\n") + "' > " + managedRcPath ] }); + + proc.execFile("sh", { + args: [ + "-c", + "echo '" + config.join("\\n") + "' > " + managedRcPath + ], + }, function(err, stdout, stderr) { + callback(err, stdout, stderr); + }); + } + + /* -- Interface ----- */ + + /** + * Show a modal upgrade progress dialog, blocking the IDE interface + * while we are updating the plugins. + */ + function showUpdateDialog() { + var dialog = new Dialog("Ajax.org", [], { + name: "plugin.updater.npm.dialog", + allowClose: false, + modal: true, + elements: [ + ], + }); + + dialog.title = "Installing Updates"; + dialog.heading = ""; + dialog.body = "Your workspace will be updated to the newest version and reload automatically."; + + dialog.show(); + + return dialog; + } + + /** + * Show an upgrade error dialog, requesting to delete and recreate the + * workspace. This is shown for critical update errors. + */ + function showErrorDialog(err) { + var dialog = new Dialog("Ajax.org", [], { + name: "plugin.updater.npm.error_dialog", + allowClose: true, + modal: true, + elements: [ + ], + }); + + var errorMessage = (err && err.message) ? "" + err.message : err; + + dialog.title = "Error installing updates"; + dialog.heading = ""; + dialog.body = "Important updates could not be installed on this workspace.

" + + "Please delete this workspace and create a new one, in order to continue " + + "working in an up-to-date environment.

" + + "
" + errorMessage + "
"; + + dialog.show(); + + return dialog; + } + + /***** Register and define API *****/ + + plugin.on("load", load); + plugin.on("unload", unload); + + /** + * @class salesforc.sync + */ + plugin.freezePublicAPI({ + }); + + register(null, { + "plugin.updater.npm" : plugin + }); + } +}); + From f9ae2af9a58dda24d890b769f44b2e2419fc56ab Mon Sep 17 00:00:00 2001 From: Alex Brausewetter Date: Mon, 17 Aug 2015 14:19:59 -0700 Subject: [PATCH 3/3] Make npmBin path configurable --- plugins/c9.ide.plugins/updater-npm.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/c9.ide.plugins/updater-npm.js b/plugins/c9.ide.plugins/updater-npm.js index 47dc9617..670695a4 100644 --- a/plugins/c9.ide.plugins/updater-npm.js +++ b/plugins/c9.ide.plugins/updater-npm.js @@ -22,8 +22,9 @@ define(function(require, exports, module) { /***** Initialization *****/ - var pluginsPath = "/home/ubuntu/.c9/plugins"; - var managedPath = "/home/ubuntu/.c9/managed"; + 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("/"); var managedEtcPath = [managedNpmPath, "etc"].join("/"); @@ -187,8 +188,6 @@ define(function(require, exports, module) { } function npmExec(command, args, callback) { - var npmBin = "/home/ubuntu/.nvm/nvm-exec"; - debug(npmBin, { args: [ "npm", command ].concat(args) }); proc.execFile(npmBin, {