#![cfg(feature = "local")]
#[cfg(target_os = "linux")]
use std::path::PathBuf;
#[cfg(target_os = "linux")]
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use microsandbox::{CreationProgress, MicrosandboxError, Sandbox, StartupPhase};
use microsandbox_db::entity::sandbox as sandbox_row;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
#[cfg(target_os = "linux")]
use serde_json::Value;
struct EmergencyCleanup {
name: String,
armed: bool,
}
impl Drop for EmergencyCleanup {
fn drop(&mut self) {
if self.armed {
let _ = std::process::Command::new(std::env::var("MSB_PATH").unwrap())
.args(["stop", "--force", &self.name])
.status();
}
}
}
#[cfg(target_os = "linux")]
fn trace() -> Vec<Value> {
let path = std::env::var("MSB_TEST_EAGER_TRACE").expect("shim trace path");
std::fs::read_to_string(path)
.unwrap_or_default()
.lines()
.filter_map(|line| serde_json::from_str(line).ok())
.collect()
}
#[cfg(target_os = "linux")]
async fn wait_for_injection(event: &str) -> Value {
tokio::time::timeout(Duration::from_secs(15), async {
loop {
if let Some(record) = trace().into_iter().find(|record| record["event"] == event) {
return record;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await
.expect("scoped shim never reached the real VMM's checkpoint-object read")
}
#[cfg(target_os = "linux")]
async fn prove_runtime_cleanup(name: &str, pid: u64) {
let backend = microsandbox::backend::default_backend();
let local = backend.as_local().unwrap();
tokio::time::timeout(Duration::from_secs(15), async {
loop {
let handle = Sandbox::get(name).await;
let ownership = microsandbox_runtime::ipc::try_acquire_lifecycle_guard(
&local.config().run_dir(),
name,
)
.unwrap();
if ownership.is_some() && !PathBuf::from(format!("/proc/{pid}")).exists() {
match handle {
Ok(handle)
if matches!(
handle.status_snapshot(),
microsandbox::sandbox::SandboxStatus::Stopped
| microsandbox::sandbox::SandboxStatus::Crashed
) =>
{
drop(ownership);
handle.remove().await.unwrap();
break;
}
Err(MicrosandboxError::SandboxNotFound(_)) => break,
_ => {}
}
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await
.expect("test cleanup retained a process, lifecycle owner, or active catalog row");
}
#[tokio::test]
#[cfg(target_os = "linux")]
#[ignore = "requires matching real VMM/agent, fresh snapshot, and scoped Linux preload shim"]
async fn eager_preparation_boundary_live() {
let mode = std::env::var("MSB_TEST_EAGER_MODE").expect("delay/error/cancel mode");
assert!(["delay", "error", "cancel"].contains(&mode.as_str()));
let snapshot = std::env::var("MSB_PROGRESS_SNAPSHOT").expect("fresh checksum snapshot");
let name = format!("eager-{}-{mode}", std::process::id());
assert!(matches!(
Sandbox::get(&name).await,
Err(MicrosandboxError::SandboxNotFound(_))
));
let mut cleanup = EmergencyCleanup {
name: name.clone(),
armed: true,
};
let started = Instant::now();
let (mut progress, mut task) = Sandbox::restore(&snapshot)
.name(&name)
.restore_with_progress()
.unwrap();
let phases = Arc::new(Mutex::new(Vec::new()));
let observed = phases.clone();
let observer = tokio::spawn(async move {
while let Some(event) = progress.recv().await {
println!(
"{} {}",
started.elapsed().as_millis(),
serde_json::to_string(&event).unwrap()
);
if let CreationProgress::Startup(event) = event {
assert!(
event
.total_bytes
.is_none_or(|total| event.completed_bytes <= total)
);
observed.lock().unwrap().push(event.phase);
}
}
});
if mode == "cancel" {
let injected = wait_for_injection("delay_begin").await;
let cancelled = Instant::now();
task.abort();
assert!(matches!(task.await, Err(error) if error.is_cancelled()));
prove_runtime_cleanup(&name, injected["pid"].as_u64().unwrap()).await;
assert!(cancelled.elapsed() < Duration::from_secs(15));
assert!(!trace().iter().any(|record| record["event"] == "delay_end"));
assert!(!trace().iter().any(|record| {
record["pid"] == injected["pid"]
&& record["event"] == "startup"
&& record["progress"]["phase"] == "activating"
}));
} else {
let result = tokio::time::timeout(Duration::from_secs(60), &mut task)
.await
.expect("bounded test safety deadline")
.unwrap();
if mode == "error" {
let error = match result {
Err(error) => error.to_string(),
Ok(_) => panic!("injected object EIO unexpectedly created a sandbox"),
};
println!("original_error={error}");
assert!(error.contains("Input/output error") || error.contains("os error 5"));
assert!(!error.contains("startup channel closed"));
let injected = wait_for_injection("read_error").await;
prove_runtime_cleanup(&name, injected["pid"].as_u64().unwrap()).await;
assert!(!trace().iter().any(|record| {
record["pid"] == injected["pid"]
&& record["event"] == "startup"
&& record["progress"]["phase"] == "activating"
}));
} else {
let sandbox = result.unwrap();
let create_ms = started.elapsed().as_millis();
let records = trace();
let begin = records
.iter()
.find(|record| record["event"] == "delay_begin")
.unwrap();
let end = records
.iter()
.find(|record| record["event"] == "delay_end")
.unwrap();
assert!(
end["monotonic_ns"].as_u64().unwrap() - begin["monotonic_ns"].as_u64().unwrap()
>= 12_000_000_000
);
let activating = records
.iter()
.find(|record| {
record["event"] == "startup"
&& record["pid"] == begin["pid"]
&& record["progress"]["phase"] == "activating"
})
.expect("actual Activating frame for the delayed runtime");
assert!(activating["monotonic_ns"].as_u64() >= end["monotonic_ns"].as_u64());
let checksum = sandbox
.exec("sha256sum", ["-c", "/work/hash"])
.await
.unwrap();
assert!(checksum.status().success);
println!("activation_assertions_passed create_ms={create_ms} checksum=ok");
sandbox.kill().await.unwrap();
prove_runtime_cleanup(&name, begin["pid"].as_u64().unwrap()).await;
println!("cleanup=explicit_kill runtime_reaped=true ownership_released=true");
}
}
tokio::time::timeout(Duration::from_secs(15), observer)
.await
.unwrap()
.unwrap();
let activating = phases.lock().unwrap().contains(&StartupPhase::Activating);
assert_eq!(activating, mode == "delay");
cleanup.armed = false;
println!(
"PASS mode={mode} elapsed_ms={}",
started.elapsed().as_millis()
);
}
#[tokio::test]
#[ignore = "requires matching runtime/agent and a fresh full snapshot containing /work/hash"]
async fn portable_eager_forked_progress_and_stop_completion() {
let snapshot = std::env::var("MSB_PROGRESS_SNAPSHOT").expect("fresh checksum snapshot");
for forked in [false, true] {
let name = format!("portable-life-{}-{forked}", std::process::id());
assert!(matches!(
Sandbox::get(&name).await,
Err(MicrosandboxError::SandboxNotFound(_))
));
let mut cleanup = EmergencyCleanup {
name: name.clone(),
armed: true,
};
let started = Instant::now();
let builder = Sandbox::restore(&snapshot).name(&name);
let builder = if forked { builder.forked() } else { builder };
let (mut progress, task) = builder.restore_with_progress().unwrap();
let mut activating = false;
while let Some(event) = progress.recv().await {
println!(
"{} {}",
started.elapsed().as_millis(),
serde_json::to_string(&event).unwrap()
);
if let CreationProgress::Startup(event) = event {
assert!(
event
.total_bytes
.is_none_or(|total| event.completed_bytes <= total)
);
activating |= event.phase == StartupPhase::Activating;
}
}
let sandbox = task.await.unwrap().unwrap();
let create_ms = started.elapsed().as_millis();
println!("created portable forked={forked} create_ms={create_ms}");
assert!(activating, "creation EOF must not replace Activating");
let zero = sandbox.stop_with_timeout(Duration::ZERO).await;
assert!(matches!(zero, Err(MicrosandboxError::StopTimeout { .. })));
let checksum = sandbox
.exec("sha256sum", ["-c", "/work/hash"])
.await
.unwrap();
assert!(
checksum.status().success,
"zero-budget Stop must not dispatch or kill"
);
sandbox.pause().await.unwrap();
let paused_stop = tokio::time::timeout(Duration::from_secs(5), sandbox.stop())
.await
.unwrap()
.expect_err("paused Stop must explicitly refuse");
assert!(paused_stop.to_string().contains("paused"));
assert!(sandbox.pause_state().await.unwrap().paused);
sandbox.resume().await.unwrap();
let checksum = sandbox
.exec("sha256sum", ["-c", "/work/hash"])
.await
.unwrap();
assert!(checksum.status().success);
let backend = microsandbox::backend::default_backend();
let local = backend.as_local().unwrap();
let pools = local.db().await.unwrap();
let stop_started = Instant::now();
tokio::time::timeout(Duration::from_secs(30), sandbox.stop())
.await
.unwrap()
.unwrap();
let stop_ms = stop_started.elapsed().as_millis();
let ownership = microsandbox_runtime::ipc::try_acquire_lifecycle_guard(
&local.config().run_dir(),
&name,
)
.unwrap()
.expect("successful Stop must already have released runtime ownership");
let terminal = sandbox_row::Entity::find()
.filter(sandbox_row::Column::Name.eq(&name))
.one(pools.read())
.await
.unwrap()
.expect("persistent Stop must retain the sandbox row");
assert!(matches!(
terminal.status,
sandbox_row::SandboxStatus::Stopped | sandbox_row::SandboxStatus::Crashed
));
println!(
"ownership_released=true terminal_status={:?}",
terminal.status
);
drop(ownership);
let remove_started = Instant::now();
sandbox.remove_persisted().await.unwrap();
let remove_ms = remove_started.elapsed().as_millis();
cleanup.armed = false;
println!(
"PASS portable forked={forked} create_ms={create_ms} stop_ms={stop_ms} remove_ms={remove_ms} total_lifecycle_ms={}",
started.elapsed().as_millis()
);
}
}