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, Printer};
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            crate::core::matches_by_filter(proc, name)
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 printer = Printer::from_flags(true, self.verbose);
142            let output = PortLookupOutput {
143                action: "on",
144                query_type: "port_to_process",
145                success: true,
146                port: Some(port_info.port),
147                protocol: Some(format!("{:?}", port_info.protocol).to_lowercase()),
148                address: port_info.address.clone(),
149                process: process.as_ref(),
150                ports: None,
151            };
152            printer.print_json(&output);
153        } else {
154            self.print_process_on_port(&port_info, process.as_ref());
155        }
156
157        Ok(())
158    }
159
160    /// Show what ports a PID is listening on
161    fn show_ports_for_pid(&self, pid: u32) -> Result<()> {
162        let process = Process::find_by_pid(pid)?
163            .ok_or_else(|| ProcError::ProcessNotFound(pid.to_string()))?;
164
165        // Apply --in and --by filters if present
166        if !self.matches_filters(&process) {
167            return Err(ProcError::ProcessNotFound(format!(
168                "PID {} (not in specified directory)",
169                pid
170            )));
171        }
172
173        let ports = find_ports_for_pid(pid)?;
174
175        if self.json {
176            let printer = Printer::from_flags(true, self.verbose);
177            let output = PortLookupOutput {
178                action: "on",
179                query_type: "process_to_ports",
180                success: true,
181                port: None,
182                protocol: None,
183                address: None,
184                process: Some(&process),
185                ports: Some(&ports),
186            };
187            printer.print_json(&output);
188        } else {
189            self.print_ports_for_process(&process, &ports);
190        }
191
192        Ok(())
193    }
194
195    /// Show what ports processes with a given name are listening on
196    fn show_ports_for_name(&self, name: &str) -> Result<()> {
197        let mut processes = resolve_target(name)?;
198
199        if processes.is_empty() {
200            return Err(ProcError::ProcessNotFound(name.to_string()));
201        }
202
203        // Apply --in and --by filters if present
204        if self.in_dir.is_some() || self.by_name.is_some() {
205            processes.retain(|p| self.matches_filters(p));
206            if processes.is_empty() {
207                return Err(ProcError::ProcessNotFound(format!(
208                    "'{}' (no matches with specified filters)",
209                    name
210                )));
211            }
212        }
213
214        let mut all_results: Vec<(Process, Vec<PortInfo>)> = Vec::new();
215
216        for proc in processes {
217            let ports = find_ports_for_pid(proc.pid)?;
218            all_results.push((proc, ports));
219        }
220
221        if self.json {
222            let printer = Printer::from_flags(true, self.verbose);
223            let results: Vec<_> = all_results
224                .iter()
225                .map(|(proc, ports)| ProcessPortsJson {
226                    process: proc,
227                    ports,
228                })
229                .collect();
230            let output = MultiProcessPortsOutput {
231                action: "on",
232                success: true,
233                count: results.len(),
234                results: &results,
235            };
236            printer.print_json(&output);
237        } else {
238            for (proc, ports) in &all_results {
239                self.print_ports_for_process(proc, ports);
240            }
241        }
242
243        Ok(())
244    }
245
246    fn print_process_on_port(&self, port_info: &PortInfo, process: Option<&Process>) {
247        println!(
248            "{} Port {} is used by:",
249            "✓".green().bold(),
250            port_info.port.to_string().cyan().bold()
251        );
252        println!();
253
254        println!(
255            "  {} {} (PID {})",
256            "Process:".bright_black(),
257            port_info.process_name.white().bold(),
258            port_info.pid.to_string().cyan()
259        );
260
261        if let Some(proc) = process {
262            if let Some(ref cwd) = proc.cwd {
263                println!("  {} {}", "Directory:".bright_black(), cwd);
264            }
265            if let Some(ref path) = proc.exe_path {
266                println!("  {} {}", "Path:".bright_black(), path.bright_black());
267            }
268        }
269
270        let addr = port_info.address.as_deref().unwrap_or("*");
271        println!(
272            "  {} {} on {}",
273            "Listening:".bright_black(),
274            format!("{:?}", port_info.protocol).to_uppercase(),
275            addr
276        );
277
278        if let Some(proc) = process {
279            println!(
280                "  {} {:.1}% CPU, {}",
281                "Resources:".bright_black(),
282                proc.cpu_percent,
283                format_memory(proc.memory_mb)
284            );
285
286            if let Some(start_time) = proc.start_time {
287                let uptime = std::time::SystemTime::now()
288                    .duration_since(std::time::UNIX_EPOCH)
289                    .map(|d| d.as_secs().saturating_sub(start_time))
290                    .unwrap_or(0);
291                println!("  {} {}", "Uptime:".bright_black(), format_duration(uptime));
292            }
293
294            if self.verbose {
295                if let Some(ref cmd) = proc.command {
296                    println!("  {} {}", "Command:".bright_black(), cmd.bright_black());
297                }
298            }
299        }
300
301        println!();
302    }
303
304    fn print_ports_for_process(&self, process: &Process, ports: &[PortInfo]) {
305        println!(
306            "{} {} (PID {}) is listening on:",
307            "✓".green().bold(),
308            process.name.white().bold(),
309            process.pid.to_string().cyan().bold()
310        );
311        println!();
312
313        if ports.is_empty() {
314            println!("  {} No listening ports", "ℹ".blue());
315        } else {
316            for port_info in ports {
317                let addr = port_info.address.as_deref().unwrap_or("*");
318                println!(
319                    "  {} :{} ({} on {})",
320                    "→".bright_black(),
321                    port_info.port.to_string().cyan(),
322                    format!("{:?}", port_info.protocol).to_uppercase(),
323                    addr
324                );
325            }
326        }
327
328        if let Some(ref cwd) = process.cwd {
329            println!();
330            println!("  {} {}", "Directory:".bright_black(), cwd);
331        }
332
333        if self.verbose {
334            if let Some(ref path) = process.exe_path {
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
346#[derive(Serialize)]
347struct PortLookupOutput<'a> {
348    action: &'static str,
349    query_type: &'static str,
350    success: bool,
351    #[serde(skip_serializing_if = "Option::is_none")]
352    port: Option<u16>,
353    #[serde(skip_serializing_if = "Option::is_none")]
354    protocol: Option<String>,
355    #[serde(skip_serializing_if = "Option::is_none")]
356    address: Option<String>,
357    #[serde(skip_serializing_if = "Option::is_none")]
358    process: Option<&'a Process>,
359    #[serde(skip_serializing_if = "Option::is_none")]
360    ports: Option<&'a [PortInfo]>,
361}
362
363#[derive(Serialize)]
364struct ProcessPortsJson<'a> {
365    process: &'a Process,
366    ports: &'a [PortInfo],
367}
368
369#[derive(Serialize)]
370struct MultiProcessPortsOutput<'a> {
371    action: &'static str,
372    success: bool,
373    count: usize,
374    results: &'a Vec<ProcessPortsJson<'a>>,
375}