From 26d4cabb2e8a041f15fb1080f421c35777128c21 Mon Sep 17 00:00:00 2001 From: dotta Date: Wed, 8 Apr 2026 09:47:02 -0500 Subject: [PATCH] Persist heartbeat child pid before stdin handoff --- .../adapter-utils/src/server-utils.test.ts | 38 +++++++++++++++++++ packages/adapter-utils/src/server-utils.ts | 25 +++++++----- 2 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 packages/adapter-utils/src/server-utils.test.ts diff --git a/packages/adapter-utils/src/server-utils.test.ts b/packages/adapter-utils/src/server-utils.test.ts new file mode 100644 index 00000000..62e395b0 --- /dev/null +++ b/packages/adapter-utils/src/server-utils.test.ts @@ -0,0 +1,38 @@ +import { randomUUID } from "node:crypto"; +import { describe, expect, it } from "vitest"; +import { runChildProcess } from "./server-utils.js"; + +describe("runChildProcess", () => { + it("waits for onSpawn before sending stdin to the child", async () => { + const spawnDelayMs = 150; + const startedAt = Date.now(); + let onSpawnCompletedAt = 0; + + const result = await runChildProcess( + randomUUID(), + process.execPath, + [ + "-e", + "let data='';process.stdin.setEncoding('utf8');process.stdin.on('data',chunk=>data+=chunk);process.stdin.on('end',()=>process.stdout.write(data));", + ], + { + cwd: process.cwd(), + env: {}, + stdin: "hello from stdin", + timeoutSec: 5, + graceSec: 1, + onLog: async () => {}, + onSpawn: async () => { + await new Promise((resolve) => setTimeout(resolve, spawnDelayMs)); + onSpawnCompletedAt = Date.now(); + }, + }, + ); + const finishedAt = Date.now(); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe("hello from stdin"); + expect(onSpawnCompletedAt).toBeGreaterThanOrEqual(startedAt + spawnDelayMs); + expect(finishedAt - startedAt).toBeGreaterThanOrEqual(spawnDelayMs); + }); +}); diff --git a/packages/adapter-utils/src/server-utils.ts b/packages/adapter-utils/src/server-utils.ts index 1c6a2795..83dbe06f 100644 --- a/packages/adapter-utils/src/server-utils.ts +++ b/packages/adapter-utils/src/server-utils.ts @@ -1069,16 +1069,12 @@ export async function runChildProcess( }) as ChildProcessWithEvents; const startedAt = new Date().toISOString(); - if (opts.stdin != null && child.stdin) { - child.stdin.write(opts.stdin); - child.stdin.end(); - } - - if (typeof child.pid === "number" && child.pid > 0 && opts.onSpawn) { - void opts.onSpawn({ pid: child.pid, startedAt }).catch((err) => { - onLogError(err, runId, "failed to record child process metadata"); - }); - } + const spawnPersistPromise = + typeof child.pid === "number" && child.pid > 0 && opts.onSpawn + ? opts.onSpawn({ pid: child.pid, startedAt }).catch((err) => { + onLogError(err, runId, "failed to record child process metadata"); + }) + : Promise.resolve(); runningProcesses.set(runId, { child, graceSec: opts.graceSec }); @@ -1116,6 +1112,15 @@ export async function runChildProcess( .catch((err) => onLogError(err, runId, "failed to append stderr log chunk")); }); + const stdin = child.stdin; + if (opts.stdin != null && stdin) { + void spawnPersistPromise.finally(() => { + if (child.killed || stdin.destroyed) return; + stdin.write(opts.stdin as string); + stdin.end(); + }); + } + child.on("error", (err: Error) => { if (timeout) clearTimeout(timeout); runningProcesses.delete(runId);