1use crate::core::{
11 find_ports_for_pid, resolve_in_dir, sort_processes, NameMatch, PortInfo, Process, SortKey,
12 StatusFilter,
13};
14use crate::error::{ProcError, Result};
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 mut results: Vec<(Process, Vec<PortInfo>)> = Vec::new();
121 for proc in processes {
122 let ports = find_ports_for_pid(proc.pid)?;
123 results.push((proc, ports));
124 }
125
126 if self.json {
128 self.print_json(&results)?;
129 } else {
130 self.print_human(&results);
131 }
132
133 Ok(())
134 }
135
136 fn resolve_path(&self, path: &str) -> Result<PathBuf> {
137 let expanded = if let Some(stripped) = path.strip_prefix("~/") {
139 if let Ok(home) = std::env::var("HOME") {
140 PathBuf::from(home).join(stripped)
141 } else {
142 PathBuf::from(path)
143 }
144 } else if path == "~" {
145 PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| ".".to_string()))
146 } else {
147 PathBuf::from(path)
148 };
149
150 let absolute = if expanded.is_relative() {
152 std::env::current_dir()?.join(expanded)
153 } else {
154 expanded
155 };
156
157 let canonical = absolute
159 .canonicalize()
160 .map_err(|_| ProcError::InvalidInput(format!("File not found: {}", path)))?;
161
162 #[cfg(windows)]
164 let canonical = PathBuf::from(crate::core::filters::strip_verbatim_prefix(
165 &canonical.to_string_lossy(),
166 ));
167
168 Ok(canonical)
169 }
170
171 fn apply_filters(&self, processes: &mut Vec<Process>) {
172 let in_dir_filter = resolve_in_dir(&self.in_dir);
173
174 processes.retain(|p| {
175 if let Some(ref dir_path) = in_dir_filter {
177 if let Some(ref proc_cwd) = p.cwd {
178 if !crate::core::filters::cwd_within(proc_cwd, dir_path) {
179 return false;
180 }
181 } else {
182 return false;
183 }
184 }
185
186 if let Some(ref name) = self.by_name {
188 if !crate::core::matches_by_filter(p, name, NameMatch::from_flag(self.cmd)) {
189 return false;
190 }
191 }
192
193 if let Some(min_cpu) = self.min_cpu {
195 if p.cpu_percent < min_cpu {
196 return false;
197 }
198 }
199
200 if let Some(min_mem) = self.min_mem {
202 if p.memory_mb < min_mem {
203 return false;
204 }
205 }
206
207 if let Some(status) = self.status {
209 if !status.matches(p.status) {
210 return false;
211 }
212 }
213
214 if let Some(min_uptime) = self.min_uptime {
216 if let Some(start_time) = p.start_time {
217 let now = std::time::SystemTime::now()
218 .duration_since(std::time::UNIX_EPOCH)
219 .map(|d| d.as_secs())
220 .unwrap_or(0);
221 if now.saturating_sub(start_time) < min_uptime {
222 return false;
223 }
224 } else {
225 return false;
226 }
227 }
228
229 true
230 });
231 }
232
233 fn print_human(&self, results: &[(Process, Vec<PortInfo>)]) {
234 let count = results.len();
235 let file_display = &self.file;
236
237 if count == 1 {
238 let (proc, ports) = &results[0];
240
241 println!(
242 "{} Found 1 process for {}",
243 "✓".green().bold(),
244 file_display.cyan().bold()
245 );
246 println!();
247
248 println!(
250 " {} {} (PID {})",
251 "Process:".bright_black(),
252 proc.name.white().bold(),
253 proc.pid.to_string().cyan()
254 );
255 if let Some(ref cwd) = proc.cwd {
256 println!(" {} {}", "Directory:".bright_black(), cwd);
257 }
258 if let Some(ref path) = proc.exe_path {
259 println!(" {} {}", "Path:".bright_black(), path.bright_black());
260 }
261 for port_info in ports {
262 let addr = port_info.address.as_deref().unwrap_or("*");
263 println!(
264 " {} :{} ({} on {})",
265 "Listening:".bright_black(),
266 port_info.port.to_string().cyan(),
267 format!("{:?}", port_info.protocol).to_uppercase(),
268 addr
269 );
270 }
271 println!(
272 " {} {:.1}% CPU, {}",
273 "Resources:".bright_black(),
274 proc.cpu_percent,
275 format_memory(proc.memory_mb)
276 );
277 if let Some(uptime) = proc.uptime_secs {
278 println!(" {} {}", "Uptime:".bright_black(), format_duration(uptime));
279 }
280 if self.verbose {
281 if let Some(ref cmd) = proc.command {
282 println!(" {} {}", "Command:".bright_black(), cmd.bright_black());
283 }
284 }
285 println!();
286 } else {
287 let processes: Vec<Process> = results.iter().map(|(p, _)| p.clone()).collect();
289 Printer::from_flags(false, self.verbose)
290 .wide(self.wide)
291 .print_processes_as("for", &processes, Some(&format!("for {}", file_display)));
292
293 let listening: Vec<_> = results
294 .iter()
295 .flat_map(|(proc, ports)| ports.iter().map(move |p| (proc, p)))
296 .collect();
297 if !listening.is_empty() {
298 println!(" {}", "Listening:".bright_black());
299 for (proc, port_info) in listening {
300 let addr = port_info.address.as_deref().unwrap_or("*");
301 println!(
302 " {} :{} ({} on {}) {} [PID {}]",
303 "→".bright_black(),
304 port_info.port.to_string().cyan(),
305 format!("{:?}", port_info.protocol).to_uppercase(),
306 addr,
307 proc.name.white(),
308 proc.pid.to_string().cyan()
309 );
310 }
311 println!();
312 }
313 }
314 }
315
316 fn print_json(&self, results: &[(Process, Vec<PortInfo>)]) -> Result<()> {
317 let printer = Printer::from_flags(true, self.verbose);
318 let items: Vec<ProcessForJson> = results
319 .iter()
320 .map(|(proc, ports)| ProcessForJson {
321 process: proc,
322 ports,
323 })
324 .collect();
325
326 printer.print_json(&ForOutput {
327 action: "for",
328 success: true,
329 count: items.len(),
330 results: &items,
331 });
332 Ok(())
333 }
334}
335
336#[derive(Serialize)]
337struct ProcessForJson<'a> {
338 process: &'a Process,
339 ports: &'a [PortInfo],
340}
341
342#[derive(Serialize)]
343struct ForOutput<'a> {
344 action: &'static str,
345 success: bool,
346 count: usize,
347 results: &'a [ProcessForJson<'a>],
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353 use crate::core::ProcessStatus;
354
355 fn command() -> ForCommand {
356 ForCommand {
357 file: "x".to_string(),
358 in_dir: None,
359 by_name: None,
360 cmd: false,
361 min_cpu: None,
362 min_mem: None,
363 status: None,
364 min_uptime: None,
365 json: false,
366 verbose: false,
367 wide: false,
368 sort: SortKey::Cpu,
369 limit: None,
370 }
371 }
372
373 fn bare_process() -> Process {
376 Process {
377 pid: 1,
378 name: "x".to_string(),
379 exe_path: None,
380 cwd: None,
381 command: None,
382 argv0: None,
383 raw: None,
384 cpu_percent: 0.0,
385 memory_mb: 0.0,
386 status: ProcessStatus::Sleeping,
387 user: None,
388 parent_pid: None,
389 start_time: None,
390 uptime_secs: None,
391 }
392 }
393
394 #[test]
395 fn a_process_with_no_working_directory_fails_in() {
396 let cmd = ForCommand {
397 in_dir: Some("/".to_string()),
398 ..command()
399 };
400 let mut processes = vec![bare_process()];
401 cmd.apply_filters(&mut processes);
402 assert!(processes.is_empty());
403
404 let mut processes = vec![bare_process()];
405 command().apply_filters(&mut processes);
406 assert_eq!(processes.len(), 1);
407 }
408
409 #[test]
410 fn a_process_with_no_start_time_fails_min_uptime() {
411 let cmd = ForCommand {
412 min_uptime: Some(0),
413 ..command()
414 };
415 let mut processes = vec![bare_process()];
416 cmd.apply_filters(&mut processes);
417 assert!(processes.is_empty());
418 }
419}