use std::path::{Path, PathBuf};
use std::time::Instant;
use serde::Serialize;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
use crate::backend::apply_snapshot_from;
use crate::backend::process::{drive_to_completion, RunningChild};
use crate::{BackendKind, Sandbox, SandboxError, SandboxOpts, SandboxOutcome};
const RUNNER_PATH_ENV: &str = "NYX_LIBKRUN_RUNNER";
const RUNNER_BINARY: &str = "libkrun-runner";
pub struct LibkrunSandbox {
runner_path: PathBuf,
env_image: Option<PathBuf>,
inner: Option<RunningChild>,
last_logs: (Vec<u8>, Vec<u8>),
}
#[derive(Debug, Clone, Serialize)]
pub struct LibkrunSpec {
pub workspace: PathBuf,
pub env_image: Option<PathBuf>,
pub argv: Vec<String>,
pub cwd: Option<PathBuf>,
pub env: Vec<(String, String)>,
pub timeout_secs: u64,
pub allow_loopback: bool,
pub max_output_bytes: usize,
}
impl LibkrunSandbox {
pub fn new() -> Result<Self, SandboxError> {
let runner_path = resolve_runner_path()?;
Ok(Self { runner_path, env_image: None, inner: None, last_logs: (Vec::new(), Vec::new()) })
}
pub fn with_runner_path(runner_path: PathBuf) -> Self {
Self { runner_path, env_image: None, inner: None, last_logs: (Vec::new(), Vec::new()) }
}
pub fn with_env_image(mut self, env_image: PathBuf) -> Self {
self.env_image = Some(env_image);
self
}
pub fn runner_path(&self) -> &Path {
&self.runner_path
}
}
impl Sandbox for LibkrunSandbox {
fn backend(&self) -> BackendKind {
BackendKind::Libkrun
}
async fn run(&mut self, mut opts: SandboxOpts) -> Result<(), SandboxError> {
if !libkrun_host_supported() {
return Err(SandboxError::BackendUnavailable {
backend: "libkrun",
reason: "requires macOS with Hypervisor.framework or Linux with KVM".into(),
});
}
if self.inner.is_some() {
return Err(SandboxError::State("a child is already running"));
}
if opts.argv.is_empty() {
return Err(SandboxError::Config("argv is empty".into()));
}
let scratch_snapshot = apply_snapshot_from(&mut opts)?;
if !opts.workspace.exists() {
return Err(SandboxError::Config(format!(
"workspace {} does not exist",
opts.workspace.display()
)));
}
if !self.runner_path.is_file() {
return Err(SandboxError::Config(format!(
"{RUNNER_BINARY} not found at {}",
self.runner_path.display()
)));
}
if let Some(img) = &self.env_image {
if !img.exists() {
return Err(SandboxError::Config(format!(
"libkrun env image {} does not exist",
img.display()
)));
}
}
let spec = LibkrunSpec {
workspace: opts.workspace.clone(),
env_image: self.env_image.clone(),
argv: opts.argv.clone(),
cwd: opts.cwd.clone(),
env: opts.env.clone(),
timeout_secs: opts.timeout.as_secs(),
allow_loopback: opts.allow_loopback,
max_output_bytes: opts.max_output_bytes,
};
let spec_json = serde_json::to_vec(&spec)
.map_err(|e| SandboxError::Config(format!("serialise libkrun spec: {e}")))?;
let mut cmd = Command::new(&self.runner_path);
cmd.env_clear();
for k in ["PATH", "HOME", "LANG", "LC_ALL"] {
if let Some(v) = std::env::var_os(k) {
cmd.env(k, v);
}
}
cmd.stdin(std::process::Stdio::piped());
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
cmd.kill_on_drop(true);
let mut child = cmd.spawn().map_err(SandboxError::Spawn)?;
{
let mut stdin = child
.stdin
.take()
.ok_or(SandboxError::State("libkrun-runner stdin unavailable"))?;
stdin.write_all(&spec_json).await.map_err(SandboxError::Io)?;
stdin.shutdown().await.map_err(SandboxError::Io)?;
}
self.inner = Some(RunningChild {
child,
started_at: Instant::now(),
timeout: opts.timeout,
max_output_bytes: opts.max_output_bytes,
killed_by_operator: false,
#[cfg(unix)]
status_fd: None,
scratch_snapshot,
workspace: opts.workspace,
capture_files: opts.capture_files,
});
Ok(())
}
async fn kill(&mut self) -> Result<(), SandboxError> {
if let Some(state) = self.inner.as_mut() {
state.killed_by_operator = true;
let _ = state.child.start_kill();
}
Ok(())
}
async fn wait(&mut self) -> Result<SandboxOutcome, SandboxError> {
let mut state = self.inner.take().ok_or(SandboxError::State("no child to wait on"))?;
let outcome = drive_to_completion(&mut state, BackendKind::Libkrun).await?;
self.last_logs = (outcome.stdout.clone(), outcome.stderr.clone());
Ok(outcome)
}
fn logs(&self) -> (&[u8], &[u8]) {
(&self.last_logs.0, &self.last_logs.1)
}
}
pub fn libkrun_host_supported() -> bool {
#[cfg(target_os = "macos")]
{
true
}
#[cfg(target_os = "linux")]
{
Path::new("/dev/kvm").exists()
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
false
}
}
fn resolve_runner_path() -> Result<PathBuf, SandboxError> {
if let Some(env) = std::env::var_os(RUNNER_PATH_ENV) {
let p = PathBuf::from(env);
if p.is_file() {
return Ok(p);
}
return Err(SandboxError::BackendUnavailable {
backend: "libkrun",
reason: format!("${RUNNER_PATH_ENV}={} is not a file", p.display()),
});
}
if let Some(p) = super::which_on_path(RUNNER_BINARY) {
return Ok(p);
}
Err(SandboxError::BackendUnavailable {
backend: "libkrun",
reason: format!("{RUNNER_BINARY} not found via ${RUNNER_PATH_ENV} or PATH"),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spec_serialises_round_trip() {
let spec = LibkrunSpec {
workspace: PathBuf::from("/tmp/ws"),
env_image: Some(PathBuf::from("/tmp/env.img")),
argv: vec!["python3".into(), "harness.py".into()],
cwd: None,
env: vec![("RUN_ID".into(), "abc".into())],
timeout_secs: 30,
allow_loopback: false,
max_output_bytes: 1 << 20,
};
let json = serde_json::to_string(&spec).expect("ser");
assert!(json.contains("/tmp/ws"));
assert!(json.contains("/tmp/env.img"));
assert!(json.contains("RUN_ID"));
}
#[test]
fn missing_runner_returns_backend_unavailable() {
let _guard = crate::ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var(RUNNER_PATH_ENV, "/definitely/does/not/exist/libkrun-runner");
let result = LibkrunSandbox::new();
std::env::remove_var(RUNNER_PATH_ENV);
match result {
Err(SandboxError::BackendUnavailable { backend, .. }) => {
assert_eq!(backend, "libkrun");
}
Err(other) => panic!("expected BackendUnavailable, got {other:?}"),
Ok(_) => panic!("must fail when env-pointed runner is missing"),
}
}
}