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, ProcessStatus, SortKey,
11};
12use crate::error::{ProcError, Result};
13use crate::ui::{format_memory, truncate_string, 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)]
44    pub status: Option<String>,
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(ref status) = self.status {
197                let status_match = match status.to_lowercase().as_str() {
198                    "running" => matches!(p.status, ProcessStatus::Running),
199                    "sleeping" | "sleep" => matches!(p.status, ProcessStatus::Sleeping),
200                    "stopped" | "stop" => matches!(p.status, ProcessStatus::Stopped),
201                    "zombie" => matches!(p.status, ProcessStatus::Zombie),
202                    _ => true,
203                };
204                if !status_match {
205                    return false;
206                }
207            }
208
209            // Uptime filter
210            if let Some(min_uptime) = self.min_uptime {
211                if let Some(start_time) = p.start_time {
212                    let now = std::time::SystemTime::now()
213                        .duration_since(std::time::UNIX_EPOCH)
214                        .map(|d| d.as_secs())
215                        .unwrap_or(0);
216                    if now.saturating_sub(start_time) < min_uptime {
217                        return false;
218                    }
219                } else {
220                    return false;
221                }
222            }
223
224            true
225        });
226    }
227
228    fn print_human(&self, results: &[(Process, Vec<PortInfo>)]) {
229        let count = results.len();
230        let file_display = &self.file;
231
232        if count == 1 {
233            // Single process - detailed view (like `on` command)
234            let (proc, ports) = &results[0];
235
236            println!(
237                "{} Found 1 process for {}",
238                "✓".green().bold(),
239                file_display.cyan().bold()
240            );
241            println!();
242
243            println!("  {}", "Process:".bright_black());
244            println!(
245                "    {} {} (PID {})",
246                "Name:".bright_black(),
247                proc.name.white().bold(),
248                proc.pid.to_string().cyan()
249            );
250            println!("    {} {:.1}%", "CPU:".bright_black(), proc.cpu_percent);
251            println!(
252                "    {} {}",
253                "MEM:".bright_black(),
254                format_memory(proc.memory_mb)
255            );
256
257            if let Some(ref path) = proc.exe_path {
258                println!("    {} {}", "Path:".bright_black(), path.bright_black());
259            }
260
261            if self.verbose {
262                if let Some(ref cwd) = proc.cwd {
263                    println!("    {} {}", "CWD:".bright_black(), cwd.bright_black());
264                }
265                if let Some(ref cmd) = proc.command {
266                    println!("    {} {}", "Command:".bright_black(), cmd.bright_black());
267                }
268            }
269
270            println!();
271
272            if ports.is_empty() {
273                println!("  {} No listening ports", "ℹ".blue());
274            } else {
275                println!("  {}", "Listening Ports:".bright_black());
276                for port_info in ports {
277                    let addr = port_info.address.as_deref().unwrap_or("*");
278                    println!(
279                        "    {} :{} ({} on {})",
280                        "→".bright_black(),
281                        port_info.port.to_string().cyan(),
282                        format!("{:?}", port_info.protocol).to_uppercase(),
283                        addr
284                    );
285                }
286            }
287        } else {
288            // Multiple processes - table view
289            println!(
290                "{} Found {} processes for {}",
291                "✓".green().bold(),
292                count.to_string().white().bold(),
293                file_display.cyan().bold()
294            );
295            println!();
296
297            // Header
298            println!(
299                "  {:>7}  {:<15}  {:>5}  {:>8}  {}",
300                "PID".bright_black(),
301                "NAME".bright_black(),
302                "CPU%".bright_black(),
303                "MEM".bright_black(),
304                "PORTS".bright_black()
305            );
306            println!("  {}", "─".repeat(60).bright_black());
307
308            for (proc, ports) in results {
309                let ports_str = if ports.is_empty() {
310                    "-".to_string()
311                } else {
312                    ports
313                        .iter()
314                        .map(|p| format!(":{}", p.port))
315                        .collect::<Vec<_>>()
316                        .join(", ")
317                };
318
319                println!(
320                    "  {:>7}  {:<15}  {:>5.1}  {:>8}  {}",
321                    proc.pid.to_string().cyan(),
322                    truncate_string(&proc.name, 15).white(),
323                    proc.cpu_percent,
324                    format_memory(proc.memory_mb),
325                    ports_str
326                );
327            }
328        }
329
330        println!();
331    }
332
333    fn print_json(&self, results: &[(Process, Vec<PortInfo>)]) -> Result<()> {
334        let printer = Printer::from_flags(true, self.verbose);
335        let items: Vec<ProcessForJson> = results
336            .iter()
337            .map(|(proc, ports)| ProcessForJson {
338                process: proc,
339                ports,
340            })
341            .collect();
342
343        printer.print_json(&ForOutput {
344            action: "for",
345            success: true,
346            count: items.len(),
347            results: &items,
348        });
349        Ok(())
350    }
351}
352
353#[derive(Serialize)]
354struct ProcessForJson<'a> {
355    process: &'a Process,
356    ports: &'a [PortInfo],
357}
358
359#[derive(Serialize)]
360struct ForOutput<'a> {
361    action: &'static str,
362    success: bool,
363    count: usize,
364    results: &'a [ProcessForJson<'a>],
365}