1use 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#[derive(Args, Debug)]
18pub struct OnCommand {
19 pub target: String,
21
22 #[arg(long, short = 'j')]
24 pub json: bool,
25
26 #[arg(long, short = 'v')]
28 pub verbose: bool,
29}
30
31impl OnCommand {
32 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 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 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 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}