prt_core/core/
container.rs1use std::collections::HashMap;
11use std::process::Command;
12use std::time::Duration;
13
14const DOCKER_TIMEOUT_SECS: u64 = 2;
16
17pub fn resolve_container_names(pids: &[u32]) -> HashMap<u32, String> {
23 if pids.is_empty() {
24 return HashMap::new();
25 }
26
27 docker_resolve(pids)
29 .or_else(|| podman_resolve(pids))
30 .unwrap_or_default()
31}
32
33pub fn has_containers(names: &HashMap<u32, String>) -> bool {
35 !names.is_empty()
36}
37
38fn docker_resolve(pids: &[u32]) -> Option<HashMap<u32, String>> {
40 let output = run_with_timeout(
42 "docker",
43 &["ps", "--no-trunc", "--format", "{{.ID}} {{.Names}}"],
44 )?;
45
46 if output.is_empty() {
47 return Some(HashMap::new());
48 }
49
50 let containers: Vec<(String, String)> = output
51 .lines()
52 .filter_map(|line| {
53 let mut parts = line.splitn(2, ' ');
54 let id = parts.next()?.trim().to_string();
55 let name = parts.next()?.trim().to_string();
56 if id.is_empty() || name.is_empty() {
57 None
58 } else {
59 Some((id, name))
60 }
61 })
62 .collect();
63
64 if containers.is_empty() {
65 return Some(HashMap::new());
66 }
67
68 let mut result = HashMap::new();
70 for (id, name) in &containers {
71 if let Some(container_pid) = get_container_pid("docker", id) {
72 if pids.contains(&container_pid) {
73 result.insert(container_pid, name.clone());
74 }
75 }
76 }
77
78 Some(result)
79}
80
81fn podman_resolve(pids: &[u32]) -> Option<HashMap<u32, String>> {
83 let output = run_with_timeout(
84 "podman",
85 &["ps", "--no-trunc", "--format", "{{.ID}} {{.Names}}"],
86 )?;
87
88 if output.is_empty() {
89 return Some(HashMap::new());
90 }
91
92 let containers: Vec<(String, String)> = output
93 .lines()
94 .filter_map(|line| {
95 let mut parts = line.splitn(2, ' ');
96 let id = parts.next()?.trim().to_string();
97 let name = parts.next()?.trim().to_string();
98 if id.is_empty() || name.is_empty() {
99 None
100 } else {
101 Some((id, name))
102 }
103 })
104 .collect();
105
106 if containers.is_empty() {
107 return Some(HashMap::new());
108 }
109
110 let mut result = HashMap::new();
111 for (id, name) in &containers {
112 if let Some(container_pid) = get_container_pid("podman", id) {
113 if pids.contains(&container_pid) {
114 result.insert(container_pid, name.clone());
115 }
116 }
117 }
118
119 Some(result)
120}
121
122fn get_container_pid(runtime: &str, container_id: &str) -> Option<u32> {
124 let output = run_with_timeout(
125 runtime,
126 &["inspect", "--format", "{{.State.Pid}}", container_id],
127 )?;
128 output.trim().parse().ok().filter(|&pid: &u32| pid > 0)
129}
130
131fn run_with_timeout(cmd: &str, args: &[&str]) -> Option<String> {
134 let mut child = Command::new(cmd)
135 .args(args)
136 .stdout(std::process::Stdio::piped())
137 .stderr(std::process::Stdio::null())
138 .spawn()
139 .ok()?;
140
141 let timeout = Duration::from_secs(DOCKER_TIMEOUT_SECS);
142 let start = std::time::Instant::now();
143
144 loop {
145 match child.try_wait() {
146 Ok(Some(status)) => {
147 if !status.success() {
148 return None;
149 }
150 let mut out = String::new();
153 if let Some(mut stdout) = child.stdout.take() {
154 use std::io::Read;
155 let _ = stdout.read_to_string(&mut out);
156 }
157 return if out.is_empty() { None } else { Some(out) };
158 }
159 Ok(None) => {
160 if start.elapsed() > timeout {
161 let _ = child.kill();
162 return None;
163 }
164 std::thread::sleep(Duration::from_millis(50));
165 }
166 Err(_) => return None,
167 }
168 }
169}
170
171#[cfg(test)]
173fn parse_ps_line(line: &str) -> Option<(String, String)> {
174 let mut parts = line.splitn(2, ' ');
175 let id = parts.next()?.trim().to_string();
176 let name = parts.next()?.trim().to_string();
177 if id.is_empty() || name.is_empty() {
178 None
179 } else {
180 Some((id, name))
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn parse_ps_line_valid() {
190 let (id, name) = parse_ps_line("abc123def456 my-nginx").unwrap();
191 assert_eq!(id, "abc123def456");
192 assert_eq!(name, "my-nginx");
193 }
194
195 #[test]
196 fn parse_ps_line_with_spaces_in_name() {
197 let (id, name) = parse_ps_line("abc123 my container").unwrap();
199 assert_eq!(id, "abc123");
200 assert_eq!(name, "my container");
201 }
202
203 #[test]
204 fn parse_ps_line_empty_returns_none() {
205 assert!(parse_ps_line("").is_none());
206 assert!(parse_ps_line(" ").is_none());
207 }
208
209 #[test]
210 fn parse_ps_line_no_name_returns_none() {
211 assert!(parse_ps_line("abc123").is_none());
212 }
213
214 #[test]
215 fn resolve_empty_pids() {
216 let result = resolve_container_names(&[]);
217 assert!(result.is_empty());
218 }
219
220 #[test]
221 fn has_containers_empty() {
222 assert!(!has_containers(&HashMap::new()));
223 }
224
225 #[test]
226 fn has_containers_with_data() {
227 let mut m = HashMap::new();
228 m.insert(1, "nginx".to_string());
229 assert!(has_containers(&m));
230 }
231}