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