UNPKG

20 kBJavaScriptView Raw
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 */
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 'mkdir -p ' + path.join(process.env.HOME, 'Library/LaunchAgents'),
279 'launchctl load -w ' + destination
280 ]
281 break;
282 case 'freebsd':
283 case 'rcd':
284 template = getTemplate('rcd');
285 service_name = (opts.serviceName || 'pm2_' + user);
286 destination = '/usr/local/etc/rc.d/' + service_name;
287 commands = [
288 'chmod 755 ' + destination,
289 'sysrc ' + service_name + '_enable=YES'
290 ];
291 break;
292 case 'openbsd':
293 case 'rcd-openbsd':
294 template = getTemplate('rcd-openbsd');
295 service_name = (opts.serviceName || 'pm2_' + user);
296 destination = path.join('/etc/rc.d/', service_name);
297 commands = [
298 'chmod 755 ' + destination,
299 'rcctl enable ' + service_name,
300 'rcctl start ' + service_name
301 ];
302 break;
303 case 'openrc':
304 template = getTemplate('openrc');
305 service_name = openrc_service_name;
306 destination = '/etc/init.d/' + service_name;
307 commands = [
308 'chmod +x ' + destination,
309 'rc-update add ' + service_name + ' default'
310 ];
311 break;
312 case 'smf':
313 case 'sunos':
314 case 'solaris':
315 template = getTemplate('smf');
316 service_name = (opts.serviceName || 'pm2_' + user);
317 destination = path.join(tmpPath(), service_name + '.xml');
318 commands = [
319 'svccfg import ' + destination,
320 'svcadm enable ' + service_name
321 ];
322 break;
323 default:
324 throw new Error('Unknown platform / init system name');
325 }
326
327 /**
328 * 4# Replace template variable value
329 */
330 var envPath
331
332 if (cst.HAS_NODE_EMBEDDED == true)
333 envPath = util.format('%s:%s', process.env.PATH || '', path.dirname(process.execPath))
334 else if (new RegExp(path.dirname(process.execPath)).test(process.env.PATH))
335 envPath = process.env.PATH
336 else
337 envPath = util.format('%s:%s', process.env.PATH || '', path.dirname(process.execPath))
338
339 template = template.replace(/%PM2_PATH%/g, process.mainModule.filename)
340 .replace(/%NODE_PATH%/g, envPath)
341 .replace(/%USER%/g, user)
342 .replace(/%HOME_PATH%/g, opts.hp ? path.resolve(opts.hp, '.pm2') : cst.PM2_ROOT_PATH)
343 .replace(/%SERVICE_NAME%/g, service_name);
344
345 Common.printOut(chalk.bold('Platform'), platform);
346 Common.printOut(chalk.bold('Template'));
347 Common.printOut(template);
348 Common.printOut(chalk.bold('Target path'));
349 Common.printOut(destination);
350 Common.printOut(chalk.bold('Command list'));
351 Common.printOut(commands);
352
353 Common.printOut(cst.PREFIX_MSG + 'Writing init configuration in ' + destination);
354 try {
355 fs.writeFileSync(destination, template);
356 } catch (e) {
357 console.error(cst.PREFIX_MSG_ERR + 'Failure when trying to write startup script');
358 console.error(e.message || e);
359 return cb(e);
360 }
361
362 Common.printOut(cst.PREFIX_MSG + 'Making script booting at startup...');
363
364 forEachLimit(commands, 1, function(command, next) {
365 Common.printOut(cst.PREFIX_MSG + '[-] Executing: %s...', chalk.bold(command));
366
367 sexec(command, function(code, stdout, stderr) {
368 if (code === 0) {
369 Common.printOut(cst.PREFIX_MSG + chalk.bold('[v] Command successfully executed.'));
370 return next();
371 } else {
372 Common.printOut(chalk.red('[ERROR] Exit code : ' + code))
373 return next(new Error(command + ' failed, see error above.'));
374 }
375 })
376
377 }, function(err) {
378 if (err) {
379 console.error(cst.PREFIX_MSG_ERR + (err.message || err));
380 return cb(err);
381 }
382 Common.printOut(chalk.bold.blue('+---------------------------------------+'));
383 Common.printOut(chalk.bold.blue((cst.PREFIX_MSG + 'Freeze a process list on reboot via:' )));
384 Common.printOut(chalk.bold('$ pm2 save'));
385 Common.printOut('');
386 Common.printOut(chalk.bold.blue(cst.PREFIX_MSG + 'Remove init script via:'));
387 Common.printOut(chalk.bold('$ pm2 unstartup ' + platform));
388
389 return cb(null, {
390 destination : destination,
391 template : template
392 });
393 });
394 };
395
396 /**
397 * DISABLED FEATURE
398 * KEEPING METHOD FOR BACKWARD COMPAT
399 */
400 CLI.prototype.autodump = function(cb) {
401 return cb()
402 }
403
404 /**
405 * Dump current processes managed by pm2 into DUMP_FILE_PATH file
406 * @method dump
407 * @param {} cb
408 * @return
409 */
410 CLI.prototype.dump = function(force, cb) {
411 var env_arr = [];
412 var that = this;
413
414 if (typeof(force) === 'function') {
415 cb = force
416 force = false
417 }
418
419 if (!cb)
420 Common.printOut(cst.PREFIX_MSG + 'Saving current process list...');
421
422 that.Client.executeRemote('getMonitorData', {}, function(err, list) {
423 if (err) {
424 Common.printError('Error retrieving process list: ' + err);
425 return cb ? cb(Common.retErr(err)) : that.exitCli(cst.ERROR_EXIT);
426 }
427
428 /**
429 * Description
430 * @method fin
431 * @param {} err
432 * @return
433 */
434 function fin(err) {
435
436 // try to fix issues with empty dump file
437 // like #3485
438 if (!force && env_arr.length === 0 && !process.env.FORCE) {
439
440 // fix : if no dump file, no process, only module and after pm2 update
441 if (!fs.existsSync(cst.DUMP_FILE_PATH)) {
442 that.clearDump(function(){});
443 }
444
445 // if no process in list don't modify dump file
446 // process list should not be empty
447 if (cb) {
448 return cb(new Error('Process list empty, cannot save empty list'));
449 } else {
450 Common.printOut(cst.PREFIX_MSG_WARNING + 'PM2 is not managing any process, skipping save...');
451 Common.printOut(cst.PREFIX_MSG_WARNING + 'To force saving use: pm2 save --force');
452 that.exitCli(cst.SUCCESS_EXIT);
453 return;
454 }
455 }
456
457 // Back up dump file
458 try {
459 if (fs.existsSync(cst.DUMP_FILE_PATH)) {
460 fs.writeFileSync(cst.DUMP_BACKUP_FILE_PATH, fs.readFileSync(cst.DUMP_FILE_PATH));
461 }
462 } catch (e) {
463 console.error(e.stack || e);
464 Common.printOut(cst.PREFIX_MSG_ERR + 'Failed to back up dump file in %s', cst.DUMP_BACKUP_FILE_PATH);
465 }
466
467 // Overwrite dump file, delete if broken and exit
468 try {
469 fs.writeFileSync(cst.DUMP_FILE_PATH, JSON.stringify(env_arr, '', 2));
470 } catch (e) {
471 console.error(e.stack || e);
472 try {
473 // try to backup file
474 if (fs.existsSync(cst.DUMP_BACKUP_FILE_PATH)) {
475 fs.writeFileSync(cst.DUMP_FILE_PATH, fs.readFileSync(cst.DUMP_BACKUP_FILE_PATH));
476 }
477 } catch (e) {
478 // don't keep broken file
479 fs.unlinkSync(cst.DUMP_FILE_PATH);
480 console.error(e.stack || e);
481 }
482 Common.printOut(cst.PREFIX_MSG_ERR + 'Failed to save dump file in %s', cst.DUMP_FILE_PATH);
483 return that.exitCli(cst.ERROR_EXIT);
484 }
485 if (cb) return cb(null, {success:true});
486
487 Common.printOut(cst.PREFIX_MSG + 'Successfully saved in %s', cst.DUMP_FILE_PATH);
488 return that.exitCli(cst.SUCCESS_EXIT);
489 }
490
491 (function ex(apps) {
492 if (!apps[0]) return fin(null);
493 delete apps[0].pm2_env.instances;
494 delete apps[0].pm2_env.pm_id;
495 delete apps[0].pm2_env.prev_restart_delay;
496 if (!apps[0].pm2_env.pmx_module)
497 env_arr.push(apps[0].pm2_env);
498 apps.shift();
499 return ex(apps);
500 })(list);
501 });
502 };
503
504 /**
505 * Remove DUMP_FILE_PATH file and DUMP_BACKUP_FILE_PATH file
506 * @method dump
507 * @param {} cb
508 * @return
509 */
510 CLI.prototype.clearDump = function(cb) {
511 fs.writeFileSync(cst.DUMP_FILE_PATH, JSON.stringify([]));
512
513 if(cb && typeof cb === 'function') return cb();
514
515 Common.printOut(cst.PREFIX_MSG + 'Successfully created %s', cst.DUMP_FILE_PATH);
516 return this.exitCli(cst.SUCCESS_EXIT);
517 };
518
519 /**
520 * Resurrect processes
521 * @method resurrect
522 * @param {} cb
523 * @return
524 */
525 CLI.prototype.resurrect = function(cb) {
526 var apps = {};
527 var that = this;
528
529 var processes;
530
531 function readDumpFile(dumpFilePath) {
532 Common.printOut(cst.PREFIX_MSG + 'Restoring processes located in %s', dumpFilePath);
533 try {
534 var apps = fs.readFileSync(dumpFilePath);
535 } catch (e) {
536 Common.printError(cst.PREFIX_MSG_ERR + 'Failed to read dump file in %s', dumpFilePath);
537 throw e;
538 }
539
540 return apps;
541 }
542
543 function parseDumpFile(dumpFilePath, apps) {
544 try {
545 var processes = Common.parseConfig(apps, 'none');
546 } catch (e) {
547 Common.printError(cst.PREFIX_MSG_ERR + 'Failed to parse dump file in %s', dumpFilePath);
548 try {
549 fs.unlinkSync(dumpFilePath);
550 } catch (e) {
551 console.error(e.stack || e);
552 }
553 throw e;
554 }
555
556 return processes;
557 }
558
559 // Read dump file, fall back to backup, delete if broken
560 try {
561 apps = readDumpFile(cst.DUMP_FILE_PATH);
562 processes = parseDumpFile(cst.DUMP_FILE_PATH, apps);
563 } catch(e) {
564 try {
565 apps = readDumpFile(cst.DUMP_BACKUP_FILE_PATH);
566 processes = parseDumpFile(cst.DUMP_BACKUP_FILE_PATH, apps);
567 } catch(e) {
568 Common.printError(cst.PREFIX_MSG_ERR + 'No processes saved; DUMP file doesn\'t exist');
569 // if (cb) return cb(Common.retErr(e));
570 // else return that.exitCli(cst.ERROR_EXIT);
571 return that.speedList();
572 }
573 }
574
575 that.Client.executeRemote('getMonitorData', {}, function(err, list) {
576 if (err) {
577 Common.printError(err);
578 return that.exitCli(1);
579 }
580
581 var current = [];
582 var target = [];
583
584 list.forEach(function(app) {
585 if (!current[app.name])
586 current[app.name] = 0;
587 current[app.name]++;
588 });
589
590 processes.forEach(function(app) {
591 if (!target[app.name])
592 target[app.name] = 0;
593 target[app.name]++;
594 });
595
596 var tostart = Object.keys(target).filter(function(i) {
597 return Object.keys(current).indexOf(i) < 0;
598 })
599
600 eachLimit(processes, cst.CONCURRENT_ACTIONS, function(app, next) {
601 if (tostart.indexOf(app.name) == -1)
602 return next();
603 that.Client.executeRemote('prepare', app, function(err, dt) {
604 if (err)
605 Common.printError(err);
606 else
607 Common.printOut(cst.PREFIX_MSG + 'Process %s restored', app.pm_exec_path);
608 next();
609 });
610 }, function(err) {
611 return cb ? cb(null, apps) : that.speedList();
612 });
613 });
614 };
615
616}