docker_wrapper/command/
top.rs1use super::{CommandExecutor, CommandOutput, DockerCommand};
6use crate::error::Result;
7use async_trait::async_trait;
8
9#[derive(Debug, Clone)]
33pub struct TopCommand {
34 container: String,
36 ps_options: Option<String>,
38 pub executor: CommandExecutor,
40}
41
42impl TopCommand {
43 #[must_use]
53 pub fn new(container: impl Into<String>) -> Self {
54 Self {
55 container: container.into(),
56 ps_options: None,
57 executor: CommandExecutor::new(),
58 }
59 }
60
61 #[must_use]
77 pub fn ps_options(mut self, options: impl Into<String>) -> Self {
78 self.ps_options = Some(options.into());
79 self
80 }
81
82 pub async fn run(&self) -> Result<TopResult> {
110 let output = self.execute().await?;
111
112 let processes = Self::parse_processes(&output.stdout);
114
115 Ok(TopResult {
116 output,
117 container: self.container.clone(),
118 processes,
119 })
120 }
121
122 fn parse_processes(stdout: &str) -> Vec<ContainerProcess> {
124 let mut processes = Vec::new();
125 let lines: Vec<&str> = stdout.lines().collect();
126
127 if lines.len() < 2 {
128 return processes;
129 }
130
131 let _headers = lines[0].split_whitespace().collect::<Vec<_>>();
133
134 for line in lines.iter().skip(1) {
136 let parts: Vec<&str> = line.split_whitespace().collect();
137
138 if !parts.is_empty() {
139 let process = ContainerProcess {
140 pid: (*parts.first().unwrap_or(&"")).to_string(),
141 user: if parts.len() > 1 {
142 parts[1].to_string()
143 } else {
144 String::new()
145 },
146 time: if parts.len() > 2 {
147 parts[2].to_string()
148 } else {
149 String::new()
150 },
151 command: if parts.len() > 3 {
152 parts[3..].join(" ")
153 } else {
154 String::new()
155 },
156 raw_line: (*line).to_string(),
157 };
158 processes.push(process);
159 }
160 }
161
162 processes
163 }
164
165 #[must_use]
167 pub fn get_executor(&self) -> &CommandExecutor {
168 &self.executor
169 }
170
171 pub fn get_executor_mut(&mut self) -> &mut CommandExecutor {
173 &mut self.executor
174 }
175
176 #[must_use]
178 pub fn build_command_args(&self) -> Vec<String> {
179 let mut args = vec!["top".to_string()];
180
181 args.push(self.container.clone());
183
184 if let Some(ref options) = self.ps_options {
186 args.push(options.clone());
187 }
188
189 args.extend(self.executor.raw_args.clone());
191
192 args
193 }
194}
195
196#[async_trait]
197impl DockerCommand for TopCommand {
198 type Output = CommandOutput;
199
200 fn get_executor(&self) -> &CommandExecutor {
201 &self.executor
202 }
203
204 fn get_executor_mut(&mut self) -> &mut CommandExecutor {
205 &mut self.executor
206 }
207
208 fn build_command_args(&self) -> Vec<String> {
209 self.build_command_args()
210 }
211
212 async fn execute(&self) -> Result<Self::Output> {
213 let args = self.build_command_args();
214 self.execute_command(args).await
215 }
216}
217
218#[derive(Debug, Clone)]
220pub struct TopResult {
221 pub output: CommandOutput,
223 pub container: String,
225 pub processes: Vec<ContainerProcess>,
227}
228
229impl TopResult {
230 #[must_use]
232 pub fn success(&self) -> bool {
233 self.output.success
234 }
235
236 #[must_use]
238 pub fn container(&self) -> &str {
239 &self.container
240 }
241
242 #[must_use]
244 pub fn processes(&self) -> &[ContainerProcess] {
245 &self.processes
246 }
247
248 #[must_use]
250 pub fn output(&self) -> &CommandOutput {
251 &self.output
252 }
253
254 #[must_use]
256 pub fn process_count(&self) -> usize {
257 self.processes.len()
258 }
259}
260
261#[derive(Debug, Clone)]
263pub struct ContainerProcess {
264 pub pid: String,
266 pub user: String,
268 pub time: String,
270 pub command: String,
272 pub raw_line: String,
274}
275
276impl ContainerProcess {
277 #[must_use]
279 pub fn pid_as_int(&self) -> Option<u32> {
280 self.pid.parse().ok()
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn test_top_basic() {
290 let cmd = TopCommand::new("test-container");
291 let args = cmd.build_command_args();
292 assert_eq!(args, vec!["top", "test-container"]);
293 }
294
295 #[test]
296 fn test_top_with_ps_options() {
297 let cmd = TopCommand::new("test-container").ps_options("aux");
298 let args = cmd.build_command_args();
299 assert_eq!(args, vec!["top", "test-container", "aux"]);
300 }
301
302 #[test]
303 fn test_top_with_custom_ps_options() {
304 let cmd = TopCommand::new("test-container").ps_options("-eo pid,ppid,cmd");
305 let args = cmd.build_command_args();
306 assert_eq!(args, vec!["top", "test-container", "-eo pid,ppid,cmd"]);
307 }
308
309 #[test]
310 fn test_parse_processes() {
311 let output = "PID USER TIME COMMAND\n1234 root 0:00 nginx: master process\n5678 www-data 0:01 nginx: worker process";
312
313 let processes = TopCommand::parse_processes(output);
314 assert_eq!(processes.len(), 2);
315
316 assert_eq!(processes[0].pid, "1234");
317 assert_eq!(processes[0].user, "root");
318 assert_eq!(processes[0].time, "0:00");
319 assert_eq!(processes[0].command, "nginx: master process");
320
321 assert_eq!(processes[1].pid, "5678");
322 assert_eq!(processes[1].user, "www-data");
323 assert_eq!(processes[1].time, "0:01");
324 assert_eq!(processes[1].command, "nginx: worker process");
325 }
326
327 #[test]
328 fn test_parse_processes_empty() {
329 let processes = TopCommand::parse_processes("");
330 assert!(processes.is_empty());
331 }
332
333 #[test]
334 fn test_parse_processes_headers_only() {
335 let output = "PID USER TIME COMMAND";
336 let processes = TopCommand::parse_processes(output);
337 assert!(processes.is_empty());
338 }
339
340 #[test]
341 fn test_container_process_pid_as_int() {
342 let process = ContainerProcess {
343 pid: "1234".to_string(),
344 user: "root".to_string(),
345 time: "0:00".to_string(),
346 command: "nginx".to_string(),
347 raw_line: "1234 root 0:00 nginx".to_string(),
348 };
349
350 assert_eq!(process.pid_as_int(), Some(1234));
351 }
352
353 #[test]
354 fn test_container_process_invalid_pid() {
355 let process = ContainerProcess {
356 pid: "invalid".to_string(),
357 user: "root".to_string(),
358 time: "0:00".to_string(),
359 command: "nginx".to_string(),
360 raw_line: "invalid root 0:00 nginx".to_string(),
361 };
362
363 assert_eq!(process.pid_as_int(), None);
364 }
365}