Fire spawn listeners for a child that exits before its first poll pass

A child spawned from a listener that runs during a poll pass, such as a
'close' listener, can exit before the next pass fires its 'spawn'
listeners. The same pass's waitpid loop then settled it with 'spawn' never
emitted. Settling an exited child now fires its pending 'spawn' listeners
first, before its stream data and 'exit', as Node emits them.
This commit is contained in:
Chris Tate
2026-10-23 05:14:51 -05:00
parent 6dfef89863
commit eb2fa3c829
4 changed files with 34 additions and 3 deletions
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7640-child-spawn-before-fast-exit.ts"
],
"diags": []
}
+7 -3
View File
@@ -2780,7 +2780,8 @@ static void scr_child_settle(ScrChild *c) {
scr_children_reffed_n--;
}
if (c->state == SCR_CHILD_EXITED) {
scr_child_stream_drain_now(c->out_stream);
if (!c->spawn_done) scr_child_fire_spawn(c);
if (!scr_exc_pending()) scr_child_stream_drain_now(c->out_stream);
if (!scr_exc_pending()) scr_child_stream_drain_now(c->err_stream);
if (!scr_exc_pending()) scr_ipc_settle(c->ipc);
}
@@ -5607,9 +5608,12 @@ static void scr_child_settle(ScrChild *c) {
/* Streams first, settled after: an exit listener registered from a
* draining 'data'/'end' callback still fires below, like Node's. The
* SPAWN-FAILURE path skips the drain — Node fires 'error' first and
* the streams' immediate EOF delivers 'end' on the next turn. */
* the streams' immediate EOF delivers 'end' on the next turn. A child
* spawned during this pass can exit before the next pass fires 'spawn';
* Node emits 'spawn' before any data or 'exit', so it fires here first. */
if (c->state == SCR_CHILD_EXITED) {
scr_child_stream_drain_now(c->out_stream);
if (!c->spawn_done) scr_child_fire_spawn(c);
if (!scr_exc_pending()) scr_child_stream_drain_now(c->out_stream);
if (!scr_exc_pending()) scr_child_stream_drain_now(c->err_stream);
if (!scr_exc_pending()) scr_ipc_settle(c->ipc);
}
@@ -0,0 +1,20 @@
// A child spawned from a 'close' listener that exits before the listener
// returns still emits 'spawn' before 'exit'.
import { spawn } from "node:child_process";
function busyWait(ms: number): void {
const end = Date.now() + ms;
while (Date.now() < end) {}
}
const events: string[] = [];
spawn("sh", ["-c", "exit 0"], { stdio: "ignore" }).on("close", () => {
const child = spawn("sh", ["-c", "exit 4"], { stdio: "ignore" });
child.on("spawn", () => events.push("spawn"));
child.on("exit", (code) => events.push(`exit ${code}`));
child.on("close", (code) => {
events.push(`close ${code}`);
console.log(events.join(", "));
});
busyWait(300);
});
@@ -0,0 +1 @@
{ "plain": 2600, "san": 4000 }