From a6168c11ad291c1e882ebef41727b719a584eb20 Mon Sep 17 00:00:00 2001 From: Rasad Regmi Date: Wed, 16 Sep 2026 21:45:11 +0545 Subject: [PATCH] child_process: emit close after parent disconnect The 'close' event was not emitted when the parent called subprocess disconnect() before the channel reached EOF. _disconnect()'s finish() closed the channel and emitted 'disconnect' but never accounted for the channel closure with maybeClose(), so _closesGot never reached _closesNeeded and the 'close' event was lost. Account for the channel closure in finish() and defer that accounting to the EOF handler only when _disconnect() was deferred by a pending handle queue, to avoid double counting. Fixes: https://github.com/nodejs/node/issues/65646 Signed-off-by: Rasad Regmi --- lib/internal/child_process.js | 9 ++-- .../test-child-process-disconnect-close.js | 43 +++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 test/parallel/test-child-process-disconnect-close.js diff --git a/lib/internal/child_process.js b/lib/internal/child_process.js index 06daf2183db3..88096def5401 100644 --- a/lib/internal/child_process.js +++ b/lib/internal/child_process.js @@ -693,9 +693,11 @@ function setupChannel(target, channel, serializationMode) { this.buffering = false; target.disconnect(); channel.onread = nop; - channel.close(); - target.channel = null; - maybeClose(target); + if (target.channel !== null) { + channel.close(); + target.channel = null; + maybeClose(target); + } } }; @@ -986,6 +988,7 @@ function setupChannel(target, channel, serializationMode) { channel.close(); target.emit('disconnect'); + maybeClose(target); } // If a message is being read, then wait for it to complete. diff --git a/test/parallel/test-child-process-disconnect-close.js b/test/parallel/test-child-process-disconnect-close.js new file mode 100644 index 000000000000..9c968983d533 --- /dev/null +++ b/test/parallel/test-child-process-disconnect-close.js @@ -0,0 +1,43 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { spawn } = require('child_process'); + +if (process.argv[2] === 'child') { + process.on('disconnect', common.mustCall(() => { + process.exit(0); + })); + process.send('ready'); +} else { + const child = spawn(process.execPath, [__filename, 'child'], { + stdio: ['ignore', 'pipe', 'pipe', 'ipc'], + }); + + let closed = false; + + child.on('disconnect', common.mustCall(() => { + assert.strictEqual(child.connected, false); + })); + + child.on('exit', common.mustCall((code) => { + assert.strictEqual(code, 0); + })); + + // Regression test for https://github.com/nodejs/node/issues/65646: + // the 'close' event must be emitted after the parent calls disconnect() + // even though the channel was closed by the parent instead of reaching EOF. + child.on('close', common.mustCall((code, signal) => { + closed = true; + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + })); + + child.on('message', common.mustCall((msg) => { + assert.strictEqual(msg, 'ready'); + child.disconnect(); + })); + + process.on('exit', () => { + assert.strictEqual(closed, true); + }); +}