1use 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#[derive(Args, Debug)]
21pub struct OnCommand {
22 pub target: String,
24
25 #[arg(long = "in", short = 'i')]
27 pub in_dir: Option<String>,
28
29 #[arg(long, short = 'j')]
31 pub json: bool,
32
33 #[arg(long, short = 'v')]
35 pub verbose: bool,
36}
37
38impl OnCommand {
39 pub fn execute(&self) -> Result<()> {
41 let targets = parse_targets(&self.target);
42
43 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 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 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 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 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 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 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 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 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 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}