Node.js使用单独的标准输出流和错误输出流交互地生成子进程

19

考虑以下 C 语言程序(test.c):

#include <stdio.h>

int main() {
  printf("string out 1\n");
  fprintf(stderr, "string err 1\n");
  getchar();
  printf("string out 2\n");
  fprintf(stderr, "string err 2\n");
  fclose(stdout);
}

这个程序应该打印一行到标准输出(stdout),一行到标准错误(stderr),然后等待用户输入,之后再打印一行到标准输出(stdout)和一行到标准错误(stderr)。非常基础!

当在命令行编译并运行时,程序完成后的输出结果(当使用getchar()获取到用户输入时):

$ ./test 
string out 1
string err 1

string out 2
string err 2

尝试使用以下代码在Node.js中作为子进程生成此程序时:

var TEST_EXEC = './test';

var spawn = require('child_process').spawn;
var test = spawn(TEST_EXEC);

test.stdout.on('data', function (data) {
  console.log('stdout: ' + data);
});

test.stderr.on('data', function (data) {
  console.log('stderr: ' + data);
});

// Simulate entering data for getchar() after 1 second
setTimeout(function() {
  test.stdin.write('\n');
}, 1000);

输出结果如下:

$ nodejs test.js 
stderr: string err 1

stdout: string out 1
string out 2

stderr: string err 2
非常不同于在终端中运行`./test`时看到的输出。这是因为当被nodejs生成时,`./test`程序并没有在交互式shell中运行。`test.c`的标准输出流被缓冲,当在终端中运行时,一旦到达`\n`,缓冲区就会被刷新,但是在以这种方式与node一起生成时,缓冲区不会被刷新。这可以通过每次打印后刷新标准输出流,或将标准输出流更改为无缓冲的流来解决。 假设`test.c`源代码不可用或不可修改,则不能实现提到的两个刷新选项。
然后我开始考虑模拟交互式shell,有一个很好的工具叫pty.js(伪终端),例如:
var spawn = require('pty.js').spawn;
var test = spawn(TEST_EXEC);

test.on('data', function (data) {
  console.log('data: ' + data);
});

// Simulate entering data for getchar() after 1 second
setTimeout(function() {
  test.write('\n');
}, 1000);

输出结果为:

$ nodejs test.js
data: string out 1
string err 1

data: 

data: string out 2
string err 2

然而,stdout和stderr被合并在一起(就像在终端中运行程序时看到的那样),我想不到分离数据流的方法。

所以问题是...

有没有办法使用Node.js实现与运行./test相同的输出,而无需修改test.c代码?可以通过终端仿真、进程生成或任何其他方法实现吗?

Cheers!


谢谢您在问题描述中付出了相当多的努力!我在从node中运行其他人的python脚本时遇到了同样的问题,但对底层缓冲机制一无所知。显然(我对Python的技能几乎没有),只需使用'python -u'即可启用无缓冲IO并解决我的问题! - Matthias
@gratz,你解决了这个问题吗?我也遇到了类似的问题。你可以从http://stackoverflow.com/questions/42130677/how-to-get-mongo-shell-outputthree-dot-for-unterminated-command查看它。 - Joey Yi Zhao
3个回答

16

我尝试了用户568109的答案,但它并不起作用,这很有道理,因为管道只会复制流之间的数据。因此,只有在缓冲区刷新时才会处理process.stdout...下面的方法似乎有效:

var TEST_EXEC = './test';

var spawn = require('child_process').spawn;
var test = spawn(TEST_EXEC, [], { stdio: 'inherit' });

//the following is unfortunately not working 
//test.stdout.on('data', function (data) {
//  console.log('stdout: ' + data);
//});

请注意,这将有效地与节点进程共享标准 I/O 流。不确定您是否能接受这一点。


4
使用 spawn() 函数并提供 { stdio: 'inherit' } 选项确实可以使输出交错到父进程流中,但是 - 至少在 0.10.10 版本中 - 这样做会导致您无法通过事件"监听"子进程流的能力:根据 child-process 对象的 .stdin.stdout.stderr 属性的文档:"如果子进程 stdio 流与父进程共享,则不会设置此属性。" http://nodejs.org/api/child_process.html#child_process_child_stdout。换句话说,在0.10.10版本中,当访问`.stdout` 属性时,您的代码会出现错误。如果您修复了这个问题,我很乐意为您的答案点赞。 - mklement0
你说得对!这很糟糕;我不认为像 process.stdout 这样的可写流上有类似于“data”事件的东西,所以我不知道是否可以以某种方式获取该输出 :-( - Matthias
3
赞同分享使用 spawn(){ stdio: 'inherit' } 技巧(尽管它不能解决 OP 的问题)。至于监听写入:有人通过“子类化”process.stdout.write() 方法找到了一种进程本身的解决方法,但可悲的是,它无法用于子进程写入共享流的情况:https://gist.github.com/bsatrom/1349384 - mklement0

6
自从node版本5.7.0之后,'spawn'命令现在提供了一个'shell'选项。我最近重新研究了一下,但很遗憾,并没有选择生成一个交互式shell的选项(我还尝试使用'shell: '/bin/sh -i',但是不起作用)。不过,我刚刚发现这篇文章建议使用'stdbuf'来改变要运行的程序的缓冲选项。将它们全部设置为0可以产生所有流的无缓冲输出,并且它们仍然保持分离。
以下是更新后的javascript代码:
var TEST_EXEC = './test';

var spawn = require('child_process').spawn;
var test = spawn('stdbuf', ['-i0', '-o0', '-e0', TEST_EXEC]);

test.stdout.on('data', function (data) {
  console.log('stdout: ' + data);
});

test.stderr.on('data', function (data) {
  console.log('stderr: ' + data);
});

// Simulate entering data for getchar() after 1 second
setTimeout(function() {
  test.stdin.write('\n');
}, 1000);

看起来这个在OSX中没有预装,在Windows中当然也不可用,可能有类似的替代方案。


1
“stdbuf” 的技巧解决了我在一个生成的 shell 中运行命令时遇到的标准输出缓冲问题(在使用 Node v0.10.29 在 Raspbian Jessie 上)。非常感谢,祝贺! - mvanallen
很高兴这能帮到你 @mvanallen - gratz

3
你可以这样做:
var TEST_EXEC = 'test';
var spawn = require('child_process').spawn;
var test = spawn(TEST_EXEC);

test.stdin.pipe(process.stdin);
test.stdout.pipe(process.stdout);
test.stderr.pipe(process.stderr);

当您使用stdoutstderr上的事件将输出打印到console.log时,由于函数的异步执行,您将获得混乱的输出。输出将独立地针对流进行排序,但输出仍然可能在stdinstdoutstderr之间交错。


除非我漏掉了什么,否则我如何在程序中以这种方式使用输出? - gratz
你可以将'test'的标准流导入到另一个进程,比如说进程'a'(这里是node本身),其输出将完全与'test'相同,同时使用你的代码来监听'test'的输出。 - user568109
但是要在测试到达'\n'时获取stdout,进程需要成为终端或伪终端(pty),以确保测试管道会像在交互式shell中运行一样刷新。 任何一种方式都意味着失去了上面示例中的stdout和stderr之间的区别? - gratz
这将非常有用: https://github.com/joyent/node/issues/2754而这就是目标——欺骗一个应用程序,让其认为自己正在运行交互式终端,但没有合并的stdout和stderr: https://dev59.com/dXM_5IYBdhLWcg3wZSE6 - gratz
你是不是指的是process.stdin.pipe(test.stdin) - Rainb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接