docker_wrapper/command/
top.rs

1//! Docker top command implementation.
2//!
3//! This module provides the `docker top` command for displaying running processes in a container.
4
5use super::{CommandExecutor, CommandOutput, DockerCommand};
6use crate::error::Result;
7use async_trait::async_trait;
8
9/// Docker top command builder
10///
11/// Display the running processes of a container.
12///
13/// # Example
14///
15/// ```no_run
16/// use docker_wrapper::TopCommand;
17///
18/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
19/// // Show processes in a container
20/// let processes = TopCommand::new("my-container")
21///     .run()
22///     .await?;
23///
24/// // Use custom ps options
25/// let detailed = TopCommand::new("my-container")
26///     .ps_options("aux")
27///     .run()
28///     .await?;
29/// # Ok(())
30/// # }
31/// ```
32#[derive(Debug, Clone)]
33pub struct TopCommand {
34    /// Container name or ID
35    container: String,
36    /// ps command options
37    ps_options: Option<String>,
38    /// Command executor
39    pub executor: CommandExecutor,
40}
41
42impl TopCommand {
43    /// Create a new top command
44    ///
45    /// # Example
46    ///
47    /// ```
48    /// use docker_wrapper::TopCommand;
49    ///
50    /// let cmd = TopCommand::new("my-container");
51    /// ```
52    #[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    /// Set ps command options
62    ///
63    /// # Example
64    ///
65    /// ```
66    /// use docker_wrapper::TopCommand;
67    ///
68    /// // Show detailed process information
69    /// let cmd = TopCommand::new("my-container")
70    ///     .ps_options("aux");
71    ///
72    /// // Show processes with specific format
73    /// let cmd = TopCommand::new("my-container")
74    ///     .ps_options("-eo pid,ppid,cmd,%mem,%cpu");
75    /// ```
76    #[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    /// Execute the top command
83    ///
84    /// # Errors
85    /// Returns an error if:
86    /// - The Docker daemon is not running
87    /// - The container doesn't exist
88    /// - The container is not running
89    ///
90    /// # Example
91    ///
92    /// ```no_run
93    /// use docker_wrapper::TopCommand;
94    ///
95    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
96    /// let result = TopCommand::new("my-container")
97    ///     .run()
98    ///     .await?;
99    ///
100    /// if result.success() {
101    ///     println!("Container processes:\n{}", result.output().stdout);
102    ///     for process in result.processes() {
103    ///         println!("PID: {}, CMD: {}", process.pid, process.command);
104    ///     }
105    /// }
106    /// # Ok(())
107    /// # }
108    /// ```
109    pub async fn run(&self) -> Result<TopResult> {
110        let output = self.execute().await?;
111
112        // Parse process information from output
113        let processes = Self::parse_processes(&output.stdout);
114
115        Ok(TopResult {
116            output,
117            container: self.container.clone(),
118            processes,
119        })
120    }
121
122    /// Parse process information from top command output
123    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        // First line contains headers, skip it
132        let _headers = lines[0].split_whitespace().collect::<Vec<_>>();
133
134        // Parse each process line
135        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    /// Gets the command executor
166    #[must_use]
167    pub fn get_executor(&self) -> &CommandExecutor {
168        &self.executor
169    }
170
171    /// Gets the command executor mutably
172    pub fn get_executor_mut(&mut self) -> &mut CommandExecutor {
173        &mut self.executor
174    }
175
176    /// Builds the command arguments for Docker top
177    #[must_use]
178    pub fn build_command_args(&self) -> Vec<String> {
179        let mut args = vec!["top".to_string()];
180
181        // Add container name/ID
182        args.push(self.container.clone());
183
184        // Add ps options if specified
185        if let Some(ref options) = self.ps_options {
186            args.push(options.clone());
187        }
188
189        // Add any additional raw arguments
190        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/// Result from the top command
219#[derive(Debug, Clone)]
220pub struct TopResult {
221    /// Raw command output
222    pub output: CommandOutput,
223    /// Container that was inspected
224    pub container: String,
225    /// Parsed process information
226    pub processes: Vec<ContainerProcess>,
227}
228
229impl TopResult {
230    /// Check if the top command was successful
231    #[must_use]
232    pub fn success(&self) -> bool {
233        self.output.success
234    }
235
236    /// Get the container name
237    #[must_use]
238    pub fn container(&self) -> &str {
239        &self.container
240    }
241
242    /// Get the parsed processes
243    #[must_use]
244    pub fn processes(&self) -> &[ContainerProcess] {
245        &self.processes
246    }
247
248    /// Get the raw command output
249    #[must_use]
250    pub fn output(&self) -> &CommandOutput {
251        &self.output
252    }
253
254    /// Get process count
255    #[must_use]
256    pub fn process_count(&self) -> usize {
257        self.processes.len()
258    }
259}
260
261/// Information about a running process in a container
262#[derive(Debug, Clone)]
263pub struct ContainerProcess {
264    /// Process ID
265    pub pid: String,
266    /// User running the process
267    pub user: String,
268    /// CPU time
269    pub time: String,
270    /// Command line
271    pub command: String,
272    /// Raw output line
273    pub raw_line: String,
274}
275
276impl ContainerProcess {
277    /// Get PID as integer
278    #[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}