Node.js spawn:保持 StdOut 和 StdErr 的原始顺序

Node.js spawn: Keep StdOut and StdErr in the original order

正在尝试 运行 windows 来自 node.js v12.6.0 的批处理脚本,并以正确的顺序实时捕获其输出。

但是在测试期间,stdout 和 stderr 的顺序经常(不总是,但在 80% 的情况下)混合。

如何保持 stdout 和 stderr 的原始顺序?


这是我用于测试的 javascript 片段:

const spawn = require('child_process').spawn;

// Using 'inherit' to fix:
// "ERROR: Input redirection is not supported, exiting the process immediately".

const options = {
    stdio: [
        'inherit', // StdIn.
        'pipe',    // StdOut.
        'pipe'     // StdErr.
    ],
};

const child = spawn('exectest.cmd', options);

let mergedOut = '';

child.stdout.setEncoding('utf8');
child.stdout.on('data', (chunk) => {
    process.stdout.write(chunk);
    mergedOut += chunk;
});

child.stderr.setEncoding('utf8');
child.stderr.on('data', (chunk) => {
    process.stderr.write(chunk);
    mergedOut += chunk;
});

child.on('close', (code, signal) => {
    console.log('-'.repeat(30));
    console.log(mergedOut);
});

我尝试将 stderr 重定向到 stdout:

child.stderr.pipe(child.stdout);

并删除 stderr 侦听器(仅处理 stdout):

child.stderr.on

为了避免竞争条件,但 stderr 没有显示在控制台中,也没有被添加到 'mergedOut'。


这是我尝试 运行 (exectest.cmd):

的批处理文件的内容
@Echo Off
ChCp 65001 >Nul

Echo 1 (stdout) ...
Echo 2 (stderr) ... 1>&2
Echo 3 (stderr) ... 1>&2
Echo 4 (stdout) ...
Echo 5 (stdout) ...
Echo 6 (stderr) ... 1>&2

当前输出:

1 (stdout) ...
2 (stderr) ...
3 (stderr) ...
6 (stderr) ...
4 (stdout) ...
5 (stdout) ...
------------------------------
1 (stdout) ...
2 (stderr) ...
3 (stderr) ...
6 (stderr) ...
4 (stdout) ...
5 (stdout) ...

预期输出:

1 (stdout) ...
2 (stderr) ...
3 (stderr) ...
4 (stdout) ...
5 (stdout) ...
6 (stderr) ...
------------------------------
1 (stdout) ...
2 (stderr) ...
3 (stderr) ...
4 (stdout) ...
5 (stdout) ...
6 (stderr) ...

编辑 1:

有:

    stdio: [
        'inherit', // StdIn.
        'inherit', // StdOut.
        'inherit'  // StdErr.
    ],

输出顺序可靠地正确,所以 node.js 本身似乎 'know' 如何正确执行此操作。

但在这种情况下,我不知道如何捕获输出:

child.stdout

是 'null',并尝试监听 node.js 本身的进程标准输出:

process.stdout.on('data' ...)

在任何配置中给出 'Error: read ENOTCONN'。


编辑 2:

如果合并流并以这种方式收听:

const mergedStream = child.stdout.wrap(child.stderr);

mergedStream.on('data' ...);

我们有一个 stdout 和 stderr 的监听器,但顺序仍然不正确。


编辑 3:

如果您想捕获输出,可以保持正确的顺序显示它。

要在不捕获输出的情况下显示正确的输出,请参阅 'EDIT 1'。

要捕获正确的输出而不实时显示,只需使用:

child.stdout.on('data', (chunk) => {
    mergedOut += chunk;
});

child.stderr.on('data', (chunk) => {
    mergedOut += chunk;
});

但是一旦您尝试在以下内容中引用 'process.stdout / process.stderr':

child.stdout.on('data' ...)

回调,排序会中断

例如:

child.stdout.on('data', (chunk) => {
    process.stdout; // <-- This line will break everything.
                    // You do not even need to .write() to it!
});

或者如果您尝试:

child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);

为了避免在 'data' 回调中引用 'process', 排序也会中断。

很遗憾,您不能保证一定会订购。

这些是具有独立缓冲和行为的不同管道。 shell 本身有时会按预期顺序获取 STDERR/STDOUT 消息。这也是其他应用程序的典型特征,甚至在 Node.js.

之外

如果不需要区分STDOUT和STDERR,可以在源头(批处理文件内)加入。然后批处理文件 returns 所有 STDERR 和 STDOUT 仅作为 STDOUT

@Echo Off
ChCp 65001 >Nul
(
Echo 1 [stdout] ...
Echo 2 [stderr] ... 1>&2
Echo 3 [stderr] ... 1>&2
Echo 4 [stdout] ...
Echo 5 [stdout] ...
Echo 6 [stderr] ... 1>&2
) 2>&1

在不修改可执行文件的情况下,可接受的方法是(注意生成选项和命令参数):

const { spawn } = require('child_process');

const options = {
    shell: true,
    stdio: [
        'inherit', // StdIn.
        'pipe',    // StdOut.
        'pipe',    // StdErr.
    ],
};

const child = spawn('exectest.cmd', ['2>&1'], options);

let mergedOut = '';

child.stdout.setEncoding('utf8');
child.stdout.on('data', (chunk) => {
    process.stdout.write(chunk, (_err) => { });
    mergedOut += chunk;
});

child.on('close', (_code, _signal) => {
    console.log('-'.repeat(30));
    console.log(mergedOut);
});

但是,它也有一些缺点:

  1. 它生成额外的控制台进程实例。
  2. 如果您想使用 'windowsHide: true' spawn options 参数来隐藏带有 GUI 的应用程序,请避免使用 'shell: true' 参数启动进程,因为只会隐藏控制台 window,而不是目标申请 window.
  3. 您无法区分StdOut 和StdErr。根据目标应用程序的输出格式,此问题的解决方案可能如下所示:
const readline = require('readline');

// ...

const lineReader = readline.createInterface({
    input: child.stdout
});

lineReader.on('line', (line) => {
    if (line.includes('[ERROR]:')) {
        console.error(line); // StdErr.
    } else {
        console.log(line);   // StdOut.
    }
});