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