Skip to content

spawn with pipes #18016

Closed
Closed
@elibarzilay

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:

  1. Using setTimeout() leads to the same behavior
  2. 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    child_processIssues and PRs related to the child_process subsystem.streamIssues and PRs related to the stream subsystem.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions