fix: add visible scripts typecheck progress
This commit is contained in:
+148
-1
@@ -1,4 +1,4 @@
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { closeSync, createWriteStream, existsSync, openSync, readSync, statSync } from "node:fs";
|
||||
|
||||
export interface CommandResult {
|
||||
@@ -9,6 +9,30 @@ export interface CommandResult {
|
||||
stderr: string;
|
||||
signal: NodeJS.Signals | null;
|
||||
timedOut: boolean;
|
||||
durationMs?: number;
|
||||
stdoutBytes?: number;
|
||||
stderrBytes?: number;
|
||||
stdoutTruncated?: boolean;
|
||||
stderrTruncated?: boolean;
|
||||
}
|
||||
|
||||
export interface CommandProgress {
|
||||
elapsedMs: number;
|
||||
timeoutMs: number | null;
|
||||
pid: number | null;
|
||||
stdoutBytes: number;
|
||||
stderrBytes: number;
|
||||
lastOutputAgeMs: number | null;
|
||||
}
|
||||
|
||||
interface ObservedCommandOptions {
|
||||
timeoutMs?: number;
|
||||
heartbeatMs?: number;
|
||||
killAfterMs?: number;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
input?: string;
|
||||
maxCaptureChars?: number;
|
||||
onProgress?: (progress: CommandProgress) => void;
|
||||
}
|
||||
|
||||
export function runCommand(command: string[], cwd: string, options: { timeoutMs?: number; env?: NodeJS.ProcessEnv; input?: string } = {}): CommandResult {
|
||||
@@ -33,6 +57,129 @@ export function runCommand(command: string[], cwd: string, options: { timeoutMs?
|
||||
};
|
||||
}
|
||||
|
||||
export async function runCommandObserved(command: string[], cwd: string, options: ObservedCommandOptions = {}): Promise<CommandResult> {
|
||||
const startedAt = Date.now();
|
||||
const timeoutMs = options.timeoutMs;
|
||||
const heartbeatMs = Math.max(0, options.heartbeatMs ?? 0);
|
||||
const maxCaptureChars = Math.max(1, options.maxCaptureChars ?? 1024 * 1024);
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let stdoutBytes = 0;
|
||||
let stderrBytes = 0;
|
||||
let stdoutTruncated = false;
|
||||
let stderrTruncated = false;
|
||||
let lastOutputAt: number | null = null;
|
||||
let timedOut = false;
|
||||
let timeoutTimer: NodeJS.Timeout | undefined;
|
||||
let killTimer: NodeJS.Timeout | undefined;
|
||||
let heartbeatTimer: NodeJS.Timeout | undefined;
|
||||
|
||||
const child = spawn(command[0], command.slice(1), {
|
||||
cwd,
|
||||
env: options.env,
|
||||
detached: process.platform !== "win32",
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
child.stdout.on("data", (chunk: Buffer | string) => {
|
||||
const text = chunk.toString();
|
||||
stdoutBytes += Buffer.byteLength(text);
|
||||
lastOutputAt = Date.now();
|
||||
const captured = appendBounded(stdout, text, maxCaptureChars);
|
||||
stdout = captured.text;
|
||||
stdoutTruncated = stdoutTruncated || captured.truncated;
|
||||
});
|
||||
child.stderr.on("data", (chunk: Buffer | string) => {
|
||||
const text = chunk.toString();
|
||||
stderrBytes += Buffer.byteLength(text);
|
||||
lastOutputAt = Date.now();
|
||||
const captured = appendBounded(stderr, text, maxCaptureChars);
|
||||
stderr = captured.text;
|
||||
stderrTruncated = stderrTruncated || captured.truncated;
|
||||
});
|
||||
|
||||
child.stdin.end(options.input ?? "");
|
||||
|
||||
if (heartbeatMs > 0) {
|
||||
heartbeatTimer = setInterval(() => {
|
||||
options.onProgress?.({
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
timeoutMs: timeoutMs ?? null,
|
||||
pid: child.pid ?? null,
|
||||
stdoutBytes,
|
||||
stderrBytes,
|
||||
lastOutputAgeMs: lastOutputAt === null ? null : Date.now() - lastOutputAt,
|
||||
});
|
||||
}, 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 completion = await new Promise<{ exitCode: number | null; signal: NodeJS.Signals | null; error?: Error }>((resolve) => {
|
||||
let resolved = false;
|
||||
const finish = (result: { exitCode: number | null; signal: NodeJS.Signals | null; error?: Error }) => {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
resolve(result);
|
||||
};
|
||||
child.on("error", (error) => finish({ exitCode: 127, signal: null, error }));
|
||||
child.on("close", (code, signal) => finish({ exitCode: code, signal }));
|
||||
});
|
||||
|
||||
if (timeoutTimer !== undefined) clearTimeout(timeoutTimer);
|
||||
if (killTimer !== undefined) clearTimeout(killTimer);
|
||||
if (heartbeatTimer !== undefined) clearInterval(heartbeatTimer);
|
||||
|
||||
if (completion.error !== undefined && stderr.length === 0) {
|
||||
stderr = completion.error.message;
|
||||
stderrBytes = Buffer.byteLength(stderr);
|
||||
}
|
||||
|
||||
return {
|
||||
command,
|
||||
cwd,
|
||||
exitCode: completion.exitCode,
|
||||
stdout,
|
||||
stderr,
|
||||
signal: completion.signal,
|
||||
timedOut,
|
||||
durationMs: Date.now() - startedAt,
|
||||
stdoutBytes,
|
||||
stderrBytes,
|
||||
stdoutTruncated,
|
||||
stderrTruncated,
|
||||
};
|
||||
}
|
||||
|
||||
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 };
|
||||
return { text: combined.slice(-maxChars), truncated: true };
|
||||
}
|
||||
|
||||
function killProcessTree(child: ChildProcessWithoutNullStreams, signal: NodeJS.Signals): boolean {
|
||||
if (child.pid === undefined) return false;
|
||||
try {
|
||||
if (process.platform === "win32") return child.kill(signal);
|
||||
process.kill(-child.pid, signal);
|
||||
return true;
|
||||
} catch {
|
||||
try {
|
||||
return child.kill(signal);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function commandOk(command: string[], cwd: string): boolean {
|
||||
return runCommand(command, cwd).exitCode === 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user