Monday, 14 September 2020

How to get stdout and stderr unbuffered or at least in order from child_process

Here's a simple node program

const log = msg => console.log(`log: ${msg}`);
const error = msg => console.error(`error: ${msg}`);

log('line1');
error('line2');
log('line3');
error('line4');

If I run it I get this output

log: line1
error: line2
log: line3
error: line4

Now I want the same output from child_process.execFile. The problem is the lines are buffered so they don't arrive to me in the same order

const child_process = require('child_process');
const lines = [];
const ch = child_process.execFile(process.argv[0], ['main.js'], () => {
  console.log(lines.join(''));
});
ch.stdout.on('data', (data) => { lines.push(data); });
ch.stderr.on('data', (data) => { lines.push(data); });

produces

log: line1
error: line2
error: line4
log: line3

As another example here's a bash script

#!/bin/sh
echo line 1
echo line 2 >&2
echo line 3
echo line 4 >&2

and running it in a terminal the output is

line 1
line 2
line 3
line 4

but running it via child_process

const child_process = require('child_process');
const lines = [];
const ch = child_process.execFile('sh', ['test.sh'], () => {
  console.log(lines.join(''));
});
ch.stdout.on('data', (data) => { lines.push(data); });
ch.stderr.on('data', (data) => { lines.push(data); });

outputs

line 1
line 3
line 2
line 4

How can I get the lines in the same order and also know which are from stdout and which are from stderr?

note, the docs says some conflicting things about buffering. On the one hand they say

By default, pipes for stdin, stdout, and stderr are established between the parent Node.js process and the spawned child. These pipes have limited (and platform-specific) capacity. If the child process writes to stdout in excess of that limit without the output being captured, the child process will block waiting for the pipe buffer to accept more data

But later they mention a setting, maxBuffer which says

maxBuffer Largest amount of data in bytes allowed on stdout or stderr. If exceeded, the child process is terminated and any output is truncated

Those 2 explanations seem in conflict. I thought maybe setting maxBuffer to 0 might be the solution but apparently it's not.

In any case though it's not clear how to unbuffer these streams.

spawn itself has an example in the docs but trying it

const child_process = require('child_process');
const lines = [];
const ch = child_process.spawn(process.argv[0], ['main.js']);

ch.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

ch.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`);
});

ch.on('close', (code) => {
  console.log(`child process exited with code ${code}`);
});

leads to the same results

stdout: log: line1

stderr: error: line2
error: line4

stdout: log: line3

child process exited with code 0

I suspect it has to do with making a Writeable stream and passing it in with spawn but it's not clear how to do that. I tried creating a Writeable and passing it in but node complained

internal/child_process.js:996
      throw new ERR_INVALID_OPT_VALUE('stdio', inspect(stdio));
      ^

TypeError [ERR_INVALID_OPT_VALUE]: The value "Writable {
  _writableState: WritableState {
    objectMode: false,
    highWaterMark: 0,
    finalCalled: false,
    needDrain: false,
    ending: false,
    ended: false,
    finished: false,
    destroyed: false,
    decodeStrings: true,
    defaultEncoding: 'utf8',
    length: 0,
    writing: false,
    corked: 0,
    sync: true,
    bufferProcessing: false,
    onwrite: [Function: bound onwrite],
    writecb: null,
    writelen: 0,
    afterWriteTickInfo: null,
    bufferedRequest: null,
    lastBufferedRequest: null,
    pendingcb: 0,
    prefinished: false,
    errorEmitted: false,
    emitClose: true,
    autoDestroy: false,
    bufferedRequestCount: 0,
    corkedRequestsFree: {
      next: null,
      entry: null,
      finish: [Function: bound onCorkedFinish]
    }
  },
  writable: true,
  _write: [Function: write],
  _events: [Object: null prototype] {},
  _eventsCount: 0,
  _maxListeners: undefined,
  [Symbol(kCapture)]: false
}" is invalid for option "stdio"

Any idea how to make a synchronous or at least correctly interleaved capture of of stdout and stderr when spawning a child process in node?


I think what I'm trying to do is apparently impossible. IIUC I need to pass in the same stream for both stdout and stderr to do keep them interleaved correctly and as soon as I do I lose the ability to tell where the output came from.



from How to get stdout and stderr unbuffered or at least in order from child_process

No comments:

Post a Comment