pm2
Version:
Production process manager for Node.JS applications with a built-in load balancer.
446 lines (362 loc) • 14.1 kB
JavaScript
const path = require('path');
const fs = require('fs');
const os = require('os');
const spawn = require('child_process').spawn;
const chalk = require('chalk');
const readline = require('readline')
const which = require('../../tools/which.js')
const sexec = require('../../tools/sexec.js')
const copydirSync = require('../../tools/copydirSync.js')
const deleteFolderRecursive = require('../../tools/deleteFolderRecursive.js')
var Configuration = require('../../Configuration.js');
var cst = require('../../../constants.js');
var Common = require('../../Common');
var Utility = require('../../Utility.js');
module.exports = {
install,
uninstall,
start,
publish,
generateSample,
localStart,
getModuleConf
}
/**
* PM2 Module System.
* Features:
* - Installed modules are listed separately from user applications
* - Always ON, a module is always up along PM2, to stop it, you need to uninstall it
* - Install a runnable module from NPM/Github/HTTP (require a package.json only)
* - Some modules add internal PM2 depencencies (like typescript, profiling...)
* - Internally it uses NPM install (https://docs.npmjs.com/cli/install)
* - Auto discover script to launch (first it checks the apps field, then bin and finally main attr)
* - Generate sample module via pm2 module:generate <module_name>
*/
function localStart(PM2, opts, cb) {
var proc_path = '',
cmd = '',
conf = {};
Common.printOut(cst.PREFIX_MSG_MOD + 'Installing local module in DEVELOPMENT MODE with WATCH auto restart');
proc_path = process.cwd();
cmd = path.join(proc_path, cst.DEFAULT_MODULE_JSON);
Common.extend(opts, {
cmd : cmd,
development_mode : true,
proc_path : proc_path
});
return StartModule(PM2, opts, function(err, dt) {
if (err) return cb(err);
Common.printOut(cst.PREFIX_MSG_MOD + 'Module successfully installed and launched');
return cb(null, dt);
});
}
function generateSample(app_name, cb) {
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function samplize(module_name) {
var cmd1 = 'git clone https://github.com/pm2-hive/sample-module.git ' + module_name + '; cd ' + module_name + '; rm -rf .git';
var cmd2 = 'cd ' + module_name + ' ; sed -i "s:sample-module:'+ module_name +':g" package.json';
var cmd3 = 'cd ' + module_name + ' ; npm install';
Common.printOut(cst.PREFIX_MSG_MOD + 'Getting sample app');
sexec(cmd1, function(err) {
if (err) Common.printError(cst.PREFIX_MSG_MOD_ERR + err.message);
sexec(cmd2, function(err) {
console.log('');
sexec(cmd3, function(err) {
console.log('');
Common.printOut(cst.PREFIX_MSG_MOD + 'Module sample created in folder: ', path.join(process.cwd(), module_name));
console.log('');
Common.printOut('Start module in development mode:');
Common.printOut('$ cd ' + module_name + '/');
Common.printOut('$ pm2 install . ');
console.log('');
Common.printOut('Module Log: ');
Common.printOut('$ pm2 logs ' + module_name);
console.log('');
Common.printOut('Uninstall module: ');
Common.printOut('$ pm2 uninstall ' + module_name);
console.log('');
Common.printOut('Force restart: ');
Common.printOut('$ pm2 restart ' + module_name);
return cb ? cb() : false;
});
});
});
}
if (app_name) return samplize(app_name);
rl.question(cst.PREFIX_MSG_MOD + "Module name: ", function(module_name) {
samplize(module_name);
});
}
function publish(opts, cb) {
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
var semver = require('semver');
var package_file = path.join(process.cwd(), 'package.json');
var package_json = require(package_file);
package_json.version = semver.inc(package_json.version, 'minor');
Common.printOut(cst.PREFIX_MSG_MOD + 'Incrementing module to: %s@%s',
package_json.name,
package_json.version);
rl.question("Write & Publish? [Y/N]", function(answer) {
if (answer != "Y")
return cb();
fs.writeFile(package_file, JSON.stringify(package_json, null, 2), function(err, data) {
if (err) return cb(err);
Common.printOut(cst.PREFIX_MSG_MOD + 'Publishing module - %s@%s',
package_json.name,
package_json.version);
sexec('npm publish', function(code) {
Common.printOut(cst.PREFIX_MSG_MOD + 'Module - %s@%s successfully published',
package_json.name,
package_json.version);
Common.printOut(cst.PREFIX_MSG_MOD + 'Pushing module on Git');
sexec('git add . ; git commit -m "' + package_json.version + '"; git push origin master', function(code) {
Common.printOut(cst.PREFIX_MSG_MOD + 'Installable with pm2 install %s', package_json.name);
return cb(null, package_json);
});
});
});
});
}
function moduleExistInLocalDB(CLI, module_name, cb) {
var modules = Configuration.getSync(cst.MODULE_CONF_PREFIX);
if (!modules) return cb(false);
var module_name_only = Utility.getCanonicModuleName(module_name)
modules = Object.keys(modules);
return cb(modules.indexOf(module_name_only) > -1 ? true : false);
};
function install(CLI, module_name, opts, cb) {
moduleExistInLocalDB(CLI, module_name, function (exists) {
if (exists) {
Common.logMod('Module already installed. Updating.');
Rollback.backup(module_name);
return uninstall(CLI, module_name, function () {
return continueInstall(CLI, module_name, opts, cb);
});
}
return continueInstall(CLI, module_name, opts, cb);
})
}
// Builtin Node Switch
function getNPMCommandLine(module_name, install_path) {
if (which('bun')) {
return spawn.bind(this, 'bun', ['install', module_name, '--loglevel=error', '--cwd', `"${install_path}"` ], {
stdio : 'inherit',
env: process.env,
windowsHide: true,
shell : true
})
}
else if (which('npm')) {
return spawn.bind(this, cst.IS_WINDOWS ? 'npm.cmd' : 'npm', ['install', module_name, '--loglevel=error', '--prefix', `"${install_path}"` ], {
stdio : 'inherit',
env: process.env,
windowsHide: true,
shell : true
})
}
else {
return spawn.bind(this, cst.BUILTIN_NODE_PATH, [cst.BUILTIN_NPM_PATH, 'install', module_name, '--loglevel=error', '--prefix', `"${install_path}"`], {
stdio : 'inherit',
env: process.env,
windowsHide: true,
shell : true
})
}
}
function continueInstall(CLI, module_name, opts, cb) {
Common.printOut(cst.PREFIX_MSG_MOD + 'Calling ' + chalk.bold.red('[NPM]') + ' to install ' + module_name + ' ...');
var canonic_module_name = Utility.getCanonicModuleName(module_name);
var install_path = path.join(cst.DEFAULT_MODULE_PATH, canonic_module_name);
require('mkdirp')(install_path)
.then(function() {
process.chdir(os.homedir());
var install_instance = getNPMCommandLine(module_name, install_path)();
install_instance.on('close', finalizeInstall);
install_instance.on('error', function (err) {
console.error(err.stack || err);
});
});
function finalizeInstall(code) {
if (code != 0) {
// If install has failed, revert to previous module version
return Rollback.revert(CLI, module_name, function() {
return cb(new Error('Installation failed via NPM, module has been restored to prev version'));
});
}
Common.printOut(cst.PREFIX_MSG_MOD + 'Module downloaded');
var proc_path = path.join(install_path, 'node_modules', canonic_module_name);
var package_json_path = path.join(proc_path, 'package.json');
// Append default configuration to module configuration
try {
var conf = JSON.parse(fs.readFileSync(package_json_path).toString()).config;
if (conf) {
Object.keys(conf).forEach(function(key) {
Configuration.setSyncIfNotExist(canonic_module_name + ':' + key, conf[key]);
});
}
} catch(e) {
Common.printError(e);
}
opts = Common.extend(opts, {
cmd : package_json_path,
development_mode : false,
proc_path : proc_path
});
Configuration.set(cst.MODULE_CONF_PREFIX + ':' + canonic_module_name, {
uid : opts.uid,
gid : opts.gid
}, function(err, data) {
if (err) return cb(err);
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 + 'Checkout module options: `$ pm2 conf`');
return cb(null, dt);
});
});
});
}
}
function start(PM2, modules, module_name, cb) {
Common.printOut(cst.PREFIX_MSG_MOD + 'Starting NPM module ' + module_name);
var install_path = path.join(cst.DEFAULT_MODULE_PATH, module_name);
var proc_path = path.join(install_path, 'node_modules', module_name);
var package_json_path = path.join(proc_path, 'package.json');
var opts = {};
// Merge with embedded configuration inside module_conf (uid, gid)
Common.extend(opts, modules[module_name]);
// Merge meta data to start module properly
Common.extend(opts, {
// package.json path
cmd : package_json_path,
// starting mode
development_mode : false,
// process cwd
proc_path : proc_path
});
StartModule(PM2, opts, function(err, dt) {
if (err) console.error(err);
return cb();
})
}
function uninstall(CLI, module_name, cb) {
var module_name_only = Utility.getCanonicModuleName(module_name)
var proc_path = path.join(cst.DEFAULT_MODULE_PATH, module_name_only);
Configuration.unsetSync(cst.MODULE_CONF_PREFIX + ':' + module_name_only);
CLI.deleteModule(module_name_only, function(err, data) {
console.log('Deleting', proc_path)
if (module_name != '.' && proc_path.includes('modules') === true) {
deleteFolderRecursive(proc_path)
}
if (err) {
Common.printError(err);
return cb(err);
}
return cb(null, data);
});
}
function getModuleConf(app_name) {
if (!app_name) throw new Error('No app_name defined');
var module_conf = Configuration.getAllSync();
var additional_env = {};
if (!module_conf[app_name]) {
additional_env = {};
additional_env[app_name] = {};
}
else {
additional_env = Common.clone(module_conf[app_name]);
additional_env[app_name] = JSON.stringify(module_conf[app_name]);
}
return additional_env;
}
function StartModule(CLI, opts, cb) {
if (!opts.cmd && !opts.package) throw new Error('module package.json not defined');
if (!opts.development_mode) opts.development_mode = false;
var package_json = require(opts.cmd || opts.package);
/**
* Script file detection
* 1- *apps* field (default pm2 json configuration)
* 2- *bin* field
* 3- *main* field
*/
if (!package_json.apps && !package_json.pm2) {
package_json.apps = {};
if (package_json.bin) {
var bin = Object.keys(package_json.bin)[0];
package_json.apps.script = package_json.bin[bin];
}
else if (package_json.main) {
package_json.apps.script = package_json.main;
}
}
Common.extend(opts, {
cwd : opts.proc_path,
watch : opts.development_mode,
force_name : package_json.name,
started_as_module : true
});
// Start the module
CLI.start(package_json, opts, function(err, data) {
if (err) return cb(err);
if (opts.safe) {
Common.printOut(cst.PREFIX_MSG_MOD + 'Monitoring module behavior for potential issue (5secs...)');
var time = typeof(opts.safe) == 'boolean' ? 3000 : parseInt(opts.safe);
return setTimeout(function() {
CLI.describe(package_json.name, function(err, apps) {
if (err || apps[0].pm2_env.restart_time > 2) {
return Rollback.revert(CLI, package_json.name, function() {
return cb(new Error('New Module is instable, restored to previous version'));
});
}
return cb(null, data);
});
}, time);
}
return cb(null, data);
});
};
var Rollback = {
revert : function(CLI, module_name, cb) {
var canonic_module_name = Utility.getCanonicModuleName(module_name);
var backup_path = path.join(require('os').tmpdir(), canonic_module_name);
var module_path = path.join(cst.DEFAULT_MODULE_PATH, canonic_module_name);
try {
fs.statSync(backup_path)
} catch(e) {
return cb(new Error('no backup found'));
}
Common.printOut(cst.PREFIX_MSG_MOD + chalk.bold.red('[[[[[ Module installation failure! ]]]]]'));
Common.printOut(cst.PREFIX_MSG_MOD + chalk.bold.red('[RESTORING TO PREVIOUS VERSION]'));
CLI.deleteModule(canonic_module_name, function() {
// Delete failing module
if (module_name.includes('modules') === true)
deleteFolderRecursive(module_path)
// Restore working version
copydirSync(backup_path, path.join(cst.DEFAULT_MODULE_PATH, canonic_module_name));
var proc_path = path.join(module_path, 'node_modules', canonic_module_name);
var package_json_path = path.join(proc_path, 'package.json');
// Start module
StartModule(CLI, {
cmd : package_json_path,
development_mode : false,
proc_path : proc_path
}, cb);
});
},
backup : function(module_name) {
// Backup current module
var tmpdir = require('os').tmpdir();
var canonic_module_name = Utility.getCanonicModuleName(module_name);
var module_path = path.join(cst.DEFAULT_MODULE_PATH, canonic_module_name);
copydirSync(module_path, path.join(tmpdir, canonic_module_name));
}
}