NPM.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. const path = require('path');
  2. const fs = require('fs');
  3. const os = require('os');
  4. const spawn = require('child_process').spawn;
  5. const chalk = require('chalk');
  6. const readline = require('readline')
  7. const which = require('../../tools/which.js')
  8. const sexec = require('../../tools/sexec.js')
  9. const copydirSync = require('../../tools/copydirSync.js')
  10. const deleteFolderRecursive = require('../../tools/deleteFolderRecursive.js')
  11. var Configuration = require('../../Configuration.js');
  12. var cst = require('../../../constants.js');
  13. var Common = require('../../Common');
  14. var Utility = require('../../Utility.js');
  15. module.exports = {
  16. install,
  17. uninstall,
  18. start,
  19. publish,
  20. generateSample,
  21. localStart,
  22. getModuleConf
  23. }
  24. /**
  25. * PM2 Module System.
  26. * Features:
  27. * - Installed modules are listed separately from user applications
  28. * - Always ON, a module is always up along PM2, to stop it, you need to uninstall it
  29. * - Install a runnable module from NPM/Github/HTTP (require a package.json only)
  30. * - Some modules add internal PM2 depencencies (like typescript, profiling...)
  31. * - Internally it uses NPM install (https://docs.npmjs.com/cli/install)
  32. * - Auto discover script to launch (first it checks the apps field, then bin and finally main attr)
  33. * - Generate sample module via pm2 module:generate <module_name>
  34. */
  35. function localStart(PM2, opts, cb) {
  36. var proc_path = '',
  37. cmd = '',
  38. conf = {};
  39. Common.printOut(cst.PREFIX_MSG_MOD + 'Installing local module in DEVELOPMENT MODE with WATCH auto restart');
  40. proc_path = process.cwd();
  41. cmd = path.join(proc_path, cst.DEFAULT_MODULE_JSON);
  42. Common.extend(opts, {
  43. cmd : cmd,
  44. development_mode : true,
  45. proc_path : proc_path
  46. });
  47. return StartModule(PM2, opts, function(err, dt) {
  48. if (err) return cb(err);
  49. Common.printOut(cst.PREFIX_MSG_MOD + 'Module successfully installed and launched');
  50. return cb(null, dt);
  51. });
  52. }
  53. function generateSample(app_name, cb) {
  54. var rl = readline.createInterface({
  55. input: process.stdin,
  56. output: process.stdout
  57. });
  58. function samplize(module_name) {
  59. var cmd1 = 'git clone https://github.com/pm2-hive/sample-module.git ' + module_name + '; cd ' + module_name + '; rm -rf .git';
  60. var cmd2 = 'cd ' + module_name + ' ; sed -i "s:sample-module:'+ module_name +':g" package.json';
  61. var cmd3 = 'cd ' + module_name + ' ; npm install';
  62. Common.printOut(cst.PREFIX_MSG_MOD + 'Getting sample app');
  63. sexec(cmd1, function(err) {
  64. if (err) Common.printError(cst.PREFIX_MSG_MOD_ERR + err.message);
  65. sexec(cmd2, function(err) {
  66. console.log('');
  67. sexec(cmd3, function(err) {
  68. console.log('');
  69. Common.printOut(cst.PREFIX_MSG_MOD + 'Module sample created in folder: ', path.join(process.cwd(), module_name));
  70. console.log('');
  71. Common.printOut('Start module in development mode:');
  72. Common.printOut('$ cd ' + module_name + '/');
  73. Common.printOut('$ pm2 install . ');
  74. console.log('');
  75. Common.printOut('Module Log: ');
  76. Common.printOut('$ pm2 logs ' + module_name);
  77. console.log('');
  78. Common.printOut('Uninstall module: ');
  79. Common.printOut('$ pm2 uninstall ' + module_name);
  80. console.log('');
  81. Common.printOut('Force restart: ');
  82. Common.printOut('$ pm2 restart ' + module_name);
  83. return cb ? cb() : false;
  84. });
  85. });
  86. });
  87. }
  88. if (app_name) return samplize(app_name);
  89. rl.question(cst.PREFIX_MSG_MOD + "Module name: ", function(module_name) {
  90. samplize(module_name);
  91. });
  92. }
  93. function publish(opts, cb) {
  94. var rl = readline.createInterface({
  95. input: process.stdin,
  96. output: process.stdout
  97. });
  98. var semver = require('semver');
  99. var package_file = path.join(process.cwd(), 'package.json');
  100. var package_json = require(package_file);
  101. package_json.version = semver.inc(package_json.version, 'minor');
  102. Common.printOut(cst.PREFIX_MSG_MOD + 'Incrementing module to: %s@%s',
  103. package_json.name,
  104. package_json.version);
  105. rl.question("Write & Publish? [Y/N]", function(answer) {
  106. if (answer != "Y")
  107. return cb();
  108. fs.writeFile(package_file, JSON.stringify(package_json, null, 2), function(err, data) {
  109. if (err) return cb(err);
  110. Common.printOut(cst.PREFIX_MSG_MOD + 'Publishing module - %s@%s',
  111. package_json.name,
  112. package_json.version);
  113. sexec('npm publish', function(code) {
  114. Common.printOut(cst.PREFIX_MSG_MOD + 'Module - %s@%s successfully published',
  115. package_json.name,
  116. package_json.version);
  117. Common.printOut(cst.PREFIX_MSG_MOD + 'Pushing module on Git');
  118. sexec('git add . ; git commit -m "' + package_json.version + '"; git push origin master', function(code) {
  119. Common.printOut(cst.PREFIX_MSG_MOD + 'Installable with pm2 install %s', package_json.name);
  120. return cb(null, package_json);
  121. });
  122. });
  123. });
  124. });
  125. }
  126. function moduleExistInLocalDB(CLI, module_name, cb) {
  127. var modules = Configuration.getSync(cst.MODULE_CONF_PREFIX);
  128. if (!modules) return cb(false);
  129. var module_name_only = Utility.getCanonicModuleName(module_name)
  130. modules = Object.keys(modules);
  131. return cb(modules.indexOf(module_name_only) > -1 ? true : false);
  132. };
  133. function install(CLI, module_name, opts, cb) {
  134. moduleExistInLocalDB(CLI, module_name, function (exists) {
  135. if (exists) {
  136. Common.logMod('Module already installed. Updating.');
  137. Rollback.backup(module_name);
  138. return uninstall(CLI, module_name, function () {
  139. return continueInstall(CLI, module_name, opts, cb);
  140. });
  141. }
  142. return continueInstall(CLI, module_name, opts, cb);
  143. })
  144. }
  145. // Builtin Node Switch
  146. function getNPMCommandLine(module_name, install_path) {
  147. if (which('npm')) {
  148. return spawn.bind(this, cst.IS_WINDOWS ? 'npm.cmd' : 'npm', ['install', module_name, '--loglevel=error', '--prefix', `"${install_path}"` ], {
  149. stdio : 'inherit',
  150. env: process.env,
  151. windowsHide: true,
  152. shell : true
  153. })
  154. }
  155. else {
  156. return spawn.bind(this, cst.BUILTIN_NODE_PATH, [cst.BUILTIN_NPM_PATH, 'install', module_name, '--loglevel=error', '--prefix', `"${install_path}"`], {
  157. stdio : 'inherit',
  158. env: process.env,
  159. windowsHide: true,
  160. shell : true
  161. })
  162. }
  163. }
  164. function continueInstall(CLI, module_name, opts, cb) {
  165. Common.printOut(cst.PREFIX_MSG_MOD + 'Calling ' + chalk.bold.red('[NPM]') + ' to install ' + module_name + ' ...');
  166. var canonic_module_name = Utility.getCanonicModuleName(module_name);
  167. var install_path = path.join(cst.DEFAULT_MODULE_PATH, canonic_module_name);
  168. require('mkdirp')(install_path)
  169. .then(function() {
  170. process.chdir(os.homedir());
  171. var install_instance = getNPMCommandLine(module_name, install_path)();
  172. install_instance.on('close', finalizeInstall);
  173. install_instance.on('error', function (err) {
  174. console.error(err.stack || err);
  175. });
  176. });
  177. function finalizeInstall(code) {
  178. if (code != 0) {
  179. // If install has failed, revert to previous module version
  180. return Rollback.revert(CLI, module_name, function() {
  181. return cb(new Error('Installation failed via NPM, module has been restored to prev version'));
  182. });
  183. }
  184. Common.printOut(cst.PREFIX_MSG_MOD + 'Module downloaded');
  185. var proc_path = path.join(install_path, 'node_modules', canonic_module_name);
  186. var package_json_path = path.join(proc_path, 'package.json');
  187. // Append default configuration to module configuration
  188. try {
  189. var conf = JSON.parse(fs.readFileSync(package_json_path).toString()).config;
  190. if (conf) {
  191. Object.keys(conf).forEach(function(key) {
  192. Configuration.setSyncIfNotExist(canonic_module_name + ':' + key, conf[key]);
  193. });
  194. }
  195. } catch(e) {
  196. Common.printError(e);
  197. }
  198. opts = Common.extend(opts, {
  199. cmd : package_json_path,
  200. development_mode : false,
  201. proc_path : proc_path
  202. });
  203. Configuration.set(cst.MODULE_CONF_PREFIX + ':' + canonic_module_name, {
  204. uid : opts.uid,
  205. gid : opts.gid
  206. }, function(err, data) {
  207. if (err) return cb(err);
  208. StartModule(CLI, opts, function(err, dt) {
  209. if (err) return cb(err);
  210. if (process.env.PM2_PROGRAMMATIC === 'true')
  211. return cb(null, dt);
  212. CLI.conf(canonic_module_name, function() {
  213. Common.printOut(cst.PREFIX_MSG_MOD + 'Module successfully installed and launched');
  214. Common.printOut(cst.PREFIX_MSG_MOD + 'Checkout module options: `$ pm2 conf`');
  215. return cb(null, dt);
  216. });
  217. });
  218. });
  219. }
  220. }
  221. function start(PM2, modules, module_name, cb) {
  222. Common.printOut(cst.PREFIX_MSG_MOD + 'Starting NPM module ' + module_name);
  223. var install_path = path.join(cst.DEFAULT_MODULE_PATH, module_name);
  224. var proc_path = path.join(install_path, 'node_modules', module_name);
  225. var package_json_path = path.join(proc_path, 'package.json');
  226. var opts = {};
  227. // Merge with embedded configuration inside module_conf (uid, gid)
  228. Common.extend(opts, modules[module_name]);
  229. // Merge meta data to start module properly
  230. Common.extend(opts, {
  231. // package.json path
  232. cmd : package_json_path,
  233. // starting mode
  234. development_mode : false,
  235. // process cwd
  236. proc_path : proc_path
  237. });
  238. StartModule(PM2, opts, function(err, dt) {
  239. if (err) console.error(err);
  240. return cb();
  241. })
  242. }
  243. function uninstall(CLI, module_name, cb) {
  244. var module_name_only = Utility.getCanonicModuleName(module_name)
  245. var proc_path = path.join(cst.DEFAULT_MODULE_PATH, module_name_only);
  246. Configuration.unsetSync(cst.MODULE_CONF_PREFIX + ':' + module_name_only);
  247. CLI.deleteModule(module_name_only, function(err, data) {
  248. console.log('Deleting', proc_path)
  249. if (module_name != '.' && proc_path.includes('modules') === true) {
  250. deleteFolderRecursive(proc_path)
  251. }
  252. if (err) {
  253. Common.printError(err);
  254. return cb(err);
  255. }
  256. return cb(null, data);
  257. });
  258. }
  259. function getModuleConf(app_name) {
  260. if (!app_name) throw new Error('No app_name defined');
  261. var module_conf = Configuration.getAllSync();
  262. var additional_env = {};
  263. if (!module_conf[app_name]) {
  264. additional_env = {};
  265. additional_env[app_name] = {};
  266. }
  267. else {
  268. additional_env = Common.clone(module_conf[app_name]);
  269. additional_env[app_name] = JSON.stringify(module_conf[app_name]);
  270. }
  271. return additional_env;
  272. }
  273. function StartModule(CLI, opts, cb) {
  274. if (!opts.cmd && !opts.package) throw new Error('module package.json not defined');
  275. if (!opts.development_mode) opts.development_mode = false;
  276. var package_json = require(opts.cmd || opts.package);
  277. /**
  278. * Script file detection
  279. * 1- *apps* field (default pm2 json configuration)
  280. * 2- *bin* field
  281. * 3- *main* field
  282. */
  283. if (!package_json.apps && !package_json.pm2) {
  284. package_json.apps = {};
  285. if (package_json.bin) {
  286. var bin = Object.keys(package_json.bin)[0];
  287. package_json.apps.script = package_json.bin[bin];
  288. }
  289. else if (package_json.main) {
  290. package_json.apps.script = package_json.main;
  291. }
  292. }
  293. Common.extend(opts, {
  294. cwd : opts.proc_path,
  295. watch : opts.development_mode,
  296. force_name : package_json.name,
  297. started_as_module : true
  298. });
  299. // Start the module
  300. CLI.start(package_json, opts, function(err, data) {
  301. if (err) return cb(err);
  302. if (opts.safe) {
  303. Common.printOut(cst.PREFIX_MSG_MOD + 'Monitoring module behavior for potential issue (5secs...)');
  304. var time = typeof(opts.safe) == 'boolean' ? 3000 : parseInt(opts.safe);
  305. return setTimeout(function() {
  306. CLI.describe(package_json.name, function(err, apps) {
  307. if (err || apps[0].pm2_env.restart_time > 2) {
  308. return Rollback.revert(CLI, package_json.name, function() {
  309. return cb(new Error('New Module is instable, restored to previous version'));
  310. });
  311. }
  312. return cb(null, data);
  313. });
  314. }, time);
  315. }
  316. return cb(null, data);
  317. });
  318. };
  319. var Rollback = {
  320. revert : function(CLI, module_name, cb) {
  321. var canonic_module_name = Utility.getCanonicModuleName(module_name);
  322. var backup_path = path.join(require('os').tmpdir(), canonic_module_name);
  323. var module_path = path.join(cst.DEFAULT_MODULE_PATH, canonic_module_name);
  324. try {
  325. fs.statSync(backup_path)
  326. } catch(e) {
  327. return cb(new Error('no backup found'));
  328. }
  329. Common.printOut(cst.PREFIX_MSG_MOD + chalk.bold.red('[[[[[ Module installation failure! ]]]]]'));
  330. Common.printOut(cst.PREFIX_MSG_MOD + chalk.bold.red('[RESTORING TO PREVIOUS VERSION]'));
  331. CLI.deleteModule(canonic_module_name, function() {
  332. // Delete failing module
  333. if (module_name.includes('modules') === true)
  334. deleteFolderRecursive(module_path)
  335. // Restore working version
  336. copydirSync(backup_path, path.join(cst.DEFAULT_MODULE_PATH, canonic_module_name));
  337. var proc_path = path.join(module_path, 'node_modules', canonic_module_name);
  338. var package_json_path = path.join(proc_path, 'package.json');
  339. // Start module
  340. StartModule(CLI, {
  341. cmd : package_json_path,
  342. development_mode : false,
  343. proc_path : proc_path
  344. }, cb);
  345. });
  346. },
  347. backup : function(module_name) {
  348. // Backup current module
  349. var tmpdir = require('os').tmpdir();
  350. var canonic_module_name = Utility.getCanonicModuleName(module_name);
  351. var module_path = path.join(cst.DEFAULT_MODULE_PATH, canonic_module_name);
  352. copydirSync(module_path, path.join(tmpdir, canonic_module_name));
  353. }
  354. }