use crate::state::ContainerId;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ExecId(String);
impl ExecId {
#[must_use]
pub fn new() -> Self {
Self(uuid::Uuid::new_v4().to_string().replace('-', ""))
}
#[must_use]
pub fn from_string(s: &str) -> Self {
Self(s.to_string())
}
}
impl Default for ExecId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for ExecId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone)]
pub struct ExecConfig {
pub container_id: ContainerId,
pub cmd: Vec<String>,
pub env: Vec<String>,
pub working_dir: Option<String>,
pub attach_stdin: bool,
pub attach_stdout: bool,
pub attach_stderr: bool,
pub tty: bool,
pub user: Option<String>,
pub privileged: bool,
}
impl Default for ExecConfig {
fn default() -> Self {
Self {
container_id: ContainerId::from_string(""),
cmd: vec![],
env: vec![],
working_dir: None,
attach_stdin: false,
attach_stdout: true,
attach_stderr: true,
tty: false,
user: None,
privileged: false,
}
}
}
#[derive(Debug, Clone)]
pub struct ExecInstance {
pub id: ExecId,
pub config: ExecConfig,
pub running: bool,
pub exit_code: Option<i32>,
pub pid: Option<u32>,
pub created: DateTime<Utc>,
}
impl ExecInstance {
#[must_use]
pub fn new(config: ExecConfig) -> Self {
Self {
id: ExecId::new(),
config,
running: false,
exit_code: None,
pid: None,
created: Utc::now(),
}
}
}
#[derive(Debug, Clone)]
pub struct ExecStartParams {
pub exec_id: String,
pub container_id: String,
pub cmd: Vec<String>,
pub env: Vec<(String, String)>,
pub working_dir: Option<String>,
pub user: Option<String>,
pub tty: bool,
pub detach: bool,
pub tty_width: u32,
pub tty_height: u32,
}
#[derive(Debug, Clone)]
pub struct ExecStartResult {
pub pid: u32,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
pub exit_code: Option<i32>,
}
#[async_trait]
pub trait ExecAgentConnection: Send + Sync {
async fn exec_start(&self, params: ExecStartParams) -> Result<ExecStartResult, String>;
async fn exec_resize(&self, exec_id: &str, width: u32, height: u32) -> Result<(), String>;
}
pub struct ExecManager {
execs: RwLock<HashMap<String, ExecInstance>>,
agent: Option<Arc<dyn ExecAgentConnection>>,
}
impl ExecManager {
#[must_use]
pub fn new() -> Self {
Self {
execs: RwLock::new(HashMap::new()),
agent: None,
}
}
#[must_use]
pub fn with_agent(agent: Arc<dyn ExecAgentConnection>) -> Self {
Self {
execs: RwLock::new(HashMap::new()),
agent: Some(agent),
}
}
pub fn set_agent(&mut self, agent: Arc<dyn ExecAgentConnection>) {
self.agent = Some(agent);
}
pub fn create(&self, config: ExecConfig) -> crate::error::Result<ExecId> {
let exec = ExecInstance::new(config);
let id = exec.id.clone();
let mut execs = self
.execs
.write()
.map_err(|_| crate::error::ContainerError::LockPoisoned)?;
execs.insert(id.to_string(), exec);
Ok(id)
}
#[must_use]
pub fn get(&self, id: &ExecId) -> Option<ExecInstance> {
self.execs.read().ok()?.get(&id.to_string()).cloned()
}
pub async fn start(
&self,
id: &ExecId,
detach: bool,
tty_width: u32,
tty_height: u32,
) -> crate::Result<ExecStartResult> {
let (config, exec_id_str) = {
let mut execs = self
.execs
.write()
.map_err(|_| crate::ContainerError::Runtime("lock poisoned".to_string()))?;
let exec = execs
.get_mut(&id.to_string())
.ok_or_else(|| crate::ContainerError::not_found(id.to_string()))?;
if exec.running {
return Err(crate::ContainerError::invalid_state(
"exec is already running".to_string(),
));
}
exec.running = true;
(exec.config.clone(), exec.id.to_string())
};
let params = ExecStartParams {
exec_id: exec_id_str.clone(),
container_id: config.container_id.to_string(),
cmd: config.cmd.clone(),
env: config
.env
.iter()
.filter_map(|s| {
let parts: Vec<&str> = s.splitn(2, '=').collect();
if parts.len() == 2 {
Some((parts[0].to_string(), parts[1].to_string()))
} else {
None
}
})
.collect(),
working_dir: config.working_dir.clone(),
user: config.user.clone(),
tty: config.tty,
detach,
tty_width,
tty_height,
};
let result = if let Some(ref agent) = self.agent {
agent.exec_start(params).await.map_err(|e| {
crate::ContainerError::Runtime(format!("agent exec_start failed: {e}"))
})?
} else {
ExecStartResult {
pid: 0,
stdout: Vec::new(),
stderr: Vec::new(),
exit_code: Some(0),
}
};
{
let mut execs = self
.execs
.write()
.map_err(|_| crate::ContainerError::Runtime("lock poisoned".to_string()))?;
if let Some(exec) = execs.get_mut(&exec_id_str) {
exec.pid = Some(result.pid);
if let Some(exit_code) = result.exit_code {
exec.running = false;
exec.exit_code = Some(exit_code);
}
}
}
Ok(result)
}
pub async fn resize(&self, id: &ExecId, width: u32, height: u32) -> crate::Result<()> {
{
let execs = self
.execs
.read()
.map_err(|_| crate::ContainerError::Runtime("lock poisoned".to_string()))?;
let exec = execs
.get(&id.to_string())
.ok_or_else(|| crate::ContainerError::not_found(id.to_string()))?;
if !exec.config.tty {
return Err(crate::ContainerError::invalid_state(
"exec does not have a TTY".to_string(),
));
}
if !exec.running {
return Err(crate::ContainerError::invalid_state(
"exec is not running".to_string(),
));
}
}
if let Some(ref agent) = self.agent {
agent
.exec_resize(&id.to_string(), width, height)
.await
.map_err(|e| {
crate::ContainerError::Runtime(format!("agent exec_resize failed: {e}"))
})?;
}
Ok(())
}
pub fn notify_exit(&self, id: &ExecId, exit_code: i32) {
if let Ok(mut execs) = self.execs.write() {
if let Some(exec) = execs.get_mut(&id.to_string()) {
exec.running = false;
exec.exit_code = Some(exit_code);
}
}
}
#[must_use]
pub fn list_for_container(&self, container_id: &ContainerId) -> Vec<ExecInstance> {
self.execs
.read()
.map(|execs| {
execs
.values()
.filter(|e| e.config.container_id == *container_id)
.cloned()
.collect()
})
.unwrap_or_default()
}
}
impl Default for ExecManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_exec() {
let manager = ExecManager::new();
let config = ExecConfig {
container_id: ContainerId::from_string("test-container"),
cmd: vec!["ls".to_string(), "-la".to_string()],
..Default::default()
};
let id = manager.create(config).unwrap();
let exec = manager.get(&id).unwrap();
assert_eq!(exec.config.cmd, vec!["ls", "-la"]);
assert!(!exec.running);
assert!(exec.exit_code.is_none());
}
#[tokio::test]
async fn test_start_exec() {
let manager = ExecManager::new();
let config = ExecConfig {
container_id: ContainerId::from_string("test-container"),
cmd: vec!["echo".to_string(), "hello".to_string()],
..Default::default()
};
let id = manager.create(config).unwrap();
let result = manager.start(&id, false, 80, 24).await.unwrap();
let exec = manager.get(&id).unwrap();
assert!(!exec.running);
assert_eq!(exec.exit_code, Some(0));
assert_eq!(result.exit_code, Some(0));
}
#[tokio::test]
async fn test_start_exec_detached() {
let manager = ExecManager::new();
let config = ExecConfig {
container_id: ContainerId::from_string("test-container"),
cmd: vec!["sleep".to_string(), "10".to_string()],
..Default::default()
};
let id = manager.create(config).unwrap();
let result = manager.start(&id, true, 80, 24).await.unwrap();
assert_eq!(result.exit_code, Some(0));
}
#[tokio::test]
async fn test_resize_without_tty() {
let manager = ExecManager::new();
let config = ExecConfig {
container_id: ContainerId::from_string("test-container"),
cmd: vec!["echo".to_string(), "hello".to_string()],
tty: false,
..Default::default()
};
let id = manager.create(config).unwrap();
let result = manager.resize(&id, 100, 40).await;
assert!(result.is_err());
}
#[test]
fn test_notify_exit() {
let manager = ExecManager::new();
let config = ExecConfig {
container_id: ContainerId::from_string("test-container"),
cmd: vec!["sleep".to_string(), "10".to_string()],
..Default::default()
};
let id = manager.create(config).unwrap();
{
let mut execs = manager.execs.write().unwrap();
if let Some(exec) = execs.get_mut(&id.to_string()) {
exec.running = true;
exec.pid = Some(12345);
}
}
manager.notify_exit(&id, 42);
let exec = manager.get(&id).unwrap();
assert!(!exec.running);
assert_eq!(exec.exit_code, Some(42));
}
}