runJob
uses process
A background job's own process, run from its source (node -e, by startJob), so it uses only deps. It runs
spec.cmd, its output going to spec.log as it comes, and says how it ended in spec.exit (a JobEnd; stopped
if a SIGTERM, stopJob's, stopped it). With call, cmd is an MCP server (tcm mcp) asked that tool call: the
tool's progress goes to the log, and what it answers to text. (Run from its source, it isn't seen running by the
coverage that caches end-to-end tests: its own tests, testJobs…, run when it changes.)
import type { spawn as Spawn } from "node:child_process"; import type nodeFs from "node:fs"; export function runJob( spec: { cmd: string[]; cwd: string; log: string; exit: string; call?: { name: string; arguments: Record<string, unknown> }; }, deps: { spawn: typeof Spawn; fs: typeof nodeFs }, ): void { const { spawn, fs } = deps; const log = fs.openSync(spec.log, "a"); let stopped = false; let ended = false; const end = (code: number, text?: string) => { if (ended) return; ended = true; // (written whole, then put in place: a reader never sees half of it) const tmp = `${spec.exit}.${process.pid}`; try { fs.writeFileSync( tmp, JSON.stringify({ code, at: Date.now(), ...(stopped ? { stopped } : {}), ...(text !== undefined ? { text } : {}), }), ); fs.renameSync(tmp, spec.exit); } catch {} }; // stopped: the command is too (the signal is to the process group), and this says so once it has ended process.on("SIGTERM", () => { stopped = true; }); const child = spawn(spec.cmd[0]!, spec.cmd.slice(1), { cwd: spec.cwd, stdio: spec.call ? ["pipe", "pipe", log] : ["ignore", log, log], }); child.on("error", (e) => { fs.writeSync(log, `${e.message}\n`); end(127, spec.call ? e.message : undefined); }); // (a server that ends without answering: why is in what it printed) child.on("close", (code) => { if (!spec.call) return end(code ?? 1); let printed = ""; try { printed = fs.readFileSync(spec.log, "utf8").trim(); } catch {} end( code ?? 1, `the MCP server answering ${spec.call.name} ended (exit ${code}) without answering${printed ? `:\n${printed}` : ""}`, ); }); const call = spec.call; if (!call) return; child.stdin!.on("error", () => {}); let buf = ""; child.stdout!.setEncoding("utf8").on("data", (d: string) => { buf += d; for (let nl = buf.indexOf("\n"); nl >= 0; nl = buf.indexOf("\n")) { const line = buf.slice(0, nl); buf = buf.slice(nl + 1); let m: { id?: number; method?: string; params?: { message?: string }; result?: { content?: { text?: string }[]; isError?: boolean }; error?: { message?: string }; }; try { m = JSON.parse(line) as typeof m; } catch { continue; } if (m.method === "notifications/progress" && m.params?.message !== undefined) fs.writeSync(log, `${m.params.message}\n`); if (m.id !== 2) continue; child.stdin!.end(); end( m.result ? (m.result.isError ? 1 : 0) : 1, m.result ? (m.result.content?.[0]?.text ?? "") : (m.error?.message ?? `${call.name} failed`), ); } }); const send = (msg: unknown) => child.stdin!.write(`${JSON.stringify(msg)}\n`); send({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "tcm job", version: "0" }, }, }); send({ jsonrpc: "2.0", id: 2, method: "tools/call", params: { ...call, _meta: { progressToken: "job" } }, }); }