@earendil-works/pi-coding-agent
Version:
Coding agent CLI with read, bash, edit, write tools and session management
106 lines • 4.06 kB
JavaScript
import { spawn as nodeSpawn, spawnSync as nodeSpawnSync, } from "node:child_process";
import crossSpawn from "cross-spawn";
const EXIT_STDIO_GRACE_MS = 100;
export function spawnProcess(command, args, options) {
return process.platform === "win32" ? crossSpawn(command, args, options) : nodeSpawn(command, args, options);
}
export function spawnProcessSync(command, args, options) {
return process.platform === "win32"
? crossSpawn.sync(command, args, options)
: nodeSpawnSync(command, args, options);
}
/**
* Wait for a child process to terminate without hanging on inherited stdio handles.
*
* A short-lived child can `exit` while a detached descendant keeps its stdout/stderr
* pipe open. We must not resolve and destroy the streams on a fixed deadline measured
* from `exit`, or output still being written past that deadline is silently lost
* (earendil-works/pi#5303). Instead, after `exit` we wait for the pipes to fall idle:
* the grace timer is re-armed on every chunk, so an actively writing descendant keeps
* us reading, while a quiet inherited handle (e.g. a Windows daemonized descendant
* that never lets `close` fire) still releases us after the grace elapses.
*/
export function waitForChildProcess(child) {
return new Promise((resolve, reject) => {
let settled = false;
let exited = false;
let exitCode = null;
let postExitTimer;
let stdoutEnded = child.stdout === null;
let stderrEnded = child.stderr === null;
const cleanup = () => {
if (postExitTimer) {
clearTimeout(postExitTimer);
postExitTimer = undefined;
}
child.removeListener("error", onError);
child.removeListener("exit", onExit);
child.removeListener("close", onClose);
child.stdout?.removeListener("end", onStdoutEnd);
child.stderr?.removeListener("end", onStderrEnd);
child.stdout?.removeListener("data", onData);
child.stderr?.removeListener("data", onData);
};
const finalize = (code) => {
if (settled)
return;
settled = true;
cleanup();
child.stdout?.destroy();
child.stderr?.destroy();
resolve(code);
};
const maybeFinalizeAfterExit = () => {
if (!exited || settled)
return;
if (stdoutEnded && stderrEnded) {
finalize(exitCode);
}
};
const armIdleTimer = () => {
if (postExitTimer)
clearTimeout(postExitTimer);
postExitTimer = setTimeout(() => finalize(exitCode), EXIT_STDIO_GRACE_MS);
};
const onData = () => {
// Output is still arriving after exit; defer finalizing so we don't
// destroy the stream mid-write and truncate the tail.
if (exited && !settled)
armIdleTimer();
};
const onStdoutEnd = () => {
stdoutEnded = true;
maybeFinalizeAfterExit();
};
const onStderrEnd = () => {
stderrEnded = true;
maybeFinalizeAfterExit();
};
const onError = (err) => {
if (settled)
return;
settled = true;
cleanup();
reject(err);
};
const onExit = (code) => {
exited = true;
exitCode = code;
maybeFinalizeAfterExit();
if (!settled) {
armIdleTimer();
}
};
const onClose = (code) => {
finalize(code);
};
child.stdout?.once("end", onStdoutEnd);
child.stderr?.once("end", onStderrEnd);
child.stdout?.on("data", onData);
child.stderr?.on("data", onData);
child.once("error", onError);
child.once("exit", onExit);
child.once("close", onClose);
});
}
//# sourceMappingURL=child-process.js.map