fix(scripts): report startup help stream errors when pipes fail (#102436)

* fix(scripts): fail startup help rendering on stream errors

* fix(scripts): preserve startup render failure provenance

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
qingminlong 2026-07-09 16:43:40 +08:00 committed by GitHub
parent ecc2cffad8
commit a5f5282816
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 86 additions and 1 deletions

View file

@ -382,14 +382,16 @@ async function spawnText(
failureMessage: string;
killGraceMs?: number;
maxOutputBytes?: number;
spawnProcess?: typeof spawn;
timeoutMs: number;
},
): Promise<string> {
const maxOutputBytes = options.maxOutputBytes ?? COMMAND_HELP_RENDER_MAX_OUTPUT_BYTES;
const killGraceMs = options.killGraceMs ?? COMMAND_HELP_RENDER_KILL_GRACE_MS;
const spawnProcess = options.spawnProcess ?? spawn;
const useProcessGroup = process.platform !== "win32";
return await new Promise((resolve, reject) => {
const child = spawn(process.execPath, args, {
const child = spawnProcess(process.execPath, args, {
cwd: options.cwd,
detached: useProcessGroup,
env: options.env,
@ -399,6 +401,7 @@ async function spawnText(
let stderr = "";
let outputBytes = 0;
let outputExceeded = false;
let outputStreamError: { streamName: "stdout" | "stderr"; error: Error } | undefined;
let settled = false;
let timedOut = false;
let waitingForKillGrace = false;
@ -483,6 +486,15 @@ async function spawnText(
};
const finishClose = (result: { code: number | null; signal: NodeJS.Signals | null }) => {
settle(() => {
if (outputStreamError) {
reject(
new Error(
`${options.failureMessage}: ${outputStreamError.streamName} read error: ${outputStreamError.error.message}`,
{ cause: outputStreamError.error },
),
);
return;
}
if (result.code === 0 && !timedOut && !outputExceeded) {
resolve(stdout);
return;
@ -522,6 +534,15 @@ async function spawnText(
signalChild("SIGTERM");
scheduleKill();
};
const failOutputStream = (streamName: "stdout" | "stderr", error: Error) => {
// Keep the first stop cause: killing for a timeout or output cap can make
// the stdio pipes fail secondarily while the child is shutting down.
if (outputStreamError || timedOut || outputExceeded) {
return;
}
outputStreamError = { streamName, error };
requestStop();
};
const timeout = setTimeout(() => {
timedOut = true;
requestStop();
@ -553,6 +574,12 @@ async function spawnText(
}
stderr += chunk;
});
child.stdout.once("error", (error: Error) => {
failOutputStream("stdout", error);
});
child.stderr.once("error", (error: Error) => {
failOutputStream("stderr", error);
});
child.once("error", (error) => {
settle(() => {
reject(error);