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