1use crate::core::filters::resolve_dir;
15use crate::core::{
16 flatten_targets, resolve_in_dir, sort_processes, NameMatch, Process, SortKey, StatusFilter,
17};
18use crate::error::Result;
19use crate::ui::Printer;
20use clap::Args;
21use std::path::PathBuf;
22
23#[derive(Args, Debug)]
25pub struct ListCommand {
26 #[arg(num_args = 0..)]
28 pub name: Vec<String>,
29
30 #[arg(long)]
32 pub cmd: bool,
33
34 #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
37 pub in_dir: Option<String>,
38
39 #[arg(long, short = 'p')]
41 pub path: Option<String>,
42
43 #[arg(long)]
45 pub min_cpu: Option<f32>,
46
47 #[arg(long)]
49 pub min_mem: Option<f64>,
50
51 #[arg(long, value_enum, ignore_case = true)]
53 pub status: Option<StatusFilter>,
54
55 #[arg(long)]
57 pub min_uptime: Option<u64>,
58
59 #[arg(long)]
61 pub parent: Option<u32>,
62
63 #[arg(long, short = 'j')]
65 pub json: bool,
66
67 #[arg(long, short = 'v')]
69 pub verbose: bool,
70
71 #[arg(long, short = 'w')]
73 pub wide: bool,
74
75 #[arg(long, short = 'n')]
77 pub limit: Option<usize>,
78
79 #[arg(long, short = 's', value_enum, default_value_t = SortKey::Cpu)]
81 pub sort: SortKey,
82}
83
84impl ListCommand {
85 pub fn execute(&self) -> Result<()> {
87 let printer = Printer::from_flags(self.json, self.verbose).wide(self.wide);
88
89 let mut processes = if !self.name.is_empty() {
92 let names = flatten_targets(&self.name);
93 let mut all = Vec::new();
94 let mut seen = std::collections::HashSet::new();
95 for n in &names {
96 if let Ok(found) = Process::find_by_name(n, NameMatch::from_flag(self.cmd)) {
97 for p in found {
98 if seen.insert(p.pid) {
99 all.push(p);
100 }
101 }
102 }
103 }
104 all
105 } else {
106 Process::find_all()?
107 };
108
109 Process::measure_cpu(&mut processes);
111
112 let in_dir_filter = resolve_in_dir(&self.in_dir)?;
114 let in_dir_has_any = in_dir_filter.as_ref().is_some_and(|dir_path| {
115 processes
116 .iter()
117 .any(|p| crate::core::filters::matches_in_filter(p, dir_path))
118 });
119 crate::core::require_in_dir(&self.in_dir, &in_dir_filter, in_dir_has_any)?;
120
121 let path_filter: Option<PathBuf> = match &self.path {
123 Some(p) => Some(resolve_dir(PathBuf::from(p))?),
124 None => None,
125 };
126
127 processes.retain(|p| self.keeps(p, &in_dir_filter, &path_filter));
129
130 sort_processes(&mut processes, self.sort);
132
133 if let Some(limit) = self.limit {
135 processes.truncate(limit);
136 }
137
138 let context = in_dir_filter
140 .as_ref()
141 .map(|p| format!("in {}", p.display()));
142
143 printer.print_processes_with_context(&processes, context.as_deref());
144 Ok(())
145 }
146
147 fn keeps(
150 &self,
151 p: &Process,
152 in_dir_filter: &Option<PathBuf>,
153 path_filter: &Option<PathBuf>,
154 ) -> bool {
155 if let Some(ref dir_path) = in_dir_filter {
157 if !crate::core::filters::matches_in_filter(p, dir_path) {
158 return false;
159 }
160 }
161
162 if let Some(ref exe_path) = path_filter {
164 if let Some(ref proc_exe) = p.exe_path {
165 let proc_path = PathBuf::from(proc_exe);
166 if !proc_path.starts_with(exe_path) {
167 return false;
168 }
169 } else {
170 return false;
171 }
172 }
173
174 if let Some(min_cpu) = self.min_cpu {
176 if p.cpu_percent < min_cpu {
177 return false;
178 }
179 }
180
181 if let Some(min_mem) = self.min_mem {
183 if p.memory_mb < min_mem {
184 return false;
185 }
186 }
187
188 if let Some(status) = self.status {
190 if !status.matches(p.status) {
191 return false;
192 }
193 }
194
195 if let Some(min_uptime) = self.min_uptime {
197 if let Some(start_time) = p.start_time {
198 let now = std::time::SystemTime::now()
199 .duration_since(std::time::UNIX_EPOCH)
200 .map(|d| d.as_secs())
201 .unwrap_or(0);
202 if now.saturating_sub(start_time) < min_uptime {
203 return false;
204 }
205 } else {
206 return false;
207 }
208 }
209
210 if let Some(ppid) = self.parent {
212 if p.parent_pid != Some(ppid) {
213 return false;
214 }
215 }
216
217 true
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use crate::core::ProcessStatus;
225
226 fn command() -> ListCommand {
227 ListCommand {
228 name: Vec::new(),
229 cmd: false,
230 in_dir: None,
231 path: None,
232 min_cpu: None,
233 min_mem: None,
234 status: None,
235 min_uptime: None,
236 parent: None,
237 json: false,
238 verbose: false,
239 wide: false,
240 limit: None,
241 sort: SortKey::Cpu,
242 }
243 }
244
245 fn bare_process() -> Process {
249 Process {
250 pid: 1,
251 name: "x".to_string(),
252 exe_path: None,
253 cwd: None,
254 command: None,
255 argv0: None,
256 raw: None,
257 cpu_percent: 0.0,
258 memory_mb: 0.0,
259 status: ProcessStatus::Sleeping,
260 user: None,
261 parent_pid: None,
262 start_time: None,
263 uptime_secs: None,
264 }
265 }
266
267 #[test]
268 fn a_process_with_no_working_directory_fails_in() {
269 let dir = Some(PathBuf::from("/"));
270 assert!(!command().keeps(&bare_process(), &dir, &None));
271 assert!(command().keeps(&bare_process(), &None, &None));
272 }
273
274 #[test]
275 fn a_process_with_no_executable_fails_path() {
276 let path = Some(PathBuf::from("/usr"));
277 assert!(!command().keeps(&bare_process(), &None, &path));
278 }
279
280 #[test]
281 fn a_process_with_no_start_time_fails_min_uptime() {
282 let cmd = ListCommand {
283 min_uptime: Some(0),
284 ..command()
285 };
286 assert!(!cmd.keeps(&bare_process(), &None, &None));
287 }
288
289 #[test]
290 fn a_process_that_passes_every_filter_is_kept() {
291 let mut process = bare_process();
292 process.cwd = Some("/srv/app".to_string());
293 process.exe_path = Some("/usr/bin/node".to_string());
294 process.start_time = Some(0);
295 process.parent_pid = Some(7);
296 let cmd = ListCommand {
297 min_cpu: Some(0.0),
298 min_mem: Some(0.0),
299 status: Some(StatusFilter::Sleeping),
300 min_uptime: Some(1),
301 parent: Some(7),
302 ..command()
303 };
304 let dir = Some(PathBuf::from("/srv"));
305 let path = Some(PathBuf::from("/usr/bin"));
306 assert!(cmd.keeps(&process, &dir, &path));
307 let other_parent = ListCommand {
309 parent: Some(8),
310 ..command()
311 };
312 assert!(!other_parent.keeps(&process, &dir, &path));
313 }
314
315 #[test]
316 fn each_filter_turns_away_a_process_that_fails_it() {
317 let mut process = bare_process();
318 process.start_time = Some(
319 std::time::SystemTime::now()
320 .duration_since(std::time::UNIX_EPOCH)
321 .unwrap()
322 .as_secs(),
323 );
324 process.memory_mb = 1.0;
325 let turned_away = [
326 ListCommand {
327 min_cpu: Some(50.0),
328 ..command()
329 },
330 ListCommand {
331 min_mem: Some(500.0),
332 ..command()
333 },
334 ListCommand {
335 status: Some(StatusFilter::Zombie),
336 ..command()
337 },
338 ListCommand {
339 min_uptime: Some(3600),
340 ..command()
341 },
342 ListCommand {
343 parent: Some(7),
344 ..command()
345 },
346 ];
347 for cmd in &turned_away {
348 assert!(!cmd.keeps(&process, &None, &None), "{cmd:?}");
349 }
350 }
351}