76 lines
2.8 KiB
TypeScript
76 lines
2.8 KiB
TypeScript
import { spawn, spawnSync } from "node:child_process";
|
|
import { appendFileSync, closeSync, createWriteStream, existsSync, openSync, readSync, statSync } from "node:fs";
|
|
|
|
export interface CommandResult {
|
|
command: string[];
|
|
cwd: string;
|
|
exitCode: number | null;
|
|
stdout: string;
|
|
stderr: string;
|
|
signal: NodeJS.Signals | null;
|
|
timedOut: boolean;
|
|
}
|
|
|
|
export function runCommand(command: string[], cwd: string, options: { timeoutMs?: number; env?: NodeJS.ProcessEnv; teeStdoutFile?: string; teeStderrFile?: string } = {}): CommandResult {
|
|
const result = spawnSync(command[0], command.slice(1), {
|
|
cwd,
|
|
encoding: "utf8",
|
|
env: options.env,
|
|
maxBuffer: 1024 * 1024 * 8,
|
|
timeout: options.timeoutMs,
|
|
});
|
|
const error = result.error as (Error & { code?: string }) | undefined;
|
|
if (options.teeStdoutFile !== undefined && result.stdout !== undefined && result.stdout.length > 0) appendFileSync(options.teeStdoutFile, result.stdout, "utf8");
|
|
const stderr = result.stderr ?? error?.message ?? "";
|
|
if (options.teeStderrFile !== undefined && stderr.length > 0) appendFileSync(options.teeStderrFile, stderr, "utf8");
|
|
return {
|
|
command,
|
|
cwd,
|
|
exitCode: result.status,
|
|
stdout: result.stdout ?? "",
|
|
stderr,
|
|
signal: result.signal,
|
|
timedOut: error?.code === "ETIMEDOUT",
|
|
};
|
|
}
|
|
|
|
export function commandOk(command: string[], cwd: string): boolean {
|
|
return runCommand(command, cwd).exitCode === 0;
|
|
}
|
|
|
|
export async function runCommandToFiles(command: string[], cwd: string, stdoutFile: string, stderrFile: string): Promise<number | null> {
|
|
const stdout = createWriteStream(stdoutFile, { flags: "a" });
|
|
const stderr = createWriteStream(stderrFile, { flags: "a" });
|
|
stdout.write(`$ ${command.map((part) => JSON.stringify(part)).join(" ")}\n`);
|
|
const child = spawn(command[0], command.slice(1), { cwd, env: { ...process.env, UNIDESK_JOB_STDOUT_FILE: stdoutFile, UNIDESK_JOB_STDERR_FILE: stderrFile } });
|
|
child.stdout.pipe(stdout, { end: false });
|
|
child.stderr.pipe(stderr, { end: false });
|
|
const exitCode = await new Promise<number | null>((resolve) => {
|
|
child.on("close", (code) => resolve(code));
|
|
child.on("error", (error) => {
|
|
stderr.write(`${error.stack ?? error.message}\n`);
|
|
resolve(127);
|
|
});
|
|
});
|
|
stdout.write(`\n[exit_code=${exitCode}]\n`);
|
|
stdout.end();
|
|
stderr.end();
|
|
return exitCode;
|
|
}
|
|
|
|
export function tailFile(path: string, maxBytes = 8192): string {
|
|
if (!existsSync(path)) return "";
|
|
const safeMaxBytes = Math.max(0, Math.floor(maxBytes));
|
|
if (safeMaxBytes === 0) return "";
|
|
const size = statSync(path).size;
|
|
const bytesToRead = Math.min(size, safeMaxBytes);
|
|
const buffer = Buffer.alloc(bytesToRead);
|
|
const fd = openSync(path, "r");
|
|
try {
|
|
readSync(fd, buffer, 0, bytesToRead, size - bytesToRead);
|
|
} finally {
|
|
closeSync(fd);
|
|
}
|
|
return buffer.toString("utf8");
|
|
}
|