UNPKG

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