mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(process): preserve descendant output under event-loop stalls (#111040)
* fix(process): drain buffered descendant output after exit * test(process): cover deferred output release phase
This commit is contained in:
committed by
GitHub
parent
8853217ec7
commit
9a94beace7
@@ -19,8 +19,7 @@ describe.skipIf(process.platform === "win32")("releaseChildProcessOutputAfterExi
|
||||
});
|
||||
|
||||
it("drains active descendant output after the parent exits", async () => {
|
||||
const command =
|
||||
'printf "HEAD\\n"; ( for i in 1 2 3 4 5 6; do sleep 0.05; printf "TICK$i\\n"; done ) &';
|
||||
const command = 'printf "HEAD\\n"; ( sleep 0.05; printf "TAIL\\n" ) &';
|
||||
child = execa("/bin/sh", ["-c", command], {
|
||||
buffer: false,
|
||||
detached: true,
|
||||
@@ -33,9 +32,18 @@ describe.skipIf(process.platform === "win32")("releaseChildProcessOutputAfterExi
|
||||
output += chunk.toString();
|
||||
});
|
||||
|
||||
// Simulate a contended worker after the direct child exits. The descendant
|
||||
// writes while JS is parked, so its pipe data and the idle timer are both
|
||||
// ready when the event loop resumes.
|
||||
await new Promise<void>((resolve) => {
|
||||
child?.once("exit", () => {
|
||||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
await child.finally(releaseOutput);
|
||||
expect(output).toContain("HEAD");
|
||||
expect(output).toContain("TICK6");
|
||||
expect(output).toContain("TAIL");
|
||||
});
|
||||
|
||||
it("releases a quiet inherited pipe after the idle grace", async () => {
|
||||
|
||||
@@ -14,6 +14,7 @@ const EXIT_STDIO_MAX_DRAIN_MS = 1_000;
|
||||
export function releaseChildProcessOutputAfterExit(child: ChildProcess): () => void {
|
||||
let exited = false;
|
||||
let idleTimer: NodeJS.Timeout | undefined;
|
||||
let idleReleaseImmediate: NodeJS.Immediate | undefined;
|
||||
let deadlineTimer: NodeJS.Timeout | undefined;
|
||||
|
||||
const clearTimers = () => {
|
||||
@@ -21,6 +22,10 @@ export function releaseChildProcessOutputAfterExit(child: ChildProcess): () => v
|
||||
clearTimeout(idleTimer);
|
||||
idleTimer = undefined;
|
||||
}
|
||||
if (idleReleaseImmediate) {
|
||||
clearImmediate(idleReleaseImmediate);
|
||||
idleReleaseImmediate = undefined;
|
||||
}
|
||||
if (deadlineTimer) {
|
||||
clearTimeout(deadlineTimer);
|
||||
deadlineTimer = undefined;
|
||||
@@ -41,7 +46,20 @@ export function releaseChildProcessOutputAfterExit(child: ChildProcess): () => v
|
||||
if (idleTimer) {
|
||||
clearTimeout(idleTimer);
|
||||
}
|
||||
idleTimer = setTimeout(release, EXIT_STDIO_GRACE_MS);
|
||||
if (idleReleaseImmediate) {
|
||||
clearImmediate(idleReleaseImmediate);
|
||||
idleReleaseImmediate = undefined;
|
||||
}
|
||||
idleTimer = setTimeout(() => {
|
||||
idleTimer = undefined;
|
||||
// A loaded event loop can observe the idle timer before already-buffered
|
||||
// pipe data. Give the poll phase one turn so that data can rearm the grace.
|
||||
idleReleaseImmediate = setImmediate(() => {
|
||||
idleReleaseImmediate = undefined;
|
||||
release();
|
||||
});
|
||||
idleReleaseImmediate.unref();
|
||||
}, EXIT_STDIO_GRACE_MS);
|
||||
idleTimer.unref();
|
||||
};
|
||||
const onData = () => {
|
||||
|
||||
@@ -389,6 +389,8 @@ describe("Windows command execution", () => {
|
||||
expect(execaMock).toHaveBeenCalledTimes(1);
|
||||
expect(command.stdout.destroyed).toBe(false);
|
||||
await vi.advanceTimersByTimeAsync(19);
|
||||
expect(command.stdout.destroyed).toBe(false);
|
||||
await vi.advanceTimersToNextTimerAsync();
|
||||
expect(command.stdout.destroyed).toBe(true);
|
||||
expect(command.stderr.destroyed).toBe(true);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user