1use crate::core::{
11 resolve_in_dir, sort_processes, with_ports, NameMatch, PortInfo, Process, SortKey, StatusFilter,
12};
13use crate::error::{ProcError, Result};
14use crate::out_println;
15use crate::ui::{format_duration, format_memory, Printer};
16use clap::Args;
17use colored::*;
18use serde::Serialize;
19use std::collections::HashSet;
20use std::path::PathBuf;
21
22#[derive(Args, Debug)]
24pub struct ForCommand {
25 pub file: String,
27
28 #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
31 pub in_dir: Option<String>,
32
33 #[arg(long = "by", short = 'b')]
35 pub by_name: Option<String>,
36
37 #[arg(long)]
39 pub cmd: bool,
40
41 #[arg(long)]
43 pub min_cpu: Option<f32>,
44
45 #[arg(long)]
47 pub min_mem: Option<f64>,
48
49 #[arg(long, value_enum, ignore_case = true)]
51 pub status: Option<StatusFilter>,
52
53 #[arg(long)]
55 pub min_uptime: Option<u64>,
56
57 #[arg(long, short = 'j')]
59 pub json: bool,
60
61 #[arg(long, short = 'v')]
63 pub verbose: bool,
64
65 #[arg(long, short = 'w')]
67 pub wide: bool,
68
69 #[arg(long, short = 's', value_enum, default_value_t = SortKey::Cpu)]
71 pub sort: SortKey,
72
73 #[arg(long, short = 'n')]
75 pub limit: Option<usize>,
76}
77
78impl ForCommand {
79 pub fn execute(&self) -> Result<()> {
81 let file_path = self.resolve_path(&self.file)?;
83
84 let exe_processes = Process::find_by_exe_path(&file_path)?;
86
87 let open_file_procs = Process::find_by_open_file(&file_path)?;
89
90 let mut seen_pids = HashSet::new();
92 let mut processes: Vec<Process> = Vec::new();
93
94 for proc in exe_processes.into_iter().chain(open_file_procs) {
97 if seen_pids.insert(proc.pid) {
98 processes.push(proc);
99 }
100 }
101
102 Process::measure_cpu(&mut processes);
104 self.apply_filters(&mut processes)?;
105
106 sort_processes(&mut processes, self.sort);
108
109 if let Some(limit) = self.limit {
111 processes.truncate(limit);
112 }
113
114 if processes.is_empty() {
116 return Err(ProcError::ProcessNotFound(self.file.clone()));
117 }
118
119 let results = with_ports(processes)?;
121
122 if self.json {
124 self.print_json(&results)?;
125 } else {
126 self.print_human(&results);
127 }
128
129 Ok(())
130 }
131
132 fn resolve_path(&self, path: &str) -> Result<PathBuf> {
133 let expanded = if let Some(stripped) = path.strip_prefix("~/") {
135 if let Ok(home) = std::env::var("HOME") {
136 PathBuf::from(home).join(stripped)
137 } else {
138 PathBuf::from(path)
139 }
140 } else if path == "~" {
141 PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| ".".to_string()))
142 } else {
143 PathBuf::from(path)
144 };
145
146 let absolute = if expanded.is_relative() {
148 crate::core::filters::current_dir_for(&expanded)?.join(expanded)
149 } else {
150 expanded
151 };
152
153 let canonical = absolute
155 .canonicalize()
156 .map_err(|_| ProcError::InvalidInput(format!("File not found: {}", path)))?;
157
158 #[cfg(windows)]
160 let canonical = PathBuf::from(crate::core::filters::strip_verbatim_prefix(
161 &canonical.to_string_lossy(),
162 ));
163
164 Ok(canonical)
165 }
166
167 fn apply_filters(&self, processes: &mut Vec<Process>) -> Result<()> {
168 let in_dir_filter = resolve_in_dir(&self.in_dir)?;
169 let in_dir_has_any = in_dir_filter.as_ref().is_some_and(|dir_path| {
170 processes
171 .iter()
172 .any(|p| crate::core::filters::matches_in_filter(p, dir_path))
173 });
174 crate::core::require_in_dir(&self.in_dir, &in_dir_filter, in_dir_has_any)?;
175
176 processes.retain(|p| {
177 if let Some(ref dir_path) = in_dir_filter {
179 if !crate::core::filters::matches_in_filter(p, dir_path) {
180 return false;
181 }
182 }
183
184 if let Some(ref name) = self.by_name {
186 if !crate::core::matches_by_filter(p, name, NameMatch::from_flag(self.cmd)) {
187 return false;
188 }
189 }
190
191 if let Some(min_cpu) = self.min_cpu {
193 if p.cpu_percent < min_cpu {
194 return false;
195 }
196 }
197
198 if let Some(min_mem) = self.min_mem {
200 if p.memory_mb < min_mem {
201 return false;
202 }
203 }
204
205 if let Some(status) = self.status {
207 if !status.matches(p.status) {
208 return false;
209 }
210 }
211
212 if let Some(min_uptime) = self.min_uptime {
214 if let Some(start_time) = p.start_time {
215 let now = std::time::SystemTime::now()
216 .duration_since(std::time::UNIX_EPOCH)
217 .map(|d| d.as_secs())
218 .unwrap_or(0);
219 if now.saturating_sub(start_time) < min_uptime {
220 return false;
221 }
222 } else {
223 return false;
224 }
225 }
226
227 true
228 });
229 Ok(())
230 }
231
232 fn print_human(&self, results: &[(Process, Vec<PortInfo>)]) {
233 let count = results.len();
234 let file_display = &self.file;
235
236 if count == 1 {
237 let (proc, ports) = &results[0];
239
240 out_println!(
241 "{} Found 1 process for {}",
242 "✓".green().bold(),
243 file_display.cyan().bold()
244 );
245 out_println!();
246
247 out_println!(
249 " {} {} (PID {})",
250 "Process:".bright_black(),
251 proc.name.white().bold(),
252 proc.pid.to_string().cyan()
253 );
254 if let Some(ref cwd) = proc.cwd {
255 out_println!(" {} {}", "Directory:".bright_black(), cwd);
256 }
257 if let Some(ref path) = proc.exe_path {
258 out_println!(" {} {}", "Path:".bright_black(), path.bright_black());
259 }
260 for port_info in ports {
261 let addr = port_info.address.as_deref().unwrap_or("*");
262 out_println!(
263 " {} :{} ({} on {})",
264 "Listening:".bright_black(),
265 port_info.port.to_string().cyan(),
266 format!("{:?}", port_info.protocol).to_uppercase(),
267 addr
268 );
269 }
270 out_println!(
271 " {} {:.1}% CPU, {}",
272 "Resources:".bright_black(),
273 proc.cpu_percent,
274 format_memory(proc.memory_mb)
275 );
276 if let Some(uptime) = proc.uptime_secs {
277 out_println!(" {} {}", "Uptime:".bright_black(), format_duration(uptime));
278 }
279 if self.verbose {
280 if let Some(ref cmd) = proc.command {
281 out_println!(" {} {}", "Command:".bright_black(), cmd.bright_black());
282 }
283 }
284 out_println!();
285 } else {
286 let processes: Vec<Process> = results.iter().map(|(p, _)| p.clone()).collect();
288 Printer::from_flags(false, self.verbose)
289 .wide(self.wide)
290 .print_processes_as("for", &processes, Some(&format!("for {}", file_display)));
291
292 let listening: Vec<_> = results
293 .iter()
294 .flat_map(|(proc, ports)| ports.iter().map(move |p| (proc, p)))
295 .collect();
296 if !listening.is_empty() {
297 out_println!(" {}", "Listening:".bright_black());
298 for (proc, port_info) in listening {
299 let addr = port_info.address.as_deref().unwrap_or("*");
300 out_println!(
301 " {} :{} ({} on {}) {} [PID {}]",
302 "→".bright_black(),
303 port_info.port.to_string().cyan(),
304 format!("{:?}", port_info.protocol).to_uppercase(),
305 addr,
306 proc.name.white(),
307 proc.pid.to_string().cyan()
308 );
309 }
310 out_println!();
311 }
312 }
313 }
314
315 fn print_json(&self, results: &[(Process, Vec<PortInfo>)]) -> Result<()> {
316 let printer = Printer::from_flags(true, self.verbose);
317 let items: Vec<ProcessForJson> = results
318 .iter()
319 .map(|(proc, ports)| ProcessForJson {
320 process: proc,
321 ports,
322 })
323 .collect();
324
325 printer.print_json(&ForOutput {
326 action: "for",
327 success: true,
328 count: items.len(),
329 results: &items,
330 });
331 Ok(())
332 }
333}
334
335#[derive(Serialize)]
336struct ProcessForJson<'a> {
337 process: &'a Process,
338 ports: &'a [PortInfo],
339}
340
341#[derive(Serialize)]
342struct ForOutput<'a> {
343 action: &'static str,
344 success: bool,
345 count: usize,
346 results: &'a [ProcessForJson<'a>],
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352 use crate::core::ProcessStatus;
353
354 fn command() -> ForCommand {
355 ForCommand {
356 file: "x".to_string(),
357 in_dir: None,
358 by_name: None,
359 cmd: false,
360 min_cpu: None,
361 min_mem: None,
362 status: None,
363 min_uptime: None,
364 json: false,
365 verbose: false,
366 wide: false,
367 sort: SortKey::Cpu,
368 limit: None,
369 }
370 }
371
372 fn bare_process() -> Process {
375 Process {
376 pid: 1,
377 name: "x".to_string(),
378 exe_path: None,
379 cwd: None,
380 command: None,
381 argv0: None,
382 raw: None,
383 cpu_percent: 0.0,
384 memory_mb: 0.0,
385 status: ProcessStatus::Sleeping,
386 user: None,
387 parent_pid: None,
388 start_time: None,
389 uptime_secs: None,
390 }
391 }
392
393 #[test]
394 fn a_process_with_no_working_directory_fails_in() {
395 let cmd = ForCommand {
396 in_dir: Some("/".to_string()),
397 ..command()
398 };
399 let mut processes = vec![bare_process()];
400 cmd.apply_filters(&mut processes).unwrap();
401 assert!(processes.is_empty());
402
403 let mut processes = vec![bare_process()];
404 command().apply_filters(&mut processes).unwrap();
405 assert_eq!(processes.len(), 1);
406 }
407
408 #[test]
409 fn a_process_with_no_start_time_fails_min_uptime() {
410 let cmd = ForCommand {
411 min_uptime: Some(0),
412 ..command()
413 };
414 let mut processes = vec![bare_process()];
415 cmd.apply_filters(&mut processes).unwrap();
416 assert!(processes.is_empty());
417 }
418}