1use super::{OptimizationAction, OptimizationResult};
10use serde::{Deserialize, Serialize};
11use std::process::Command;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ContainerInfo {
16 pub id: String,
17 pub name: String,
18 pub image: String,
19 pub status: ContainerStatus,
20 pub memory_mb: f64,
21 pub memory_limit_mb: f64,
22 pub memory_percent: f64,
23 pub cpu_percent: f64,
24 pub created: String,
25 pub ports: Vec<String>,
26 pub is_idle: bool,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31pub enum ContainerStatus {
32 Running,
33 Paused,
34 Exited,
35 Created,
36 Restarting,
37 Dead,
38 Unknown,
39}
40
41impl ContainerStatus {
42 fn from_str(s: &str) -> Self {
43 let s = s.to_lowercase();
44 if s.contains("running") {
45 ContainerStatus::Running
46 } else if s.contains("paused") {
47 ContainerStatus::Paused
48 } else if s.contains("exited") {
49 ContainerStatus::Exited
50 } else if s.contains("created") {
51 ContainerStatus::Created
52 } else if s.contains("restarting") {
53 ContainerStatus::Restarting
54 } else if s.contains("dead") {
55 ContainerStatus::Dead
56 } else {
57 ContainerStatus::Unknown
58 }
59 }
60}
61
62impl ContainerInfo {
63 pub fn get_suggested_action(&self) -> OptimizationAction {
65 match self.status {
66 ContainerStatus::Running => {
67 if self.memory_mb > 2000.0 {
68 OptimizationAction::StopContainer
69 } else if self.is_idle && self.memory_mb > 500.0 {
70 OptimizationAction::PauseContainer
71 } else {
72 OptimizationAction::None
73 }
74 }
75 ContainerStatus::Paused => OptimizationAction::None,
76 _ => OptimizationAction::None,
77 }
78 }
79}
80
81pub struct DockerManager {
83 containers: Vec<ContainerInfo>,
84 docker_available: bool,
85 last_update: std::time::Instant,
86}
87
88impl DockerManager {
89 pub fn new() -> Self {
90 let mut manager = Self {
91 containers: Vec::new(),
92 docker_available: false,
93 last_update: std::time::Instant::now(),
94 };
95 manager.check_docker();
96 manager
97 }
98
99 fn check_docker(&mut self) {
101 self.docker_available = Command::new("docker")
102 .arg("version")
103 .output()
104 .map(|o| o.status.success())
105 .unwrap_or(false);
106 }
107
108 pub fn is_available(&self) -> bool {
110 self.docker_available
111 }
112
113 pub fn refresh(&mut self) {
115 if !self.docker_available {
116 return;
117 }
118
119 self.containers.clear();
120
121 let output = Command::new("docker")
124 .args([
125 "stats",
126 "--no-stream",
127 "--format",
128 "{{.ID}}|{{.Name}}|{{.Container}}|{{.MemUsage}}|{{.MemPerc}}|{{.CPUPerc}}",
129 ])
130 .output();
131
132 if let Ok(output) = output {
133 if output.status.success() {
134 let stdout = String::from_utf8_lossy(&output.stdout);
135 for line in stdout.lines() {
136 if let Some(container) = self.parse_stats_line(line) {
137 self.containers.push(container);
138 }
139 }
140 }
141 }
142
143 self.enrich_container_info();
145
146 self.last_update = std::time::Instant::now();
147 }
148
149 fn parse_stats_line(&self, line: &str) -> Option<ContainerInfo> {
151 let parts: Vec<&str> = line.split('|').collect();
152 if parts.len() < 6 {
153 return None;
154 }
155
156 let id = parts[0].trim().to_string();
157 let name = parts[1].trim().to_string();
158
159 let mem_parts: Vec<&str> = parts[3].split('/').collect();
161 let memory_mb = parse_memory_string(mem_parts.get(0).unwrap_or(&"0"));
162 let memory_limit_mb = parse_memory_string(mem_parts.get(1).unwrap_or(&"0"));
163
164 let memory_percent = parts[4]
166 .trim()
167 .trim_end_matches('%')
168 .parse::<f64>()
169 .unwrap_or(0.0);
170 let cpu_percent = parts[5]
171 .trim()
172 .trim_end_matches('%')
173 .parse::<f64>()
174 .unwrap_or(0.0);
175
176 Some(ContainerInfo {
177 id,
178 name,
179 image: String::new(),
180 status: ContainerStatus::Running,
181 memory_mb,
182 memory_limit_mb,
183 memory_percent,
184 cpu_percent,
185 created: String::new(),
186 ports: Vec::new(),
187 is_idle: cpu_percent < 1.0,
188 })
189 }
190
191 fn enrich_container_info(&mut self) {
193 for container in &mut self.containers {
194 let output = Command::new("docker")
196 .args([
197 "inspect",
198 "--format",
199 "{{.Config.Image}}|{{.State.Status}}|{{.Created}}",
200 &container.id,
201 ])
202 .output();
203
204 if let Ok(output) = output {
205 if output.status.success() {
206 let stdout = String::from_utf8_lossy(&output.stdout);
207 let parts: Vec<&str> = stdout.trim().split('|').collect();
208 if parts.len() >= 3 {
209 container.image = parts[0].to_string();
210 container.status = ContainerStatus::from_str(parts[1]);
211 container.created = parts[2].to_string();
212 }
213 }
214 }
215
216 let output = Command::new("docker")
218 .args(["port", &container.id])
219 .output();
220
221 if let Ok(output) = output {
222 if output.status.success() {
223 let stdout = String::from_utf8_lossy(&output.stdout);
224 container.ports = stdout.lines().map(|s| s.to_string()).collect();
225 }
226 }
227 }
228 }
229
230 pub fn get_containers(&self) -> &[ContainerInfo] {
232 &self.containers
233 }
234
235 pub fn get_running(&self) -> Vec<&ContainerInfo> {
237 self.containers
238 .iter()
239 .filter(|c| c.status == ContainerStatus::Running)
240 .collect()
241 }
242
243 pub fn total_memory_mb(&self) -> f64 {
245 self.containers.iter().map(|c| c.memory_mb).sum()
246 }
247
248 pub fn get_idle_containers(&self) -> Vec<&ContainerInfo> {
250 self.containers
251 .iter()
252 .filter(|c| c.status == ContainerStatus::Running && c.is_idle)
253 .collect()
254 }
255
256 pub fn pause_container(&self, id: &str) -> OptimizationResult {
258 let output = Command::new("docker")
259 .args(["pause", id])
260 .output();
261
262 let container_name = self.containers
263 .iter()
264 .find(|c| c.id == id || c.name == id)
265 .map(|c| c.name.clone())
266 .unwrap_or_else(|| id.to_string());
267
268 match output {
269 Ok(output) if output.status.success() => OptimizationResult {
270 app_name: container_name,
271 action: OptimizationAction::PauseContainer,
272 success: true,
273 memory_freed_mb: 0.0, message: "Container paused successfully".to_string(),
275 },
276 Ok(output) => OptimizationResult {
277 app_name: container_name,
278 action: OptimizationAction::PauseContainer,
279 success: false,
280 memory_freed_mb: 0.0,
281 message: String::from_utf8_lossy(&output.stderr).to_string(),
282 },
283 Err(e) => OptimizationResult {
284 app_name: container_name,
285 action: OptimizationAction::PauseContainer,
286 success: false,
287 memory_freed_mb: 0.0,
288 message: e.to_string(),
289 },
290 }
291 }
292
293 pub fn unpause_container(&self, id: &str) -> OptimizationResult {
295 let output = Command::new("docker")
296 .args(["unpause", id])
297 .output();
298
299 let container_name = self.containers
300 .iter()
301 .find(|c| c.id == id || c.name == id)
302 .map(|c| c.name.clone())
303 .unwrap_or_else(|| id.to_string());
304
305 match output {
306 Ok(output) if output.status.success() => OptimizationResult {
307 app_name: container_name,
308 action: OptimizationAction::None,
309 success: true,
310 memory_freed_mb: 0.0,
311 message: "Container unpaused successfully".to_string(),
312 },
313 Ok(output) => OptimizationResult {
314 app_name: container_name,
315 action: OptimizationAction::None,
316 success: false,
317 memory_freed_mb: 0.0,
318 message: String::from_utf8_lossy(&output.stderr).to_string(),
319 },
320 Err(e) => OptimizationResult {
321 app_name: container_name,
322 action: OptimizationAction::None,
323 success: false,
324 memory_freed_mb: 0.0,
325 message: e.to_string(),
326 },
327 }
328 }
329
330 pub fn stop_container(&self, id: &str) -> OptimizationResult {
332 let container = self.containers
333 .iter()
334 .find(|c| c.id == id || c.name == id);
335
336 let (container_name, memory_freed) = container
337 .map(|c| (c.name.clone(), c.memory_mb))
338 .unwrap_or_else(|| (id.to_string(), 0.0));
339
340 let output = Command::new("docker")
341 .args(["stop", id])
342 .output();
343
344 match output {
345 Ok(output) if output.status.success() => OptimizationResult {
346 app_name: container_name,
347 action: OptimizationAction::StopContainer,
348 success: true,
349 memory_freed_mb: memory_freed,
350 message: format!("Container stopped, freed ~{:.0} MB", memory_freed),
351 },
352 Ok(output) => OptimizationResult {
353 app_name: container_name,
354 action: OptimizationAction::StopContainer,
355 success: false,
356 memory_freed_mb: 0.0,
357 message: String::from_utf8_lossy(&output.stderr).to_string(),
358 },
359 Err(e) => OptimizationResult {
360 app_name: container_name,
361 action: OptimizationAction::StopContainer,
362 success: false,
363 memory_freed_mb: 0.0,
364 message: e.to_string(),
365 },
366 }
367 }
368
369 pub fn get_suggestions(&self) -> Vec<(String, OptimizationAction, String)> {
371 let mut suggestions = Vec::new();
372
373 for container in &self.containers {
374 let action = container.get_suggested_action();
375 if action != OptimizationAction::None {
376 let reason = match &action {
377 OptimizationAction::StopContainer => {
378 format!(
379 "Container '{}' is using {:.0} MB - consider stopping if not needed",
380 container.name, container.memory_mb
381 )
382 }
383 OptimizationAction::PauseContainer => {
384 format!(
385 "Container '{}' is idle but using {:.0} MB - consider pausing",
386 container.name, container.memory_mb
387 )
388 }
389 _ => continue,
390 };
391
392 suggestions.push((container.name.clone(), action, reason));
393 }
394 }
395
396 suggestions
397 }
398
399 pub fn print_summary(&self) {
401 if !self.docker_available {
402 println!("\nπ³ Docker: Not available or not running");
403 return;
404 }
405
406 println!("\nπ³ Docker Container Resource Usage\n");
407
408 if self.containers.is_empty() {
409 println!("No running containers found.");
410 return;
411 }
412
413 println!("ββββββββββββββββββββββββ¬ββββββββββββ¬βββββββββββ¬βββββββββββ¬βββββββββββ");
414 println!("β Container β Memory β CPU β Status β Image β");
415 println!("ββββββββββββββββββββββββΌββββββββββββΌβββββββββββΌβββββββββββΌβββββββββββ€");
416
417 let mut containers: Vec<_> = self.containers.iter().collect();
418 containers.sort_by(|a, b| b.memory_mb.partial_cmp(&a.memory_mb).unwrap());
419
420 for container in &containers {
421 let status = match container.status {
422 ContainerStatus::Running if container.is_idle => "π€ Idle",
423 ContainerStatus::Running => "π’ Running",
424 ContainerStatus::Paused => "βΈοΈ Paused",
425 ContainerStatus::Exited => "βΉοΈ Exited",
426 _ => "β Unknown",
427 };
428
429 let image = truncate(&container.image, 8);
430
431 println!(
432 "β {:20} β {:>7.0} MB β {:>6.1}% β {:8} β {:8} β",
433 truncate(&container.name, 20),
434 container.memory_mb,
435 container.cpu_percent,
436 status,
437 image
438 );
439 }
440
441 println!("ββββββββββββββββββββββββ΄ββββββββββββ΄βββββββββββ΄βββββββββββ΄βββββββββββ");
442
443 let total: f64 = containers.iter().map(|c| c.memory_mb).sum();
444 let running = containers.iter().filter(|c| c.status == ContainerStatus::Running).count();
445 let idle = self.get_idle_containers().len();
446
447 println!(
448 "\nTotal: {:.0} MB across {} containers ({} running, {} idle)",
449 total,
450 containers.len(),
451 running,
452 idle
453 );
454
455 let suggestions = self.get_suggestions();
457 if !suggestions.is_empty() {
458 println!("\nπ‘ Suggestions:");
459 for (_, _, reason) in suggestions.iter().take(3) {
460 println!(" β’ {}", reason);
461 }
462 }
463 }
464}
465
466impl Default for DockerManager {
467 fn default() -> Self {
468 Self::new()
469 }
470}
471
472fn parse_memory_string(s: &str) -> f64 {
474 let s = s.trim().to_lowercase();
475
476 let mut num_str = String::new();
478 let mut unit = String::new();
479
480 for c in s.chars() {
481 if c.is_ascii_digit() || c == '.' {
482 num_str.push(c);
483 } else if c.is_alphabetic() {
484 unit.push(c);
485 }
486 }
487
488 let num: f64 = num_str.parse().unwrap_or(0.0);
489
490 match unit.as_str() {
491 "b" => num / (1024.0 * 1024.0),
492 "kib" | "kb" | "k" => num / 1024.0,
493 "mib" | "mb" | "m" => num,
494 "gib" | "gb" | "g" => num * 1024.0,
495 "tib" | "tb" | "t" => num * 1024.0 * 1024.0,
496 _ => num,
497 }
498}
499
500fn truncate(s: &str, max: usize) -> String {
501 if s.len() <= max {
502 format!("{:width$}", s, width = max)
503 } else {
504 format!("{}...", &s[..max.saturating_sub(3)])
505 }
506}