Fire spawn listeners for a child that exits before its first poll pass (#1986)

This commit is contained in:
Chris Tate
2026-10-23 05:23:16 -05:00
committed by GitHub
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 }