initial
This commit is contained in:
141
ts/classes.firecrackerprocess.ts
Normal file
141
ts/classes.firecrackerprocess.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
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<typeof plugins.smartshell.Smartshell>;
|
||||
private smartExitInstance: InstanceType<typeof plugins.smartexit.SmartExit> | 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<void> {
|
||||
// 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<boolean> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user