Files
openclaw/src/process/child-process.ts
Peter Steinberger 4f287dd740 refactor(process): adopt Execa execution layer (#105939)
* refactor(process): adopt execa execution layer

* fix(process): preserve launch error semantics

* chore(process): ratchet exec size baseline

* fix(process): preserve Windows and error contracts

* chore(plugin-sdk): ratchet wildcard surface budget

* test(process): allow resolved Windows executable paths

* fix(process): preserve Windows shim completion

* test(process): isolate Execa mocks

* style(process): format Windows exec test

* style(process): apply Windows test formatting

* test(process): normalize Windows system path casing

* fix(process): harden execa edge handling

* test(tui): preserve ESM fixture semantics

* fix(process): preserve PATHEXT lookup contract

* fix(process): keep invocation type internal
2026-07-13 02:21:08 -07:00

64 lines
1.7 KiB
TypeScript

/**
* Child-process output cleanup for commands whose descendants inherit pipes.
*/
import type { ChildProcess } from "node:child_process";
const EXIT_STDIO_GRACE_MS = 100;
const EXIT_STDIO_MAX_DRAIN_MS = 1_000;
/**
* Execa waits for stdout/stderr after the direct child exits. Bound that wait
* when detached descendants keep inherited pipes open, while still draining
* short output tails. The returned cleanup must run after awaiting the child.
*/
export function releaseChildProcessOutputAfterExit(child: ChildProcess): () => void {
let exited = false;
let idleTimer: NodeJS.Timeout | undefined;
let deadlineTimer: NodeJS.Timeout | undefined;
const clearTimers = () => {
if (idleTimer) {
clearTimeout(idleTimer);
idleTimer = undefined;
}
if (deadlineTimer) {
clearTimeout(deadlineTimer);
deadlineTimer = undefined;
}
};
const cleanup = () => {
clearTimers();
child.removeListener("exit", onExit);
child.stdout?.removeListener("data", onData);
child.stderr?.removeListener("data", onData);
};
const release = () => {
cleanup();
child.stdout?.destroy();
child.stderr?.destroy();
};
const armIdleTimer = () => {
if (idleTimer) {
clearTimeout(idleTimer);
}
idleTimer = setTimeout(release, EXIT_STDIO_GRACE_MS);
idleTimer.unref();
};
const onData = () => {
if (exited) {
armIdleTimer();
}
};
const onExit = () => {
exited = true;
armIdleTimer();
deadlineTimer = setTimeout(release, EXIT_STDIO_MAX_DRAIN_MS);
deadlineTimer.unref();
};
child.stdout?.on("data", onData);
child.stderr?.on("data", onData);
child.once("exit", onExit);
return cleanup;
}