pub use a3s_box_core::vmm::{VmHandler, VmMetrics, DEFAULT_SHUTDOWN_TIMEOUT_MS};
use a3s_box_core::error::Result;
use std::process::Child;
use std::sync::Mutex;
use sysinfo::{Pid, System};
pub struct ShimHandler {
pid: u32,
box_id: String,
process: Option<Child>,
metrics_sys: Mutex<System>,
exit_code: Option<i32>,
}
impl ShimHandler {
pub fn from_child(process: Child, box_id: String) -> Self {
let pid = process.id();
Self {
pid,
box_id,
process: Some(process),
metrics_sys: Mutex::new(System::new()),
exit_code: None,
}
}
pub fn from_pid(pid: u32, box_id: String) -> Self {
Self {
pid,
box_id,
process: None,
metrics_sys: Mutex::new(System::new()),
exit_code: None,
}
}
pub fn box_id(&self) -> &str {
&self.box_id
}
}
impl VmHandler for ShimHandler {
fn pid(&self) -> u32 {
self.pid
}
#[cfg(unix)]
fn stop(&mut self, signal: i32, timeout_ms: u64) -> Result<()> {
if let Some(mut process) = self.process.take() {
let pid = process.id();
tracing::debug!(pid, box_id = %self.box_id, signal, "Sending stop signal to VM process");
unsafe {
libc::kill(pid as i32, signal);
}
let start = std::time::Instant::now();
loop {
match process.try_wait() {
Ok(Some(status)) => {
tracing::debug!(pid, ?status, "VM process exited gracefully");
self.exit_code = status.code();
return Ok(());
}
Ok(None) => {
if start.elapsed().as_millis() > timeout_ms as u128 {
tracing::warn!(
pid,
timeout_ms,
"VM process did not exit gracefully, sending SIGKILL"
);
let _ = process.kill();
if let Ok(status) = process.wait() {
self.exit_code = status.code();
}
return Ok(());
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
Err(e) => {
tracing::warn!(pid, error = %e, "Error checking process status, forcing kill");
let _ = process.kill();
let _ = process.wait();
return Ok(());
}
}
}
} else {
tracing::debug!(pid = self.pid, box_id = %self.box_id, signal, "Sending stop signal to attached VM process");
unsafe {
libc::kill(self.pid as i32, signal);
}
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 {
tracing::debug!(pid = self.pid, "VM process exited gracefully");
return Ok(());
}
if result < 0 {
let exists = unsafe { libc::kill(self.pid as i32, 0) } == 0;
if !exists {
return Ok(()); }
}
if start.elapsed().as_millis() > timeout_ms as u128 {
tracing::warn!(
pid = self.pid,
timeout_ms,
"VM process did not exit gracefully, sending SIGKILL"
);
unsafe {
libc::kill(self.pid as i32, libc::SIGKILL);
}
return Ok(());
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
}
}
#[cfg(windows)]
fn stop(&mut self, _signal: i32, timeout_ms: u64) -> Result<()> {
if let Some(mut process) = self.process.take() {
tracing::debug!(pid = self.pid, box_id = %self.box_id, "Terminating VM process");
let start = std::time::Instant::now();
loop {
match process.try_wait() {
Ok(Some(status)) => {
tracing::debug!(pid = self.pid, ?status, "VM process exited");
self.exit_code = status.code();
return Ok(());
}
Ok(None) => {
if start.elapsed().as_millis() > timeout_ms as u128 {
tracing::warn!(pid = self.pid, "VM process did not exit, forcing kill");
let _ = process.kill();
if let Ok(status) = process.wait() {
self.exit_code = status.code();
}
return Ok(());
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
Err(e) => {
tracing::warn!(pid = self.pid, error = %e, "Error checking process, forcing kill");
let _ = process.kill();
let _ = process.wait();
return Ok(());
}
}
}
} else {
tracing::debug!(pid = self.pid, "Checking attached VM process");
let start = std::time::Instant::now();
while start.elapsed().as_millis() <= timeout_ms as u128 {
if !self.is_running() {
return Ok(());
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
tracing::warn!(
pid = self.pid,
"Attached VM process still running after timeout"
);
Ok(())
}
}
fn metrics(&self) -> VmMetrics {
let pid = Pid::from_u32(self.pid);
let mut sys = match self.metrics_sys.lock() {
Ok(guard) => guard,
Err(e) => {
tracing::warn!(error = %e, "metrics_sys lock poisoned");
return VmMetrics::default();
}
};
sys.refresh_process(pid);
if let Some(proc_info) = sys.process(pid) {
return VmMetrics {
cpu_percent: Some(proc_info.cpu_usage()),
memory_bytes: Some(proc_info.memory()),
};
}
VmMetrics::default()
}
#[cfg(unix)]
fn is_running(&self) -> bool {
unsafe { libc::kill(self.pid as i32, 0) == 0 }
}
#[cfg(windows)]
fn is_running(&self) -> bool {
let mut sys = System::new();
sys.refresh_process(Pid::from_u32(self.pid));
sys.process(Pid::from_u32(self.pid)).is_some()
}
fn exit_code(&self) -> Option<i32> {
self.exit_code
}
fn try_wait_exit(&mut self) -> Result<Option<i32>> {
if self.exit_code.is_some() {
return Ok(self.exit_code);
}
let Some(process) = self.process.as_mut() else {
return Ok(None);
};
match process.try_wait() {
Ok(Some(status)) => {
self.exit_code = status.code();
Ok(self.exit_code)
}
Ok(None) => Ok(None),
Err(e) => Err(a3s_box_core::error::BoxError::ExecError(format!(
"Failed to poll VM process {}: {}",
self.pid, e
))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vm_metrics_default() {
let m = VmMetrics::default();
assert!(m.cpu_percent.is_none());
assert!(m.memory_bytes.is_none());
}
#[test]
fn test_vm_metrics_clone() {
let m = VmMetrics {
cpu_percent: Some(50.0),
memory_bytes: Some(1024 * 1024),
};
let cloned = m.clone();
assert_eq!(cloned.cpu_percent, Some(50.0));
assert_eq!(cloned.memory_bytes, Some(1024 * 1024));
}
#[test]
fn test_shim_handler_from_pid() {
let handler = ShimHandler::from_pid(12345, "box-abc".to_string());
assert_eq!(handler.pid(), 12345);
assert_eq!(handler.box_id(), "box-abc");
assert_eq!(handler.exit_code(), None);
}
#[test]
fn test_shim_handler_try_wait_exit_captures_child_exit_code() {
let child = std::process::Command::new("sh")
.arg("-c")
.arg("exit 7")
.spawn()
.unwrap();
let mut handler = ShimHandler::from_child(child, "box-child-exit".to_string());
let exit_code = loop {
if let Some(code) = handler.try_wait_exit().unwrap() {
break code;
}
std::thread::sleep(std::time::Duration::from_millis(10));
};
assert_eq!(exit_code, 7);
assert_eq!(handler.exit_code(), Some(7));
assert_eq!(handler.try_wait_exit().unwrap(), Some(7));
}
#[cfg(unix)]
#[test]
fn test_shim_handler_stop_terminates_owned_child() {
let child = std::process::Command::new("sh")
.arg("-c")
.arg("trap 'exit 0' TERM; while :; do sleep 0.05; done")
.spawn()
.unwrap();
let mut handler = ShimHandler::from_child(child, "box-child-stop".to_string());
assert!(handler.is_running());
handler.stop(libc::SIGTERM, 2_000).unwrap();
assert!(!handler.is_running());
assert_eq!(handler.exit_code(), None);
assert_eq!(handler.try_wait_exit().unwrap(), None);
}
#[cfg(unix)]
#[test]
fn test_shim_handler_stop_attached_missing_pid_is_ok() {
let mut handler = ShimHandler::from_pid(999_999_999, "missing".to_string());
handler.stop(libc::SIGTERM, 10).unwrap();
assert!(!handler.is_running());
assert_eq!(handler.exit_code(), None);
}
#[test]
fn test_shim_handler_is_running_nonexistent_pid() {
let handler = ShimHandler::from_pid(999_999_999, "test".to_string());
assert!(!handler.is_running());
}
#[test]
fn test_shim_handler_metrics_nonexistent_pid() {
let handler = ShimHandler::from_pid(999_999_999, "test".to_string());
let m = handler.metrics();
assert!(m.cpu_percent.is_none() || m.cpu_percent == Some(0.0));
}
#[test]
fn test_shim_handler_is_running_current_process() {
let pid = std::process::id();
let handler = ShimHandler::from_pid(pid, "self".to_string());
assert!(handler.is_running());
}
#[test]
fn test_default_shutdown_timeout() {
assert_eq!(DEFAULT_SHUTDOWN_TIMEOUT_MS, 10_000);
}
#[test]
fn test_vm_metrics_debug() {
let m = VmMetrics {
cpu_percent: Some(25.5),
memory_bytes: Some(512),
};
let debug = format!("{:?}", m);
assert!(debug.contains("25.5"));
assert!(debug.contains("512"));
}
}