Skip to main content

proc_cli/commands/
for_file.rs

1//! `proc for` - Find processes by file path
2//!
3//! Examples:
4//!   proc for script.py           # What's running this script?
5//!   proc for ./app               # Relative path
6//!   proc for /var/log/app.log    # What has this file open?
7//!   proc for ~/bin/myapp         # Tilde expansion
8
9use crate::core::{
10    find_ports_for_pid, resolve_in_dir, sort_processes, PortInfo, Process, SortKey, StatusFilter,
11};
12use crate::error::{ProcError, Result};
13use crate::ui::{format_duration, format_memory, Printer};
14use clap::Args;
15use colored::*;
16use serde::Serialize;
17use std::collections::HashSet;
18use std::path::PathBuf;
19
20/// Find processes by file path
21#[derive(Args, Debug)]
22pub struct ForCommand {
23    /// File path (relative, absolute, or with ~)
24    pub file: String,
25
26    /// Filter by working directory (defaults to current directory if no path given)
27    #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
28    pub in_dir: Option<String>,
29
30    /// Filter by process name
31    #[arg(long = "by", short = 'b')]
32    pub by_name: Option<String>,
33
34    /// Only show processes using more than this CPU %
35    #[arg(long)]
36    pub min_cpu: Option<f32>,
37
38    /// Only show processes using more than this memory (MB)
39    #[arg(long)]
40    pub min_mem: Option<f64>,
41
42    /// Filter by status: running, sleeping, stopped, zombie
43    #[arg(long, value_enum, ignore_case = true)]
44    pub status: Option<StatusFilter>,
45
46    /// Only show processes running longer than this (seconds)
47    #[arg(long)]
48    pub min_uptime: Option<u64>,
49
50    /// Output as JSON
51    #[arg(long, short = 'j')]
52    pub json: bool,
53
54    /// Show verbose output
55    #[arg(long, short = 'v')]
56    pub verbose: bool,
57
58    /// Sort by: cpu, mem, pid, name
59    #[arg(long, short = 's', value_enum, default_value_t = SortKey::Cpu)]
60    pub sort: SortKey,
61
62    /// Limit the number of results
63    #[arg(long, short = 'n')]
64    pub limit: Option<usize>,
65}
66
67impl ForCommand {
68    /// Executes the for command, finding processes by file path.
69    pub fn execute(&self) -> Result<()> {
70        // 1. Resolve file path (relative, tilde, canonicalize)
71        let file_path = self.resolve_path(&self.file)?;
72
73        // 2. Find processes by executable path
74        let exe_processes = Process::find_by_exe_path(&file_path)?;
75
76        // 3. Find processes with file open (lsof)
77        let open_file_procs = Process::find_by_open_file(&file_path)?;
78
79        // 4. Merge and deduplicate by PID
80        let mut seen_pids = HashSet::new();
81        let mut processes: Vec<Process> = Vec::new();
82
83        for proc in exe_processes {
84            if seen_pids.insert(proc.pid) {
85                processes.push(proc);
86            }
87        }
88
89        for proc in open_file_procs {
90            if seen_pids.insert(proc.pid) {
91                processes.push(proc);
92            }
93        }
94
95        // 5. Apply filters
96        self.apply_filters(&mut processes);
97
98        // 6. Sort processes
99        sort_processes(&mut processes, self.sort);
100
101        // 7. Apply limit if specified
102        if let Some(limit) = self.limit {
103            processes.truncate(limit);
104        }
105
106        // 8. Handle no results
107        if processes.is_empty() {
108            return Err(ProcError::ProcessNotFound(format!(
109                "No processes found for file: {}",
110                self.file
111            )));
112        }
113
114        // 9. For each process, get ports
115        let mut results: Vec<(Process, Vec<PortInfo>)> = Vec::new();
116        for proc in processes {
117            let ports = find_ports_for_pid(proc.pid)?;
118            results.push((proc, ports));
119        }
120
121        // 10. Output
122        if self.json {
123            self.print_json(&results)?;
124        } else {
125            self.print_human(&results);
126        }
127
128        Ok(())
129    }
130
131    fn resolve_path(&self, path: &str) -> Result<PathBuf> {
132        // Tilde expansion
133        let expanded = if let Some(stripped) = path.strip_prefix("~/") {
134            if let Ok(home) = std::env::var("HOME") {
135                PathBuf::from(home).join(stripped)
136            } else {
137                PathBuf::from(path)
138            }
139        } else if path == "~" {
140            PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| ".".to_string()))
141        } else {
142            PathBuf::from(path)
143        };
144
145        // Make absolute if relative
146        let absolute = if expanded.is_relative() {
147            std::env::current_dir()?.join(expanded)
148        } else {
149            expanded
150        };
151
152        // Canonicalize (resolve symlinks, normalize)
153        absolute
154            .canonicalize()
155            .map_err(|_| ProcError::InvalidInput(format!("File not found: {}", path)))
156    }
157
158    fn apply_filters(&self, processes: &mut Vec<Process>) {
159        let in_dir_filter = resolve_in_dir(&self.in_dir);
160
161        processes.retain(|p| {
162            // Directory filter (--in)
163            if let Some(ref dir_path) = in_dir_filter {
164                if let Some(ref proc_cwd) = p.cwd {
165                    let proc_path = PathBuf::from(proc_cwd);
166                    if !proc_path.starts_with(dir_path) {
167                        return false;
168                    }
169                } else {
170                    return false;
171                }
172            }
173
174            // Name filter (--by)
175            if let Some(ref name) = self.by_name {
176                if !crate::core::matches_by_filter(p, name) {
177                    return false;
178                }
179            }
180
181            // CPU filter
182            if let Some(min_cpu) = self.min_cpu {
183                if p.cpu_percent < min_cpu {
184                    return false;
185                }
186            }
187
188            // Memory filter
189            if let Some(min_mem) = self.min_mem {
190                if p.memory_mb < min_mem {
191                    return false;
192                }
193            }
194
195            // Status filter
196            if let Some(status) = self.status {
197                if !status.matches(p.status) {
198                    return false;
199                }
200            }
201
202            // Uptime filter
203            if let Some(min_uptime) = self.min_uptime {
204                if let Some(start_time) = p.start_time {
205                    let now = std::time::SystemTime::now()
206                        .duration_since(std::time::UNIX_EPOCH)
207                        .map(|d| d.as_secs())
208                        .unwrap_or(0);
209                    if now.saturating_sub(start_time) < min_uptime {
210                        return false;
211                    }
212                } else {
213                    return false;
214                }
215            }
216
217            true
218        });
219    }
220
221    fn print_human(&self, results: &[(Process, Vec<PortInfo>)]) {
222        let count = results.len();
223        let file_display = &self.file;
224
225        if count == 1 {
226            // Single process - detailed view (like `on` command)
227            let (proc, ports) = &results[0];
228
229            println!(
230                "{} Found 1 process for {}",
231                "✓".green().bold(),
232                file_display.cyan().bold()
233            );
234            println!();
235
236            // Same labels and order as `proc on :port`
237            println!(
238                "  {} {} (PID {})",
239                "Process:".bright_black(),
240                proc.name.white().bold(),
241                proc.pid.to_string().cyan()
242            );
243            if let Some(ref cwd) = proc.cwd {
244                println!("  {} {}", "Directory:".bright_black(), cwd);
245            }
246            if let Some(ref path) = proc.exe_path {
247                println!("  {} {}", "Path:".bright_black(), path.bright_black());
248            }
249            for port_info in ports {
250                let addr = port_info.address.as_deref().unwrap_or("*");
251                println!(
252                    "  {} :{} ({} on {})",
253                    "Listening:".bright_black(),
254                    port_info.port.to_string().cyan(),
255                    format!("{:?}", port_info.protocol).to_uppercase(),
256                    addr
257                );
258            }
259            println!(
260                "  {} {:.1}% CPU, {}",
261                "Resources:".bright_black(),
262                proc.cpu_percent,
263                format_memory(proc.memory_mb)
264            );
265            if let Some(uptime) = proc.uptime_secs {
266                println!("  {} {}", "Uptime:".bright_black(), format_duration(uptime));
267            }
268            if self.verbose {
269                if let Some(ref cmd) = proc.command {
270                    println!("  {} {}", "Command:".bright_black(), cmd.bright_black());
271                }
272            }
273            println!();
274        } else {
275            // Multiple processes: the standard process table, then who listens where
276            let processes: Vec<Process> = results.iter().map(|(p, _)| p.clone()).collect();
277            Printer::from_flags(false, self.verbose).print_processes_as(
278                "for",
279                &processes,
280                Some(&format!("for {}", file_display)),
281            );
282
283            let listening: Vec<_> = results
284                .iter()
285                .flat_map(|(proc, ports)| ports.iter().map(move |p| (proc, p)))
286                .collect();
287            if !listening.is_empty() {
288                println!("  {}", "Listening:".bright_black());
289                for (proc, port_info) in listening {
290                    let addr = port_info.address.as_deref().unwrap_or("*");
291                    println!(
292                        "    {} :{} ({} on {})  {} [PID {}]",
293                        "→".bright_black(),
294                        port_info.port.to_string().cyan(),
295                        format!("{:?}", port_info.protocol).to_uppercase(),
296                        addr,
297                        proc.name.white(),
298                        proc.pid.to_string().cyan()
299                    );
300                }
301                println!();
302            }
303        }
304    }
305
306    fn print_json(&self, results: &[(Process, Vec<PortInfo>)]) -> Result<()> {
307        let printer = Printer::from_flags(true, self.verbose);
308        let items: Vec<ProcessForJson> = results
309            .iter()
310            .map(|(proc, ports)| ProcessForJson {
311                process: proc,
312                ports,
313            })
314            .collect();
315
316        printer.print_json(&ForOutput {
317            action: "for",
318            success: true,
319            count: items.len(),
320            results: &items,
321        });
322        Ok(())
323    }
324}
325
326#[derive(Serialize)]
327struct ProcessForJson<'a> {
328    process: &'a Process,
329    ports: &'a [PortInfo],
330}
331
332#[derive(Serialize)]
333struct ForOutput<'a> {
334    action: &'static str,
335    success: bool,
336    count: usize,
337    results: &'a [ProcessForJson<'a>],
338}