Description
- Version: v9.3.0
- Platform: mainly Linux, compared with Windows
- Subsystem: child_process
Disclaimer: the below is using my understanding of the docs which might
be wrong.
I'm trying to figure out the best way to create a pipeline of two
processes, and there seem to be some subtle issues that are either bugs
or maybe a result of under-documentation... I'll describe the three
variants that I tried below -- note that I'm trying to avoid the
explicit piping in JS that the "very elaborate" example in the docs is
doing, since my goal is to let the system do its thing when possible.
The main question here is whether the first code sample should work, and
if not, then what is the right way to do this.
0. Setup
In all of these examples the current directory has two files: foo.js
with the code, and a bar
file. This is just an artificial setup to
make sure that the resulting output works.
1. Simple code
The first thing I tried is what I thought should work fine:
"use strict";
const { spawn } = require("child_process");
let p1 = spawn("find", [".", "-type", "f"],
{stdio: [process.stdin, "pipe", process.stderr]});
p1.on("error", e => console.log("p1 error:", e));
p1.on("exit", (code, signal) => console.log("p1 done", code, signal));
// console.log("!!!");
let p2 = spawn("grep", ["foo"],
{stdio: [p1.stdio[1], process.stdout, process.stdout]});
p2.on("error", e => console.log("p2 error:", e));
p2.on("exit", (code, signal) => console.log("p2 done", code, signal));
On Windows (using cygwin) this works as expected:
./foo.js
p1 done 0 null
p2 done 0 null
but on Linux it sometimes works fine (as above) and sometimes it fails
inexplicably:
p1 done 0 null
p2 done 1 null
To make things more weird, uncommenting the console.log("!!!")
line
makes it work fine (on both platforms). This lead me to believe that
there might be some race condition somewhere.
2. Delayed start of the second process
Following the above guess, I tried to delay the second process to make
sure that the pipe gets generated in a consistent way:
"use strict";
const { spawn } = require("child_process");
let p1 = spawn("find", [".", "-type", "f"],
{stdio: [process.stdin, "pipe", process.stderr]});
p1.on("error", e => console.log("p1 error:", e));
p1.on("exit", (code, signal) => console.log("p1 done", code, signal));
setImmediate(() => {
let p2 = spawn("grep", ["foo"],
{stdio: [p1.stdio[1], process.stdout, process.stdout]});
p2.on("error", e => console.log("p2 error:", e));
p2.on("exit", (code, signal) => console.log("p2 done", code, signal));
});
This either fails as before (rarely), or (more commonly) fails with an error:
p1 done 0 null
internal/child_process.js:917
throw new errors.TypeError('ERR_INVALID_OPT_VALUE', 'stdio',
^
TypeError [ERR_INVALID_OPT_VALUE]: The value "Socket {
connecting: false, ... [Symbol(bytesRead)]: 15 }" is invalid for option "stdio"
at internal/child_process.js:917:13
at Array.reduce (<anonymous>)
at _validateStdio (internal/child_process.js:843:17)
at ChildProcess.spawn (internal/child_process.js:283:11)
at exports.spawn (child_process.js:499:9)
at Immediate.setImmediate (/home/eli/work/ms/x/foo.js:10:12)
at runCallback (timers.js:773:18)
at tryOnImmediate (timers.js:734:5)
at processImmediate [as _immediateCallback] (timers.js:711:5)
This might be a different error where the stream gets into a state where
it cannot be used as an input. Note that:
- Using
setTimeout()
leads to the same behavior - The failure is only on Linux -- on Windows it works fine as it did
previously
3. Starting the processes from the end
I finally tried creating the processes going from the end. This was
close, but when the p1
process ends, it doesn't close its pre-existing
stdout, so I have to do that manually:
"use strict";
const { spawn } = require("child_process");
let p2 = spawn("grep", ["foo"],
{stdio: ["pipe", process.stdout, process.stdout]});
p2.on("error", e => console.log("p2 error:", e));
p2.on("exit", (code, signal) => console.log("p2 done", code, signal));
let p1 = spawn("find", [".", "-type", "f"],
{stdio: [process.stdin, p2.stdio[0], process.stderr]});
p1.on("error", e => console.log("p1 error:", e));
p1.on("exit", (code, signal) => {
console.log("p1 done", code, signal);
p2.stdio[0].end();
});
This seems like it works fine, but I dislike closing the pipe in my own
code, since it is a step towards doing more work in node, instead of
letting the OS manage the pipe in a more natural way.
Activity