Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
//! Error types for the RVF launcher.
use std::fmt;
use std::io;
use std::path::PathBuf;
/// All errors that the launcher can produce.
#[derive(Debug)]
pub enum LaunchError {
/// QEMU binary not found on the system.
QemuNotFound { searched: Vec<String> },
/// KVM is required but not available.
KvmRequired,
/// The RVF file does not contain a KERNEL_SEG.
NoKernelSegment { path: PathBuf },
/// Failed to extract kernel from the RVF file.
KernelExtraction(String),
/// Failed to create a temporary file for the extracted kernel.
TempFile(io::Error),
/// QEMU process failed to start.
QemuSpawn(io::Error),
/// QEMU process exited with a non-zero code.
QemuExited { code: Option<i32>, stderr: String },
/// Timeout waiting for the VM to become ready.
Timeout { seconds: u64 },
/// QMP protocol error.
Qmp(String),
/// I/O error communicating with QMP socket.
QmpIo(io::Error),
/// Port is already in use.
PortInUse { port: u16 },
/// The VM process has already exited.
VmNotRunning,
/// Generic I/O error.
Io(io::Error),
}
impl fmt::Display for LaunchError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::QemuNotFound { searched } => {
write!(f, "QEMU not found; searched: {}", searched.join(", "))
}
Self::KvmRequired => {
write!(
f,
"KVM is required by kernel flags but /dev/kvm is not accessible"
)
}
Self::NoKernelSegment { path } => {
write!(f, "no KERNEL_SEG found in {}", path.display())
}
Self::KernelExtraction(msg) => write!(f, "kernel extraction failed: {msg}"),
Self::TempFile(e) => write!(f, "failed to create temp file: {e}"),
Self::QemuSpawn(e) => write!(f, "failed to spawn QEMU: {e}"),
Self::QemuExited { code, stderr } => {
write!(f, "QEMU exited with code {code:?}")?;
if !stderr.is_empty() {
write!(f, ": {stderr}")?;
}
Ok(())
}
Self::Timeout { seconds } => {
write!(f, "VM did not become ready within {seconds}s")
}
Self::Qmp(msg) => write!(f, "QMP error: {msg}"),
Self::QmpIo(e) => write!(f, "QMP I/O error: {e}"),
Self::PortInUse { port } => write!(f, "port {port} is already in use"),
Self::VmNotRunning => write!(f, "VM process is not running"),
Self::Io(e) => write!(f, "I/O error: {e}"),
}
}
}
impl std::error::Error for LaunchError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::TempFile(e) | Self::QemuSpawn(e) | Self::QmpIo(e) | Self::Io(e) => Some(e),
_ => None,
}
}
}
impl From<io::Error> for LaunchError {
fn from(e: io::Error) -> Self {
Self::Io(e)
}
}

View File

@@ -0,0 +1,200 @@
//! Kernel and initramfs extraction from RVF files.
//!
//! Opens the RVF store read-only, locates the KERNEL_SEG, parses the
//! KernelHeader, and writes the kernel image and optional initramfs to
//! temporary files that persist until the returned handles are dropped.
use std::io::Write;
use std::path::{Path, PathBuf};
use rvf_runtime::RvfStore;
use rvf_types::kernel::KernelHeader;
use crate::error::LaunchError;
/// Extracted kernel artifacts ready for QEMU consumption.
pub struct ExtractedKernel {
/// Path to the extracted kernel image (bzImage or equivalent).
pub kernel_path: PathBuf,
/// Path to the extracted initramfs, if present in the KERNEL_SEG.
pub initramfs_path: Option<PathBuf>,
/// The parsed KernelHeader.
pub header: KernelHeader,
/// The kernel command line string.
pub cmdline: String,
/// Temp directory holding the extracted files (kept alive via ownership).
_tempdir: tempfile::TempDir,
}
impl std::fmt::Debug for ExtractedKernel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExtractedKernel")
.field("kernel_path", &self.kernel_path)
.field("initramfs_path", &self.initramfs_path)
.field("cmdline", &self.cmdline)
.finish_non_exhaustive()
}
}
/// Extract kernel and initramfs from an RVF file to temporary files.
///
/// The returned `ExtractedKernel` owns a `TempDir`; the files are cleaned
/// up when it is dropped.
pub fn extract_kernel(rvf_path: &Path) -> Result<ExtractedKernel, LaunchError> {
let store = RvfStore::open_readonly(rvf_path)
.map_err(|e| LaunchError::KernelExtraction(format!("failed to open store: {e:?}")))?;
let (header_bytes, remainder) = store
.extract_kernel()
.map_err(|e| LaunchError::KernelExtraction(format!("segment read error: {e:?}")))?
.ok_or_else(|| LaunchError::NoKernelSegment {
path: rvf_path.to_path_buf(),
})?;
if header_bytes.len() < 128 {
return Err(LaunchError::KernelExtraction(
"KernelHeader too short".into(),
));
}
let mut hdr_array = [0u8; 128];
hdr_array.copy_from_slice(&header_bytes[..128]);
let header = KernelHeader::from_bytes(&hdr_array)
.map_err(|e| LaunchError::KernelExtraction(format!("bad KernelHeader: {e:?}")))?;
// The wire format after the 128-byte KernelHeader (which is already
// split off into `header_bytes`) is:
//
// For simple embed_kernel (no binding):
// kernel_image || cmdline
//
// For embed_kernel_with_binding:
// KernelBinding(128) || cmdline || kernel_image
//
// We determine the layout from header.image_size which tells us the
// kernel image length. The cmdline is header.cmdline_length bytes.
let image_size = header.image_size as usize;
let cmdline_length = header.cmdline_length as usize;
// Simple format: image comes first in the remainder, then cmdline
let (kernel_image, cmdline) = if image_size > 0 && image_size <= remainder.len() {
let img = &remainder[..image_size];
let cmd = if cmdline_length > 0 && image_size + cmdline_length <= remainder.len() {
String::from_utf8_lossy(&remainder[image_size..image_size + cmdline_length])
.into_owned()
} else {
String::new()
};
(img, cmd)
} else {
// Fallback: treat entire remainder as kernel image
(&remainder[..], String::new())
};
// Write to temp files
let tempdir = tempfile::tempdir().map_err(LaunchError::TempFile)?;
let kernel_file_path = tempdir.path().join("vmlinuz");
{
let mut f = std::fs::File::create(&kernel_file_path).map_err(LaunchError::TempFile)?;
f.write_all(kernel_image).map_err(LaunchError::TempFile)?;
f.sync_all().map_err(LaunchError::TempFile)?;
}
// For now we do not split out a separate initramfs from the kernel
// image. A future version could detect an appended initramfs using
// the standard Linux trailer magic (0x6d65736800000000).
let initramfs_path = None;
Ok(ExtractedKernel {
kernel_path: kernel_file_path,
initramfs_path,
header,
cmdline,
_tempdir: tempdir,
})
}
#[cfg(test)]
mod tests {
use super::*;
use rvf_runtime::options::RvfOptions;
use rvf_types::kernel::KernelArch;
#[test]
fn extract_from_store_with_kernel() {
let dir = tempfile::tempdir().unwrap();
let rvf_path = dir.path().join("test.rvf");
let opts = RvfOptions {
dimension: 4,
..Default::default()
};
let mut store = RvfStore::create(&rvf_path, opts).unwrap();
let image = b"MZ\x00fake-kernel-image-for-testing";
store
.embed_kernel(
KernelArch::X86_64 as u8,
0x01,
0,
image,
8080,
Some("console=ttyS0"),
)
.unwrap();
store.close().unwrap();
let extracted = extract_kernel(&rvf_path).unwrap();
assert!(extracted.kernel_path.exists());
let on_disk = std::fs::read(&extracted.kernel_path).unwrap();
assert_eq!(on_disk, image);
assert_eq!(extracted.header.api_port, 8080);
assert_eq!(extracted.cmdline, "console=ttyS0");
}
#[test]
fn extract_kernel_no_cmdline() {
let dir = tempfile::tempdir().unwrap();
let rvf_path = dir.path().join("no_cmd.rvf");
let opts = RvfOptions {
dimension: 4,
..Default::default()
};
let mut store = RvfStore::create(&rvf_path, opts).unwrap();
let image = b"fake-kernel";
store
.embed_kernel(KernelArch::X86_64 as u8, 0x01, 0, image, 9090, None)
.unwrap();
store.close().unwrap();
let extracted = extract_kernel(&rvf_path).unwrap();
let on_disk = std::fs::read(&extracted.kernel_path).unwrap();
assert_eq!(on_disk, image);
assert!(extracted.cmdline.is_empty());
}
#[test]
fn extract_returns_error_when_no_kernel() {
let dir = tempfile::tempdir().unwrap();
let rvf_path = dir.path().join("no_kernel.rvf");
let opts = RvfOptions {
dimension: 4,
..Default::default()
};
let store = RvfStore::create(&rvf_path, opts).unwrap();
store.close().unwrap();
let result = extract_kernel(&rvf_path);
assert!(result.is_err());
match result.unwrap_err() {
LaunchError::NoKernelSegment { .. } => {}
other => panic!("expected NoKernelSegment, got: {other}"),
}
}
}

View File

@@ -0,0 +1,717 @@
//! QEMU microVM launcher for RVF computational containers.
//!
//! This crate extracts a kernel image from an RVF file's KERNEL_SEG,
//! builds a QEMU command line, launches the VM, and provides a handle
//! for management (query, shutdown, kill) via QMP.
pub mod error;
pub mod extract;
pub mod qemu;
pub mod qmp;
use std::io::Read;
use std::net::TcpStream;
use std::path::PathBuf;
use std::process::{Child, Stdio};
use std::time::{Duration, Instant};
use rvf_types::kernel::KernelArch;
pub use error::LaunchError;
/// Configuration for launching an RVF microVM.
#[derive(Clone, Debug)]
pub struct LaunchConfig {
/// Path to the RVF store file.
pub rvf_path: PathBuf,
/// Memory allocation in MiB.
pub memory_mb: u32,
/// Number of virtual CPUs.
pub vcpus: u32,
/// Host port to forward to the VM's API port (guest :8080).
pub api_port: u16,
/// Optional host port to forward to the VM's SSH port (guest :2222).
pub ssh_port: Option<u16>,
/// Whether to enable KVM acceleration (falls back to TCG if unavailable
/// unless the kernel requires KVM).
pub enable_kvm: bool,
/// Override the QEMU binary path.
pub qemu_binary: Option<PathBuf>,
/// Extra arguments to pass to QEMU.
pub extra_args: Vec<String>,
/// Override the kernel image path (skip extraction from RVF).
pub kernel_path: Option<PathBuf>,
/// Override the initramfs path.
pub initramfs_path: Option<PathBuf>,
}
impl Default for LaunchConfig {
fn default() -> Self {
Self {
rvf_path: PathBuf::new(),
memory_mb: 128,
vcpus: 1,
api_port: 8080,
ssh_port: None,
enable_kvm: true,
qemu_binary: None,
extra_args: Vec::new(),
kernel_path: None,
initramfs_path: None,
}
}
}
/// Current status of the microVM.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VmStatus {
/// QEMU process is running.
Running,
/// QEMU process has exited.
Exited(Option<i32>),
}
/// A running QEMU microVM.
pub struct MicroVm {
process: Child,
api_port: u16,
ssh_port: Option<u16>,
qmp_socket: PathBuf,
pid: u32,
/// Holds the extracted kernel temp files alive.
_extracted: Option<extract::ExtractedKernel>,
/// Holds the work directory alive.
_workdir: tempfile::TempDir,
}
/// Result of a requirements check.
#[derive(Clone, Debug)]
pub struct RequirementsReport {
/// Whether qemu-system-x86_64 (or arch equivalent) was found.
pub qemu_found: bool,
/// Path to the QEMU binary, if found.
pub qemu_path: Option<PathBuf>,
/// Whether KVM acceleration is available.
pub kvm_available: bool,
/// Platform-specific install instructions if QEMU is missing.
pub install_hint: String,
}
impl std::fmt::Display for RequirementsReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.qemu_found {
writeln!(
f,
"QEMU: found at {}",
self.qemu_path.as_ref().unwrap().display()
)?;
} else {
writeln!(f, "QEMU: NOT FOUND")?;
writeln!(f, " Install instructions:")?;
writeln!(f, " {}", self.install_hint)?;
}
writeln!(
f,
"KVM: {}",
if self.kvm_available {
"available"
} else {
"not available (will use TCG)"
}
)
}
}
/// Description of what a launch would execute, without spawning QEMU.
#[derive(Clone, Debug)]
pub struct DryRunResult {
/// The full QEMU command line that would be executed.
pub command_line: Vec<String>,
/// Path to the kernel image that would be used.
pub kernel_path: PathBuf,
/// Path to the initramfs, if any.
pub initramfs_path: Option<PathBuf>,
/// The kernel command line that would be passed.
pub cmdline: String,
/// Whether KVM would be used.
pub use_kvm: bool,
/// Memory allocation in MiB.
pub memory_mb: u32,
/// Number of virtual CPUs.
pub vcpus: u32,
/// The API port mapping.
pub api_port: u16,
}
impl std::fmt::Display for DryRunResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Dry run - QEMU command that would be executed:")?;
writeln!(f, " {}", self.command_line.join(" "))?;
writeln!(f, "")?;
writeln!(f, " Kernel: {}", self.kernel_path.display())?;
if let Some(ref initrd) = self.initramfs_path {
writeln!(f, " Initramfs: {}", initrd.display())?;
}
writeln!(f, " Cmdline: {}", self.cmdline)?;
writeln!(
f,
" KVM: {}",
if self.use_kvm { "yes" } else { "no (TCG)" }
)?;
writeln!(f, " Memory: {} MiB", self.memory_mb)?;
writeln!(f, " vCPUs: {}", self.vcpus)?;
writeln!(f, " API port: {}", self.api_port)
}
}
/// Top-level launcher API.
pub struct Launcher;
impl Launcher {
/// Check whether all requirements for launching a microVM are met.
///
/// Returns a `RequirementsReport` with details about what was found
/// and platform-specific install instructions if QEMU is missing.
pub fn check_requirements(arch: KernelArch) -> RequirementsReport {
let qemu_result = qemu::find_qemu(arch);
let kvm = qemu::kvm_available();
let install_hint = match std::env::consts::OS {
"linux" => {
// Detect package manager
if std::path::Path::new("/usr/bin/apt").exists()
|| std::path::Path::new("/usr/bin/apt-get").exists()
{
"sudo apt install qemu-system-x86".to_string()
} else if std::path::Path::new("/usr/bin/dnf").exists() {
"sudo dnf install qemu-system-x86".to_string()
} else if std::path::Path::new("/usr/bin/pacman").exists() {
"sudo pacman -S qemu-system-x86".to_string()
} else if std::path::Path::new("/sbin/apk").exists() {
"sudo apk add qemu-system-x86_64".to_string()
} else {
"Install QEMU via your distribution's package manager \
(e.g. apt, dnf, pacman)"
.to_string()
}
}
"macos" => "brew install qemu".to_string(),
_ => "Download QEMU from https://www.qemu.org/download/".to_string(),
};
match qemu_result {
Ok(path) => RequirementsReport {
qemu_found: true,
qemu_path: Some(path),
kvm_available: kvm,
install_hint,
},
Err(_) => RequirementsReport {
qemu_found: false,
qemu_path: None,
kvm_available: kvm,
install_hint,
},
}
}
/// Extract kernel from an RVF file and launch it in a QEMU microVM.
///
/// Calls `check_requirements()` first and returns a helpful error if
/// QEMU is not found.
pub fn launch(config: &LaunchConfig) -> Result<MicroVm, LaunchError> {
if !config.rvf_path.exists() {
return Err(LaunchError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("RVF file not found: {}", config.rvf_path.display()),
)));
}
// Check requirements first (unless user provided a custom binary)
if config.qemu_binary.is_none() {
let report = Self::check_requirements(KernelArch::X86_64);
if !report.qemu_found {
return Err(LaunchError::QemuNotFound {
searched: vec![format!(
"QEMU not found. Install it with: {}",
report.install_hint,
)],
});
}
}
// Extract kernel from RVF
let extracted = extract::extract_kernel(&config.rvf_path)?;
// Create a working directory for QMP socket, logs, etc.
let workdir = tempfile::tempdir().map_err(LaunchError::TempFile)?;
// Build the QEMU command
let qemu_cmd = qemu::build_command(config, &extracted, workdir.path())?;
let qmp_socket = qemu_cmd.qmp_socket.clone();
// Spawn QEMU
let mut command = qemu_cmd.command;
command
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let child = command.spawn().map_err(LaunchError::QemuSpawn)?;
let pid = child.id();
Ok(MicroVm {
process: child,
api_port: config.api_port,
ssh_port: config.ssh_port,
qmp_socket,
pid,
_extracted: Some(extracted),
_workdir: workdir,
})
}
/// Show what WOULD be executed without actually spawning QEMU.
///
/// Useful for CI/testing and debugging launch configuration. Extracts
/// the kernel from the RVF file and builds the full command line, but
/// does not spawn any process.
pub fn dry_run(config: &LaunchConfig) -> Result<DryRunResult, LaunchError> {
if !config.rvf_path.exists() {
return Err(LaunchError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("RVF file not found: {}", config.rvf_path.display()),
)));
}
let extracted = extract::extract_kernel(&config.rvf_path)?;
let workdir = tempfile::tempdir().map_err(LaunchError::TempFile)?;
let qemu_cmd = qemu::build_command(config, &extracted, workdir.path())?;
// Reconstruct the command line as a Vec<String>
let cmd = &qemu_cmd.command;
let program = cmd.get_program().to_string_lossy().to_string();
let args: Vec<String> = cmd
.get_args()
.map(|a| a.to_string_lossy().to_string())
.collect();
let mut command_line = vec![program];
command_line.extend(args);
let kernel_path = config
.kernel_path
.clone()
.unwrap_or_else(|| extracted.kernel_path.clone());
let initramfs_path = config
.initramfs_path
.clone()
.or_else(|| extracted.initramfs_path.clone());
let use_kvm = config.enable_kvm && qemu::kvm_available();
Ok(DryRunResult {
command_line,
kernel_path,
initramfs_path,
cmdline: extracted.cmdline,
use_kvm,
memory_mb: config.memory_mb,
vcpus: config.vcpus,
api_port: config.api_port,
})
}
/// Find the QEMU binary for the given architecture.
pub fn find_qemu(arch: KernelArch) -> Result<PathBuf, LaunchError> {
qemu::find_qemu(arch)
}
/// Check if KVM is available on this host.
pub fn kvm_available() -> bool {
qemu::kvm_available()
}
}
impl MicroVm {
/// Wait for the VM's API port to accept TCP connections.
pub fn wait_ready(&mut self, timeout: Duration) -> Result<(), LaunchError> {
let start = Instant::now();
let addr = format!("127.0.0.1:{}", self.api_port);
loop {
// Check if the process has exited
if let Some(exit) = self.try_wait_process()? {
let mut stderr_buf = String::new();
if let Some(ref mut stderr) = self.process.stderr {
let _ = stderr.read_to_string(&mut stderr_buf);
}
return Err(LaunchError::QemuExited {
code: exit,
stderr: stderr_buf,
});
}
// Try connecting to the API port
if TcpStream::connect_timeout(&addr.parse().unwrap(), Duration::from_millis(200))
.is_ok()
{
return Ok(());
}
if start.elapsed() >= timeout {
return Err(LaunchError::Timeout {
seconds: timeout.as_secs(),
});
}
std::thread::sleep(Duration::from_millis(250));
}
}
/// Send a vector query to the running VM's HTTP API.
pub fn query(
&self,
vector: &[f32],
k: usize,
) -> Result<Vec<rvf_runtime::SearchResult>, LaunchError> {
let _url = format!("http://127.0.0.1:{}/query", self.api_port);
// Build JSON payload
let payload = serde_json::json!({
"vector": vector,
"k": k,
});
let body =
serde_json::to_vec(&payload).map_err(|e| LaunchError::Io(std::io::Error::other(e)))?;
// Use a raw TCP connection to send an HTTP POST (avoids depending
// on a full HTTP client library).
let addr = format!("127.0.0.1:{}", self.api_port);
let mut stream = TcpStream::connect_timeout(&addr.parse().unwrap(), Duration::from_secs(5))
.map_err(LaunchError::Io)?;
stream
.set_read_timeout(Some(Duration::from_secs(30)))
.map_err(LaunchError::Io)?;
use std::io::Write;
let request = format!(
"POST /query HTTP/1.1\r\n\
Host: 127.0.0.1:{}\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
Connection: close\r\n\
\r\n",
self.api_port,
body.len(),
);
stream
.write_all(request.as_bytes())
.map_err(LaunchError::Io)?;
stream.write_all(&body).map_err(LaunchError::Io)?;
let mut response = String::new();
stream
.read_to_string(&mut response)
.map_err(LaunchError::Io)?;
// Parse the HTTP response body (skip headers)
let body_start = response.find("\r\n\r\n").map(|i| i + 4).unwrap_or(0);
let resp_body = &response[body_start..];
#[derive(serde::Deserialize)]
struct QueryResult {
id: u64,
distance: f32,
}
let results: Vec<QueryResult> = serde_json::from_str(resp_body).map_err(|e| {
LaunchError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e))
})?;
Ok(results
.into_iter()
.map(|r| rvf_runtime::SearchResult {
id: r.id,
distance: r.distance,
retrieval_quality: rvf_types::quality::RetrievalQuality::Full,
})
.collect())
}
/// Get the current VM status.
pub fn status(&mut self) -> VmStatus {
match self.process.try_wait() {
Ok(Some(status)) => VmStatus::Exited(status.code()),
Ok(None) => VmStatus::Running,
Err(_) => VmStatus::Exited(None),
}
}
/// Graceful shutdown: try QMP `system_powerdown`, fall back to SIGTERM.
pub fn shutdown(&mut self) -> Result<(), LaunchError> {
// Try QMP first
if self.qmp_socket.exists() {
match qmp::QmpClient::connect(&self.qmp_socket, Duration::from_secs(5)) {
Ok(mut client) => {
let _ = client.system_powerdown();
// Wait up to 10 seconds for the VM to shut down
let start = Instant::now();
while start.elapsed() < Duration::from_secs(10) {
if let Ok(Some(_)) = self.process.try_wait() {
return Ok(());
}
std::thread::sleep(Duration::from_millis(200));
}
// Still running, try quit
let _ = client.quit();
let start = Instant::now();
while start.elapsed() < Duration::from_secs(5) {
if let Ok(Some(_)) = self.process.try_wait() {
return Ok(());
}
std::thread::sleep(Duration::from_millis(200));
}
}
Err(_) => {
// QMP not available, fall through to SIGTERM
}
}
}
// Fall back to SIGTERM (via kill on Unix)
#[cfg(unix)]
{
unsafe {
libc_kill(self.pid as i32);
}
let start = Instant::now();
while start.elapsed() < Duration::from_secs(5) {
if let Ok(Some(_)) = self.process.try_wait() {
return Ok(());
}
std::thread::sleep(Duration::from_millis(100));
}
}
// Last resort: kill -9
let _ = self.process.kill();
let _ = self.process.wait();
Ok(())
}
/// Force-kill the VM process immediately.
pub fn kill(&mut self) -> Result<(), LaunchError> {
self.process.kill().map_err(LaunchError::Io)?;
let _ = self.process.wait();
Ok(())
}
/// Get the QEMU process PID.
pub fn pid(&self) -> u32 {
self.pid
}
/// Get the API port.
pub fn api_port(&self) -> u16 {
self.api_port
}
/// Get the SSH port, if configured.
pub fn ssh_port(&self) -> Option<u16> {
self.ssh_port
}
/// Get the QMP socket path.
pub fn qmp_socket(&self) -> &PathBuf {
&self.qmp_socket
}
fn try_wait_process(&mut self) -> Result<Option<Option<i32>>, LaunchError> {
match self.process.try_wait() {
Ok(Some(status)) => Ok(Some(status.code())),
Ok(None) => Ok(None),
Err(e) => Err(LaunchError::Io(e)),
}
}
}
impl Drop for MicroVm {
fn drop(&mut self) {
// Best-effort cleanup: try to kill the process if still running.
if let Ok(None) = self.process.try_wait() {
let _ = self.process.kill();
let _ = self.process.wait();
}
}
}
/// Send SIGTERM on Unix. Avoids a libc dependency by using a raw syscall.
#[cfg(unix)]
unsafe fn libc_kill(pid: i32) {
// SIGTERM = 15 on all Unix platforms
// We use std::process::Command as a portable way to send signals.
let _ = std::process::Command::new("kill")
.args(["-TERM", &pid.to_string()])
.output();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config() {
let config = LaunchConfig::default();
assert_eq!(config.memory_mb, 128);
assert_eq!(config.vcpus, 1);
assert_eq!(config.api_port, 8080);
assert!(config.enable_kvm);
}
#[test]
fn vm_status_variants() {
assert_eq!(VmStatus::Running, VmStatus::Running);
assert_eq!(VmStatus::Exited(Some(0)), VmStatus::Exited(Some(0)));
assert_ne!(VmStatus::Running, VmStatus::Exited(None));
}
#[test]
fn check_requirements_returns_report() {
let report = Launcher::check_requirements(KernelArch::X86_64);
// Install hint should never be empty
assert!(!report.install_hint.is_empty());
// Display formatting should work
let display = format!("{report}");
assert!(display.contains("QEMU:"));
assert!(display.contains("KVM:"));
if report.qemu_found {
assert!(report.qemu_path.is_some());
} else {
assert!(report.qemu_path.is_none());
}
}
#[test]
fn check_requirements_has_platform_install_hint() {
let report = Launcher::check_requirements(KernelArch::X86_64);
// On Linux CI we expect an apt/dnf/pacman hint
#[cfg(target_os = "linux")]
{
assert!(
report.install_hint.contains("apt")
|| report.install_hint.contains("dnf")
|| report.install_hint.contains("pacman")
|| report.install_hint.contains("apk")
|| report.install_hint.contains("package manager"),
"expected Linux install hint, got: {}",
report.install_hint,
);
}
}
#[test]
fn launch_rejects_missing_rvf() {
let config = LaunchConfig {
rvf_path: PathBuf::from("/nonexistent/test.rvf"),
..Default::default()
};
let result = Launcher::launch(&config);
assert!(result.is_err());
}
#[test]
fn dry_run_rejects_missing_rvf() {
let config = LaunchConfig {
rvf_path: PathBuf::from("/nonexistent/test.rvf"),
..Default::default()
};
let result = Launcher::dry_run(&config);
assert!(result.is_err());
}
#[test]
fn dry_run_with_real_rvf() {
use rvf_runtime::options::RvfOptions;
use rvf_runtime::RvfStore;
let dir = tempfile::tempdir().unwrap();
let rvf_path = dir.path().join("dry_run.rvf");
let opts = RvfOptions {
dimension: 4,
..Default::default()
};
let mut store = RvfStore::create(&rvf_path, opts).unwrap();
let image = b"MZ\x00fake-kernel-for-dry-run-test";
store
.embed_kernel(
KernelArch::X86_64 as u8,
0x01,
0,
image,
8080,
Some("console=ttyS0"),
)
.unwrap();
store.close().unwrap();
let config = LaunchConfig {
rvf_path: rvf_path.clone(),
memory_mb: 256,
vcpus: 2,
api_port: 9090,
..Default::default()
};
let result = Launcher::dry_run(&config);
// dry_run may fail if QEMU binary not found - that is expected
match result {
Ok(dry) => {
assert!(!dry.command_line.is_empty());
assert!(dry.command_line[0].contains("qemu"));
assert_eq!(dry.memory_mb, 256);
assert_eq!(dry.vcpus, 2);
assert_eq!(dry.api_port, 9090);
assert_eq!(dry.cmdline, "console=ttyS0");
// Display should work
let display = format!("{dry}");
assert!(display.contains("Dry run"));
assert!(display.contains("256 MiB"));
}
Err(LaunchError::QemuNotFound { .. }) => {
// Expected in environments without QEMU
}
Err(other) => panic!("unexpected error: {other}"),
}
}
#[test]
fn requirements_report_display() {
let report = RequirementsReport {
qemu_found: true,
qemu_path: Some(PathBuf::from("/usr/bin/qemu-system-x86_64")),
kvm_available: false,
install_hint: "sudo apt install qemu-system-x86".to_string(),
};
let s = format!("{report}");
assert!(s.contains("/usr/bin/qemu-system-x86_64"));
assert!(s.contains("not available"));
let report_missing = RequirementsReport {
qemu_found: false,
qemu_path: None,
kvm_available: false,
install_hint: "brew install qemu".to_string(),
};
let s2 = format!("{report_missing}");
assert!(s2.contains("NOT FOUND"));
assert!(s2.contains("brew install qemu"));
}
}

View File

@@ -0,0 +1,241 @@
//! QEMU command-line builder.
//!
//! Constructs the `qemu-system-*` command line for launching a microVM
//! from an extracted RVF kernel.
use std::path::{Path, PathBuf};
use std::process::Command;
use rvf_types::kernel::{KernelArch, KERNEL_FLAG_REQUIRES_KVM};
use crate::error::LaunchError;
use crate::extract::ExtractedKernel;
use crate::LaunchConfig;
/// Resolved QEMU invocation ready to be spawned.
pub struct QemuCommand {
pub command: Command,
pub qmp_socket: PathBuf,
}
/// Check if KVM is available on this host.
pub fn kvm_available() -> bool {
Path::new("/dev/kvm").exists()
&& std::fs::metadata("/dev/kvm")
.map(|m| {
use std::os::unix::fs::PermissionsExt;
let mode = m.permissions().mode();
// Check if the file is readable+writable by someone
mode & 0o666 != 0
})
.unwrap_or(false)
}
/// Locate the QEMU binary for the given architecture.
pub fn find_qemu(arch: KernelArch) -> Result<PathBuf, LaunchError> {
let candidates = match arch {
KernelArch::X86_64 | KernelArch::Universal | KernelArch::Unknown => {
vec![
"qemu-system-x86_64",
"/usr/bin/qemu-system-x86_64",
"/usr/local/bin/qemu-system-x86_64",
]
}
KernelArch::Aarch64 => {
vec![
"qemu-system-aarch64",
"/usr/bin/qemu-system-aarch64",
"/usr/local/bin/qemu-system-aarch64",
]
}
KernelArch::Riscv64 => {
vec![
"qemu-system-riscv64",
"/usr/bin/qemu-system-riscv64",
"/usr/local/bin/qemu-system-riscv64",
]
}
};
for candidate in &candidates {
if let Ok(output) = std::process::Command::new("which").arg(candidate).output() {
if output.status.success() {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
return Ok(PathBuf::from(path));
}
}
// Also check if the path exists directly (absolute paths)
let p = Path::new(candidate);
if p.is_absolute() && p.exists() {
return Ok(p.to_path_buf());
}
}
Err(LaunchError::QemuNotFound {
searched: candidates.iter().map(|s| s.to_string()).collect(),
})
}
/// Build a QEMU command for the given config and extracted kernel.
pub fn build_command(
config: &LaunchConfig,
extracted: &ExtractedKernel,
work_dir: &Path,
) -> Result<QemuCommand, LaunchError> {
let arch = KernelArch::try_from(extracted.header.arch).unwrap_or(KernelArch::X86_64);
// Resolve QEMU binary
let qemu_bin = match &config.qemu_binary {
Some(p) => {
if !p.exists() {
return Err(LaunchError::QemuNotFound {
searched: vec![p.display().to_string()],
});
}
p.clone()
}
None => find_qemu(arch)?,
};
// KVM
let use_kvm = if config.enable_kvm {
if kvm_available() {
true
} else if extracted.header.kernel_flags & KERNEL_FLAG_REQUIRES_KVM != 0 {
return Err(LaunchError::KvmRequired);
} else {
false
}
} else {
false
};
let qmp_socket = work_dir.join("qmp.sock");
let mut cmd = Command::new(&qemu_bin);
// Machine type
match arch {
KernelArch::X86_64 | KernelArch::Universal | KernelArch::Unknown => {
if use_kvm {
cmd.args(["-machine", "microvm,accel=kvm"]);
cmd.args(["-cpu", "host"]);
} else {
cmd.args(["-machine", "microvm,accel=tcg"]);
cmd.args(["-cpu", "qemu64"]);
}
}
KernelArch::Aarch64 => {
if use_kvm {
cmd.args(["-machine", "virt,accel=kvm"]);
cmd.args(["-cpu", "host"]);
} else {
cmd.args(["-machine", "virt,accel=tcg"]);
cmd.args(["-cpu", "cortex-a72"]);
}
}
KernelArch::Riscv64 => {
if use_kvm {
cmd.args(["-machine", "virt,accel=kvm"]);
cmd.args(["-cpu", "host"]);
} else {
cmd.args(["-machine", "virt,accel=tcg"]);
cmd.args(["-cpu", "rv64"]);
}
}
}
// Memory and CPUs
cmd.arg("-m").arg(format!("{}M", config.memory_mb));
cmd.arg("-smp").arg(config.vcpus.to_string());
// Kernel image
let kernel_path = config
.kernel_path
.as_deref()
.unwrap_or(&extracted.kernel_path);
cmd.arg("-kernel").arg(kernel_path);
// Initramfs
let initramfs = config
.initramfs_path
.as_deref()
.or(extracted.initramfs_path.as_deref());
if let Some(initrd) = initramfs {
cmd.arg("-initrd").arg(initrd);
}
// Kernel command line
let default_cmdline = format!(
"console=ttyS0 reboot=t panic=-1 rvf.port={}",
config.api_port
);
let cmdline = if extracted.cmdline.is_empty() {
default_cmdline
} else {
format!("{} {}", extracted.cmdline, default_cmdline)
};
cmd.arg("-append").arg(&cmdline);
// RVF file as a virtio-blk device (read-only)
cmd.arg("-drive").arg(format!(
"id=rvf,file={},format=raw,if=none,readonly=on",
config.rvf_path.display()
));
cmd.args(["-device", "virtio-blk-device,drive=rvf"]);
// Network: forward API port and optional SSH port
let mut hostfwd = format!("user,id=net0,hostfwd=tcp::{}:-:8080", config.api_port);
if let Some(ssh_port) = config.ssh_port {
hostfwd.push_str(&format!(",hostfwd=tcp::{}:-:2222", ssh_port));
}
cmd.arg("-netdev").arg(&hostfwd);
cmd.args(["-device", "virtio-net-device,netdev=net0"]);
// Serial console on stdio
cmd.args(["-chardev", "stdio,id=char0"]);
cmd.args(["-serial", "chardev:char0"]);
// QMP socket for management
cmd.arg("-qmp")
.arg(format!("unix:{},server,nowait", qmp_socket.display()));
// No graphics, no reboot on panic
cmd.arg("-nographic");
cmd.arg("-no-reboot");
// Extra user-specified arguments
for arg in &config.extra_args {
cmd.arg(arg);
}
Ok(QemuCommand {
command: cmd,
qmp_socket,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kvm_detection_does_not_panic() {
// Just ensure the function runs without panicking.
let _ = kvm_available();
}
#[test]
fn find_qemu_returns_result() {
// In CI, QEMU may not be installed, so we just check it returns
// either Ok or a proper error.
let result = find_qemu(KernelArch::X86_64);
match result {
Ok(path) => assert!(path.to_str().unwrap().contains("qemu")),
Err(LaunchError::QemuNotFound { searched }) => {
assert!(!searched.is_empty());
}
Err(other) => panic!("unexpected error: {other}"),
}
}
}

View File

@@ -0,0 +1,106 @@
//! QMP (QEMU Machine Protocol) client.
//!
//! Implements just enough of the QMP JSON protocol to negotiate
//! capabilities and issue `system_powerdown` / `quit` commands for
//! graceful or forced VM shutdown.
use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;
use std::path::Path;
use std::time::Duration;
use crate::error::LaunchError;
/// A minimal QMP client connected via a Unix socket.
pub struct QmpClient {
stream: UnixStream,
}
impl QmpClient {
/// Connect to the QMP Unix socket and perform the capability
/// negotiation handshake.
pub fn connect(socket_path: &Path, timeout: Duration) -> Result<Self, LaunchError> {
let stream = UnixStream::connect(socket_path).map_err(LaunchError::QmpIo)?;
stream
.set_read_timeout(Some(timeout))
.map_err(LaunchError::QmpIo)?;
stream
.set_write_timeout(Some(timeout))
.map_err(LaunchError::QmpIo)?;
let mut client = Self { stream };
// Read the server greeting (QMP banner).
let greeting = client.read_line()?;
if !greeting.contains("\"QMP\"") {
return Err(LaunchError::Qmp(format!(
"unexpected QMP greeting: {greeting}"
)));
}
// Negotiate capabilities.
client.send_command(r#"{"execute":"qmp_capabilities"}"#)?;
let resp = client.read_line()?;
if !resp.contains("\"return\"") {
return Err(LaunchError::Qmp(format!("qmp_capabilities failed: {resp}")));
}
Ok(client)
}
/// Send `system_powerdown` for a graceful ACPI shutdown.
pub fn system_powerdown(&mut self) -> Result<(), LaunchError> {
self.send_command(r#"{"execute":"system_powerdown"}"#)?;
let resp = self.read_line()?;
if resp.contains("\"error\"") {
return Err(LaunchError::Qmp(format!("system_powerdown failed: {resp}")));
}
Ok(())
}
/// Send `quit` to force QEMU to exit immediately.
pub fn quit(&mut self) -> Result<(), LaunchError> {
self.send_command(r#"{"execute":"quit"}"#)?;
// QEMU may close the socket before we can read the response.
let _ = self.read_line();
Ok(())
}
/// Send `query-status` to check the VM's run state.
pub fn query_status(&mut self) -> Result<String, LaunchError> {
self.send_command(r#"{"execute":"query-status"}"#)?;
self.read_line()
}
fn send_command(&mut self, cmd: &str) -> Result<(), LaunchError> {
self.stream
.write_all(cmd.as_bytes())
.map_err(LaunchError::QmpIo)?;
self.stream.write_all(b"\n").map_err(LaunchError::QmpIo)?;
self.stream.flush().map_err(LaunchError::QmpIo)?;
Ok(())
}
fn read_line(&mut self) -> Result<String, LaunchError> {
let mut reader = BufReader::new(&self.stream);
let mut line = String::new();
reader.read_line(&mut line).map_err(LaunchError::QmpIo)?;
Ok(line)
}
}
#[cfg(test)]
mod tests {
// QMP tests require a running QEMU instance, so we only test
// construction logic here. Full integration tests belong in
// tests/rvf-integration.
#[test]
fn connect_to_nonexistent_socket_fails() {
use super::*;
let result = QmpClient::connect(
Path::new("/tmp/nonexistent_qmp.sock"),
Duration::from_secs(1),
);
assert!(result.is_err());
}
}