From 7d6f1730787043a9067ff059ce6adc058db4567c Mon Sep 17 00:00:00 2001 From: Unitech Date: Sun, 2 Jul 2017 19:28:41 +0200 Subject: [PATCH] #2144 #1060 #2957 #2033 #1872 #2938 #971 Select application uid/gid via --uid --gid (CLI+JSON) + pm2 install --uid / --gid --- CHANGELOG.md | 6 +- bin/pm2 | 5 +- lib/API.js | 16 ++++- lib/API/CliUx.js | 51 ++++++++++++-- lib/API/Modules/Modularizer.js | 92 +++++++++++++++++--------- lib/API/Modules/Modules.js | 9 ++- lib/API/schema.json | 6 ++ lib/Common.js | 15 +++-- lib/ProcessContainer.js | 17 +++++ lib/ProcessContainerFork.js | 17 +++++ lib/Utility.js | 4 ++ package.json | 2 +- test/pm2_programmatic_tests.sh | 2 + test/programmatic/conf_update.mocha.js | 46 +++++++++++++ test/programmatic/modules.mocha.js | 68 +++++++++++++++++++ 15 files changed, 306 insertions(+), 50 deletions(-) create mode 100644 test/programmatic/conf_update.mocha.js create mode 100644 test/programmatic/modules.mocha.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 48fc525e0..03565f7a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,13 @@ ## 2.6 -- #2968 pm2 attach + pm2-runtime allows to attach to process stdin / stdout +- #2144 #1060 #2957 #2033 #1872 #2938 #971 Select application uid/gid via --uid --gid (CLI+JSON) + display user via pm2 ls +- pm2 install module-name --uid --gid possible +- #2968 pm2 attach to attach to process stdin / stdout +- pm2-runtime -> drop in replacement for the node.js binary - #2951 pm2 reload command locker via timestamped lock file - #2977 pm2 reloadLogs protected - #2958 Allow to delete attribute via --attribute null +- expose cwd on CLI via --cwd - multiple pm2-docker enhacements - Alias pm2.link and pm2.unlink to pm2.interact and pm2._pre_interact - Allow to customize kill signal via PM2_KILL_SIGNAL diff --git a/bin/pm2 b/bin/pm2 index 62bb8dbd4..7337dbb5c 100755 --- a/bin/pm2 +++ b/bin/pm2 @@ -38,6 +38,9 @@ commander.version(pkg.version) .option('-x --execute-command', 'execute a program using fork system') .option('--max-restarts [count]', 'only restart the script COUNT times') .option('-u --user ', 'define user when generating startup script') + .option('--uid ', 'run target script with rights') + .option('--gid ', 'run target script with rights') + .option('--cwd ', 'run target script as ') .option('--hp ', 'define home path when generating startup script') .option('-c --cron ', 'restart a running process based on a cron pattern') .option('-w --write', 'write configuration in local folder') @@ -452,7 +455,7 @@ commander.command('install ') .alias('module:install') .description('install or update a module and run it forever') .action(function(plugin_name) { - pm2.install(plugin_name); + pm2.install(plugin_name, commander); }); commander.command('module:update ') diff --git a/lib/API.js b/lib/API.js index 1ef860564..607650541 100644 --- a/lib/API.js +++ b/lib/API.js @@ -689,7 +689,6 @@ API.prototype._startScript = function(script, opts, cb) { return cb ? cb(Common.retErr(appConf)) : that.exitCli(conf.ERROR_EXIT); app_conf = appConf[0]; - /** * If -w option, write configuration to configuration.json file */ @@ -913,18 +912,29 @@ API.prototype._startJson = function(file, opts, action, pipe, cb) { var apps_name = []; var proc_list = {}; + // Here we pick only the field we want from the CLI when starting a JSON appConf.forEach(function(app) { + // --only if (opts.only && opts.only != app.name) return false; + // --watch if (!app.watch && opts.watch && opts.watch === true) app.watch = true; + // --ignore-watch if (!app.ignore_watch && opts.ignore_watch) app.ignore_watch = opts.ignore_watch; + // --instances if (opts.instances && typeof(opts.instances) === 'number') app.instances = opts.instances; - if (app.append_env_to_name && opts.env) { + // --uid + if (opts.uid) + app.uid = opts.uid; + // --gid + if (opts.gid) + app.gid = opts.gid; + // Specific + if (app.append_env_to_name && opts.env) app.name += ('-' + opts.env); - } apps_name.push(app.name); }); diff --git a/lib/API/CliUx.js b/lib/API/CliUx.js index ad455a301..f9a0a899d 100644 --- a/lib/API/CliUx.js +++ b/lib/API/CliUx.js @@ -8,7 +8,7 @@ var p = require('path'); var chalk = require('chalk'); var Common = require('../Common'); var Spinner = require('./Spinner.js'); - +var os = require('os'); var UX = module.exports = {}; /** @@ -199,9 +199,9 @@ UX.describeTable = function(process) { UX.dispAsTable = function(list, interact_infos) { var stacked = (process.stdout.columns || 90) < 90; var app_head = stacked ? ['Name', 'mode', 'status', '↺', 'cpu', 'memory'] : - ['App name', 'id', 'mode', 'pid', 'status', 'restart', 'uptime', 'cpu', 'mem', 'watching']; + ['App name', 'id', 'mode', 'pid', 'status', 'restart', 'uptime', 'cpu', 'mem', 'user', 'watching']; var mod_head = stacked ? ['Module', 'status', 'cpu', 'mem'] : - ['Module', 'version', 'target PID', 'status', 'restart', 'cpu', 'memory']; + ['Module', 'version', 'target PID', 'status', 'restart', 'cpu', 'memory', 'user']; var app_table = new Table({ head : app_head, @@ -218,6 +218,13 @@ UX.dispAsTable = function(list, interact_infos) { if (!list) return console.log('list empty'); + var current_user = ''; + + if (os.userInfo) + current_user = os.userInfo().username; + else + current_user = process.env.USER || process.env.LNAME || process.env.USERNAME || process.env.SUDO_USER || process.env.C9_USER || process.env.LOGNAME; + ; list.sort(function(a, b) { if (a.pm2_env.name < b.pm2_env.name) return -1; @@ -246,31 +253,65 @@ UX.dispAsTable = function(list, interact_infos) { } if (l.pm2_env.pmx_module == true) { + // pm2 ls for Modules obj[key] = []; + + // Module version + PID if (!stacked) - obj[key].push(chalk.bold(l.pm2_env.axm_options.module_version || 'N/A'), - typeof(l.pm2_env.axm_options.pid) === 'number' ? l.pm2_env.axm_options.pid : 'N/A' ); + obj[key].push(chalk.bold(l.pm2_env.axm_options.module_version || 'N/A'), typeof(l.pm2_env.axm_options.pid) === 'number' ? l.pm2_env.axm_options.pid : 'N/A' ); + + // Status obj[key].push(colorStatus(status)); + + // Restart if (!stacked) obj[key].push(l.pm2_env.restart_time ? l.pm2_env.restart_time : 0); + + // CPU + Memory obj[key].push(l.monit ? (l.monit.cpu + '%') : 'N/A', l.monit ? UX.bytesToSize(l.monit.memory, 3) : 'N/A' ); + // User + if (!stacked) + obj[key].push(chalk.bold(l.pm2_env.uid || current_user)); + safe_push(module_table, obj); } else { + // pm2 ls for Applications obj[key] = []; + + // PM2 ID if (!stacked) obj[key].push(l.pm2_env.pm_id); + + // Exec mode obj[key].push(mode == 'fork_mode' ? chalk.inverse.bold('fork') : chalk.blue.bold('cluster')); + + // PID if (!stacked) obj[key].push(l.pid); + + // Status obj[key].push(colorStatus(status)); + + // Restart obj[key].push(l.pm2_env.restart_time ? l.pm2_env.restart_time : 0); + + // Uptime if (!stacked) obj[key].push((l.pm2_env.pm_uptime && status == 'online') ? timeSince(l.pm2_env.pm_uptime) : 0); + // CPU obj[key].push(l.monit ? l.monit.cpu + '%' : 'N/A'); + + // Memory obj[key].push(l.monit ? UX.bytesToSize(l.monit.memory, 1) : 'N/A'); + + // User + if (!stacked) + obj[key].push(chalk.bold(l.pm2_env.uid || current_user)); + + // Watch status if (!stacked) obj[key].push(l.pm2_env.watch ? chalk.green.bold('enabled') : chalk.grey('disabled')); diff --git a/lib/API/Modules/Modularizer.js b/lib/API/Modules/Modularizer.js index ea2e6cdb9..9e311175d 100644 --- a/lib/API/Modules/Modularizer.js +++ b/lib/API/Modules/Modularizer.js @@ -3,20 +3,20 @@ * Use of this source code is governed by a license that * can be found in the LICENSE file. */ -var shelljs = require('shelljs'); -var path = require('path'); -var fs = require('fs'); -var async = require('async'); -var p = path; -var readline = require('readline'); -var spawn = require('child_process').spawn; -var chalk = require('chalk'); +var shelljs = require('shelljs'); +var path = require('path'); +var fs = require('fs'); +var async = require('async'); +var p = path; +var readline = require('readline'); +var spawn = require('child_process').spawn; +var chalk = require('chalk'); var Configuration = require('../../Configuration.js'); var cst = require('../../../constants.js'); var Common = require('../../Common'); var UX = require('../CliUx.js'); var Utility = require('../../Utility.js'); -var semver = require('semver'); +var semver = require('semver'); var Modularizer = module.exports = {}; @@ -61,24 +61,31 @@ function startModule(CLI, opts, cb) { return cb(new Error('Invalid module (script is missing)')); } - // Start the module - CLI.start(package_json, { + Common.extend(opts, { cwd : opts.proc_path, watch : opts.development_mode, force_name : package_json.name, started_as_module : true - }, function(err, data) { + }); + + // Start the module + CLI.start(package_json, opts, function(err, data) { if (err) return cb(err); return cb(null, data); }); }; -function installModule(CLI, module_name, cb) { +function installModule(CLI, module_name, opts, cb) { var proc_path = '', cmd = '', conf = {}, development_mode = false; + if (typeof(opts) == 'function') { + cb = opts; + opts = {}; + } + if (module_name == '.') { /** * Development mode @@ -89,11 +96,13 @@ function installModule(CLI, module_name, cb) { cmd = p.join(proc_path, cst.DEFAULT_MODULE_JSON); - startModule(CLI, { + Common.extend(opts, { cmd : cmd, development_mode : development_mode, proc_path : proc_path - }, function(err, dt) { + }); + + startModule(CLI, opts, function(err, dt) { if (err) return cb(err); Common.printOut(cst.PREFIX_MSG_MOD + 'Module successfully installed and launched'); return cb(null, dt); @@ -160,14 +169,23 @@ function installModule(CLI, module_name, cb) { Common.printError(e); } - Configuration.set(MODULE_CONF_PREFIX + ':' + canonic_module_name, 'true', function(err, data) { - startModule(CLI, { - cmd : cmd, - development_mode : development_mode, - proc_path : proc_path - }, function(err, dt) { + Common.extend(opts, { + cmd : cmd, + development_mode : development_mode, + proc_path : proc_path + }); + + Configuration.set(MODULE_CONF_PREFIX + ':' + canonic_module_name, { + uid : opts.uid || null, + gid : opts.gid || null + }, function(err, data) { + + startModule(CLI, opts, function(err, dt) { if (err) return cb(err); + if (process.env.PM2_PROGRAMMATIC === 'true') + return cb(null, dt); + CLI.conf(canonic_module_name, function() { Common.printOut(cst.PREFIX_MSG_MOD + 'Module successfully installed and launched'); Common.printOut(cst.PREFIX_MSG_MOD + 'Edit configuration via: `pm2 conf`'); @@ -231,7 +249,7 @@ function installLangModule(module_name, cb) { */ var listModules = function() { var module_folder = p.join(cst.PM2_ROOT_PATH, 'node_modules'); - var ret = []; + var ret = {}; shelljs.config.silent = true; var modules = shelljs.ls(module_folder); @@ -239,7 +257,7 @@ var listModules = function() { modules.forEach(function(module_name) { if (module_name.indexOf('pm2-') > -1) - ret.push(module_name); + ret[module_name] = {}; }); return ret; @@ -254,13 +272,13 @@ var listModulesV2 = Modularizer.listModules = function() { if (!config) { var modules_already_installed = listModules(); - modules_already_installed.forEach(function(module_name) { - Configuration.setSync(MODULE_CONF_PREFIX + ':' + module_name, true); + Object.keys(modules_already_installed).forEach(function(module_name) { + Configuration.setSync(MODULE_CONF_PREFIX + ':' + module_name, {}); }); return modules_already_installed; } - return Object.keys(config); + return config; }; Modularizer.getAdditionalConf = function(app_name) { @@ -286,16 +304,24 @@ Modularizer.launchAll = function(CLI, cb) { var modules = listModulesV2(); - async.eachLimit(modules, 1, function(module, next) { + async.eachLimit(Object.keys(modules), 1, function(module, next) { var pmod = p.join(module_folder, module, cst.DEFAULT_MODULE_JSON); Common.printOut(cst.PREFIX_MSG_MOD + 'Starting module ' + module); - startModule(CLI, { + var opts = {}; + + if (modules[module] != true) { + Common.extend(opts, modules[module]); + } + + Common.extend(opts, { cmd : pmod, development_mode : false, proc_path : p.join(module_folder, module) - }, function(err, dt) { + }); + + startModule(CLI, opts, function(err, dt) { if (err) console.error(err); return next(); }); @@ -305,7 +331,7 @@ Modularizer.launchAll = function(CLI, cb) { }); }; -Modularizer.install = function(CLI, module_name, cb) { +Modularizer.install = function(CLI, module_name, opts, cb) { Common.printOut(cst.PREFIX_MSG_MOD + 'Installing module ' + module_name); var canonic_module_name = Utility.getCanonicModuleName(module_name); @@ -353,7 +379,7 @@ Modularizer.install = function(CLI, module_name, cb) { Common.printOut(cst.PREFIX_MSG_MOD + 'Module already installed. Updating.'); uninstallModule(CLI, canonic_module_name, function(err) { - return installModule(CLI, module_name, cb); + return installModule(CLI, module_name, opts, cb); }); return false; @@ -362,7 +388,7 @@ Modularizer.install = function(CLI, module_name, cb) { /** * Install */ - installModule(CLI, module_name, cb); + installModule(CLI, module_name, opts, cb); }; /** @@ -377,7 +403,7 @@ Modularizer.uninstall = function(CLI, module_name, cb) { //} if (module_name == 'all') { var modules = listModulesV2(); - async.forEachLimit(modules, 1, function(module, next) { + async.forEachLimit(Object.keys(modules), 1, function(module, next) { uninstallModule(CLI, module, next); }, cb); return false; diff --git a/lib/API/Modules/Modules.js b/lib/API/Modules/Modules.js index 99f27f7e5..fdf35900c 100644 --- a/lib/API/Modules/Modules.js +++ b/lib/API/Modules/Modules.js @@ -70,10 +70,15 @@ module.exports = function(CLI) { /** * Install / Update a module */ - CLI.prototype.install = function(module_name, cb) { + CLI.prototype.install = function(module_name, opts, cb) { var that = this; - Modularizer.install(this, module_name, function(err, data) { + if (typeof(opts) == 'function') { + cb = opts; + opts = {}; + } + + Modularizer.install(this, module_name, opts, function(err, data) { if (err) { Common.printError(cst.PREFIX_MSG_ERR + (err.message || err)); return cb ? cb(Common.retErr(err)) : that.speedList(cst.ERROR_EXIT); diff --git a/lib/API/schema.json b/lib/API/schema.json index 0e58bb564..1c36cec38 100644 --- a/lib/API/schema.json +++ b/lib/API/schema.json @@ -29,6 +29,12 @@ "ext_type": "sbyte", "desc": "it should be a NUMBER - byte, \"[NUMBER]G\"(Gigabyte), \"[NUMBER]M\"(Megabyte) or \"[NUMBER]K\"(Kilobyte)" }, + "uid" : { + "type" : "string" + }, + "gid" : { + "type" : "string" + }, "restart_delay": { "type" : "number" }, diff --git a/lib/Common.js b/lib/Common.js index aaeb2f277..5c048c54c 100644 --- a/lib/Common.js +++ b/lib/Common.js @@ -4,6 +4,10 @@ * can be found in the LICENSE file. */ +/** + * Common Utilities ONLY USED IN ->CLI<- + */ + var fs = require('fs'); var path = require('path'); var util = require('util'); @@ -21,10 +25,6 @@ var extItps = require('./API/interpreter.json'); var Config = require('./tools/Config'); var KMDaemon = require('./Interactor/InteractorDaemonizer.js'); -/** - * Common methods (used by CLI and God) - */ - var Common = module.exports; function homedir() { @@ -581,6 +581,13 @@ Common.verifyConfs = function(appConfs){ delete app.disable_trace; } + if ((app.uid || app.gid) && app.force !== true) { + if (process.getuid && process.getuid() !== 0) { + Common.printError(cst.PREFIX_MSG_ERR + 'To use --uid and --gid please run pm2 as root'); + return new Error('To use UID and GID please run PM2 as root'); + } + } + // Warn deprecates. checkDeprecates(app); diff --git a/lib/ProcessContainer.js b/lib/ProcessContainer.js index 00480f627..04984d08c 100644 --- a/lib/ProcessContainer.js +++ b/lib/ProcessContainer.js @@ -83,6 +83,23 @@ delete process.env.pm2_env; err: errFile }; stdFile && (stds.std = stdFile); + + // uid/gid management + if (pm2_env.uid || pm2_env.gid) { + try { + if (pm2_env.uid) + process.setuid(pm2_env.uid); + if (process.env.gid) + process.setgid(pm2_env.gid); + } catch(e) { + setTimeout(function() { + console.error('%s on call %s', e.message, e.syscall); + console.error('%s is not accessible', pm2_env.uid); + return process.exit(1); + }, 100); + } + } + exec(script, stds); if (cronRestart) diff --git a/lib/ProcessContainerFork.js b/lib/ProcessContainerFork.js index a4d632c7d..fdcd12e64 100644 --- a/lib/ProcessContainerFork.js +++ b/lib/ProcessContainerFork.js @@ -48,6 +48,23 @@ if (process.connected && 'node_version': process.versions.node }); + +// uid/gid management +if (process.env.uid || process.env.gid) { + try { + if (process.env.uid) + process.setuid(process.env.uid); + if (process.env.gid) + process.setgid(process.env.gid); + } catch(e) { + setTimeout(function() { + console.error('%s on call %s', e.message, e.syscall); + console.error('%s is not accessible', process.env.uid); + return process.exit(1); + }, 100); + } +} + // Require the real application if (process.env.pm_exec_path) require('module')._load(process.env.pm_exec_path, null, true); diff --git a/lib/Utility.js b/lib/Utility.js index 1429d3bbb..d794d2c74 100644 --- a/lib/Utility.js +++ b/lib/Utility.js @@ -4,6 +4,10 @@ * can be found in the LICENSE file. */ +/** + * Common Utilities ONLY USED IN ->DAEMON<- + */ + var fclone = require('fclone'); var fs = require('fs'); var path = require('path'); diff --git a/package.json b/package.json index 524f2e13f..a9ed0c0ed 100644 --- a/package.json +++ b/package.json @@ -158,7 +158,7 @@ "dependencies": { "async": "^2.5", "blessed": "^0.1.81", - "chalk": "^2", + "chalk": "^1.1", "chokidar": "^1.7", "cli-table-redemption": "^1.0.0", "commander": "^2.9", diff --git a/test/pm2_programmatic_tests.sh b/test/pm2_programmatic_tests.sh index b0ca6225c..a3cac335a 100644 --- a/test/pm2_programmatic_tests.sh +++ b/test/pm2_programmatic_tests.sh @@ -70,6 +70,8 @@ mocha --opts ./mocha.opts ./signals.js spec "SIGINT signal interception + delay customization" mocha --opts ./mocha.opts ./send_data_process.mocha.js spec "Send data to a process" +mocha --opts ./mocha.opts ./modules.mocha.js +spec "Module API testing" mocha --opts ./mocha.opts ./json_validation.mocha.js spec "JSON validation test" diff --git a/test/programmatic/conf_update.mocha.js b/test/programmatic/conf_update.mocha.js new file mode 100644 index 000000000..8ec781af9 --- /dev/null +++ b/test/programmatic/conf_update.mocha.js @@ -0,0 +1,46 @@ + +const PM2 = require('../..'); +const should = require('should'); + +process.chdir(__dirname); + +describe('Modules programmatic testing', function() { + var pm2; + + after(function(done) { + pm2.destroy(done); + }); + + it('should instanciate PM2', function() { + pm2 = new PM2.custom({ + independent : true, + cwd : '../fixtures' + }); + }); + + it('should start 4 processes', function(done) { + pm2.start({ + script : './echo.js', + instances : 4, + uid : process.env.USER, + force : true + }, function(err, procs) { + should(err).eql(null); + should(procs.length).eql(4); + should(procs[0].pm2_env.uid).eql(process.env.USER); + done(); + }); + }); + + it('should start 4 processes', function(done) { + pm2.restart('echo', { + uid : process.env.USER + }, function(err, procs) { + console.log(JSON.stringify(procs[0].pm2_env, '', 2)); + should(err).eql(null); + should(procs.length).eql(4); + should(procs[0].pm2_env.uid).eql(process.env.USER); + done(); + }); + }); +}); diff --git a/test/programmatic/modules.mocha.js b/test/programmatic/modules.mocha.js new file mode 100644 index 000000000..e435335bc --- /dev/null +++ b/test/programmatic/modules.mocha.js @@ -0,0 +1,68 @@ + +const PM2 = require('../..'); +const should = require('should'); + +describe('Modules programmatic testing', function() { + var pm2; + + after(function(done) { + pm2.destroy(done); + }); + + it('should instanciate PM2', function() { + pm2 = new PM2.custom({ + independent : true, + daemon_mode : true + }); + }); + + it('should install a module', function(done) { + pm2.install('pm2-server-monit', function(err, apps) { + should(err).eql(null); + should(apps.length).eql(1); + var pm2_env = apps[0].pm2_env; + should.exist(pm2_env); + done(); + }); + }); + + it('should list one module', function(done) { + pm2.list(function(err, apps) { + should(err).eql(null); + should(apps.length).eql(1); + var pm2_env = apps[0].pm2_env; + should(pm2_env.status).eql('online'); + done(); + }); + }); + + it('should install (update) a module with uid option', function(done) { + pm2.install('pm2-server-monit', { + uid : process.env.USER + }, function(err, apps) { + should(err).eql(null); + should(apps.length).eql(1); + var pm2_env = apps[0].pm2_env; + should.exist(pm2_env); + should(pm2_env.uid).eql(process.env.USER); + done(); + }); + }); + + it('should have uid option via pm2 list', function(done) { + pm2.list(function(err, apps) { + should(err).eql(null); + should(apps.length).eql(1); + var pm2_env = apps[0].pm2_env; + should.exist(pm2_env); + should(pm2_env.uid).eql(process.env.USER); + done(); + }); + }); + + it('should uninstall all modules', function(done) { + pm2.uninstall('all', function(err, apps) { + done(); + }); + }); +});