UNPKG

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