use crate::BoxID;
use crate::disk::Disk;
#[cfg(target_os = "linux")]
use crate::fs::BindMountHandle;
use crate::images::ContainerImageConfig;
use crate::litebox::config::BoxConfig;
use crate::portal::GuestSession;
use crate::portal::interfaces::ContainerRootfsInitConfig;
use crate::runtime::layout::BoxFilesystemLayout;
use crate::runtime::options::VolumeSpec;
use crate::runtime::rt_impl::SharedRuntimeImpl;
use crate::vmm::controller::VmmHandler;
use crate::volumes::{ContainerMount, GuestVolumeManager};
use boxlite_shared::errors::{BoxliteError, BoxliteResult};
use std::path::PathBuf;
use std::sync::atomic::Ordering;
pub const USE_OVERLAYFS: bool = true;
pub const USE_DISK_ROOTFS: bool = true;
#[derive(Debug, Clone)]
pub struct ResolvedVolume {
pub tag: String,
pub host_path: PathBuf,
pub guest_path: String,
pub read_only: bool,
pub owner_uid: u32,
pub owner_gid: u32,
}
pub fn resolve_user_volumes(volumes: &[VolumeSpec]) -> BoxliteResult<Vec<ResolvedVolume>> {
let mut resolved = Vec::with_capacity(volumes.len());
for (i, vol) in volumes.iter().enumerate() {
let host_path = PathBuf::from(&vol.host_path);
if !host_path.exists() {
return Err(BoxliteError::Config(format!(
"Volume host path does not exist: {}",
vol.host_path
)));
}
let resolved_path = host_path.canonicalize().map_err(|e| {
BoxliteError::Config(format!(
"Failed to resolve volume path '{}': {}",
vol.host_path, e
))
})?;
if !resolved_path.is_dir() {
return Err(BoxliteError::Config(format!(
"Volume host path is not a directory: {}",
vol.host_path
)));
}
let tag = format!("uservol{}", i);
let (owner_uid, owner_gid) = {
use std::os::unix::fs::MetadataExt;
let meta = std::fs::metadata(&resolved_path).map_err(|e| {
BoxliteError::Config(format!(
"Failed to stat volume path '{}': {}",
resolved_path.display(),
e
))
})?;
(meta.uid(), meta.gid())
};
tracing::debug!(
tag = %tag,
host_path = %resolved_path.display(),
guest_path = %vol.guest_path,
read_only = vol.read_only,
owner_uid,
owner_gid,
"Resolved user volume"
);
resolved.push(ResolvedVolume {
tag,
host_path: resolved_path,
guest_path: vol.guest_path.clone(),
read_only: vol.read_only,
owner_uid,
owner_gid,
});
}
Ok(resolved)
}
#[derive(Debug)]
pub enum ContainerRootfsPrepResult {
#[allow(dead_code)]
Merged(PathBuf),
#[allow(dead_code)] Layers {
layers_dir: PathBuf,
layer_names: Vec<String>,
},
DiskImage {
base_disk_path: PathBuf,
disk_size: u64,
},
}
pub struct CleanupGuard {
runtime: SharedRuntimeImpl,
box_id: BoxID,
layout: Option<BoxFilesystemLayout>,
handler: Option<Box<dyn VmmHandler>>,
armed: bool,
last_error: Option<String>,
}
impl CleanupGuard {
pub fn new(runtime: SharedRuntimeImpl, box_id: BoxID) -> Self {
Self {
runtime,
box_id,
layout: None,
handler: None,
armed: true,
last_error: None,
}
}
pub fn set_last_error(&mut self, err: &BoxliteError) {
self.last_error = Some(err.to_string());
}
pub fn set_layout(&mut self, layout: BoxFilesystemLayout) {
self.layout = Some(layout);
}
pub fn set_handler(&mut self, handler: Box<dyn VmmHandler>) {
self.handler = Some(handler);
}
pub fn take_handler(&mut self) -> Option<Box<dyn VmmHandler>> {
self.handler.take()
}
pub fn handler_pid(&self) -> Option<u32> {
self.handler.as_ref().map(|h| h.pid())
}
pub fn disarm(&mut self) {
self.armed = false;
}
}
impl Drop for CleanupGuard {
fn drop(&mut self) {
if !self.armed {
return;
}
let reason = self
.last_error
.as_deref()
.unwrap_or("box initialization failed (no cause captured)");
tracing::warn!(box_id = %self.box_id, reason = %reason, "Box initialization failed, cleaning up");
if let Some(ref mut handler) = self.handler
&& let Err(e) = handler.stop()
{
tracing::warn!("Failed to stop handler during cleanup: {}", e);
}
if let Some(ref layout) = self.layout {
tracing::error!(
"Box failed. Diagnostic files preserved at:\n {}\n\nTo destroy: issue DESTROY_SANDBOX or `boxlite rm {}`",
layout.root().display(),
self.box_id
);
}
match self.runtime.box_manager.update_box(&self.box_id) {
Ok(mut state) => {
state.mark_failed(reason);
if let Err(e) = self.runtime.box_manager.save_box(&self.box_id, &state) {
tracing::warn!(
box_id = %self.box_id,
"Failed to persist Failed state during cleanup: {}", e
);
}
}
Err(e) => {
tracing::warn!(
box_id = %self.box_id,
"Could not load state to mark Failed (record may have been deleted concurrently): {}", e
);
}
}
self.runtime
.runtime_metrics
.boxes_failed
.fetch_add(1, Ordering::Relaxed);
}
}
pub struct InitPipelineContext {
pub config: BoxConfig,
pub runtime: SharedRuntimeImpl,
pub guard: CleanupGuard,
pub reuse_rootfs: bool,
pub skip_guest_wait: bool,
pub layout: Option<BoxFilesystemLayout>,
pub container_image_config: Option<ContainerImageConfig>,
pub container_disk: Option<Disk>,
pub guest_disk: Option<Disk>,
pub volume_mgr: Option<GuestVolumeManager>,
pub rootfs_init: Option<ContainerRootfsInitConfig>,
pub container_mounts: Option<Vec<ContainerMount>>,
pub guest_session: Option<GuestSession>,
pub ca_cert_pem: Option<String>,
#[cfg(target_os = "linux")]
pub bind_mount: Option<BindMountHandle>,
}
impl InitPipelineContext {
pub fn new(
config: BoxConfig,
runtime: SharedRuntimeImpl,
reuse_rootfs: bool,
skip_guest_wait: bool,
) -> Self {
let guard = CleanupGuard::new(runtime.clone(), config.id.clone());
Self {
config,
runtime,
guard,
reuse_rootfs,
skip_guest_wait,
layout: None,
container_image_config: None,
container_disk: None,
guest_disk: None,
volume_mgr: None,
rootfs_init: None,
container_mounts: None,
guest_session: None,
ca_cert_pem: None,
#[cfg(target_os = "linux")]
bind_mount: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime::options::VolumeSpec;
#[test]
fn resolve_volume_gets_owner_uid() {
let tmp = tempfile::tempdir().unwrap();
let volumes = vec![VolumeSpec {
host_path: tmp.path().to_str().unwrap().to_string(),
guest_path: "/data".to_string(),
read_only: false,
}];
let resolved = resolve_user_volumes(&volumes).unwrap();
assert_eq!(resolved.len(), 1);
use std::os::unix::fs::MetadataExt;
let expected_uid = std::fs::metadata(tmp.path()).unwrap().uid();
let expected_gid = std::fs::metadata(tmp.path()).unwrap().gid();
assert_eq!(resolved[0].owner_uid, expected_uid);
assert_eq!(resolved[0].owner_gid, expected_gid);
assert_eq!(resolved[0].tag, "uservol0");
}
#[test]
fn resolve_volume_nonexistent_path_errors() {
let volumes = vec![VolumeSpec {
host_path: "/nonexistent/path/12345".to_string(),
guest_path: "/data".to_string(),
read_only: false,
}];
let result = resolve_user_volumes(&volumes);
assert!(result.is_err());
}
#[test]
fn resolve_volume_file_not_dir_errors() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let volumes = vec![VolumeSpec {
host_path: tmp.path().to_str().unwrap().to_string(),
guest_path: "/data".to_string(),
read_only: false,
}];
let result = resolve_user_volumes(&volumes);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not a directory"));
}
#[test]
fn cleanup_guard_drop_persists_failed_state_and_keeps_record() {
use crate::litebox::config::{BoxConfig, ContainerRuntimeConfig};
use crate::runtime::id::BoxID;
use crate::runtime::options::{BoxOptions, BoxliteOptions, RootfsSpec};
use crate::runtime::rt_impl::RuntimeImpl;
use crate::runtime::types::{BoxState, BoxStatus, ContainerID};
use crate::vmm::VmmKind;
use boxlite_shared::Transport;
use boxlite_test_utils::home::PerTestBoxHome;
use chrono::Utc;
use std::path::PathBuf;
let home = PerTestBoxHome::isolated_in("/tmp");
let runtime = RuntimeImpl::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: vec![],
})
.expect("create runtime");
let box_id = BoxID::parse("01HJK4TNRPQSXYZ8WM6NCVT9CG1").unwrap();
let config = BoxConfig {
id: box_id.clone(),
name: None,
created_at: Utc::now(),
container: ContainerRuntimeConfig {
id: ContainerID::new(),
},
options: BoxOptions {
rootfs: RootfsSpec::Image("test:latest".to_string()),
..Default::default()
},
engine_kind: VmmKind::Libkrun,
transport: Transport::unix(PathBuf::from("/tmp/test.sock")),
box_home: PathBuf::from("/tmp/box"),
ready_socket_path: PathBuf::from("/tmp/ready"),
};
runtime
.box_manager
.add_box(&config, &BoxState::new())
.expect("seed Configured box");
let err =
BoxliteError::Engine("Box CL84LvGx7RBE failed to start: timeout after 30s".to_string());
let err_display = err.to_string();
{
let mut guard = CleanupGuard::new(runtime.clone(), box_id.clone());
guard.set_last_error(&err);
}
assert!(
runtime.box_manager.has_box(&box_id).unwrap(),
"CleanupGuard::drop must preserve the box record"
);
let persisted = runtime.box_manager.update_box(&box_id).unwrap();
assert_eq!(persisted.status, BoxStatus::Failed);
let reason = persisted
.error_reason
.as_deref()
.expect("error_reason populated by Drop");
assert!(
reason.contains(&err_display),
"error_reason should round-trip BoxliteError::Display; got {reason:?}"
);
}
}