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