mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
599a8ee11d
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.
74 lines
2.4 KiB
TypeScript
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");
|