1use crate::state::ContainerId;
6use async_trait::async_trait;
7use chrono::{DateTime, Utc};
8use std::collections::HashMap;
9use std::sync::{Arc, RwLock};
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub struct ExecId(String);
14
15impl ExecId {
16 #[must_use]
18 pub fn new() -> Self {
19 Self(uuid::Uuid::new_v4().to_string().replace('-', ""))
20 }
21
22 #[must_use]
24 pub fn from_string(s: &str) -> Self {
25 Self(s.to_string())
26 }
27}
28
29impl Default for ExecId {
30 fn default() -> Self {
31 Self::new()
32 }
33}
34
35impl std::fmt::Display for ExecId {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 write!(f, "{}", self.0)
38 }
39}
40
41#[derive(Debug, Clone)]
43pub struct ExecConfig {
44 pub container_id: ContainerId,
46 pub cmd: Vec<String>,
48 pub env: Vec<String>,
50 pub working_dir: Option<String>,
52 pub attach_stdin: bool,
54 pub attach_stdout: bool,
56 pub attach_stderr: bool,
58 pub tty: bool,
60 pub user: Option<String>,
62 pub privileged: bool,
64}
65
66impl Default for ExecConfig {
67 fn default() -> Self {
68 Self {
69 container_id: ContainerId::from_string(""),
70 cmd: vec![],
71 env: vec![],
72 working_dir: None,
73 attach_stdin: false,
74 attach_stdout: true,
75 attach_stderr: true,
76 tty: false,
77 user: None,
78 privileged: false,
79 }
80 }
81}
82
83#[derive(Debug, Clone)]
85pub struct ExecInstance {
86 pub id: ExecId,
88 pub config: ExecConfig,
90 pub running: bool,
92 pub exit_code: Option<i32>,
94 pub pid: Option<u32>,
96 pub created: DateTime<Utc>,
98}
99
100impl ExecInstance {
101 #[must_use]
103 pub fn new(config: ExecConfig) -> Self {
104 Self {
105 id: ExecId::new(),
106 config,
107 running: false,
108 exit_code: None,
109 pid: None,
110 created: Utc::now(),
111 }
112 }
113}
114
115#[derive(Debug, Clone)]
117pub struct ExecStartParams {
118 pub exec_id: String,
120 pub container_id: String,
122 pub cmd: Vec<String>,
124 pub env: Vec<(String, String)>,
126 pub working_dir: Option<String>,
128 pub user: Option<String>,
130 pub tty: bool,
132 pub detach: bool,
134 pub tty_width: u32,
136 pub tty_height: u32,
138}
139
140#[derive(Debug, Clone)]
142pub struct ExecStartResult {
143 pub pid: u32,
145 pub stdout: Vec<u8>,
147 pub stderr: Vec<u8>,
149 pub exit_code: Option<i32>,
151}
152
153#[async_trait]
157pub trait ExecAgentConnection: Send + Sync {
158 async fn exec_start(&self, params: ExecStartParams) -> Result<ExecStartResult, String>;
160
161 async fn exec_resize(&self, exec_id: &str, width: u32, height: u32) -> Result<(), String>;
163}
164
165pub struct ExecManager {
167 execs: RwLock<HashMap<String, ExecInstance>>,
169 agent: Option<Arc<dyn ExecAgentConnection>>,
171}
172
173impl ExecManager {
174 #[must_use]
176 pub fn new() -> Self {
177 Self {
178 execs: RwLock::new(HashMap::new()),
179 agent: None,
180 }
181 }
182
183 #[must_use]
185 pub fn with_agent(agent: Arc<dyn ExecAgentConnection>) -> Self {
186 Self {
187 execs: RwLock::new(HashMap::new()),
188 agent: Some(agent),
189 }
190 }
191
192 pub fn set_agent(&mut self, agent: Arc<dyn ExecAgentConnection>) {
194 self.agent = Some(agent);
195 }
196
197 pub fn create(&self, config: ExecConfig) -> crate::error::Result<ExecId> {
203 let exec = ExecInstance::new(config);
204 let id = exec.id.clone();
205
206 let mut execs = self
207 .execs
208 .write()
209 .map_err(|_| crate::error::ContainerError::LockPoisoned)?;
210 execs.insert(id.to_string(), exec);
211
212 Ok(id)
213 }
214
215 #[must_use]
217 pub fn get(&self, id: &ExecId) -> Option<ExecInstance> {
218 self.execs.read().ok()?.get(&id.to_string()).cloned()
219 }
220
221 pub async fn start(
237 &self,
238 id: &ExecId,
239 detach: bool,
240 tty_width: u32,
241 tty_height: u32,
242 ) -> crate::Result<ExecStartResult> {
243 let (config, exec_id_str) = {
245 let mut execs = self
246 .execs
247 .write()
248 .map_err(|_| crate::ContainerError::Runtime("lock poisoned".to_string()))?;
249
250 let exec = execs
251 .get_mut(&id.to_string())
252 .ok_or_else(|| crate::ContainerError::not_found(id.to_string()))?;
253
254 if exec.running {
255 return Err(crate::ContainerError::invalid_state(
256 "exec is already running".to_string(),
257 ));
258 }
259
260 exec.running = true;
261 (exec.config.clone(), exec.id.to_string())
262 };
263
264 let params = ExecStartParams {
266 exec_id: exec_id_str.clone(),
267 container_id: config.container_id.to_string(),
268 cmd: config.cmd.clone(),
269 env: config
270 .env
271 .iter()
272 .filter_map(|s| {
273 let parts: Vec<&str> = s.splitn(2, '=').collect();
274 if parts.len() == 2 {
275 Some((parts[0].to_string(), parts[1].to_string()))
276 } else {
277 None
278 }
279 })
280 .collect(),
281 working_dir: config.working_dir.clone(),
282 user: config.user.clone(),
283 tty: config.tty,
284 detach,
285 tty_width,
286 tty_height,
287 };
288
289 let result = if let Some(ref agent) = self.agent {
291 agent.exec_start(params).await.map_err(|e| {
292 crate::ContainerError::Runtime(format!("agent exec_start failed: {e}"))
293 })?
294 } else {
295 ExecStartResult {
297 pid: 0,
298 stdout: Vec::new(),
299 stderr: Vec::new(),
300 exit_code: Some(0),
301 }
302 };
303
304 {
306 let mut execs = self
307 .execs
308 .write()
309 .map_err(|_| crate::ContainerError::Runtime("lock poisoned".to_string()))?;
310
311 if let Some(exec) = execs.get_mut(&exec_id_str) {
312 exec.pid = Some(result.pid);
313
314 if let Some(exit_code) = result.exit_code {
315 exec.running = false;
317 exec.exit_code = Some(exit_code);
318 }
319 }
321 }
322
323 Ok(result)
324 }
325
326 pub async fn resize(&self, id: &ExecId, width: u32, height: u32) -> crate::Result<()> {
334 {
336 let execs = self
337 .execs
338 .read()
339 .map_err(|_| crate::ContainerError::Runtime("lock poisoned".to_string()))?;
340
341 let exec = execs
342 .get(&id.to_string())
343 .ok_or_else(|| crate::ContainerError::not_found(id.to_string()))?;
344
345 if !exec.config.tty {
346 return Err(crate::ContainerError::invalid_state(
347 "exec does not have a TTY".to_string(),
348 ));
349 }
350
351 if !exec.running {
352 return Err(crate::ContainerError::invalid_state(
353 "exec is not running".to_string(),
354 ));
355 }
356 }
357
358 if let Some(ref agent) = self.agent {
360 agent
361 .exec_resize(&id.to_string(), width, height)
362 .await
363 .map_err(|e| {
364 crate::ContainerError::Runtime(format!("agent exec_resize failed: {e}"))
365 })?;
366 }
367
368 Ok(())
369 }
370
371 pub fn notify_exit(&self, id: &ExecId, exit_code: i32) {
375 if let Ok(mut execs) = self.execs.write() {
376 if let Some(exec) = execs.get_mut(&id.to_string()) {
377 exec.running = false;
378 exec.exit_code = Some(exit_code);
379 }
380 }
381 }
382
383 #[must_use]
385 pub fn list_for_container(&self, container_id: &ContainerId) -> Vec<ExecInstance> {
386 self.execs
387 .read()
388 .map(|execs| {
389 execs
390 .values()
391 .filter(|e| e.config.container_id == *container_id)
392 .cloned()
393 .collect()
394 })
395 .unwrap_or_default()
396 }
397}
398
399impl Default for ExecManager {
400 fn default() -> Self {
401 Self::new()
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn test_create_exec() {
411 let manager = ExecManager::new();
412 let config = ExecConfig {
413 container_id: ContainerId::from_string("test-container"),
414 cmd: vec!["ls".to_string(), "-la".to_string()],
415 ..Default::default()
416 };
417
418 let id = manager.create(config).unwrap();
419 let exec = manager.get(&id).unwrap();
420
421 assert_eq!(exec.config.cmd, vec!["ls", "-la"]);
422 assert!(!exec.running);
423 assert!(exec.exit_code.is_none());
424 }
425
426 #[tokio::test]
427 async fn test_start_exec() {
428 let manager = ExecManager::new();
429 let config = ExecConfig {
430 container_id: ContainerId::from_string("test-container"),
431 cmd: vec!["echo".to_string(), "hello".to_string()],
432 ..Default::default()
433 };
434
435 let id = manager.create(config).unwrap();
436 let result = manager.start(&id, false, 80, 24).await.unwrap();
438
439 let exec = manager.get(&id).unwrap();
440 assert!(!exec.running);
441 assert_eq!(exec.exit_code, Some(0));
442 assert_eq!(result.exit_code, Some(0));
443 }
444
445 #[tokio::test]
446 async fn test_start_exec_detached() {
447 let manager = ExecManager::new();
448 let config = ExecConfig {
449 container_id: ContainerId::from_string("test-container"),
450 cmd: vec!["sleep".to_string(), "10".to_string()],
451 ..Default::default()
452 };
453
454 let id = manager.create(config).unwrap();
455 let result = manager.start(&id, true, 80, 24).await.unwrap();
457
458 assert_eq!(result.exit_code, Some(0));
460 }
461
462 #[tokio::test]
463 async fn test_resize_without_tty() {
464 let manager = ExecManager::new();
465 let config = ExecConfig {
466 container_id: ContainerId::from_string("test-container"),
467 cmd: vec!["echo".to_string(), "hello".to_string()],
468 tty: false,
469 ..Default::default()
470 };
471
472 let id = manager.create(config).unwrap();
473
474 let result = manager.resize(&id, 100, 40).await;
476 assert!(result.is_err());
477 }
478
479 #[test]
480 fn test_notify_exit() {
481 let manager = ExecManager::new();
482 let config = ExecConfig {
483 container_id: ContainerId::from_string("test-container"),
484 cmd: vec!["sleep".to_string(), "10".to_string()],
485 ..Default::default()
486 };
487
488 let id = manager.create(config).unwrap();
489
490 {
492 let mut execs = manager.execs.write().unwrap();
493 if let Some(exec) = execs.get_mut(&id.to_string()) {
494 exec.running = true;
495 exec.pid = Some(12345);
496 }
497 }
498
499 manager.notify_exit(&id, 42);
501
502 let exec = manager.get(&id).unwrap();
503 assert!(!exec.running);
504 assert_eq!(exec.exit_code, Some(42));
505 }
506}