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