Files
rea/src/process/ExecFileBoundedOutput.ts
T
morluto 599a8ee11d fix(process): preserve cleanup ownership and normalize captures once
Retain unresolved process identity and launch evidence through retryable cleanup. Share capture path normalization and preserve failed filesystem observations. Bound child output and drain Windows pipes without one-chunk throttling.

Reviewed fixes include #1628, #2632, #2644, #2784, #2928, #2931, #2965, #2992, and #3011.

Validation: 92 focused source tests passed; four platform-specific skips. Real Windows and Hopper lifecycle coverage was not rerun.
2026-10-14 08:19:07 +08:00

74 lines
2.4 KiB
TypeScript

import { spawn } from "node:child_process";
export interface BoundedCommandOutput {
readonly exitCode: string | number | null;
readonly stdout: string;
readonly stderr: string;
readonly outputTruncated: boolean;
}
/** Drain both streams while retaining bounded head and tail evidence. */
export const execFileBoundedOutput = (
executable: string,
args: readonly string[],
options: {
readonly env: Readonly<NodeJS.ProcessEnv>;
readonly limit: number;
},
): Promise<BoundedCommandOutput> =>
new Promise((resolve) => {
const child = spawn(executable, [...args], {
env: options.env,
stdio: ["ignore", "pipe", "pipe"],
});
const headLimit = Math.ceil(options.limit / 2);
const tailLimit = options.limit - headLimit;
const stdout = { head: Buffer.alloc(0), tail: Buffer.alloc(0), total: 0 };
const stderr = { head: Buffer.alloc(0), tail: Buffer.alloc(0), total: 0 };
let outputTruncated = false;
const capture =
(stream: typeof stdout) =>
(chunk: Buffer): void => {
stream.total += chunk.length;
const headCount = Math.min(
chunk.length,
Math.max(0, headLimit - stream.head.length),
);
if (headCount > 0)
stream.head = Buffer.concat([
stream.head,
chunk.subarray(0, headCount),
]);
const rest = chunk.subarray(headCount);
if (rest.length > 0 && tailLimit > 0)
stream.tail = Buffer.concat([stream.tail, rest]).subarray(-tailLimit);
if (stream.total > options.limit) outputTruncated = true;
};
child.stdout.on("data", capture(stdout));
child.stderr.on("data", capture(stderr));
let spawnError: NodeJS.ErrnoException | undefined;
child.on("error", (error: NodeJS.ErrnoException) => {
spawnError = error;
});
child.on("close", (code, signal) => {
resolve({
exitCode: spawnError?.code ?? code ?? signal,
stdout: retainedOutput(stdout, options.limit),
stderr: retainedOutput(stderr, options.limit),
outputTruncated,
});
});
});
const retainedOutput = (
stream: {
readonly head: Buffer;
readonly tail: Buffer;
readonly total: number;
},
limit: number,
): string =>
stream.total > limit
? `${stream.head.toString("utf8")} … [output omitted] … ${stream.tail.toString("utf8")}`
: Buffer.concat([stream.head, stream.tail]).toString("utf8");