UNPKG

20.4 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 env : util._extend({
260 'SILENT' : that.conf.DEBUG ? !that.conf.DEBUG : true,
261 'PM2_HOME' : that.pm2_home
262 }, process.env),
263 stdio : ['ipc', out, err]
264 });
265
266 function onError(e) {
267 console.error(e.message || e);
268 return cb ? cb(e.message || e) : false;
269 }
270
271 child.once('error', onError);
272
273 child.unref();
274
275 child.once('message', function(msg) {
276 debug('PM2 daemon launched with return message: ', msg);
277 child.removeListener('error', onError);
278 child.disconnect();
279
280 if (opts && opts.interactor == false)
281 return cb(null, child);
282
283 if (process.env.PM2_NO_INTERACTION == 'true')
284 return cb(null, child);
285
286 /**
287 * Here the Keymetrics agent is launched automaticcaly if
288 * it has been already configured before (via pm2 link)
289 */
290 KMDaemon.launchAndInteract(that.conf, {
291 machine_name : that.machine_name,
292 public_key : that.public_key,
293 secret_key : that.secret_key,
294 pm2_version : pkg.version
295 }, function(err, data, interactor_proc) {
296 that.interactor_process = interactor_proc;
297 return cb(null, child);
298 });
299 });
300};
301
302/**
303 * Ping the daemon to know if it alive or not
304 * @api public
305 * @method pingDaemon
306 * @param {} cb
307 * @return
308 */
309Client.prototype.pingDaemon = function pingDaemon(cb) {
310 var req = axon.socket('req');
311 var client = new rpc.Client(req);
312 var that = this;
313
314 debug('[PING PM2] Trying to connect to server');
315
316 client.sock.once('reconnect attempt', function() {
317 client.sock.close();
318 debug('Daemon not launched');
319 process.nextTick(function() {
320 return cb(false);
321 });
322 });
323
324 client.sock.once('error', function(e) {
325 if (e.code === 'EACCES') {
326 fs.stat(that.conf.DAEMON_RPC_PORT, function(e, stats) {
327 if (stats.uid === 0) {
328 console.error(that.conf.PREFIX_MSG_ERR + 'Permission denied, to give access to current user:');
329 console.log('$ sudo chown ' + process.env.USER + ':' + process.env.USER + ' ' + that.conf.DAEMON_RPC_PORT + ' ' + that.conf.DAEMON_PUB_PORT);
330 }
331 else
332 console.error(that.conf.PREFIX_MSG_ERR + 'Permission denied, check permissions on ' + that.conf.DAEMON_RPC_PORT);
333
334 process.exit(1);
335 });
336 }
337 else
338 console.error(e.message || e);
339 });
340
341 client.sock.once('connect', function() {
342 client.sock.once('close', function() {
343 return cb(true);
344 });
345 client.sock.close();
346 debug('Daemon alive');
347 });
348
349 req.connect(this.rpc_socket_file);
350};
351
352/**
353 * Methods to interact with the Daemon via RPC
354 * This method wait to be connected to the Daemon
355 * Once he's connected it trigger the command parsing (on ./bin/pm2 file, at the end)
356 * @method launchRPC
357 * @params {function} [cb]
358 * @return
359 */
360Client.prototype.launchRPC = function launchRPC(cb) {
361 var self = this;
362 debug('Launching RPC client on socket file %s', this.rpc_socket_file);
363 var req = axon.socket('req');
364 this.client = new rpc.Client(req);
365
366 var connectHandler = function() {
367 self.client.sock.removeListener('error', errorHandler);
368 debug('RPC Connected to Daemon');
369 if (cb) {
370 setTimeout(function() {
371 cb(null);
372 }, 4);
373 }
374 };
375
376 var errorHandler = function(e) {
377 self.client.sock.removeListener('connect', connectHandler);
378 if (cb) {
379 return cb(e);
380 }
381 };
382
383 this.client.sock.once('connect', connectHandler);
384 this.client.sock.once('error', errorHandler);
385 this.client_sock = req.connect(this.rpc_socket_file);
386};
387
388/**
389 * Methods to close the RPC connection
390 * @callback cb
391 */
392Client.prototype.disconnectRPC = function disconnectRPC(cb) {
393 var that = this;
394 if (!cb) cb = noop;
395
396 if (!this.client_sock || !this.client_sock.close) {
397 this.client = null;
398 return process.nextTick(function() {
399 cb(new Error('SUB connection to PM2 is not launched'));
400 });
401 }
402
403 if (this.client_sock.connected === false ||
404 this.client_sock.closing === true) {
405 this.client = null;
406 return process.nextTick(function() {
407 cb(new Error('RPC already being closed'));
408 });
409 }
410
411 try {
412 var timer;
413
414 that.client_sock.once('close', function() {
415 clearTimeout(timer);
416 that.client = null;
417 debug('PM2 RPC cleanly closed');
418 return cb(null, { msg : 'RPC Successfully closed' });
419 });
420
421 timer = setTimeout(function() {
422 if (that.client_sock.destroy)
423 that.client_sock.destroy();
424 that.client = null;
425 return cb(null, { msg : 'RPC Successfully closed via timeout' });
426 }, 200);
427
428 that.client_sock.close();
429 } catch(e) {
430 debug('Error while disconnecting RPC PM2', e.stack || e);
431 return cb(e);
432 }
433 return false;
434};
435
436Client.prototype.launchBus = function launchEventSystem(cb) {
437 var self = this;
438 this.sub = axon.socket('sub-emitter');
439 this.sub_sock = this.sub.connect(this.pub_socket_file);
440
441 this.sub_sock.once('connect', function() {
442 return cb(null, self.sub, self.sub_sock);
443 });
444};
445
446Client.prototype.disconnectBus = function disconnectBus(cb) {
447 if (!cb) cb = noop;
448
449 var that = this;
450
451 if (!this.sub_sock || !this.sub_sock.close) {
452 that.sub = null;
453 return process.nextTick(function() {
454 cb(null, { msg : 'bus was not connected'});
455 });
456 }
457
458 if (this.sub_sock.connected === false ||
459 this.sub_sock.closing === true) {
460 that.sub = null;
461 return process.nextTick(function() {
462 cb(new Error('SUB connection is already being closed'));
463 });
464 }
465
466 try {
467 var timer;
468
469 that.sub_sock.once('close', function() {
470 that.sub = null;
471 clearTimeout(timer);
472 debug('PM2 PUB cleanly closed');
473 return cb();
474 });
475
476 timer = setTimeout(function() {
477 if (Client.sub_sock.destroy)
478 that.sub_sock.destroy();
479 return cb();
480 }, 200);
481
482 this.sub_sock.close();
483 } catch(e) {
484 return cb(e);
485 }
486};
487
488/**
489 * Description
490 * @method gestExposedMethods
491 * @param {} cb
492 * @return
493 */
494Client.prototype.getExposedMethods = function getExposedMethods(cb) {
495 this.client.methods(cb);
496};
497
498/**
499 * Description
500 * @method executeRemote
501 * @param {} method
502 * @param {} env
503 * @param {} fn
504 * @return
505 */
506Client.prototype.executeRemote = function executeRemote(method, app_conf, fn) {
507 var self = this;
508
509 // stop watch on stop | env is the process id
510 if (method.indexOf('stop') !== -1) {
511 this.stopWatch(method, app_conf);
512 }
513 // stop watching when process is deleted
514 else if (method.indexOf('delete') !== -1) {
515 this.stopWatch(method, app_conf);
516 }
517 // stop everything on kill
518 else if (method.indexOf('kill') !== -1) {
519 this.stopWatch('deleteAll', app_conf);
520 }
521 else if (method.indexOf('restartProcessId') !== -1 && process.argv.indexOf('--watch') > -1) {
522 delete app_conf.env.current_conf.watch;
523 this.toggleWatch(method, app_conf);
524 }
525
526 if (!this.client || !this.client.call) {
527 this.start(function(error) {
528 if (error) {
529 if (fn)
530 return fn(error);
531 console.error(error);
532 return process.exit(0);
533 }
534 if (self.client) {
535 return self.client.call(method, app_conf, fn);
536 }
537 });
538 return false;
539 }
540
541 debug('Calling daemon method pm2:%s on rpc socket:%s', method, this.rpc_socket_file);
542 return this.client.call(method, app_conf, fn);
543};
544
545Client.prototype.notifyGod = function(action_name, id, cb) {
546 this.executeRemote('notifyByProcessId', {
547 id : id,
548 action_name : action_name,
549 manually : true
550 }, function() {
551 debug('God notified');
552 return cb ? cb() : false;
553 });
554};
555
556Client.prototype.killDaemon = function killDaemon(fn) {
557 var timeout;
558 var that = this;
559
560 function quit() {
561 that.close(function() {
562 return fn ? fn(null, {success:true}) : false;
563 });
564 }
565
566 // under unix, we listen for signal (that is send by daemon to notify us that its shuting down)
567 if (process.platform !== 'win32' && process.platform !== 'win64') {
568 process.once('SIGQUIT', function() {
569 debug('Received SIGQUIT from pm2 daemon');
570 clearTimeout(timeout);
571 quit();
572 });
573 }
574 else {
575 // if under windows, try to ping the daemon to see if it still here
576 setTimeout(function() {
577 that.pingDaemon(function(alive) {
578 if (!alive) {
579 clearTimeout(timeout);
580 return quit();
581 }
582 });
583 }, 250)
584 }
585
586 timeout = setTimeout(function() {
587 quit();
588 }, 3000);
589
590 // Kill daemon
591 this.executeRemote('killMe', {pid : process.pid});
592};
593
594
595/**
596 * Description
597 * @method toggleWatch
598 * @param {String} pm2 method name
599 * @param {Object} application environment, should include id
600 * @param {Function} callback
601 */
602Client.prototype.toggleWatch = function toggleWatch(method, env, fn) {
603 debug('Calling toggleWatch');
604 this.client.call('toggleWatch', method, env, function() {
605 return fn ? fn() : false;
606 });
607};
608
609/**
610 * Description
611 * @method startWatch
612 * @param {String} pm2 method name
613 * @param {Object} application environment, should include id
614 * @param {Function} callback
615 */
616Client.prototype.startWatch = function restartWatch(method, env, fn) {
617 debug('Calling startWatch');
618 this.client.call('startWatch', method, env, function() {
619 return fn ? fn() : false;
620 });
621};
622
623/**
624 * Description
625 * @method stopWatch
626 * @param {String} pm2 method name
627 * @param {Object} application environment, should include id
628 * @param {Function} callback
629 */
630Client.prototype.stopWatch = function stopWatch(method, env, fn) {
631 debug('Calling stopWatch');
632 this.client.call('stopWatch', method, env, function() {
633 return fn ? fn() : false;
634 });
635};
636
637Client.prototype.getAllProcess = function(cb) {
638 var found_proc = [];
639
640 this.executeRemote('getMonitorData', {}, function(err, procs) {
641 if (err) {
642 Common.printError('Error retrieving process list: ' + err);
643 return cb(err);
644 }
645
646 return cb(null, procs);
647 });
648};
649
650Client.prototype.getAllProcessId = function(cb) {
651 var found_proc = [];
652
653 this.executeRemote('getMonitorData', {}, function(err, procs) {
654 if (err) {
655 Common.printError('Error retrieving process list: ' + err);
656 return cb(err);
657 }
658
659 return cb(null, procs.map(proc => proc.pm_id));
660 });
661};
662
663Client.prototype.getAllProcessIdWithoutModules = function(cb) {
664 var found_proc = [];
665
666 this.executeRemote('getMonitorData', {}, function(err, procs) {
667 if (err) {
668 Common.printError('Error retrieving process list: ' + err);
669 return cb(err);
670 }
671
672 var proc_ids = procs
673 .filter(proc => !proc.pm2_env.pmx_module)
674 .map(proc => proc.pm_id)
675
676 return cb(null, proc_ids);
677 });
678};
679
680Client.prototype.getProcessIdByName = function(name, force_all, cb) {
681 var found_proc = [];
682 var full_details = {};
683
684 if (typeof(cb) === 'undefined') {
685 cb = force_all;
686 force_all = false;
687 }
688
689 if (typeof(name) == 'number')
690 name = name.toString();
691
692 this.executeRemote('getMonitorData', {}, function(err, list) {
693 if (err) {
694 Common.printError('Error retrieving process list: ' + err);
695 return cb(err);
696 }
697
698 list.forEach(function(proc) {
699 if (proc.pm2_env.name == name || proc.pm2_env.pm_exec_path == path.resolve(name)) {
700 found_proc.push(proc.pm_id);
701 full_details[proc.pm_id] = proc;
702 }
703 });
704
705 return cb(null, found_proc, full_details);
706 });
707};
708
709Client.prototype.getProcessIdsByNamespace = function(namespace, force_all, cb) {
710 var found_proc = [];
711 var full_details = {};
712
713 if (typeof(cb) === 'undefined') {
714 cb = force_all;
715 force_all = false;
716 }
717
718 if (typeof(namespace) == 'number')
719 namespace = namespace.toString();
720
721 this.executeRemote('getMonitorData', {}, function(err, list) {
722 if (err) {
723 Common.printError('Error retrieving process list: ' + err);
724 return cb(err);
725 }
726
727 list.forEach(function(proc) {
728 if (proc.pm2_env.namespace == namespace) {
729 found_proc.push(proc.pm_id);
730 full_details[proc.pm_id] = proc;
731 }
732 });
733
734 return cb(null, found_proc, full_details);
735 });
736};
737
738Client.prototype.getProcessByName = function(name, cb) {
739 var found_proc = [];
740
741 this.executeRemote('getMonitorData', {}, function(err, list) {
742 if (err) {
743 Common.printError('Error retrieving process list: ' + err);
744 return cb(err);
745 }
746
747 list.forEach(function(proc) {
748 if (proc.pm2_env.name == name ||
749 proc.pm2_env.pm_exec_path == path.resolve(name)) {
750 found_proc.push(proc);
751 }
752 });
753
754 return cb(null, found_proc);
755 });
756};
757
758Client.prototype.getProcessByNameOrId = function (nameOrId, cb) {
759 var foundProc = [];
760
761 this.executeRemote('getMonitorData', {}, function (err, list) {
762 if (err) {
763 Common.printError('Error retrieving process list: ' + err);
764 return cb(err);
765 }
766
767 list.forEach(function (proc) {
768 if (proc.pm2_env.name === nameOrId ||
769 proc.pm2_env.pm_exec_path === path.resolve(nameOrId) ||
770 proc.pid === parseInt(nameOrId) ||
771 proc.pm2_env.pm_id === parseInt(nameOrId)) {
772 foundProc.push(proc);
773 }
774 });
775
776 return cb(null, foundProc);
777 });
778};