regression: child_process stdin pipe close event not emitted
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 122k
- Forks
- 37.3k
- Avg merge
- 4d 2h
- Merged PRs (30d)
- 283
Description
Summary
- Version: v8.12.0 and newer, all v10.x and all v11.x
- Platform: Linux and macOS amd64
- Subsystem: child_process
Since #18701 Node.js doesn't emit a close event when a child process closes its stdin pipe. The close is only detected if the parent tries to write to the child's stdin and receives an EPIPE.
The original behaviour is preferable because it allows immediate detection of the close event, rather than waiting for a failed write.
Reproducer
This works as expected in v8.11.4 and fails in all newer versions of v8.x, as well as all v10.x and all v11.x.
const {spawn} = require('child_process');
const cp = spawn(
'node', [
'-e',
'fs.closeSync(0); setTimeout(() => {}, 2000)'
],
{stdio: ['pipe', 'inherit', 'inherit']}
)
setTimeout(() => {
console.log('BUGGY! stdin close event was not emitted')
process.exit(1);
}, 1000);
cp.stdin.on('close', () => {
console.log('Ok!')
process.exit(0);
});
Problem detail
As far as I can tell the only way the close event can be emitted is if the Socket constructor calls this.read(0). This triggers onStreamRead() to be called, which does stream.push(null) and eventually results in the close event. This is a little weird because stdin isn't a readable stream :)
In Node 8.11.4 this worked because child_process.js:createSocket() called the Socket constructor with options.readable === undefined. So the Socket constructor sees that options.readable !== false and runs this.read(0)
In PR #18701 createSocket() was changed to call the Socket constructor with options.readable === true. This stops this.read(0) from being called and the close event is not emitted.
Hacky solution
--- a/lib/internal/child_process.js
+++ b/lib/internal/child_process.js
@@ -286,3 +286,3 @@ function flushStdio(subprocess) {
function createSocket(pipe, readable) {
- return net.Socket({ handle: pipe, readable, writable: !readable });
+ return net.Socket({ handle: pipe, readable, writable: !readable, childProcess: true });
}
--- a/lib/net.js
+++ b/lib/net.js
@@ -315,3 +315,5 @@ function Socket(options) {
// buffer. if not, then this will happen when we connect
- if (this._handle && options.readable !== false) {
+ if (options.childProcess) {
+ this.read(0);
+ } else if (this._handle && options.readable !== false) {
if (options.pauseOnCreate) {
This passes the full test suite with a minor change to test-pipewrap.js. I haven't raised a PR because I'm sure somebody could think of a better solution. If someone can point me in the right direction I'm happy to try and implement it.
Thanks :)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with lib/internal/child_process.js and lib/net.js, then compare the socket creation change from #18701. Run the provided child_process reproducer and inspect test-pipewrap.js, including its minor change mentioned in the issue. Done means stdin emits close promptly when the child closes the pipe and the relevant tests pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- backend, operating-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 48/100