fix: 修复 control-plane status 长时无输出

This commit is contained in:
Codex
2026-07-10 22:00:56 +02:00
parent 16579dbc44
commit d0c2c3f250
9 changed files with 630 additions and 81 deletions
+54 -10
View File
@@ -14,6 +14,10 @@ export interface CommandResult {
stderrBytes?: number;
stdoutTruncated?: boolean;
stderrTruncated?: boolean;
aborted?: boolean;
terminationReason?: "timeout" | "abort" | null;
processGroupId?: number | null;
processGroupAliveAfterCleanup?: boolean | null;
}
export interface CommandProgress {
@@ -25,13 +29,15 @@ export interface CommandProgress {
lastOutputAgeMs: number | null;
}
interface ObservedCommandOptions {
export interface ObservedCommandOptions {
timeoutMs?: number;
heartbeatMs?: number;
killAfterMs?: number;
env?: NodeJS.ProcessEnv;
input?: string;
maxCaptureChars?: number;
signal?: AbortSignal;
onStderr?: (text: string) => void;
onProgress?: (progress: CommandProgress) => void;
}
@@ -70,9 +76,12 @@ export async function runCommandObserved(command: string[], cwd: string, options
let stderrTruncated = false;
let lastOutputAt: number | null = null;
let timedOut = false;
let aborted = false;
let terminationReason: "timeout" | "abort" | null = null;
let timeoutTimer: NodeJS.Timeout | undefined;
let killTimer: NodeJS.Timeout | undefined;
let heartbeatTimer: NodeJS.Timeout | undefined;
let abortListener: (() => void) | undefined;
const child = spawn(command[0], command.slice(1), {
cwd,
@@ -91,6 +100,7 @@ export async function runCommandObserved(command: string[], cwd: string, options
});
child.stderr.on("data", (chunk: Buffer | string) => {
const text = chunk.toString();
options.onStderr?.(text);
stderrBytes += Buffer.byteLength(text);
lastOutputAt = Date.now();
const captured = appendBounded(stderr, text, maxCaptureChars);
@@ -113,14 +123,23 @@ export async function runCommandObserved(command: string[], cwd: string, options
}, heartbeatMs);
}
if (timeoutMs !== undefined) {
timeoutTimer = setTimeout(() => {
timedOut = true;
killProcessTree(child, "SIGTERM");
killTimer = setTimeout(() => {
killProcessTree(child, "SIGKILL");
}, Math.max(100, options.killAfterMs ?? 5_000));
}, timeoutMs);
const terminate = (reason: "timeout" | "abort") => {
if (terminationReason !== null) return;
terminationReason = reason;
timedOut = reason === "timeout";
aborted = reason === "abort";
killProcessTree(child, "SIGTERM");
killTimer = setTimeout(() => {
killProcessTree(child, "SIGKILL");
killTimer = undefined;
}, Math.max(100, options.killAfterMs ?? 5_000));
};
if (timeoutMs !== undefined) timeoutTimer = setTimeout(() => terminate("timeout"), timeoutMs);
if (options.signal !== undefined) {
abortListener = () => terminate("abort");
options.signal.addEventListener("abort", abortListener, { once: true });
if (options.signal.aborted) abortListener();
}
const completion = await new Promise<{ exitCode: number | null; signal: NodeJS.Signals | null; error?: Error }>((resolve) => {
@@ -135,8 +154,20 @@ export async function runCommandObserved(command: string[], cwd: string, options
});
if (timeoutTimer !== undefined) clearTimeout(timeoutTimer);
if (killTimer !== undefined) clearTimeout(killTimer);
if (killTimer !== undefined) {
clearTimeout(killTimer);
killTimer = undefined;
}
if (heartbeatTimer !== undefined) clearInterval(heartbeatTimer);
if (abortListener !== undefined) options.signal?.removeEventListener("abort", abortListener);
const processGroupId = process.platform === "win32" ? null : child.pid ?? null;
let processGroupAliveAfterCleanup: boolean | null = null;
if (terminationReason !== null && processGroupId !== null) {
killProcessTree(child, "SIGKILL");
await new Promise((resolve) => setTimeout(resolve, 25));
processGroupAliveAfterCleanup = processGroupExists(processGroupId);
}
if (completion.error !== undefined && stderr.length === 0) {
stderr = completion.error.message;
@@ -156,9 +187,22 @@ export async function runCommandObserved(command: string[], cwd: string, options
stderrBytes,
stdoutTruncated,
stderrTruncated,
aborted,
terminationReason,
processGroupId,
processGroupAliveAfterCleanup,
};
}
function processGroupExists(processGroupId: number): boolean {
try {
process.kill(-processGroupId, 0);
return true;
} catch {
return false;
}
}
function appendBounded(current: string, next: string, maxChars: number): { text: string; truncated: boolean } {
const combined = `${current}${next}`;
if (combined.length <= maxChars) return { text: combined, truncated: false };