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 1234       # What ports is PID 1234 listening on?
6//!   proc on node       # What ports are node processes listening on?
7
8use crate::core::{
9    find_ports_for_pid, parse_target, resolve_target, PortInfo, Process, TargetType,
10};
11use crate::error::{ProcError, Result};
12use clap::Args;
13use colored::*;
14use serde::Serialize;
15
16/// Show what's on a port, or what ports a process is on
17#[derive(Args, Debug)]
18pub struct OnCommand {
19    /// Target: :port, PID, or process name
20    pub target: String,
21
22    /// Output as JSON
23    #[arg(long, short = 'j')]
24    pub json: bool,
25
26    /// Show verbose output (full command line)
27    #[arg(long, short = 'v')]
28    pub verbose: bool,
29}
30
31impl OnCommand {
32    pub fn execute(&self) -> Result<()> {
33        match parse_target(&self.target) {
34            TargetType::Port(port) => self.show_process_on_port(port),
35            TargetType::Pid(pid) => self.show_ports_for_pid(pid),
36            TargetType::Name(name) => self.show_ports_for_name(&name),
37        }
38    }
39
40    /// Show what process is on a specific port
41    fn show_process_on_port(&self, port: u16) -> Result<()> {
42        let port_info = match PortInfo::find_by_port(port)? {
43            Some(info) => info,
44            None => return Err(ProcError::PortNotFound(port)),
45        };
46
47        let process = Process::find_by_pid(port_info.pid)?;
48
49        if self.json {
50            let output = PortLookupOutput {
51                action: "on",
52                query_type: "port_to_process",
53                success: true,
54                port: Some(port_info.port),
55                protocol: Some(format!("{:?}", port_info.protocol).to_lowercase()),
56                address: port_info.address.clone(),
57                process: process.as_ref(),
58                ports: None,
59            };
60            println!("{}", serde_json::to_string_pretty(&output)?);
61        } else {
62            self.print_process_on_port(&port_info, process.as_ref());
63        }
64
65        Ok(())
66    }
67
68    /// Show what ports a PID is listening on
69    fn show_ports_for_pid(&self, pid: u32) -> Result<()> {
70        let process = Process::find_by_pid(pid)?
71            .ok_or_else(|| ProcError::ProcessNotFound(pid.to_string()))?;
72
73        let ports = find_ports_for_pid(pid)?;
74
75        if self.json {
76            let output = PortLookupOutput {
77                action: "on",
78                query_type: "process_to_ports",
79                success: true,
80                port: None,
81                protocol: None,
82                address: None,
83                process: Some(&process),
84                ports: Some(&ports),
85            };
86            println!("{}", serde_json::to_string_pretty(&output)?);
87        } else {
88            self.print_ports_for_process(&process, &ports);
89        }
90
91        Ok(())
92    }
93
94    /// Show what ports processes with a given name are listening on
95    fn show_ports_for_name(&self, name: &str) -> Result<()> {
96        let processes = resolve_target(name)?;
97
98        if processes.is_empty() {
99            return Err(ProcError::ProcessNotFound(name.to_string()));
100        }
101
102        let mut all_results: Vec<(Process, Vec<PortInfo>)> = Vec::new();
103
104        for proc in processes {
105            let ports = find_ports_for_pid(proc.pid)?;
106            all_results.push((proc, ports));
107        }
108
109        if self.json {
110            let output: Vec<_> = all_results
111                .iter()
112                .map(|(proc, ports)| ProcessPortsJson {
113                    process: proc,
114                    ports,
115                })
116                .collect();
117            println!("{}", serde_json::to_string_pretty(&output)?);
118        } else {
119            for (proc, ports) in &all_results {
120                self.print_ports_for_process(proc, ports);
121            }
122        }
123
124        Ok(())
125    }
126
127    fn print_process_on_port(&self, port_info: &PortInfo, process: Option<&Process>) {
128        println!(
129            "{} Port {} is used by:",
130            "✓".green().bold(),
131            port_info.port.to_string().cyan().bold()
132        );
133        println!();
134
135        println!(
136            "  {} {} (PID {})",
137            "Process:".bright_black(),
138            port_info.process_name.white().bold(),
139            port_info.pid.to_string().cyan()
140        );
141
142        if let Some(proc) = process {
143            if let Some(ref path) = proc.exe_path {
144                println!("  {} {}", "Path:".bright_black(), path.bright_black());
145            }
146        }
147
148        let addr = port_info.address.as_deref().unwrap_or("*");
149        println!(
150            "  {} {} on {}",
151            "Listening:".bright_black(),
152            format!("{:?}", port_info.protocol).to_uppercase(),
153            addr
154        );
155
156        if let Some(proc) = process {
157            println!(
158                "  {} {:.1}% CPU, {:.1} MB",
159                "Resources:".bright_black(),
160                proc.cpu_percent,
161                proc.memory_mb
162            );
163
164            if let Some(start_time) = proc.start_time {
165                let uptime = std::time::SystemTime::now()
166                    .duration_since(std::time::UNIX_EPOCH)
167                    .map(|d| d.as_secs().saturating_sub(start_time))
168                    .unwrap_or(0);
169                println!("  {} {}", "Uptime:".bright_black(), format_duration(uptime));
170            }
171
172            if self.verbose {
173                if let Some(ref cmd) = proc.command {
174                    println!("  {} {}", "Command:".bright_black(), cmd.bright_black());
175                }
176            }
177        }
178
179        println!();
180    }
181
182    fn print_ports_for_process(&self, process: &Process, ports: &[PortInfo]) {
183        println!(
184            "{} {} (PID {}) is listening on:",
185            "✓".green().bold(),
186            process.name.white().bold(),
187            process.pid.to_string().cyan().bold()
188        );
189        println!();
190
191        if ports.is_empty() {
192            println!("  {} No listening ports", "ℹ".blue());
193        } else {
194            for port_info in ports {
195                let addr = port_info.address.as_deref().unwrap_or("*");
196                println!(
197                    "  {} :{} ({} on {})",
198                    "→".bright_black(),
199                    port_info.port.to_string().cyan(),
200                    format!("{:?}", port_info.protocol).to_uppercase(),
201                    addr
202                );
203            }
204        }
205
206        if self.verbose {
207            if let Some(ref path) = process.exe_path {
208                println!();
209                println!("  {} {}", "Path:".bright_black(), path.bright_black());
210            }
211            if let Some(ref cmd) = process.command {
212                println!("  {} {}", "Command:".bright_black(), cmd.bright_black());
213            }
214        }
215
216        println!();
217    }
218}
219
220fn format_duration(secs: u64) -> String {
221    if secs < 60 {
222        format!("{}s", secs)
223    } else if secs < 3600 {
224        format!("{}m {}s", secs / 60, secs % 60)
225    } else if secs < 86400 {
226        format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
227    } else {
228        format!("{}d {}h", secs / 86400, (secs % 86400) / 3600)
229    }
230}
231
232#[derive(Serialize)]
233struct PortLookupOutput<'a> {
234    action: &'static str,
235    query_type: &'static str,
236    success: bool,
237    #[serde(skip_serializing_if = "Option::is_none")]
238    port: Option<u16>,
239    #[serde(skip_serializing_if = "Option::is_none")]
240    protocol: Option<String>,
241    #[serde(skip_serializing_if = "Option::is_none")]
242    address: Option<String>,
243    #[serde(skip_serializing_if = "Option::is_none")]
244    process: Option<&'a Process>,
245    #[serde(skip_serializing_if = "Option::is_none")]
246    ports: Option<&'a [PortInfo]>,
247}
248
249#[derive(Serialize)]
250struct ProcessPortsJson<'a> {
251    process: &'a Process,
252    ports: &'a [PortInfo],
253}