use std::{path::PathBuf, process::Child, sync::Mutex, time::Instant};
use crate::{
BoxID,
runtime::layout::BoxFilesystemLayout,
vmm::{InstanceSpec, VmmKind},
};
use boxlite_shared::errors::{BoxliteError, BoxliteResult};
use super::watchdog;
use super::{
VmmController, VmmHandler as VmmHandlerTrait, VmmMetrics,
spawn::{ShimSpawner, SpawnedShim},
};
pub struct ShimHandler {
pid: u32,
#[allow(dead_code)]
box_id: BoxID,
process: Option<Child>,
#[allow(dead_code)]
keepalive: Option<watchdog::Keepalive>,
metrics_sys: Mutex<sysinfo::System>,
}
impl ShimHandler {
pub fn from_spawned(spawned: SpawnedShim, box_id: BoxID) -> Self {
let pid = spawned.child.id();
Self {
pid,
box_id,
process: Some(spawned.child),
keepalive: spawned.keepalive,
metrics_sys: Mutex::new(sysinfo::System::new()),
}
}
pub fn from_pid(pid: u32, box_id: BoxID) -> Self {
Self {
pid,
box_id,
process: None,
keepalive: None,
metrics_sys: Mutex::new(sysinfo::System::new()),
}
}
}
impl VmmHandlerTrait for ShimHandler {
fn pid(&self) -> u32 {
self.pid
}
fn stop(&mut self) -> BoxliteResult<()> {
const GRACEFUL_SHUTDOWN_TIMEOUT_MS: u64 = 2000;
if let Some(mut process) = self.process.take() {
let pid = process.id();
unsafe {
libc::kill(pid as i32, libc::SIGTERM);
}
let start = std::time::Instant::now();
loop {
match process.try_wait() {
Ok(Some(_)) => {
return Ok(());
}
Ok(None) => {
if start.elapsed().as_millis() > GRACEFUL_SHUTDOWN_TIMEOUT_MS as u128 {
let _ = process.kill();
let _ = process.wait();
return Ok(());
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
Err(_) => {
let _ = process.kill();
let _ = process.wait();
return Ok(());
}
}
}
} else {
unsafe {
libc::kill(self.pid as i32, libc::SIGTERM);
}
let start = std::time::Instant::now();
loop {
let mut status: i32 = 0;
let result = unsafe { libc::waitpid(self.pid as i32, &mut status, libc::WNOHANG) };
if result > 0 {
return Ok(());
}
if result < 0 {
let exists = crate::util::is_process_alive(self.pid);
if !exists {
return Ok(()); }
}
if start.elapsed().as_millis() > GRACEFUL_SHUTDOWN_TIMEOUT_MS as u128 {
unsafe {
libc::kill(self.pid as i32, libc::SIGKILL);
}
return Ok(());
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
}
#[allow(unreachable_code)]
Ok(())
}
fn metrics(&self) -> BoxliteResult<VmmMetrics> {
use sysinfo::Pid;
let pid = Pid::from_u32(self.pid);
let mut sys = self
.metrics_sys
.lock()
.map_err(|e| BoxliteError::Internal(format!("metrics_sys lock poisoned: {}", e)))?;
sys.refresh_process(pid);
if let Some(proc_info) = sys.process(pid) {
return Ok(VmmMetrics {
cpu_percent: Some(proc_info.cpu_usage()),
memory_bytes: Some(proc_info.memory()),
disk_bytes: None, });
}
Ok(VmmMetrics::default())
}
fn is_running(&self) -> bool {
crate::util::is_process_alive(self.pid)
}
}
pub struct ShimController {
binary_path: PathBuf,
engine_type: VmmKind,
box_id: BoxID,
options: crate::runtime::options::BoxOptions,
layout: BoxFilesystemLayout,
}
impl ShimController {
pub fn new(
binary_path: PathBuf,
engine_type: VmmKind,
box_id: BoxID,
options: crate::runtime::options::BoxOptions,
layout: BoxFilesystemLayout,
) -> BoxliteResult<Self> {
if !binary_path.exists() {
return Err(BoxliteError::Engine(format!(
"Box runner binary not found: {}",
binary_path.display()
)));
}
Ok(Self {
binary_path,
engine_type,
box_id,
options,
layout,
})
}
}
#[async_trait::async_trait]
impl VmmController for ShimController {
async fn start(&mut self, config: &InstanceSpec) -> BoxliteResult<Box<dyn VmmHandlerTrait>> {
tracing::debug!(
"Preparing config: entrypoint.executable={}, entrypoint.args={:?}",
config.guest_entrypoint.executable,
config.guest_entrypoint.args
);
let mut env = config.guest_entrypoint.env.clone();
if let Ok(rust_log) = std::env::var("RUST_LOG") {
env.push(("RUST_LOG".to_string(), rust_log.clone()));
}
let mut guest_entrypoint = config.guest_entrypoint.clone();
guest_entrypoint.env = env;
let serializable_config = InstanceSpec {
engine: self.engine_type,
box_id: self.box_id.to_string(),
security: self.options.advanced.security.clone(),
cpus: config.cpus,
memory_mib: config.memory_mib,
fs_shares: config.fs_shares.clone(),
block_devices: config.block_devices.clone(),
guest_entrypoint,
transport: config.transport.clone(),
ready_transport: config.ready_transport.clone(),
guest_rootfs: config.guest_rootfs.clone(),
network_config: config.network_config.clone(), network_backend_endpoint: None, disable_network: config.disable_network,
home_dir: config.home_dir.clone(),
console_output: config.console_output.clone(),
exit_file: config.exit_file.clone(),
detach: config.detach,
};
let config_json = serde_json::to_string(&serializable_config)
.map_err(|e| BoxliteError::Engine(format!("Failed to serialize config: {}", e)))?;
if let boxlite_shared::Transport::Unix { socket_path } = &config.transport
&& socket_path.exists()
{
tracing::warn!("Removing stale Unix socket: {}", socket_path.display());
let _ = std::fs::remove_file(socket_path);
}
tracing::info!(
engine = ?self.engine_type,
transport = ?config.transport,
"Starting Box subprocess"
);
tracing::debug!(binary = %self.binary_path.display(), "Box runner binary");
tracing::trace!(config = %config_json, "Box configuration");
let shim_spawn_start = Instant::now();
let spawner = ShimSpawner::new(
&self.binary_path,
&self.layout,
self.box_id.as_str(),
&self.options,
);
let spawned = spawner.spawn(&config_json, config.detach)?;
let shim_spawn_duration = shim_spawn_start.elapsed();
let pid = spawned.child.id();
tracing::info!(
box_id = %self.box_id,
pid = pid,
shim_spawn_duration_ms = shim_spawn_duration.as_millis(),
"boxlite-shim subprocess spawned"
);
let handler = ShimHandler::from_spawned(spawned, self.box_id.clone());
tracing::info!(
box_id = %self.box_id,
"VM subprocess started successfully"
);
Ok(Box::new(handler))
}
}