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