NodeJS child_process stdout, if process is waiting for stdin

Viewed 724

I'm working on an application, which allows to compile and execute code given over an api.

The binary I want to execute is saved as input_c and should print a text, asking the user for his name and print out another text after the input is received.

It correctly works using the code below: first text - input (on terminal) - second text.

const {spawn} = require('child_process');
let cmd = spawn('input_c', [], {stdio: [process.stdin, process.stdout, process.stderr]});

Output:

$ node test.js
Hello, what is your name? Heinz
Hi Heinz, nice to meet you!

I like to handle stdout, stderr and stdin seperately and not write it to the terminal. The following code was my attempt to achieve the same behaviour as above:

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

let cmd = spawn('input_c');

cmd.stdout.on('data', data => {
    console.log(data.toString());
});

cmd.stderr.on('data', data => {
    console.log(data.toString());
});

cmd.on('error', data => {
    console.log(data.toString());
});

// simulating user input
setTimeout(function() {
    console.log('Heinz');
    cmd.stdin.write('Heinz\n');
}, 3000);

Output:

$ node test.js
Heinz
Hello, what is your name? Hi Heinz, nice to meet you!

To simulate user input I'm writing to stdin after 3000ms. But here I'm not receiving the first data in stdout directly on run, it seems to wait for stdin and outputs everything at once.

How can I achieve the same behaviour for my second case?

The following C-Code was used to compile the binary, but any application waiting for user input can be used for this:

#include <stdio.h>

int main() {
    char name[32];
    printf("Hello, what is your name? ");
    scanf("%s", name);
    printf("Hi %s, nice to meet you!", name);
    return 0;
}
2 Answers

node-pty can be used here to prevent buffered output of child process.

const pty = require('node-pty');

let cmd = pty.spawn('./input_c');

cmd.on('data', data => {
    console.log(data.toString());
});

// simulating user input
setTimeout(function() {
    console.log('Heinz');
    cmd.write('Heinz\n');
}, 3000);

output:

Hello, what is your name?
Heinz
Heinz
Hi Heinz, nice to meet you!

The problem you're facing with stdout.on() events not being triggered after spawn() appears because of how node.js spawns the child process. Essentially, it creates stream.pipe() (by default), which allows child process to buffer the output before sending it to stdout it was given by node, which is, in general, good for performance.

But, since you want real-time output and also you're in charge of the binary, you might simply disable internal buffering. In C you can achieve that by adding setbuf(stdout, NULL); to the beginning of your program:

#include <stdio.h>

int main() {
    setbuf(stdout, NULL);
    char name[32];
    printf("Hello, what is your name? ");
    scanf("%31s", name);
    printf("Hi %s, nice to meet you!", name);
    return 0;
}

Alternatively, you can call fflush(stdout); after each printf(), puts(), etc:

#include <stdio.h>

int main() {
    char name[32];
    printf("Hello, what is your name? "); fflush(stdout);
    scanf("%31s", name);
    printf("Hi %s, nice to meet you!", name); fflush(stdout);
    return 0;
}

Upon disabling internal buffering or triggering explicit flushes in the child process, you will immediately get the behavior you expect, without any external dependencies.

UPD:

Many applications intentionally suppress or, at least, allow suppressing stdio buffering, so you may find related startup arguments. For example, you can launch python interpreter binary with -u option, which will force stdin, stdout and stderr to be totally unbuffered. There are also several older questions related nodejs and stdio buffering problems, you might find useful, like this one: How can I flush a child process from nodejs

Related