Skip to main content

proc_cli/commands/
on.rs

1//! `proc on` - Port/process lookup
2//!
3//! Usage:
4//!   proc on :3000              # What process is on port 3000?
5//!   proc on :3000,:8080        # What's on multiple ports?
6//!   proc on 1234               # What ports is PID 1234 listening on?
7//!   proc on node               # What ports are node processes listening on?
8//!   proc on node --in .        # Node processes in cwd and their ports
9
10use crate::core::{
11    find_ports_for_pid, parse_target, parse_targets, resolve_target, PortInfo, Process, TargetType,
12};
13use crate::error::{ProcError, Result};
14use clap::Args;
15use colored::*;
16use serde::Serialize;
17use std::path::PathBuf;
18
19/// Show what's on a port, or what ports a process is on
20#[derive(Args, Debug)]
21pub struct OnCommand {
22    /// Target(s): :port, PID, or process name (comma-separated for multiple)
23    pub target: String,
24
25    /// Filter by directory (for name targets)
26    #[arg(long = "in", short = 'i')]
27    pub in_dir: Option<String>,
28
29    /// Filter by process name
30    #[arg(long = "by", short = 'b')]
31    pub by_name: Option<String>,
32
33    /// Output as JSON
34    #[arg(long, short = 'j')]
35    pub json: bool,
36
37    /// Show verbose output (full command line)
38    #[arg(long, short = 'v')]
39    pub verbose: bool,
40}
41
42impl OnCommand {
43    /// Executes the on command, performing bidirectional port/process lookup.
44    pub fn execute(&self) -> Result<()> {
45        let targets = parse_targets(&self.target);
46
47        // For single target, use original behavior
48        if targets.len() == 1 {
49            return match parse_target(&targets[0]) {
50                TargetType::Port(port) => self.show_process_on_port(port),
51                TargetType::Pid(pid) => self.show_ports_for_pid(pid),
52                TargetType::Name(name) => self.show_ports_for_name(&name),
53            };
54        }
55
56        // Multi-target handling
57        let mut not_found = Vec::new();
58
59        for target in &targets {
60            match parse_target(target) {
61                TargetType::Port(port) => {
62                    if let Err(e) = self.show_process_on_port(port) {
63                        if !self.json {
64                            println!("{} Port {}: {}", "⚠".yellow(), port, e);
65                        }
66                        not_found.push(target.clone());
67                    }
68                }
69                TargetType::Pid(pid) => {
70                    if let Err(e) = self.show_ports_for_pid(pid) {
71                        if !self.json {
72                            println!("{} PID {}: {}", "⚠".yellow(), pid, e);
73                        }
74                        not_found.push(target.clone());
75                    }
76                }
77                TargetType::Name(ref name) => {
78                    if let Err(e) = self.show_ports_for_name(name) {
79                        if !self.json {
80                            println!("{} '{}': {}", "⚠".yellow(), name, e);
81                        }
82                        not_found.push(target.clone());
83                    }
84                }
85            }
86        }
87
88        Ok(())
89    }
90
91    /// Resolve --in filter path
92    fn resolve_in_dir(&self) -> Option<PathBuf> {
93        self.in_dir.as_ref().map(|p| {
94            if p == "." {
95                std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
96            } else {
97                let path = PathBuf::from(p);
98                if path.is_relative() {
99                    std::env::current_dir()
100                        .unwrap_or_else(|_| PathBuf::from("."))
101                        .join(path)
102                } else {
103                    path
104                }
105            }
106        })
107    }
108
109    /// Check if process matches --in filter
110    fn matches_in_filter(&self, proc: &Process) -> bool {
111        if let Some(ref dir_path) = self.resolve_in_dir() {
112            if let Some(ref proc_cwd) = proc.cwd {
113                let proc_path = PathBuf::from(proc_cwd);
114                proc_path.starts_with(dir_path)
115            } else {
116                false
117            }
118        } else {
119            true
120        }
121    }
122
123    /// Check if process matches --by filter
124    fn matches_by_filter(&self, proc: &Process) -> bool {
125        if let Some(ref name) = self.by_name {
126            proc.name.to_lowercase().contains(&name.to_lowercase())
127        } else {
128            true
129        }
130    }
131
132    /// Check if process matches all filters (--in and --by)
133    fn matches_filters(&self, proc: &Process) -> bool {
134        self.matches_in_filter(proc) && self.matches_by_filter(proc)
135    }
136
137    /// Show what process is on a specific port
138    fn show_process_on_port(&self, port: u16) -> Result<()> {
139        let port_info = match PortInfo::find_by_port(port)? {
140            Some(info) => info,
141            None => return Err(ProcError::PortNotFound(port)),
142        };
143
144        let process = Process::find_by_pid(port_info.pid)?;
145
146        // Apply --in and --by filters if present
147        if let Some(ref proc) = process {
148            if !self.matches_filters(proc) {
149                return Err(ProcError::ProcessNotFound(format!(
150                    "port {} (process not in specified directory)",
151                    port
152                )));
153            }
154        }
155
156        if self.json {
157            let output = PortLookupOutput {
158                action: "on",
159                query_type: "port_to_process",
160                success: true,
161                port: Some(port_info.port),
162                protocol: Some(format!("{:?}", port_info.protocol).to_lowercase()),
163                address: port_info.address.clone(),
164                process: process.as_ref(),
165                ports: None,
166            };
167            println!("{}", serde_json::to_string_pretty(&output)?);
168        } else {
169            self.print_process_on_port(&port_info, process.as_ref());
170        }
171
172        Ok(())
173    }
174
175    /// Show what ports a PID is listening on
176    fn show_ports_for_pid(&self, pid: u32) -> Result<()> {
177        let process = Process::find_by_pid(pid)?
178            .ok_or_else(|| ProcError::ProcessNotFound(pid.to_string()))?;
179
180        // Apply --in and --by filters if present
181        if !self.matches_filters(&process) {
182            return Err(ProcError::ProcessNotFound(format!(
183                "PID {} (not in specified directory)",
184                pid
185            )));
186        }
187
188        let ports = find_ports_for_pid(pid)?;
189
190        if self.json {
191            let output = PortLookupOutput {
192                action: "on",
193                query_type: "process_to_ports",
194                success: true,
195                port: None,
196                protocol: None,
197                address: None,
198                process: Some(&process),
199                ports: Some(&ports),
200            };
201            println!("{}", serde_json::to_string_pretty(&output)?);
202        } else {
203            self.print_ports_for_process(&process, &ports);
204        }
205
206        Ok(())
207    }
208
209    /// Show what ports processes with a given name are listening on
210    fn show_ports_for_name(&self, name: &str) -> Result<()> {
211        let mut processes = resolve_target(name)?;
212
213        if processes.is_empty() {
214            return Err(ProcError::ProcessNotFound(name.to_string()));
215        }
216
217        // Apply --in and --by filters if present
218        if self.in_dir.is_some() || self.by_name.is_some() {
219            processes.retain(|p| self.matches_filters(p));
220            if processes.is_empty() {
221                return Err(ProcError::ProcessNotFound(format!(
222                    "'{}' (no matches with specified filters)",
223                    name
224                )));
225            }
226        }
227
228        let mut all_results: Vec<(Process, Vec<PortInfo>)> = Vec::new();
229
230        for proc in processes {
231            let ports = find_ports_for_pid(proc.pid)?;
232            all_results.push((proc, ports));
233        }
234
235        if self.json {
236            let output: Vec<_> = all_results
237                .iter()
238                .map(|(proc, ports)| ProcessPortsJson {
239                    process: proc,
240                    ports,
241                })
242                .collect();
243            println!("{}", serde_json::to_string_pretty(&output)?);
244        } else {
245            for (proc, ports) in &all_results {
246                self.print_ports_for_process(proc, ports);
247            }
248        }
249
250        Ok(())
251    }
252
253    fn print_process_on_port(&self, port_info: &PortInfo, process: Option<&Process>) {
254        println!(
255            "{} Port {} is used by:",
256            "✓".green().bold(),
257            port_info.port.to_string().cyan().bold()
258        );
259        println!();
260
261        println!(
262            "  {} {} (PID {})",
263            "Process:".bright_black(),
264            port_info.process_name.white().bold(),
265            port_info.pid.to_string().cyan()
266        );
267
268        if let Some(proc) = process {
269            if let Some(ref path) = proc.exe_path {
270                println!("  {} {}", "Path:".bright_black(), path.bright_black());
271            }
272        }
273
274        let addr = port_info.address.as_deref().unwrap_or("*");
275        println!(
276            "  {} {} on {}",
277            "Listening:".bright_black(),
278            format!("{:?}", port_info.protocol).to_uppercase(),
279            addr
280        );
281
282        if let Some(proc) = process {
283            println!(
284                "  {} {:.1}% CPU, {:.1} MB",
285                "Resources:".bright_black(),
286                proc.cpu_percent,
287                proc.memory_mb
288            );
289
290            if let Some(start_time) = proc.start_time {
291                let uptime = std::time::SystemTime::now()
292                    .duration_since(std::time::UNIX_EPOCH)
293                    .map(|d| d.as_secs().saturating_sub(start_time))
294                    .unwrap_or(0);
295                println!("  {} {}", "Uptime:".bright_black(), format_duration(uptime));
296            }
297
298            if self.verbose {
299                if let Some(ref cmd) = proc.command {
300                    println!("  {} {}", "Command:".bright_black(), cmd.bright_black());
301                }
302            }
303        }
304
305        println!();
306    }
307
308    fn print_ports_for_process(&self, process: &Process, ports: &[PortInfo]) {
309        println!(
310            "{} {} (PID {}) is listening on:",
311            "✓".green().bold(),
312            process.name.white().bold(),
313            process.pid.to_string().cyan().bold()
314        );
315        println!();
316
317        if ports.is_empty() {
318            println!("  {} No listening ports", "ℹ".blue());
319        } else {
320            for port_info in ports {
321                let addr = port_info.address.as_deref().unwrap_or("*");
322                println!(
323                    "  {} :{} ({} on {})",
324                    "→".bright_black(),
325                    port_info.port.to_string().cyan(),
326                    format!("{:?}", port_info.protocol).to_uppercase(),
327                    addr
328                );
329            }
330        }
331
332        if self.verbose {
333            if let Some(ref path) = process.exe_path {
334                println!();
335                println!("  {} {}", "Path:".bright_black(), path.bright_black());
336            }
337            if let Some(ref cmd) = process.command {
338                println!("  {} {}", "Command:".bright_black(), cmd.bright_black());
339            }
340        }
341
342        println!();
343    }
344}
345
346fn format_duration(secs: u64) -> String {
347    if secs < 60 {
348        format!("{}s", secs)
349    } else if secs < 3600 {
350        format!("{}m {}s", secs / 60, secs % 60)
351    } else if secs < 86400 {
352        format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
353    } else {
354        format!("{}d {}h", secs / 86400, (secs % 86400) / 3600)
355    }
356}
357
358#[derive(Serialize)]
359struct PortLookupOutput<'a> {
360    action: &'static str,
361    query_type: &'static str,
362    success: bool,
363    #[serde(skip_serializing_if = "Option::is_none")]
364    port: Option<u16>,
365    #[serde(skip_serializing_if = "Option::is_none")]
366    protocol: Option<String>,
367    #[serde(skip_serializing_if = "Option::is_none")]
368    address: Option<String>,
369    #[serde(skip_serializing_if = "Option::is_none")]
370    process: Option<&'a Process>,
371    #[serde(skip_serializing_if = "Option::is_none")]
372    ports: Option<&'a [PortInfo]>,
373}
374
375#[derive(Serialize)]
376struct ProcessPortsJson<'a> {
377    process: &'a Process,
378    ports: &'a [PortInfo],
379}