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<()> {
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 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 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 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}