Persist heartbeat child pid before stdin handoff

This commit is contained in:
dotta 2026-04-08 09:47:02 -05:00
parent 3baebee2df
commit 26d4cabb2e
2 changed files with 53 additions and 10 deletions

View file

@ -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);
});
});

View file

@ -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);