fix: 修复 control-plane status 长时无输出
This commit is contained in:
+54
-10
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user