import * as plugins from './plugins.js'; import type { IFirecrackerProcessOptions } from './interfaces/index.js'; import { SmartVMError } from './interfaces/index.js'; import { SocketClient } from './classes.socketclient.js'; /** * Manages a single Firecracker child process, including startup, readiness polling, and shutdown. */ export class FirecrackerProcess { private options: IFirecrackerProcessOptions; private streaming: any | null = null; private shell: InstanceType; private smartExitInstance: InstanceType | null = null; public socketClient: SocketClient; constructor(options: IFirecrackerProcessOptions) { this.options = options; this.shell = new plugins.smartshell.Smartshell({ executor: 'bash' }); this.socketClient = new SocketClient({ socketPath: options.socketPath }); } /** * Start the Firecracker process and wait for the API socket to become ready. */ public async start(): Promise { // Remove any stale socket file if (plugins.fs.existsSync(this.options.socketPath)) { plugins.fs.unlinkSync(this.options.socketPath); } // Build the command let cmd = `${this.options.binaryPath} --api-sock ${this.options.socketPath}`; if (this.options.logLevel) { cmd += ` --level ${this.options.logLevel}`; } // Spawn the process this.streaming = await this.shell.execStreaming(cmd, true); // Register with smartexit for automatic cleanup if (this.streaming?.childProcess) { this.smartExitInstance = new plugins.smartexit.SmartExit({ silent: true }); this.smartExitInstance.addProcess(this.streaming.childProcess); } // Wait for the socket file to appear const socketReady = await this.waitForSocket(10000); if (!socketReady) { await this.stop(); throw new SmartVMError( 'Firecracker socket did not become ready within timeout', 'SOCKET_TIMEOUT', ); } // Wait for the API to be responsive const apiReady = await this.socketClient.isReady(5000); if (!apiReady) { await this.stop(); throw new SmartVMError( 'Firecracker API did not become responsive within timeout', 'API_TIMEOUT', ); } } /** * Poll for the socket file to appear on disk. */ private async waitForSocket(timeoutMs: number): Promise { const start = Date.now(); while (Date.now() - start < timeoutMs) { if (plugins.fs.existsSync(this.options.socketPath)) { return true; } await plugins.smartdelay.delayFor(100); } return false; } /** * Gracefully stop the Firecracker process with SIGTERM, then SIGKILL after timeout. */ public async stop(): Promise { if (!this.streaming) return; try { // Try graceful termination first await this.streaming.terminate(); // Wait up to 5 seconds for the process to exit const exitPromise = Promise.race([ this.streaming.finalPromise, plugins.smartdelay.delayFor(5000), ]); await exitPromise; } catch { // If termination fails, force kill try { await this.streaming.kill(); } catch { // Process may already be dead } } this.streaming = null; } /** * Clean up the socket file. */ public async cleanup(): Promise { await this.stop(); // Remove the socket file if (plugins.fs.existsSync(this.options.socketPath)) { plugins.fs.unlinkSync(this.options.socketPath); } } /** * Check if the process is currently running. */ public isRunning(): boolean { if (!this.streaming?.childProcess) return false; try { // Sending signal 0 tests if process exists without actually sending a signal process.kill(this.streaming.childProcess.pid, 0); return true; } catch { return false; } } /** * Get the child process PID. */ public getPid(): number | null { return this.streaming?.childProcess?.pid ?? null; } }