Spaces:
Paused
Paused
File size: 3,095 Bytes
ff34739 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | import {
execFile,
spawn,
type ChildProcess,
type ProcessEnvOptions,
} from "node:child_process";
import { promisify } from "node:util";
import { closeSync, openSync } from "node:fs";
const execFileP = promisify(execFile);
export interface ProbeResult {
ok: boolean;
stdout: string;
stderr: string;
code: number | null;
}
/**
* Fire a short read-only command (e.g. `claude --version`) and capture output.
* NEVER runs through a shell — cmd + args only, so user input can't be injected.
*/
export async function probe(
cmd: string,
args: string[],
opts: { cwd?: string; timeoutMs?: number } = {},
): Promise<ProbeResult> {
try {
const { stdout, stderr } = await execFileP(cmd, args, {
cwd: opts.cwd,
timeout: opts.timeoutMs ?? 6000,
maxBuffer: 4 * 1024 * 1024,
});
return { ok: true, stdout, stderr, code: 0 };
} catch (e) {
const err = e as { stdout?: string; stderr?: string; code?: number; message?: string };
return {
ok: false,
stdout: err.stdout ?? "",
stderr: err.stderr ?? err.message ?? String(e),
code: typeof err.code === "number" ? err.code : null,
};
}
}
export interface SpawnedStep {
pid: number;
child: ChildProcess;
/** resolves with the exit code once the process ends (or -1 on spawn error) */
done: Promise<number>;
}
/**
* Spawn a long-running step DETACHED (its own process group) with stdout+stderr
* appended to a log file. Detached so cancel can kill the whole group — the
* python step spawns Rust/claude children that a bare SIGTERM to the pid misses.
*/
export function spawnLogged(opts: {
cmd: string;
args: string[];
cwd: string;
logFile: string;
env?: NodeJS.ProcessEnv;
}): SpawnedStep {
const fd = openSync(opts.logFile, "a");
const spawnOpts: ProcessEnvOptions & {
detached: boolean;
stdio: ["ignore", number, number];
} = {
cwd: opts.cwd,
detached: true,
stdio: ["ignore", fd, fd],
env: { ...process.env, ...opts.env },
};
const child = spawn(opts.cmd, opts.args, spawnOpts);
const done = new Promise<number>((resolve) => {
let settled = false;
const finish = (code: number) => {
if (settled) return;
settled = true;
try {
closeSync(fd);
} catch {
/* already closed */
}
resolve(code);
};
child.on("exit", (code) => finish(code ?? -1));
child.on("error", () => finish(-1));
});
return { pid: child.pid ?? -1, child, done };
}
/** Is a pid still alive? Used to reconcile run state after a dev-server reload. */
export function isAlive(pid: number): boolean {
if (!pid || pid <= 0) return false;
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
/** Kill an entire detached process group (pid was the group leader). */
export function killGroup(pid: number, signal: NodeJS.Signals = "SIGTERM"): void {
if (!pid || pid <= 0) return;
try {
process.kill(-pid, signal);
} catch {
try {
process.kill(pid, signal);
} catch {
/* already gone */
}
}
}
|