use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;
use bon::Builder;
use bytes::Bytes;
use conflaguration::{Settings, Validate, ValidationMessage};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use proxima_primitives::pipe::SendPipe;
use proxima_protocols::process::{ChildResponse, ReadResponse};
use proxima_vm::dispatch;
use proxima_vm::elf;
use crate::error::ProximaError;
use crate::pipe::{PipeHandle, into_handle};
use crate::pipe_factory::PipeFactory;
use crate::request::{Request, Response};
const MAX_SEGMENTS: usize = 4;
const MMIO_EMITTED_CAPACITY: usize = 256;
const NET_EMITTED_CAPACITY: usize = 256;
const BLK_EMITTED_CAPACITY: usize = 2048;
const PL011_EMITTED_CAPACITY: usize = 256;
fn default_name() -> String {
"vm".to_string()
}
fn default_max_hypercalls() -> usize {
16
}
fn default_emitted_capacity() -> usize {
256
}
fn default_response_eof() -> bool {
true
}
#[derive(Debug, Clone)]
pub struct VmSpec {
pub guest_image_path: PathBuf,
pub max_hypercalls: usize,
pub emitted_capacity: usize,
pub response_bytes: Vec<u8>,
pub response_eof: bool,
}
#[derive(Debug, Clone, PartialEq, Builder, Deserialize, Serialize, Settings)]
#[settings(prefix = "PROXIMA_VM")]
#[builder(derive(Clone, Debug), on(String, into))]
pub struct VmConfig {
pub guest_image_path: String,
#[setting(default = "vm")]
#[serde(default = "default_name")]
#[builder(default = default_name())]
pub name: String,
#[setting(default = 16)]
#[serde(default = "default_max_hypercalls")]
#[builder(default = default_max_hypercalls())]
pub max_hypercalls: usize,
#[setting(default = 256)]
#[serde(default = "default_emitted_capacity")]
#[builder(default = default_emitted_capacity())]
pub emitted_capacity: usize,
#[setting(default)]
#[serde(default)]
#[builder(default)]
pub response: String,
#[setting(default = true)]
#[serde(default = "default_response_eof")]
#[builder(default = default_response_eof())]
pub response_eof: bool,
}
impl Validate for VmConfig {
fn validate(&self) -> conflaguration::Result<()> {
let mut errors = Vec::new();
if self.guest_image_path.is_empty() {
errors.push(ValidationMessage::new(
"guest_image_path",
"must not be empty",
));
}
if self.max_hypercalls == 0 {
errors.push(ValidationMessage::new(
"max_hypercalls",
"must be greater than zero",
));
}
if errors.is_empty() {
Ok(())
} else {
Err(conflaguration::Error::Validation { errors })
}
}
}
impl VmConfig {
pub fn into_spec(self) -> Result<VmSpec, ProximaError> {
self.validate()
.map_err(|err| ProximaError::Config(format!("{err}")))?;
Ok(VmSpec {
guest_image_path: PathBuf::from(self.guest_image_path),
max_hypercalls: self.max_hypercalls,
emitted_capacity: self.emitted_capacity,
response_bytes: self.response.into_bytes(),
response_eof: self.response_eof,
})
}
}
pub struct VmUpstream {
label: String,
spec: VmSpec,
}
impl VmUpstream {
#[must_use]
pub fn new(label: impl Into<String>, spec: VmSpec) -> Self {
Self {
label: label.into(),
spec,
}
}
#[must_use]
pub fn spec(&self) -> &VmSpec {
&self.spec
}
}
fn boot_and_run(image: &[u8], spec: &VmSpec) -> Result<Bytes, ProximaError> {
let (entry, segments) = elf::parse_elf::<MAX_SEGMENTS>(image)
.map_err(|error| ProximaError::Upstream(format!("parse guest ELF: {error}")))?;
let configured = ChildResponse::Read(ReadResponse {
bytes: spec.response_bytes.clone(),
eof: spec.response_eof,
});
let (_requests, emitted, _mmio_emitted, _net_emitted, _blk_emitted, _pl011_emitted, _, _, _) =
dispatch::run_dispatch_loop(
entry,
&segments,
configured,
spec.max_hypercalls,
spec.emitted_capacity,
MMIO_EMITTED_CAPACITY,
NET_EMITTED_CAPACITY,
BLK_EMITTED_CAPACITY,
PL011_EMITTED_CAPACITY,
dispatch::GUEST_MEMORY_SIZE,
)?;
Ok(Bytes::from(emitted))
}
impl SendPipe for VmUpstream {
type In = Request<Bytes>;
type Out = Response<Bytes>;
type Err = ProximaError;
fn call(
&self,
_request: Request<Bytes>,
) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> {
let label = self.label.clone();
let spec = self.spec.clone();
async move {
let image = std::fs::read(&spec.guest_image_path).map_err(|error| {
ProximaError::Upstream(format!(
"vm `{label}`: read guest image `{}`: {error}",
spec.guest_image_path.display()
))
})?;
boot_and_run(&image, &spec)
.map(Response::ok)
.map_err(|error| ProximaError::Upstream(format!("vm `{label}`: {error}")))
}
}
}
pub struct VmPipeFactory;
impl PipeFactory for VmPipeFactory {
fn name(&self) -> &str {
"vm"
}
fn build(
&self,
spec: &Value,
_inner: Option<PipeHandle>,
) -> Pin<Box<dyn Future<Output = Result<PipeHandle, ProximaError>> + Send + '_>> {
let spec = spec.clone();
Box::pin(async move {
let config: VmConfig = serde_json::from_value(spec)
.map_err(|err| ProximaError::Config(format!("vm config: {err}")))?;
let label = config.name.clone();
let parsed = config.into_spec()?;
Ok(into_handle(VmUpstream::new(label, parsed)))
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn parity_fluent_builder_and_config_value_match() {
let from_value: VmConfig = serde_json::from_value(json!({
"name": "my-fn",
"guest_image_path": "/tmp/guest.elf",
"max_hypercalls": 8,
"emitted_capacity": 128,
"response": "canned",
"response_eof": false,
}))
.expect("from_value");
let from_value = from_value.into_spec().expect("into_spec value");
let from_builder = VmConfig::builder()
.name("my-fn")
.guest_image_path("/tmp/guest.elf")
.max_hypercalls(8)
.emitted_capacity(128)
.response("canned")
.response_eof(false)
.build()
.into_spec()
.expect("into_spec builder");
assert_eq!(from_value.guest_image_path, from_builder.guest_image_path);
assert_eq!(from_value.max_hypercalls, from_builder.max_hypercalls);
assert_eq!(from_value.emitted_capacity, from_builder.emitted_capacity);
assert_eq!(from_value.response_bytes, from_builder.response_bytes);
assert_eq!(from_value.response_eof, from_builder.response_eof);
}
#[test]
fn loaded_config_reseeds_the_builder_and_keeps_chaining() {
let loaded: VmConfig = serde_json::from_value(json!({
"guest_image_path": "/tmp/a.elf",
"max_hypercalls": 4,
}))
.expect("from_value");
let extended = VmConfig::builder()
.guest_image_path(loaded.guest_image_path.clone())
.max_hypercalls(32)
.build();
assert_eq!(extended.guest_image_path, loaded.guest_image_path);
assert_eq!(extended.max_hypercalls, 32);
}
#[test]
fn defaults_match_the_composed_m1_driver_example() {
let config = VmConfig::builder()
.guest_image_path("/tmp/guest.elf")
.build();
assert_eq!(config.name, "vm");
assert_eq!(config.max_hypercalls, 16);
assert_eq!(config.emitted_capacity, 256);
assert!(config.response_eof);
}
#[test]
fn empty_guest_image_path_returns_config_error() {
let outcome = VmConfig::builder().guest_image_path("").build().into_spec();
assert!(matches!(outcome, Err(ProximaError::Config(_))));
}
#[test]
fn zero_max_hypercalls_returns_config_error() {
let outcome = VmConfig::builder()
.guest_image_path("/tmp/guest.elf")
.max_hypercalls(0)
.build()
.into_spec();
assert!(matches!(outcome, Err(ProximaError::Config(_))));
}
#[proxima::test]
async fn missing_guest_image_path_returns_config_error() {
let factory = VmPipeFactory;
let outcome = factory.build(&json!({"name": "no_path"}), None).await;
assert!(matches!(outcome, Err(ProximaError::Config(_))));
}
#[test]
fn env_loader_from_path_and_builder_yield_identical_state() {
let directory = tempfile::tempdir().expect("create tempdir");
let guest_path = directory.path().join("guest.elf");
std::fs::write(&guest_path, b"not-a-real-elf-this-test-never-parses-it")
.expect("write guest stub");
let guest_image_path = guest_path.to_string_lossy().into_owned();
let from_env: VmConfig = temp_env::with_vars(
[
(
"PROXIMA_VM_GUEST_IMAGE_PATH",
Some(guest_image_path.as_str()),
),
("PROXIMA_VM_NAME", Some("my-fn")),
("PROXIMA_VM_MAX_HYPERCALLS", Some("8")),
("PROXIMA_VM_EMITTED_CAPACITY", Some("128")),
("PROXIMA_VM_RESPONSE", Some("canned")),
("PROXIMA_VM_RESPONSE_EOF", Some("false")),
],
|| VmConfig::from_env().expect("VmConfig::from_env"),
);
let config_path = directory.path().join("proxima.toml");
let toml = format!(
"[upstreams.my-fn]\ntype = \"vm\"\nname = \"my-fn\"\nguest_image_path = \"{guest_image_path}\"\nmax_hypercalls = 8\nemitted_capacity = 128\nresponse = \"canned\"\nresponse_eof = false\n"
);
std::fs::write(&config_path, toml).expect("write proxima.toml");
let settings =
crate::settings::ProximaSettings::from_path(&config_path).expect("load settings");
let entry = settings
.upstreams
.get("my-fn")
.expect("my-fn registered by config")
.clone();
let from_path: VmConfig =
serde_json::from_value(entry.spec).expect("decode VmConfig from registry entry spec");
let from_builder = VmConfig::builder()
.name("my-fn")
.guest_image_path(guest_image_path.clone())
.max_hypercalls(8)
.emitted_capacity(128)
.response("canned")
.response_eof(false)
.build();
assert_eq!(from_env, from_path, "env loader and from_path diverged");
assert_eq!(from_path, from_builder, "from_path and builder diverged");
}
#[proxima::test]
async fn nonexistent_guest_image_returns_upstream_error_at_call_time() {
let factory = VmPipeFactory;
let handle = factory
.build(
&json!({
"name": "ghost",
"guest_image_path": "/no/such/guest.elf",
}),
None,
)
.await
.expect("build succeeds — the image is only read at call time");
let outcome = SendPipe::call(
&handle,
Request::builder()
.method("GET")
.path("/")
.build()
.expect("request"),
)
.await;
assert!(matches!(outcome, Err(ProximaError::Upstream(_))));
}
}