/** * Shared Firecracker VM lifecycle helpers. * Used by vm.ts, snapshot.ts, and agent-manager.ts. */ import { spawn, type ChildProcess } from "node:child_process"; import { existsSync, unlinkSync, mkdirSync } from "node:fs"; import { CONFIG } from "./config.js"; import * as api from "./firecracker-api.js"; import { ensureBridge, ensureNat, createTap, deleteTap, macFromOctet, } from "./network.js"; export interface BootOptions { socketPath: string; kernelPath?: string; rootfsPath: string; extraDrives?: { id: string; path: string; readOnly?: boolean }[]; tapDevice: string; ip: string; octet: number; vcpu?: number; mem?: number; } /** * Wait for a Firecracker API socket to appear. */ export function waitForSocket( socketPath: string, timeoutMs = 5_000 ): Promise { return new Promise((resolve, reject) => { const deadline = Date.now() + timeoutMs; const check = () => { if (existsSync(socketPath)) { setTimeout(resolve, 200); return; } if (Date.now() > deadline) { reject(new Error("Firecracker socket did not appear")); return; } setTimeout(check, 50); }; check(); }); } /** * Set up network for a VM: ensure bridge, NAT, and create tap device. * Cleans stale tap first. */ export function setupNetwork(tapDevice: string) { ensureBridge(); ensureNat(); deleteTap(tapDevice); createTap(tapDevice); } /** * Spawn a Firecracker process and wait for the API socket. */ export async function spawnFirecracker( socketPath: string, opts?: { detached?: boolean } ): Promise { // Clean stale socket try { unlinkSync(socketPath); } catch {} mkdirSync(CONFIG.socketDir, { recursive: true }); const proc = spawn( CONFIG.firecrackerBin, ["--api-sock", socketPath], { stdio: "pipe", detached: opts?.detached ?? false, } ); if (opts?.detached) proc.unref(); await waitForSocket(socketPath); return proc; } /** * Configure and start a Firecracker VM via its API. */ export async function bootVM(opts: BootOptions) { const kernel = opts.kernelPath ?? CONFIG.kernelPath; const vcpu = opts.vcpu ?? CONFIG.vm.vcpuCount; const mem = opts.mem ?? CONFIG.vm.memSizeMib; const bootArgs = [ "console=ttyS0", "reboot=k", "panic=1", "pci=off", "root=/dev/vda", "rw", `ip=${opts.ip}::${CONFIG.bridge.gateway}:${CONFIG.bridge.netmask}::eth0:off`, ].join(" "); await api.putBootSource(opts.socketPath, kernel, bootArgs); await api.putDrive(opts.socketPath, "rootfs", opts.rootfsPath); if (opts.extraDrives) { for (const drive of opts.extraDrives) { await api.putDrive( opts.socketPath, drive.id, drive.path, drive.readOnly ?? false, false ); } } await api.putNetworkInterface( opts.socketPath, "eth0", opts.tapDevice, macFromOctet(opts.octet) ); await api.putMachineConfig(opts.socketPath, vcpu, mem); await api.startInstance(opts.socketPath); } /** * Kill a Firecracker process and clean up its socket. */ export async function killFirecracker( proc: ChildProcess | null, socketPath: string, signal: NodeJS.Signals = "SIGTERM" ) { if (proc && !proc.killed) { proc.kill(signal); await new Promise((resolve) => { const timer = setTimeout(() => { if (proc && !proc.killed) { proc.kill("SIGKILL"); } resolve(); }, 2_000); proc.on("exit", () => { clearTimeout(timer); resolve(); }); }); } try { unlinkSync(socketPath); } catch {} }