runa/vscode/integration/server.ts

103 lines
3.3 KiB
TypeScript

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<void> }> {
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<string>((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<void> {
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<unknown> {
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<string> {
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;
}