Startup.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. /**
  2. * Copyright 2013-2022 the PM2 project authors. All rights reserved.
  3. * Use of this source code is governed by a license that
  4. * can be found in the LICENSE file.
  5. */
  6. var chalk = require('chalk');
  7. var path = require('path');
  8. var fs = require('fs');
  9. var forEachLimit = require('async/forEachLimit');
  10. var eachLimit = require('async/eachLimit');
  11. var Common = require('../Common.js');
  12. var cst = require('../../constants.js');
  13. var util = require('util');
  14. var tmpPath = require('os').tmpdir;
  15. var which = require('../tools/which.js');
  16. var sexec = require('../tools/sexec')
  17. module.exports = function(CLI) {
  18. /**
  19. * If command is launched without root right
  20. * Display helper
  21. */
  22. function isNotRoot(startup_mode, platform, opts, cb) {
  23. Common.printOut(`${cst.PREFIX_MSG}To ${startup_mode} the Startup Script, copy/paste the following command:`);
  24. if (opts.user) {
  25. console.log('sudo env PATH=$PATH:' + path.dirname(process.execPath) + ' pm2 ' + opts.args[1].name() + ' ' + platform + ' -u ' + opts.user + ' --hp ' + process.env.HOME);
  26. return cb(new Error('You have to run this with elevated rights'));
  27. }
  28. return sexec('whoami', {silent: true}, function(err, stdout, stderr) {
  29. console.log('sudo env PATH=$PATH:' + path.dirname(process.execPath) + ' ' + require.main.filename + ' ' + opts.args[1].name() + ' ' + platform + ' -u ' + stdout.trim() + ' --hp ' + process.env.HOME);
  30. return cb(new Error('You have to run this with elevated rights'));
  31. });
  32. }
  33. /**
  34. * Detect running init system
  35. */
  36. function detectInitSystem() {
  37. var hash_map = {
  38. 'systemctl' : 'systemd',
  39. 'update-rc.d': 'upstart',
  40. 'chkconfig' : 'systemv',
  41. 'rc-update' : 'openrc',
  42. 'launchctl' : 'launchd',
  43. 'sysrc' : 'rcd',
  44. 'rcctl' : 'rcd-openbsd',
  45. 'svcadm' : 'smf'
  46. };
  47. var init_systems = Object.keys(hash_map);
  48. for (var i = 0; i < init_systems.length; i++) {
  49. if (which(init_systems[i]) != null) {
  50. break;
  51. }
  52. }
  53. if (i >= init_systems.length) {
  54. Common.printError(cst.PREFIX_MSG_ERR + 'Init system not found');
  55. return null;
  56. }
  57. Common.printOut(cst.PREFIX_MSG + 'Init System found: ' + chalk.bold(hash_map[init_systems[i]]));
  58. return hash_map[init_systems[i]];
  59. }
  60. CLI.prototype.uninstallStartup = function(platform, opts, cb) {
  61. var commands;
  62. var that = this;
  63. var actual_platform = detectInitSystem();
  64. var user = opts.user || process.env.USER || process.env.LOGNAME; // Use LOGNAME on Solaris-like systems
  65. var service_name = (opts.serviceName || 'pm2-' + user);
  66. var openrc_service_name = 'pm2';
  67. var launchd_service_name = (opts.serviceName || 'pm2.' + user);
  68. if (!platform)
  69. platform = actual_platform;
  70. else if (actual_platform && actual_platform !== platform) {
  71. Common.printOut('-----------------------------------------------------------')
  72. Common.printOut(' PM2 detected ' + actual_platform + ' but you precised ' + platform)
  73. Common.printOut(' Please verify that your choice is indeed your init system')
  74. Common.printOut(' If you arent sure, just run : pm2 startup')
  75. Common.printOut('-----------------------------------------------------------')
  76. }
  77. if (platform === null)
  78. throw new Error('Init system not found')
  79. if (!cb) {
  80. cb = function(err, data) {
  81. if (err)
  82. return that.exitCli(cst.ERROR_EXIT);
  83. return that.exitCli(cst.SUCCESS_EXIT);
  84. }
  85. }
  86. if (process.getuid() != 0) {
  87. return isNotRoot('unsetup', platform, opts, cb);
  88. }
  89. if (fs.existsSync('/etc/init.d/pm2-init.sh')) {
  90. platform = 'oldsystem';
  91. }
  92. switch(platform) {
  93. case 'systemd':
  94. commands = [
  95. 'systemctl stop ' + service_name,
  96. 'systemctl disable ' + service_name,
  97. 'rm /etc/systemd/system/' + service_name + '.service'
  98. ];
  99. break;
  100. case 'systemv':
  101. commands = [
  102. 'chkconfig ' + service_name + ' off',
  103. 'rm /etc/init.d/' + service_name
  104. ];
  105. break;
  106. case 'oldsystem':
  107. Common.printOut(cst.PREFIX_MSG + 'Disabling and deleting old startup system');
  108. commands = [
  109. 'update-rc.d pm2-init.sh disable',
  110. 'update-rc.d -f pm2-init.sh remove',
  111. 'rm /etc/init.d/pm2-init.sh'
  112. ];
  113. break;
  114. case 'openrc':
  115. service_name = openrc_service_name;
  116. commands = [
  117. '/etc/init.d/' + service_name + ' stop',
  118. 'rc-update delete ' + service_name + ' default',
  119. 'rm /etc/init.d/' + service_name
  120. ];
  121. break;
  122. case 'upstart':
  123. commands = [
  124. 'update-rc.d ' + service_name + ' disable',
  125. 'update-rc.d -f ' + service_name + ' remove',
  126. 'rm /etc/init.d/' + service_name
  127. ];
  128. break;
  129. case 'launchd':
  130. var destination = path.join(process.env.HOME, 'Library/LaunchAgents/' + launchd_service_name + '.plist');
  131. commands = [
  132. 'launchctl remove ' + launchd_service_name + ' || true',
  133. 'rm ' + destination
  134. ];
  135. break;
  136. case 'rcd':
  137. service_name = (opts.serviceName || 'pm2_' + user);
  138. commands = [
  139. '/usr/local/etc/rc.d/' + service_name + ' stop',
  140. 'sysrc -x ' + service_name + '_enable',
  141. 'rm /usr/local/etc/rc.d/' + service_name
  142. ];
  143. break;
  144. case 'rcd-openbsd':
  145. service_name = (opts.serviceName || 'pm2_' + user);
  146. var destination = path.join('/etc/rc.d', service_name);
  147. commands = [
  148. 'rcctl stop ' + service_name,
  149. 'rcctl disable ' + service_name,
  150. 'rm ' + destination
  151. ];
  152. break;
  153. case 'smf':
  154. service_name = (opts.serviceName || 'pm2_' + user);
  155. commands = [
  156. 'svcadm disable ' + service_name,
  157. 'svccfg delete -f ' + service_name
  158. ]
  159. };
  160. sexec(commands.join('&& '), function(code, stdout, stderr) {
  161. Common.printOut(stdout);
  162. Common.printOut(stderr);
  163. if (code == 0) {
  164. Common.printOut(cst.PREFIX_MSG + chalk.bold('Init file disabled.'));
  165. } else {
  166. Common.printOut(cst.ERROR_MSG + chalk.bold('Return code : ' + code));
  167. }
  168. cb(null, {
  169. commands : commands,
  170. platform : platform
  171. });
  172. });
  173. };
  174. /**
  175. * Startup script generation
  176. * @method startup
  177. * @param {string} platform type (centos|redhat|amazon|gentoo|systemd|smf)
  178. */
  179. CLI.prototype.startup = function(platform, opts, cb) {
  180. var that = this;
  181. var actual_platform = detectInitSystem();
  182. var user = (opts.user || process.env.USER || process.env.LOGNAME); // Use LOGNAME on Solaris-like systems
  183. var service_name = (opts.serviceName || 'pm2-' + user);
  184. var openrc_service_name = 'pm2';
  185. var launchd_service_name = (opts.serviceName || 'pm2.' + user);
  186. if (!platform)
  187. platform = actual_platform;
  188. else if (actual_platform && actual_platform !== platform) {
  189. Common.printOut('-----------------------------------------------------------')
  190. Common.printOut(' PM2 detected ' + actual_platform + ' but you precised ' + platform)
  191. Common.printOut(' Please verify that your choice is indeed your init system')
  192. Common.printOut(' If you arent sure, just run : pm2 startup')
  193. Common.printOut('-----------------------------------------------------------')
  194. }
  195. if (platform == null)
  196. throw new Error('Init system not found');
  197. if (!cb) {
  198. cb = function(err, data) {
  199. if (err)
  200. return that.exitCli(cst.ERROR_EXIT);
  201. return that.exitCli(cst.SUCCESS_EXIT);
  202. }
  203. }
  204. if (process.getuid() != 0) {
  205. return isNotRoot('setup', platform, opts, cb);
  206. }
  207. var destination;
  208. var commands;
  209. var template;
  210. function getTemplate(type) {
  211. return fs.readFileSync(path.join(__dirname, '..', 'templates/init-scripts', type + '.tpl'), {encoding: 'utf8'});
  212. }
  213. switch(platform) {
  214. case 'ubuntu':
  215. case 'centos':
  216. case 'arch':
  217. case 'oracle':
  218. case 'systemd':
  219. if (opts.waitIp)
  220. template = getTemplate('systemd-online');
  221. else
  222. template = getTemplate('systemd');
  223. destination = '/etc/systemd/system/' + service_name + '.service';
  224. commands = [
  225. 'systemctl enable ' + service_name
  226. ];
  227. break;
  228. case 'ubuntu14':
  229. case 'ubuntu12':
  230. case 'upstart':
  231. template = getTemplate('upstart');
  232. destination = '/etc/init.d/' + service_name;
  233. commands = [
  234. 'chmod +x ' + destination,
  235. 'mkdir -p /var/lock/subsys',
  236. 'touch /var/lock/subsys/' + service_name,
  237. 'update-rc.d ' + service_name + ' defaults'
  238. ];
  239. break;
  240. case 'systemv':
  241. case 'amazon':
  242. case 'centos6':
  243. template = getTemplate('upstart');
  244. destination = '/etc/init.d/' + service_name;
  245. commands = [
  246. 'chmod +x ' + destination,
  247. 'mkdir -p /var/lock/subsys',
  248. 'touch /var/lock/subsys/' + service_name,
  249. 'chkconfig --add ' + service_name,
  250. 'chkconfig ' + service_name + ' on',
  251. 'initctl list'
  252. ];
  253. break;
  254. case 'macos':
  255. case 'darwin':
  256. case 'launchd':
  257. template = getTemplate('launchd');
  258. destination = path.join(process.env.HOME, 'Library/LaunchAgents/' + launchd_service_name + '.plist');
  259. commands = [
  260. 'mkdir -p ' + path.join(process.env.HOME, 'Library/LaunchAgents'),
  261. 'launchctl load -w ' + destination
  262. ]
  263. break;
  264. case 'freebsd':
  265. case 'rcd':
  266. template = getTemplate('rcd');
  267. service_name = (opts.serviceName || 'pm2_' + user);
  268. destination = '/usr/local/etc/rc.d/' + service_name;
  269. commands = [
  270. 'chmod 755 ' + destination,
  271. 'sysrc ' + service_name + '_enable=YES'
  272. ];
  273. break;
  274. case 'openbsd':
  275. case 'rcd-openbsd':
  276. template = getTemplate('rcd-openbsd');
  277. service_name = (opts.serviceName || 'pm2_' + user);
  278. destination = path.join('/etc/rc.d/', service_name);
  279. commands = [
  280. 'chmod 755 ' + destination,
  281. 'rcctl enable ' + service_name,
  282. 'rcctl start ' + service_name
  283. ];
  284. break;
  285. case 'openrc':
  286. template = getTemplate('openrc');
  287. service_name = openrc_service_name;
  288. destination = '/etc/init.d/' + service_name;
  289. commands = [
  290. 'chmod +x ' + destination,
  291. 'rc-update add ' + service_name + ' default'
  292. ];
  293. break;
  294. case 'smf':
  295. case 'sunos':
  296. case 'solaris':
  297. template = getTemplate('smf');
  298. service_name = (opts.serviceName || 'pm2_' + user);
  299. destination = path.join(tmpPath(), service_name + '.xml');
  300. commands = [
  301. 'svccfg import ' + destination,
  302. 'svcadm enable ' + service_name
  303. ];
  304. break;
  305. default:
  306. throw new Error('Unknown platform / init system name');
  307. }
  308. /**
  309. * 4# Replace template variable value
  310. */
  311. var envPath
  312. if (cst.HAS_NODE_EMBEDDED == true)
  313. envPath = util.format('%s:%s', process.env.PATH || '', path.dirname(process.execPath))
  314. else if (new RegExp(path.dirname(process.execPath)).test(process.env.PATH))
  315. envPath = process.env.PATH
  316. else
  317. envPath = util.format('%s:%s', process.env.PATH || '', path.dirname(process.execPath))
  318. template = template.replace(/%PM2_PATH%/g, process.mainModule.filename)
  319. .replace(/%NODE_PATH%/g, envPath)
  320. .replace(/%USER%/g, user)
  321. .replace(/%HOME_PATH%/g, opts.hp ? path.resolve(opts.hp, '.pm2') : cst.PM2_ROOT_PATH)
  322. .replace(/%SERVICE_NAME%/g, service_name);
  323. Common.printOut(chalk.bold('Platform'), platform);
  324. Common.printOut(chalk.bold('Template'));
  325. Common.printOut(template);
  326. Common.printOut(chalk.bold('Target path'));
  327. Common.printOut(destination);
  328. Common.printOut(chalk.bold('Command list'));
  329. Common.printOut(commands);
  330. Common.printOut(cst.PREFIX_MSG + 'Writing init configuration in ' + destination);
  331. try {
  332. fs.writeFileSync(destination, template);
  333. } catch (e) {
  334. console.error(cst.PREFIX_MSG_ERR + 'Failure when trying to write startup script');
  335. console.error(e.message || e);
  336. return cb(e);
  337. }
  338. Common.printOut(cst.PREFIX_MSG + 'Making script booting at startup...');
  339. forEachLimit(commands, 1, function(command, next) {
  340. Common.printOut(cst.PREFIX_MSG + '[-] Executing: %s...', chalk.bold(command));
  341. sexec(command, function(code, stdout, stderr) {
  342. if (code === 0) {
  343. Common.printOut(cst.PREFIX_MSG + chalk.bold('[v] Command successfully executed.'));
  344. return next();
  345. } else {
  346. Common.printOut(chalk.red('[ERROR] Exit code : ' + code))
  347. return next(new Error(command + ' failed, see error above.'));
  348. }
  349. })
  350. }, function(err) {
  351. if (err) {
  352. console.error(cst.PREFIX_MSG_ERR + (err.message || err));
  353. return cb(err);
  354. }
  355. Common.printOut(chalk.bold.blue('+---------------------------------------+'));
  356. Common.printOut(chalk.bold.blue((cst.PREFIX_MSG + 'Freeze a process list on reboot via:' )));
  357. Common.printOut(chalk.bold('$ pm2 save'));
  358. Common.printOut('');
  359. Common.printOut(chalk.bold.blue(cst.PREFIX_MSG + 'Remove init script via:'));
  360. Common.printOut(chalk.bold('$ pm2 unstartup ' + platform));
  361. return cb(null, {
  362. destination : destination,
  363. template : template
  364. });
  365. });
  366. };
  367. /**
  368. * DISABLED FEATURE
  369. * KEEPING METHOD FOR BACKWARD COMPAT
  370. */
  371. CLI.prototype.autodump = function(cb) {
  372. return cb()
  373. }
  374. /**
  375. * Dump current processes managed by pm2 into DUMP_FILE_PATH file
  376. * @method dump
  377. * @param {} cb
  378. * @return
  379. */
  380. CLI.prototype.dump = function(force, cb) {
  381. var env_arr = [];
  382. var that = this;
  383. if (typeof(force) === 'function') {
  384. cb = force
  385. force = false
  386. }
  387. if (!cb)
  388. Common.printOut(cst.PREFIX_MSG + 'Saving current process list...');
  389. that.Client.executeRemote('getMonitorData', {}, function(err, list) {
  390. if (err) {
  391. Common.printError('Error retrieving process list: ' + err);
  392. return cb ? cb(Common.retErr(err)) : that.exitCli(cst.ERROR_EXIT);
  393. }
  394. /**
  395. * Description
  396. * @method fin
  397. * @param {} err
  398. * @return
  399. */
  400. function fin(err) {
  401. // try to fix issues with empty dump file
  402. // like #3485
  403. if (!force && env_arr.length === 0 && !process.env.FORCE) {
  404. // fix : if no dump file, no process, only module and after pm2 update
  405. if (!fs.existsSync(cst.DUMP_FILE_PATH)) {
  406. that.clearDump(function(){});
  407. }
  408. // if no process in list don't modify dump file
  409. // process list should not be empty
  410. if (cb) {
  411. return cb(new Error('Process list empty, cannot save empty list'));
  412. } else {
  413. Common.printOut(cst.PREFIX_MSG_WARNING + 'PM2 is not managing any process, skipping save...');
  414. Common.printOut(cst.PREFIX_MSG_WARNING + 'To force saving use: pm2 save --force');
  415. that.exitCli(cst.SUCCESS_EXIT);
  416. return;
  417. }
  418. }
  419. // Back up dump file
  420. try {
  421. if (fs.existsSync(cst.DUMP_FILE_PATH)) {
  422. fs.writeFileSync(cst.DUMP_BACKUP_FILE_PATH, fs.readFileSync(cst.DUMP_FILE_PATH));
  423. }
  424. } catch (e) {
  425. console.error(e.stack || e);
  426. Common.printOut(cst.PREFIX_MSG_ERR + 'Failed to back up dump file in %s', cst.DUMP_BACKUP_FILE_PATH);
  427. }
  428. // Overwrite dump file, delete if broken and exit
  429. try {
  430. fs.writeFileSync(cst.DUMP_FILE_PATH, JSON.stringify(env_arr, '', 2));
  431. } catch (e) {
  432. console.error(e.stack || e);
  433. try {
  434. // try to backup file
  435. if (fs.existsSync(cst.DUMP_BACKUP_FILE_PATH)) {
  436. fs.writeFileSync(cst.DUMP_FILE_PATH, fs.readFileSync(cst.DUMP_BACKUP_FILE_PATH));
  437. }
  438. } catch (e) {
  439. // don't keep broken file
  440. fs.unlinkSync(cst.DUMP_FILE_PATH);
  441. console.error(e.stack || e);
  442. }
  443. Common.printOut(cst.PREFIX_MSG_ERR + 'Failed to save dump file in %s', cst.DUMP_FILE_PATH);
  444. return that.exitCli(cst.ERROR_EXIT);
  445. }
  446. if (cb) return cb(null, {success:true});
  447. Common.printOut(cst.PREFIX_MSG + 'Successfully saved in %s', cst.DUMP_FILE_PATH);
  448. return that.exitCli(cst.SUCCESS_EXIT);
  449. }
  450. (function ex(apps) {
  451. if (!apps[0]) return fin(null);
  452. delete apps[0].pm2_env.instances;
  453. delete apps[0].pm2_env.pm_id;
  454. delete apps[0].pm2_env.prev_restart_delay;
  455. if (!apps[0].pm2_env.pmx_module)
  456. env_arr.push(apps[0].pm2_env);
  457. apps.shift();
  458. return ex(apps);
  459. })(list);
  460. });
  461. };
  462. /**
  463. * Remove DUMP_FILE_PATH file and DUMP_BACKUP_FILE_PATH file
  464. * @method dump
  465. * @param {} cb
  466. * @return
  467. */
  468. CLI.prototype.clearDump = function(cb) {
  469. fs.writeFileSync(cst.DUMP_FILE_PATH, JSON.stringify([]));
  470. if(cb && typeof cb === 'function') return cb();
  471. Common.printOut(cst.PREFIX_MSG + 'Successfully created %s', cst.DUMP_FILE_PATH);
  472. return this.exitCli(cst.SUCCESS_EXIT);
  473. };
  474. /**
  475. * Resurrect processes
  476. * @method resurrect
  477. * @param {} cb
  478. * @return
  479. */
  480. CLI.prototype.resurrect = function(cb) {
  481. var apps = {};
  482. var that = this;
  483. var processes;
  484. function readDumpFile(dumpFilePath) {
  485. Common.printOut(cst.PREFIX_MSG + 'Restoring processes located in %s', dumpFilePath);
  486. try {
  487. var apps = fs.readFileSync(dumpFilePath);
  488. } catch (e) {
  489. Common.printError(cst.PREFIX_MSG_ERR + 'Failed to read dump file in %s', dumpFilePath);
  490. throw e;
  491. }
  492. return apps;
  493. }
  494. function parseDumpFile(dumpFilePath, apps) {
  495. try {
  496. var processes = Common.parseConfig(apps, 'none');
  497. } catch (e) {
  498. Common.printError(cst.PREFIX_MSG_ERR + 'Failed to parse dump file in %s', dumpFilePath);
  499. try {
  500. fs.unlinkSync(dumpFilePath);
  501. } catch (e) {
  502. console.error(e.stack || e);
  503. }
  504. throw e;
  505. }
  506. return processes;
  507. }
  508. // Read dump file, fall back to backup, delete if broken
  509. try {
  510. apps = readDumpFile(cst.DUMP_FILE_PATH);
  511. processes = parseDumpFile(cst.DUMP_FILE_PATH, apps);
  512. } catch(e) {
  513. try {
  514. apps = readDumpFile(cst.DUMP_BACKUP_FILE_PATH);
  515. processes = parseDumpFile(cst.DUMP_BACKUP_FILE_PATH, apps);
  516. } catch(e) {
  517. Common.printError(cst.PREFIX_MSG_ERR + 'No processes saved; DUMP file doesn\'t exist');
  518. // if (cb) return cb(Common.retErr(e));
  519. // else return that.exitCli(cst.ERROR_EXIT);
  520. return that.speedList();
  521. }
  522. }
  523. that.Client.executeRemote('getMonitorData', {}, function(err, list) {
  524. if (err) {
  525. Common.printError(err);
  526. return that.exitCli(1);
  527. }
  528. var current = [];
  529. var target = [];
  530. list.forEach(function(app) {
  531. if (!current[app.name])
  532. current[app.name] = 0;
  533. current[app.name]++;
  534. });
  535. processes.forEach(function(app) {
  536. if (!target[app.name])
  537. target[app.name] = 0;
  538. target[app.name]++;
  539. });
  540. var tostart = Object.keys(target).filter(function(i) {
  541. return Object.keys(current).indexOf(i) < 0;
  542. })
  543. eachLimit(processes, cst.CONCURRENT_ACTIONS, function(app, next) {
  544. if (tostart.indexOf(app.name) == -1)
  545. return next();
  546. that.Client.executeRemote('prepare', app, function(err, dt) {
  547. if (err)
  548. Common.printError(err);
  549. else
  550. Common.printOut(cst.PREFIX_MSG + 'Process %s restored', app.pm_exec_path);
  551. next();
  552. });
  553. }, function(err) {
  554. return cb ? cb(null, apps) : that.speedList();
  555. });
  556. });
  557. };
  558. }