mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-26 00:01:14 +00:00
* 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
64 lines
1.7 KiB
TypeScript
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;
|
|
}
|