import { ChildProcess, spawn } from "node:child_process"; import { once } from "node:events"; import { createConnection } from "node:net"; import { resolve } from "node:path"; import { createInterface } from "node:readline"; import { json } from "../test/fixtures"; export async function startImage(): Promise<{ address: string; stop: () => Promise }> { const executable = resolve( __dirname, "../../../target/debug", process.platform === "win32" ? "runa-server.exe" : "runa-server", ); const child = spawn(executable, ["--listen", "127.0.0.1:0"], { stdio: ["ignore", "ignore", "pipe"], }); const stop = () => stopImage(child); let address: string; try { address = await new Promise((resolve, reject) => { const lines = createInterface({ input: child.stderr }); let diagnostics = ""; const cleanup = () => { clearTimeout(timer); child.off("error", fail); child.off("exit", exited); lines.close(); child.stderr.resume(); }; const fail = (error: Error) => { cleanup(); reject(error); }; const exited = () => fail(new Error(`Image exited during startup: ${diagnostics}`)); const timer = setTimeout( () => fail(new Error(`Image startup timed out: ${diagnostics}`)), 10000, ); child.once("error", fail); child.once("exit", exited); lines.on("line", (line) => { diagnostics = (diagnostics + line + "\n").slice(-8192); const address = line.match(/^runa-server: listening on (.+)$/)?.[1]; if (address) { cleanup(); resolve(address); } }); }); } catch (error) { await stop(); throw error; } return { address, stop }; } async function stopImage(child: ChildProcess): Promise { if (child.exitCode !== null || child.signalCode !== null || child.pid === undefined) return; const exited = once(child, "exit"); child.kill(); await exited; } export async function control(address: string, command: unknown): Promise { const port = Number(address.split(":")[1]); const socket = createConnection({ host: "127.0.0.1", port }); socket.setTimeout(5000, () => socket.destroy(new Error("Control request timed out"))); const lines = createInterface({ input: socket }); try { await once(socket, "connect"); const response = Promise.race([ once(lines, "line").then((args) => { const line: unknown = args[0]; if (typeof line !== "string") throw new Error("Missing control response"); const decoded: unknown = JSON.parse(line); return decoded; }), once(socket, "close").then(() => { throw new Error("Control connection closed"); }), ]); socket.write(json({ version: 3, request_id: "integration-control", command }) + "\n"); return await response; } finally { lines.close(); socket.destroy(); } } export async function spawnCounter(address: string): Promise { const reply = await control(address, { operation: "spawn", parameters: { behavior_name: "counter", state: { type: "integer", data: 9223372036854775807n }, mailbox_capacity: 4, }, }); const ref = json(reply).match(/"actor_ref":"([A-Za-z0-9_-]{21})"/)?.[1]; if (!ref) throw new Error(`Spawn failed: ${json(reply)}`); return ref; }