UNPKG

20.5 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 */
6
7var debug = require('debug')('pm2:client');
8var Common = require('./Common.js');
9var KMDaemon = require('@pm2/agent/src/InteractorClient');
10var rpc = require('pm2-axon-rpc');
11var forEach = require('async/forEach');
12var axon = require('pm2-axon');
13var util = require('util');
14var fs = require('fs');
15var path = require('path');
16var pkg = require('../package.json');
17var which = require('./tools/which.js');
18
19function noop() {}
20
21var Client = module.exports = function(opts) {
22 if (!opts) opts = {};
23
24 if (!opts.conf)
25 this.conf = require('../constants.js');
26 else {
27 this.conf = opts.conf;
28 }
29
30 this.daemon_mode = typeof(opts.daemon_mode) === 'undefined' ? true : opts.daemon_mode;
31 this.pm2_home = this.conf.PM2_ROOT_PATH;
32 this.secret_key = opts.secret_key;
33 this.public_key = opts.public_key;
34 this.machine_name = opts.machine_name;
35
36 // Create all folders and files needed
37 // Client depends to that to interact with PM2 properly
38 this.initFileStructure(this.conf);
39
40 debug('Using RPC file %s', this.conf.DAEMON_RPC_PORT);
41 debug('Using PUB file %s', this.conf.DAEMON_PUB_PORT);
42 this.rpc_socket_file = this.conf.DAEMON_RPC_PORT;
43 this.pub_socket_file = this.conf.DAEMON_PUB_PORT;
44};
45
46// @breaking change (noDaemonMode has been drop)
47// @todo ret err
48Client.prototype.start = function(cb) {
49 var that = this;
50
51 this.pingDaemon(function(daemonAlive) {
52 if (daemonAlive === true)
53 return that.launchRPC(function(err, meta) {
54 return cb(null, {
55 daemon_mode : that.conf.daemon_mode,
56 new_pm2_instance : false,
57 rpc_socket_file : that.rpc_socket_file,
58 pub_socket_file : that.pub_socket_file,
59 pm2_home : that.pm2_home
60 });
61 });
62
63 /**
64 * No Daemon mode
65 */
66 if (that.daemon_mode === false) {
67 var Daemon = require('./Daemon.js');
68
69 var daemon = new Daemon({
70 pub_socket_file : that.conf.DAEMON_PUB_PORT,
71 rpc_socket_file : that.conf.DAEMON_RPC_PORT,
72 pid_file : that.conf.PM2_PID_FILE_PATH,
73 ignore_signals : true
74 });
75
76 console.log('Launching in no daemon mode');
77
78 daemon.innerStart(function() {
79 KMDaemon.launchAndInteract(that.conf, {
80 machine_name : that.machine_name,
81 public_key : that.public_key,
82 secret_key : that.secret_key,
83 pm2_version : pkg.version
84 }, function(err, data, interactor_proc) {
85 that.interactor_process = interactor_proc;
86 });
87
88 that.launchRPC(function(err, meta) {
89 return cb(null, {
90 daemon_mode : that.conf.daemon_mode,
91 new_pm2_instance : true,
92 rpc_socket_file : that.rpc_socket_file,
93 pub_socket_file : that.pub_socket_file,
94 pm2_home : that.pm2_home
95 });
96 });
97 });
98 return false;
99 }
100
101 /**
102 * Daemon mode
103 */
104 that.launchDaemon(function(err, child) {
105 if (err) {
106 Common.printError(err);
107 return cb ? cb(err) : process.exit(that.conf.ERROR_EXIT);
108 }
109
110 if (!process.env.PM2_DISCRETE_MODE)
111 Common.printOut(that.conf.PREFIX_MSG + 'PM2 Successfully daemonized');
112
113 that.launchRPC(function(err, meta) {
114 return cb(null, {
115 daemon_mode : that.conf.daemon_mode,
116 new_pm2_instance : true,
117 rpc_socket_file : that.rpc_socket_file,
118 pub_socket_file : that.pub_socket_file,
119 pm2_home : that.pm2_home
120 });
121 });
122 });
123 });
124};
125
126// Init file structure of pm2_home
127// This includes
128// - pm2 pid and log path
129// - rpc and pub socket for command execution
130Client.prototype.initFileStructure = function (opts) {
131 if (!fs.existsSync(opts.DEFAULT_LOG_PATH)) {
132 try {
133 require('mkdirp').sync(opts.DEFAULT_LOG_PATH);
134 } catch (e) {
135 console.error(e.stack || e);
136 }
137 }
138
139 if (!fs.existsSync(opts.DEFAULT_PID_PATH)) {
140 try {
141 require('mkdirp').sync(opts.DEFAULT_PID_PATH);
142 } catch (e) {
143 console.error(e.stack || e);
144 }
145 }
146
147 if (!fs.existsSync(opts.PM2_MODULE_CONF_FILE)) {
148 try {
149 fs.writeFileSync(opts.PM2_MODULE_CONF_FILE, "{}");
150 } catch (e) {
151 console.error(e.stack || e);
152 }
153 }
154
155 if (!fs.existsSync(opts.DEFAULT_MODULE_PATH)) {
156 try {
157 require('mkdirp').sync(opts.DEFAULT_MODULE_PATH);
158 } catch (e) {
159 console.error(e.stack || e);
160 }
161 }
162
163 if (process.env.PM2_DISCRETE_MODE) {
164 try {
165 fs.writeFileSync(path.join(opts.PM2_HOME, 'touch'), Date.now().toString());
166 } catch(e) {
167 debug(e.stack || e);
168 }
169 }
170
171 if (!process.env.PM2_PROGRAMMATIC && !fs.existsSync(path.join(opts.PM2_HOME, 'touch'))) {
172
173 var vCheck = require('./VersionCheck.js')
174
175 vCheck({
176 state: 'install',
177 version: pkg.version
178 })
179
180 var dt = fs.readFileSync(path.join(__dirname, opts.PM2_BANNER));
181 console.log(dt.toString());
182 try {
183 fs.writeFileSync(path.join(opts.PM2_HOME, 'touch'), Date.now().toString());
184 } catch(e) {
185 debug(e.stack || e);
186 }
187 }
188};
189
190Client.prototype.close = function(cb) {
191 var that = this;
192
193 forEach([
194 that.disconnectRPC.bind(that),
195 that.disconnectBus.bind(that)
196 ], function(fn, next) {
197 fn(next)
198 }, cb);
199};
200
201/**
202 * Launch the Daemon by forking this same file
203 * The method Client.remoteWrapper will be called
204 *
205 * @method launchDaemon
206 * @param {Object} opts
207 * @param {Object} [opts.interactor=true] allow to disable interaction on launch
208 */
209Client.prototype.launchDaemon = function(opts, cb) {
210 if (typeof(opts) == 'function') {
211 cb = opts;
212 opts = {
213 interactor : true
214 };
215 }
216
217 var that = this
218 var ClientJS = path.resolve(path.dirname(module.filename), 'Daemon.js');
219 var node_args = [];
220 var out, err;
221
222 // if (process.env.TRAVIS) {
223 // // Redirect PM2 internal err and out to STDERR STDOUT when running with Travis
224 // out = 1;
225 // err = 2;
226 // }
227 // else {
228 out = fs.openSync(that.conf.PM2_LOG_FILE_PATH, 'a'),
229 err = fs.openSync(that.conf.PM2_LOG_FILE_PATH, 'a');
230 //}
231
232 if (this.conf.LOW_MEMORY_ENVIRONMENT) {
233 var os = require('os');
234 node_args.push('--gc-global'); // Does full GC (smaller memory footprint)
235 node_args.push('--max-old-space-size=' + Math.floor(os.totalmem() / 1024 / 1024));
236 }
237
238 // Node.js tuning for better performance
239 //node_args.push('--expose-gc'); // Allows manual GC in the code
240
241 /**
242 * Add node [arguments] depending on PM2_NODE_OPTIONS env variable
243 */
244 if (process.env.PM2_NODE_OPTIONS)
245 node_args = node_args.concat(process.env.PM2_NODE_OPTIONS.split(' '));
246 node_args.push(ClientJS);
247
248 if (!process.env.PM2_DISCRETE_MODE)
249 Common.printOut(that.conf.PREFIX_MSG + 'Spawning PM2 daemon with pm2_home=' + this.pm2_home);
250
251 var interpreter = 'node';
252
253 if (which('node') == null)
254 interpreter = process.execPath;
255
256 var child = require('child_process').spawn(interpreter, node_args, {
257 detached : true,
258 cwd : that.conf.cwd || process.cwd(),
259 windowsHide: true,
260 env : util._extend({
261 'SILENT' : that.conf.DEBUG ? !that.conf.DEBUG : true,
262 'PM2_HOME' : that.pm2_home
263 }, process.env),
264 stdio : ['ipc', out, err]
265 });
266
267 function onError(e) {
268 console.error(e.message || e);
269 return cb ? cb(e.message || e) : false;
270 }
271
272 child.once('error', onError);
273
274 child.unref();
275
276 child.once('message', function(msg) {
277 debug('PM2 daemon launched with return message: ', msg);
278 child.removeListener('error', onError);
279 child.disconnect();
280
281 if (opts && opts.interactor == false)
282 return cb(null, child);
283
284 if (process.env.PM2_NO_INTERACTION == 'true')
285 return cb(null, child);
286
287 /**
288 * Here the Keymetrics agent is launched automaticcaly if
289 * it has been already configured before (via pm2 link)
290 */
291 KMDaemon.launchAndInteract(that.conf, {
292 machine_name : that.machine_name,
293 public_key : that.public_key,
294 secret_key : that.secret_key,
295 pm2_version : pkg.version
296 }, function(err, data, interactor_proc) {
297 that.interactor_process = interactor_proc;
298 return cb(null, child);
299 });
300 });
301};
302
303/**
304 * Ping the daemon to know if it alive or not
305 * @api public
306 * @method pingDaemon
307 * @param {} cb
308 * @return
309 */
310Client.prototype.pingDaemon = function pingDaemon(cb) {
311 var req = axon.socket('req');
312 var client = new rpc.Client(req);
313 var that = this;
314
315 debug('[PING PM2] Trying to connect to server');
316
317 client.sock.once('reconnect attempt', function() {
318 client.sock.close();
319 debug('Daemon not launched');
320 process.nextTick(function() {
321 return cb(false);
322 });
323 });
324
325 client.sock.once('error', function(e) {
326 if (e.code === 'EACCES') {
327 fs.stat(that.conf.DAEMON_RPC_PORT, function(e, stats) {
328 if (stats.uid === 0) {
329 console.error(that.conf.PREFIX_MSG_ERR + 'Permission denied, to give access to current user:');
330 console.log('$ sudo chown ' + process.env.USER + ':' + process.env.USER + ' ' + that.conf.DAEMON_RPC_PORT + ' ' + that.conf.DAEMON_PUB_PORT);
331 }
332 else
333 console.error(that.conf.PREFIX_MSG_ERR + 'Permission denied, check permissions on ' + that.conf.DAEMON_RPC_PORT);
334
335 process.exit(1);
336 });
337 }
338 else
339 console.error(e.message || e);
340 });
341
342 client.sock.once('connect', function() {
343 client.sock.once('close', function() {
344 return cb(true);
345 });
346 client.sock.close();
347 debug('Daemon alive');
348 });
349
350 req.connect(this.rpc_socket_file);
351};
352
353/**
354 * Methods to interact with the Daemon via RPC
355 * This method wait to be connected to the Daemon
356 * Once he's connected it trigger the command parsing (on ./bin/pm2 file, at the end)
357 * @method launchRPC
358 * @params {function} [cb]
359 * @return
360 */
361Client.prototype.launchRPC = function launchRPC(cb) {
362 var self = this;
363 debug('Launching RPC client on socket file %s', this.rpc_socket_file);
364 var req = axon.socket('req');
365 this.client = new rpc.Client(req);
366
367 var connectHandler = function() {
368 self.client.sock.removeListener('error', errorHandler);
369 debug('RPC Connected to Daemon');
370 if (cb) {
371 setTimeout(function() {
372 cb(null);
373 }, 4);
374 }
375 };
376
377 var errorHandler = function(e) {
378 self.client.sock.removeListener('connect', connectHandler);
379 if (cb) {
380 return cb(e);
381 }
382 };
383
384 this.client.sock.once('connect', connectHandler);
385 this.client.sock.once('error', errorHandler);
386 this.client_sock = req.connect(this.rpc_socket_file);
387};
388
389/**
390 * Methods to close the RPC connection
391 * @callback cb
392 */
393Client.prototype.disconnectRPC = function disconnectRPC(cb) {
394 var that = this;
395 if (!cb) cb = noop;
396
397 if (!this.client_sock || !this.client_sock.close) {
398 this.client = null;
399 return process.nextTick(function() {
400 cb(new Error('SUB connection to PM2 is not launched'));
401 });
402 }
403
404 if (this.client_sock.connected === false ||
405 this.client_sock.closing === true) {
406 this.client = null;
407 return process.nextTick(function() {
408 cb(new Error('RPC already being closed'));
409 });
410 }
411
412 try {
413 var timer;
414
415 that.client_sock.once('close', function() {
416 clearTimeout(timer);
417 that.client = null;
418 debug('PM2 RPC cleanly closed');
419 return cb(null, { msg : 'RPC Successfully closed' });
420 });
421
422 timer = setTimeout(function() {
423 if (that.client_sock.destroy)
424 that.client_sock.destroy();
425 that.client = null;
426 return cb(null, { msg : 'RPC Successfully closed via timeout' });
427 }, 200);
428
429 that.client_sock.close();
430 } catch(e) {
431 debug('Error while disconnecting RPC PM2', e.stack || e);
432 return cb(e);
433 }
434 return false;
435};
436
437Client.prototype.launchBus = function launchEventSystem(cb) {
438 var self = this;
439 this.sub = axon.socket('sub-emitter');
440 this.sub_sock = this.sub.connect(this.pub_socket_file);
441
442 this.sub_sock.once('connect', function() {
443 return cb(null, self.sub, self.sub_sock);
444 });
445};
446
447Client.prototype.disconnectBus = function disconnectBus(cb) {
448 if (!cb) cb = noop;
449
450 var that = this;
451
452 if (!this.sub_sock || !this.sub_sock.close) {
453 that.sub = null;
454 return process.nextTick(function() {
455 cb(null, { msg : 'bus was not connected'});
456 });
457 }
458
459 if (this.sub_sock.connected === false ||
460 this.sub_sock.closing === true) {
461 that.sub = null;
462 return process.nextTick(function() {
463 cb(new Error('SUB connection is already being closed'));
464 });
465 }
466
467 try {
468 var timer;
469
470 that.sub_sock.once('close', function() {
471 that.sub = null;
472 clearTimeout(timer);
473 debug('PM2 PUB cleanly closed');
474 return cb();
475 });
476
477 timer = setTimeout(function() {
478 if (Client.sub_sock.destroy)
479 that.sub_sock.destroy();
480 return cb();
481 }, 200);
482
483 this.sub_sock.close();
484 } catch(e) {
485 return cb(e);
486 }
487};
488
489/**
490 * Description
491 * @method gestExposedMethods
492 * @param {} cb
493 * @return
494 */
495Client.prototype.getExposedMethods = function getExposedMethods(cb) {
496 this.client.methods(cb);
497};
498
499/**
500 * Description
501 * @method executeRemote
502 * @param {} method
503 * @param {} env
504 * @param {} fn
505 * @return
506 */
507Client.prototype.executeRemote = function executeRemote(method, app_conf, fn) {
508 var self = this;
509
510 // stop watch on stop | env is the process id
511 if (method.indexOf('stop') !== -1) {
512 this.stopWatch(method, app_conf);
513 }
514 // stop watching when process is deleted
515 else if (method.indexOf('delete') !== -1) {
516 this.stopWatch(method, app_conf);
517 }
518 // stop everything on kill
519 else if (method.indexOf('kill') !== -1) {
520 this.stopWatch('deleteAll', app_conf);
521 }
522 else if (method.indexOf('restartProcessId') !== -1 && process.argv.indexOf('--watch') > -1) {
523 delete app_conf.env.current_conf.watch;
524 this.toggleWatch(method, app_conf);
525 }
526
527 if (!this.client || !this.client.call) {
528 this.start(function(error) {
529 if (error) {
530 if (fn)
531 return fn(error);
532 console.error(error);
533 return process.exit(0);
534 }
535 if (self.client) {
536 return self.client.call(method, app_conf, fn);
537 }
538 });
539 return false;
540 }
541
542 debug('Calling daemon method pm2:%s on rpc socket:%s', method, this.rpc_socket_file);
543 return this.client.call(method, app_conf, fn);
544};
545
546Client.prototype.notifyGod = function(action_name, id, cb) {
547 this.executeRemote('notifyByProcessId', {
548 id : id,
549 action_name : action_name,
550 manually : true
551 }, function() {
552 debug('God notified');
553 return cb ? cb() : false;
554 });
555};
556
557Client.prototype.killDaemon = function killDaemon(fn) {
558 var timeout;
559 var that = this;
560
561 function quit() {
562 that.close(function() {
563 return fn ? fn(null, {success:true}) : false;
564 });
565 }
566
567 // under unix, we listen for signal (that is send by daemon to notify us that its shuting down)
568 if (process.platform !== 'win32' && process.platform !== 'win64') {
569 process.once('SIGQUIT', function() {
570 debug('Received SIGQUIT from pm2 daemon');
571 clearTimeout(timeout);
572 quit();
573 });
574 }
575 else {
576 // if under windows, try to ping the daemon to see if it still here
577 setTimeout(function() {
578 that.pingDaemon(function(alive) {
579 if (!alive) {
580 clearTimeout(timeout);
581 return quit();
582 }
583 });
584 }, 250)
585 }
586
587 timeout = setTimeout(function() {
588 quit();
589 }, 3000);
590
591 // Kill daemon
592 this.executeRemote('killMe', {pid : process.pid});
593};
594
595
596/**
597 * Description
598 * @method toggleWatch
599 * @param {String} pm2 method name
600 * @param {Object} application environment, should include id
601 * @param {Function} callback
602 */
603Client.prototype.toggleWatch = function toggleWatch(method, env, fn) {
604 debug('Calling toggleWatch');
605 this.client.call('toggleWatch', method, env, function() {
606 return fn ? fn() : false;
607 });
608};
609
610/**
611 * Description
612 * @method startWatch
613 * @param {String} pm2 method name
614 * @param {Object} application environment, should include id
615 * @param {Function} callback
616 */
617Client.prototype.startWatch = function restartWatch(method, env, fn) {
618 debug('Calling startWatch');
619 this.client.call('startWatch', method, env, function() {
620 return fn ? fn() : false;
621 });
622};
623
624/**
625 * Description
626 * @method stopWatch
627 * @param {String} pm2 method name
628 * @param {Object} application environment, should include id
629 * @param {Function} callback
630 */
631Client.prototype.stopWatch = function stopWatch(method, env, fn) {
632 debug('Calling stopWatch');
633 this.client.call('stopWatch', method, env, function() {
634 return fn ? fn() : false;
635 });
636};
637
638Client.prototype.getAllProcess = function(cb) {
639 var found_proc = [];
640
641 this.executeRemote('getMonitorData', {}, function(err, procs) {
642 if (err) {
643 Common.printError('Error retrieving process list: ' + err);
644 return cb(err);
645 }
646
647 return cb(null, procs);
648 });
649};
650
651Client.prototype.getAllProcessId = function(cb) {
652 var found_proc = [];
653
654 this.executeRemote('getMonitorData', {}, function(err, procs) {
655 if (err) {
656 Common.printError('Error retrieving process list: ' + err);
657 return cb(err);
658 }
659
660 return cb(null, procs.map(proc => proc.pm_id));
661 });
662};
663
664Client.prototype.getAllProcessIdWithoutModules = function(cb) {
665 var found_proc = [];
666
667 this.executeRemote('getMonitorData', {}, function(err, procs) {
668 if (err) {
669 Common.printError('Error retrieving process list: ' + err);
670 return cb(err);
671 }
672
673 var proc_ids = procs
674 .filter(proc => !proc.pm2_env.pmx_module)
675 .map(proc => proc.pm_id)
676
677 return cb(null, proc_ids);
678 });
679};
680
681Client.prototype.getProcessIdByName = function(name, force_all, cb) {
682 var found_proc = [];
683 var full_details = {};
684
685 if (typeof(cb) === 'undefined') {
686 cb = force_all;
687 force_all = false;
688 }
689
690 if (typeof(name) == 'number')
691 name = name.toString();
692
693 this.executeRemote('getMonitorData', {}, function(err, list) {
694 if (err) {
695 Common.printError('Error retrieving process list: ' + err);
696 return cb(err);
697 }
698
699 list.forEach(function(proc) {
700 if (proc.pm2_env.name == name || proc.pm2_env.pm_exec_path == path.resolve(name)) {
701 found_proc.push(proc.pm_id);
702 full_details[proc.pm_id] = proc;
703 }
704 });
705
706 return cb(null, found_proc, full_details);
707 });
708};
709
710Client.prototype.getProcessIdsByNamespace = function(namespace, force_all, cb) {
711 var found_proc = [];
712 var full_details = {};
713
714 if (typeof(cb) === 'undefined') {
715 cb = force_all;
716 force_all = false;
717 }
718
719 if (typeof(namespace) == 'number')
720 namespace = namespace.toString();
721
722 this.executeRemote('getMonitorData', {}, function(err, list) {
723 if (err) {
724 Common.printError('Error retrieving process list: ' + err);
725 return cb(err);
726 }
727
728 list.forEach(function(proc) {
729 if (proc.pm2_env.namespace == namespace) {
730 found_proc.push(proc.pm_id);
731 full_details[proc.pm_id] = proc;
732 }
733 });
734
735 return cb(null, found_proc, full_details);
736 });
737};
738
739Client.prototype.getProcessByName = function(name, cb) {
740 var found_proc = [];
741
742 this.executeRemote('getMonitorData', {}, function(err, list) {
743 if (err) {
744 Common.printError('Error retrieving process list: ' + err);
745 return cb(err);
746 }
747
748 list.forEach(function(proc) {
749 if (proc.pm2_env.name == name ||
750 proc.pm2_env.pm_exec_path == path.resolve(name)) {
751 found_proc.push(proc);
752 }
753 });
754
755 return cb(null, found_proc);
756 });
757};
758
759Client.prototype.getProcessByNameOrId = function (nameOrId, cb) {
760 var foundProc = [];
761
762 this.executeRemote('getMonitorData', {}, function (err, list) {
763 if (err) {
764 Common.printError('Error retrieving process list: ' + err);
765 return cb(err);
766 }
767
768 list.forEach(function (proc) {
769 if (proc.pm2_env.name === nameOrId ||
770 proc.pm2_env.pm_exec_path === path.resolve(nameOrId) ||
771 proc.pid === parseInt(nameOrId) ||
772 proc.pm2_env.pm_id === parseInt(nameOrId)) {
773 foundProc.push(proc);
774 }
775 });
776
777 return cb(null, foundProc);
778 });
779};