From d9fcdb32895b207919e3c80c3b8fc426ba712451 Mon Sep 17 00:00:00 2001 From: qwertycxz Date: Thu, 8 May 2025 12:44:18 +0800 Subject: [PATCH 1/5] feat: add features to stdin commands 1. Add support for input commands in `pm2 logs` 2. Add support for using names in `pm2 attach` and `pm2 send` 3. Update documentation for new features --- examples/interact-via-stdin/README.md | 12 +++ lib/API/Extra.js | 132 ++++++++++++++++++-------- lib/Client.js | 13 +++ lib/binaries/CLI.js | 17 ++-- 4 files changed, 127 insertions(+), 47 deletions(-) diff --git a/examples/interact-via-stdin/README.md b/examples/interact-via-stdin/README.md index a3b89e9fa1..ec67871831 100644 --- a/examples/interact-via-stdin/README.md +++ b/examples/interact-via-stdin/README.md @@ -11,3 +11,15 @@ Then to attach to it: ``` $ pm2 attach 0 ``` + +Or: + +``` +$ pm2 logs --attach +``` + +Then send a message (e.g., *Lorem Ipsum*) to the app: + +``` +> 0 Lorem Ipsum +``` diff --git a/lib/API/Extra.js b/lib/API/Extra.js index e30aba1218..5c93854ffa 100644 --- a/lib/API/Extra.js +++ b/lib/API/Extra.js @@ -321,10 +321,10 @@ module.exports = function(CLI) { } /** - * Description + * Send a line to stdin of a process * @method sendLineToStdin */ - CLI.prototype.sendLineToStdin = function(pm_id, line, separator, cb) { + CLI.prototype.sendLineToStdin = function(id, line, separator, cb) { var that = this; if (!cb && typeof(separator) == 'function') { @@ -332,63 +332,84 @@ module.exports = function(CLI) { separator = null; } - var packet = { - pm_id : pm_id, - line : line + (separator || '\n') - }; + function handleError(err) { + Common.printError(cst.PREFIX_MSG_ERR + err); + return cb ? cb(Common.retErr(err)) : that.exitCli(cst.ERROR_EXIT); + } - that.Client.executeRemote('sendLineToStdin', packet, function(err, res) { - if (err) { - Common.printError(cst.PREFIX_MSG_ERR + err); - return cb ? cb(Common.retErr(err)) : that.exitCli(cst.ERROR_EXIT); - } - return cb ? cb(null, res) : that.speedList(); - }); + function sendLine(pm_id) { + var packet = { + pm_id : pm_id, + line : line + (separator || '\n') + }; + + that.Client.executeRemote('sendLineToStdin', packet, function(err, res) { + if (err) return handleError(err); + return cb ? cb(null, res) : that.speedList(); + }); + } + + if (isNaN(id)) { + that.Client.getUniqueProcessIdByName(id, function(err, pm_id) { + if (err) return handleError(err); + sendLine(pm_id); + }); + return; + } + sendLine(id); }; /** - * Description - * @method attachToProcess + * Attach to a process stdin and stdout + * @method attach */ - CLI.prototype.attach = function(pm_id, separator, cb) { + CLI.prototype.attach = function(id, separator, cb) { var that = this; var readline = require('readline'); - if (isNaN(pm_id)) { - Common.printError('pm_id must be a process number (not a process name)'); - return cb ? cb(Common.retErr('pm_id must be number')) : that.exitCli(cst.ERROR_EXIT); - } - if (typeof(separator) == 'function') { cb = separator; separator = null; } - var rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); + function handleError(err) { + Common.printError(cst.PREFIX_MSG_ERR + err); + return cb ? cb(Common.retErr(err)) : that.exitCli(cst.ERROR_EXIT); + } - rl.on('close', function() { - return cb ? cb() : that.exitCli(cst.SUCCESS_EXIT); - }); + function attachProcess(pm_id) { + var rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); - that.Client.launchBus(function(err, bus, socket) { - if (err) { - Common.printError(err); - return cb ? cb(Common.retErr(err)) : that.exitCli(cst.ERROR_EXIT); - } + rl.on('close', function() { + return cb ? cb() : that.exitCli(cst.SUCCESS_EXIT); + }); + + that.Client.launchBus(function(err, bus, socket) { + if (err) return handleError(err); - bus.on('log:*', function(type, packet) { - if (packet.process.pm_id !== parseInt(pm_id)) - return; - process.stdout.write(packet.data); + bus.on('log:*', function(type, packet) { + if (packet.process.pm_id !== parseInt(pm_id)) + return; + process.stdout.write(packet.data); + }); }); - }); - rl.on('line', function(line) { - that.sendLineToStdin(pm_id, line, separator, function() {}); - }); + rl.on('line', function(line) { + that.sendLineToStdin(pm_id, line, separator, function() {}); + }); + } + + if (isNaN(id)) { + that.Client.getUniqueProcessIdByName(id, function(err, pm_id) { + if (err) return handleError(err); + attachProcess(pm_id); + }); + return; + } + attachProcess(id); }; /** @@ -772,4 +793,33 @@ module.exports = function(CLI) { that.exitCli(cst.SUCCESS_EXIT); }); }; + + /** + * Attach stdin to the client. You could use this to send commands to a process + * + * Input ` [line] ...` to send stdin to + * + * Any error will exit the CLI + * @method attachInput + */ + CLI.prototype.attachInput = function() { + var that = this; + var readline = require('readline'); + + var rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + rl.on('close', function() { + that.exitCli(); + }); + rl.on('line', function(input) { + var line = input.split(' '); + that.sendLineToStdin(line.shift(), line, function(err) { + if (err) { + that.exitCli(); + } + }); + }); + }; }; diff --git a/lib/Client.js b/lib/Client.js index 1fb73bb6a5..bd220df49b 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -704,6 +704,19 @@ Client.prototype.getProcessIdByName = function(name, force_all, cb) { }); }; +/** + * Get process id by name and check if only one process is hitted + * @param {string} name Name of the process to search for + * @param {(err: string, id: string) => void} cb Callback function to handle the result + */ +Client.prototype.getUniqueProcessIdByName = function(name, cb) { + this.getProcessIdByName(name, function(err, list) { + if (err) return cb(err); + if (list.length === 1) return cb(null, list[0]); + cb(`Expected exactly one process with name "${name}", but found ${list.length}.`); + }); +}; + Client.prototype.getProcessIdsByNamespace = function(namespace, force_all, cb) { var found_proc = []; var full_details = {}; diff --git a/lib/binaries/CLI.js b/lib/binaries/CLI.js index 2ef3523ef6..bb3b08f50b 100644 --- a/lib/binaries/CLI.js +++ b/lib/binaries/CLI.js @@ -672,18 +672,18 @@ commander.command('cleardump') // // Save processes to file // -commander.command('send ') - .description('send stdin to ') - .action(function(pm_id, line) { - pm2.sendLineToStdin(pm_id, line); +commander.command('send ') + .description('send stdin to ') + .action(function(id, line) { + pm2.sendLineToStdin(id, line); }); // // Attach to stdin/stdout // Not TTY ready // -commander.command('attach [command separator]') - .description('attach stdin/stdout to application identified by ') +commander.command('attach [command separator]') + .description('attach stdin/stdout to ') .action(function(pm_id, separator) { pm2.attach(pm_id, separator); }); @@ -892,6 +892,7 @@ commander.command('logs [id|name|namespace]') .option('--timestamp [format]', 'add timestamps (default format YYYY-MM-DD-HH:mm:ss)') .option('--nostream', 'print logs without launching the log stream') .option('--highlight [value]', 'highlights the given value') + .option('--attach-input', 'input ` [line] ...` to send stdin to ') .description('stream logs file. Default stream all logs') .action(function(id, cmd) { var Logs = require('../API/Log.js'); @@ -931,6 +932,10 @@ commander.command('logs [id|name|namespace]') Logs.formatStream(pm2.Client, id, false, 'YYYY-MM-DD-HH:mm:ssZZ', exclusive, highlight); else pm2.streamLogs(id, line, raw, timestamp, exclusive, highlight); + + if (cmd.attachInput) { + pm2.attachInput(); + } }); From f57f74830c65d9bd55fdf8582c1fe53fbf9dcfe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?qwerty=E5=90=83=E5=B0=8F=E5=BA=84?= Date: Thu, 8 May 2025 16:06:39 +0800 Subject: [PATCH 2/5] docs: typo --- examples/interact-via-stdin/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/interact-via-stdin/README.md b/examples/interact-via-stdin/README.md index ec67871831..5bc956e9d4 100644 --- a/examples/interact-via-stdin/README.md +++ b/examples/interact-via-stdin/README.md @@ -15,7 +15,7 @@ $ pm2 attach 0 Or: ``` -$ pm2 logs --attach +$ pm2 logs --attach-input ``` Then send a message (e.g., *Lorem Ipsum*) to the app: From 7552737c4c7b62d1b88abf27b277cdad7bff2a33 Mon Sep 17 00:00:00 2001 From: qwertycxz Date: Thu, 8 May 2025 19:16:21 +0800 Subject: [PATCH 3/5] docs: update types/index.d.ts --- types/index.d.ts | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/types/index.d.ts b/types/index.d.ts index ab61446d37..ef769257e2 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -191,6 +191,47 @@ export function startup(platform: Platform, errback: ErrResultCallback): void; */ export function sendDataToProcessId(proc_id: number, packet: object, cb: ErrResultCallback): void; +/** + * Send a line to stdin of a process + * @param id id or name of the process + * @param line line to send to stdin + * @param cb if success, the id and the line will be returned + */ +export function sendLineToStdin(id: string, line: string, cb: ErrResultCallback): void + +/** + * Send a line to stdin of a process + * @param id id or name of the process + * @param line line to send to stdin + * @param separator separator of the line, default is '\n' + * @param cb if success, the id and the line will be returned + */ +export function sendLineToStdin(id: string, line: string, separator: string, cb: ErrResultCallback): void + +/** + * Attach to stdio of a process + * @param id id or name of the process + * @param cb if success, null will be returned + */ +export function attach(id: string, cb: ErrResultCallback): void + +/** + * Attach to stdio of a process + * @param id id or name of the process + * @param separator separator of the line, default is '\n' + * @param cb if success, null will be returned + */ +export function attach(id: string, separator: string, cb: ErrResultCallback): void + +/** + * Attach stdin to the client. You could use this to send commands to a process + * + * Input ` [line] ...` to send stdin to + * + * Any error will exit the CLI + */ +export function attachInput(): void + // Interfaces export interface Proc { From d51d94352acc1a11fa274a9d819d417116da5c68 Mon Sep 17 00:00:00 2001 From: qwertycxz Date: Thu, 8 May 2025 22:22:07 +0800 Subject: [PATCH 4/5] fix: string join bug --- lib/API/Extra.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/API/Extra.js b/lib/API/Extra.js index 5c93854ffa..b7bb9a52c6 100644 --- a/lib/API/Extra.js +++ b/lib/API/Extra.js @@ -815,7 +815,7 @@ module.exports = function(CLI) { }); rl.on('line', function(input) { var line = input.split(' '); - that.sendLineToStdin(line.shift(), line, function(err) { + that.sendLineToStdin(line.shift(), line.join(' '), function(err) { if (err) { that.exitCli(); } From ebd629efafe5bff192319d0b4d6f4e7bbf594c5f Mon Sep 17 00:00:00 2001 From: Unitech Date: Mon, 4 May 2026 09:47:20 +0200 Subject: [PATCH 5/5] Silence cluster worker events to prevent boot crashes --- CHANGELOG.md | 3 +++ lib/ProcessContainer.js | 19 ++++++++++++------- lib/ProcessContainerBun.js | 19 ++++++++++++------- modules/pm2-io-bpm/transports/IPCTransport.js | 15 ++++++--------- 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e21c96280..1a7d7e40c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Bug Fixes - Fix `pm2 serve` returning 403 Forbidden on Windows — traversal guard used hardcoded `/` separator #6109 +- Silence cluster worker `error` events to prevent boot crashes +- Wrap `process.send` before BPM injection to avoid send-on-disconnected during boot +- BPM IPC transport: log instead of `process.exit(1)` on disconnected send ## 7.0.1 diff --git a/lib/ProcessContainer.js b/lib/ProcessContainer.js index 6aaef41818..e79269cdcd 100644 --- a/lib/ProcessContainer.js +++ b/lib/ProcessContainer.js @@ -9,6 +9,11 @@ * - pid */ +var cluster = require('cluster'); +if (cluster.isWorker) { + cluster.worker.on('error', function () {}); +} + var p = require('path'); var cst = require('../constants'); var Utility = require('./Utility.js'); @@ -36,6 +41,13 @@ delete process.env.pm2_env; (function ProcessContainer() { var fs = require('fs'); + var original_send = process.send; + + process.send = function() { + if (process.connected) + original_send.apply(this, arguments); + }; + ProcessUtils.injectModules() var stdFile = pm2_env.pm_log_path; @@ -44,13 +56,6 @@ delete process.env.pm2_env; var pidFile = pm2_env.pm_pid_path; var script = pm2_env.pm_exec_path; - var original_send = process.send; - - process.send = function() { - if (process.connected) - original_send.apply(this, arguments); - }; - //send node version if (process.versions && process.versions.node) { process.send({ diff --git a/lib/ProcessContainerBun.js b/lib/ProcessContainerBun.js index ac98d99f4d..f9ef676c70 100644 --- a/lib/ProcessContainerBun.js +++ b/lib/ProcessContainerBun.js @@ -4,6 +4,11 @@ * can be found in the LICENSE file. */ +var cluster = require('cluster'); +if (cluster.isWorker) { + cluster.worker.on('error', function () {}); +} + var p = require('path'); var cst = require('../constants'); var Utility = require('./Utility.js'); @@ -31,6 +36,13 @@ delete process.env.pm2_env; (function ProcessContainer() { var fs = require('fs'); + var original_send = process.send; + + process.send = function() { + if (process.connected) + original_send.apply(this, arguments); + }; + ProcessUtils.injectModules() var stdFile = pm2_env.pm_log_path; @@ -39,13 +51,6 @@ delete process.env.pm2_env; var pidFile = pm2_env.pm_pid_path; var script = pm2_env.pm_exec_path; - var original_send = process.send; - - process.send = function() { - if (process.connected) - original_send.apply(this, arguments); - }; - //send node version if (process.versions && process.versions.node) { process.send({ diff --git a/modules/pm2-io-bpm/transports/IPCTransport.js b/modules/pm2-io-bpm/transports/IPCTransport.js index 5f8e1c8a04..417fcaf018 100644 --- a/modules/pm2-io-bpm/transports/IPCTransport.js +++ b/modules/pm2-io-bpm/transports/IPCTransport.js @@ -86,18 +86,15 @@ class IPCTransport extends EventEmitter { send (channel, payload) { if (typeof process.send !== 'function') return -1 - if (process.connected === false) { - console.error('Process disconnected from parent! (not connected)') - return process.exit(1) - } + if (process.connected === false) return -1 + this.logger(`Send on channel ${channel}`) try { - this.logger(`Send on channel ${channel}`) - process.send({ type: channel, data: payload }) + process.send({ type: channel, data: payload }, (err) => { + if (err) this.logger('async send failed: %s', err && err.code) + }) } catch (err) { - this.logger('Process disconnected from parent !') - this.logger(err) - return process.exit(1) + this.logger('Process disconnected from parent: %s', err && err.message) } }