Node: Piping `process.stdin` to `child.stdin` leaves behind an open handle

Created on 15 Mar 2020  路  14Comments  路  Source: nodejs/node

  • Version: 13.11.0 (tried on 8.3.0 too, same result)
  • Platform: Linux
  • Subsystem: Ubuntu 19.10

What steps will reproduce the bug?

foo.c

#include <stdio.h>
int main() {
  char str[100];
  gets(str);
  puts(str);
  return 0;
}

Compile it:

$ gcc -o foo foo.c

index.js:

const { spawn } = require('child_process')

const child = spawn('./foo')
process.stdin.pipe(child.stdin)
child.on('exit', () => {
  console.log('done')
  process.stdin.unpipe(child.stdin)
})

Launch index.js:

$ node index.js
input
done

After entering some input, the child process exits and done is printed. However the main process does not exit (although it should). It looks like process.stdin has a left handle. After entering newline again on stdin, the process now exits.

How often does it reproduce?

Always

What is the expected behavior?

See above.

What do you see instead?

See above.

Most helpful comment

I would like to add that destroying the stream is not always an option in cases where the function does not know whether the stream will be re-used (in which case it should not be destroyed) or not (in which case it should not leave a handle open, keeping the parent process running).

All 14 comments

You could replace process.stdin.unpipe(child.stdin) with process.stdin.destroy().

@mscdex That鈥檚 true, but it still seems to me like when we鈥檙e not reading from the only stream in use, that that should stop the event loop?

Thanks @mscdex, this does make the process exit properly.

But as @addaleax points out, this still indicates an underlying issue: the main process should exit without having to destroy process.stdin.

I think ultimately the issue here is that once we鈥檝e told a stream to start reading, we have no way of telling it to stop until it does read some data. We have a hack specifically for process.stdin.pause() in place, but that won鈥檛 work for other streams or, apparently, .unpipe().

@ronag @nodejs/streams Any ideas? Would it be possible to figure out when a stream is no longer actively being consumed and e.g. emit a event in that case that stops an in-progress _read()?

Any ideas? Would it be possible to figure out when a stream is no longer actively being consumed and e.g. emit a event in that case that stops an in-progress _read()?

We do not expose readStop() to the end users: stopping/aborting an ongoing request is something we have no agreed way to do. The closer we got is the stream.destroy() mechanism.
For legacy reasons, pipe() does not destroy streams. Therefore this is not a bug and it works as expected.

The "correct" implementation is:

const { spawn } = require('child_process')
const { pipeline } = require('stream')

const child = spawn('./foo')
// pipeline calls destroy for you
pipeline(process.stdin, child.stdin, (err) => {
  console.log('done', err)
})

@mcollina I鈥檓 sorry but I think that answer misses the point here.

We do not expose readStop() to the end users: stopping/aborting an ongoing request is something we have no agreed way to do. The closer we got is the stream.destroy() mechanism.

Yes. I am asking for a new mechanism to be introduced for this, essentially.

For legacy reasons, pipe() does not destroy streams. Therefore this is not a bug and it works as expected.

That may be helpful for the issue that OP is running into, but it assumes that you always want to destroy a stream when you are unpiping it, and that鈥檚 definitely not true. What if stdin were to be piped into another destination after the child process ended?

This problem also arises even without the use of pipes:

'use strict';
function handler(chunk) {
  console.log('got', chunk);
  process.stdin.removeListener('data', handler);
};

process.stdin.on('data', handler);

After the first chunk, there is nothing in the process that should keep the event loop alive, but the process does not stop trying to read from stdin.

I understand now.

The property of _handle is managed by net.Socket (and equivalents, e.g. tty). Are you asking for a callback that is called when pause or unpipe happen and implement it into net.Socket?

@mcollina Yes, pause or unpipe or when reading stops in another way such as removing all data/readable listeners :+1:

I think that's possible. It means adding some more intelligence to: https://github.com/nodejs/node/blob/40b559a376ae1db031132a86a76834decf6f0c2d/lib/_stream_readable.js#L876-L924.

Original OP here: the short version is that I performed one pipe with a corresponding unpipe and expected that to correctly dereference anything attached to the stream such that the event loop would be free to exit. Basically, shouldn't there be a reference count of some sort on the internal resources that gets decremented (freed) on unpipe?

I would like to add that destroying the stream is not always an option in cases where the function does not know whether the stream will be re-used (in which case it should not be destroyed) or not (in which case it should not leave a handle open, keeping the parent process running).

does not know whether the stream will be re-used

Do you maybe have a concrete example of this? Shouldn't it be up to the "root" consumer/app then to ensure that resources are released/destroyed? If resources are not properly cleaned up then it should keep running, no?

Yes you're right. What I meant was more along the following lines. In an example like:

const { spawn } = require('child_process')

const child = spawn('./foo')
process.stdin.pipe(child.stdin)
child.on('exit', () => {
  console.log('done')
  process.stdin.destroy()
})

This seems to be a problem that spawning a child process that uses the parent stdin requires destroying it to do a proper cleanup, instead of just reverting the pipe().

Hm, this is not my area of expertise but I would expect any open handles/references to be closed on 'exit' and data just to be buffered in memory on the stdin stream (i.e. it should just have buffered data without any handle reference). What keeps the process alive in this case?

Was this page helpful?
0 / 5 - 0 ratings