use sea_orm::EntityTrait;
use std::sync::Arc;
use crate::{
MicrosandboxResult, agent::AgentClient, db::entity::sandbox as sandbox_entity,
runtime::SpawnMode,
};
use super::{Sandbox, SandboxConfig, SandboxStatus};
#[derive(Debug)]
pub struct SandboxHandle {
db_id: i32,
name: String,
status: SandboxStatus,
config_json: String,
created_at: Option<chrono::DateTime<chrono::Utc>>,
updated_at: Option<chrono::DateTime<chrono::Utc>>,
pid: Option<i32>,
}
impl SandboxHandle {
pub(super) fn new(model: sandbox_entity::Model, pid: Option<i32>) -> Self {
Self {
db_id: model.id,
name: model.name,
status: model.status,
config_json: model.config,
created_at: model.created_at.map(|dt| dt.and_utc()),
updated_at: model.updated_at.map(|dt| dt.and_utc()),
pid,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn status(&self) -> SandboxStatus {
self.status
}
pub fn config_json(&self) -> &str {
&self.config_json
}
pub fn config(&self) -> MicrosandboxResult<SandboxConfig> {
Ok(serde_json::from_str(&self.config_json)?)
}
pub fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
self.created_at
}
pub fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
self.updated_at
}
pub async fn metrics(&self) -> MicrosandboxResult<super::SandboxMetrics> {
if self.status != SandboxStatus::Running && self.status != SandboxStatus::Draining {
return Err(crate::MicrosandboxError::Custom(format!(
"sandbox '{}' is not running (status: {:?})",
self.name, self.status
)));
}
let db =
crate::db::init_global(Some(crate::config::config().database.max_connections)).await?;
super::metrics::metrics_for_sandbox(
db,
self.db_id,
u64::from(self.config()?.memory_mib) * 1024 * 1024,
)
.await
}
pub async fn start(&self) -> MicrosandboxResult<Sandbox> {
Sandbox::start_with_mode(&self.name, SpawnMode::Attached).await
}
pub async fn start_detached(&self) -> MicrosandboxResult<Sandbox> {
Sandbox::start_with_mode(&self.name, SpawnMode::Detached).await
}
pub async fn connect(&self) -> MicrosandboxResult<Sandbox> {
if self.status != SandboxStatus::Running && self.status != SandboxStatus::Draining {
return Err(crate::MicrosandboxError::Custom(format!(
"sandbox '{}' is not running (status: {:?})",
self.name, self.status
)));
}
let global = crate::config::config();
let sock_path = global
.sandboxes_dir()
.join(&self.name)
.join("runtime")
.join("agent.sock");
let client = AgentClient::connect(&sock_path).await?;
let config: SandboxConfig = serde_json::from_str(&self.config_json)?;
Ok(Sandbox {
db_id: self.db_id,
config,
handle: None,
client: Arc::new(client),
})
}
pub async fn stop(&self) -> MicrosandboxResult<()> {
if self.status != SandboxStatus::Running && self.status != SandboxStatus::Draining {
return Ok(());
}
signal_pid(self.pid, nix::sys::signal::Signal::SIGTERM)?;
Ok(())
}
pub async fn kill(&mut self) -> MicrosandboxResult<()> {
if self.status != SandboxStatus::Running && self.status != SandboxStatus::Draining {
return Ok(());
}
let pids = signal_pid(self.pid, nix::sys::signal::Signal::SIGKILL)?;
if !pids.is_empty() {
wait_for_exit(&pids, std::time::Duration::from_secs(5)).await;
}
let all_dead = pids.is_empty() || pids.iter().all(|pid| !super::pid_is_alive(*pid));
if all_dead {
let db = crate::db::init_global(Some(crate::config::config().database.max_connections))
.await?;
if let Err(e) =
super::update_sandbox_status(db, self.db_id, SandboxStatus::Stopped).await
{
tracing::warn!(sandbox = %self.name, error = %e, "failed to update sandbox status after kill");
}
self.status = SandboxStatus::Stopped;
}
Ok(())
}
pub async fn remove(&self) -> MicrosandboxResult<()> {
if self.status == SandboxStatus::Running || self.status == SandboxStatus::Draining {
return Err(crate::MicrosandboxError::SandboxStillRunning(format!(
"cannot remove sandbox '{}': still running",
self.name
)));
}
let db =
crate::db::init_global(Some(crate::config::config().database.max_connections)).await?;
super::remove_dir_if_exists(&crate::config::config().sandboxes_dir().join(&self.name))?;
sandbox_entity::Entity::delete_by_id(self.db_id)
.exec(db)
.await?;
Ok(())
}
}
fn signal_pid(pid: Option<i32>, signal: nix::sys::signal::Signal) -> MicrosandboxResult<Vec<i32>> {
if let Some(pid) = pid.filter(|pid| super::pid_is_alive(*pid)) {
nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid), signal)?;
return Ok(vec![pid]);
}
Ok(vec![])
}
async fn wait_for_exit(pids: &[i32], timeout: std::time::Duration) {
let start = std::time::Instant::now();
let poll_interval = std::time::Duration::from_millis(50);
while start.elapsed() < timeout {
if pids.iter().all(|pid| !super::pid_is_alive(*pid)) {
return;
}
tokio::time::sleep(poll_interval).await;
}
}