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 = "by", short = 'b')]
31 pub by_name: Option<String>,
32
33 #[arg(long, short = 'j')]
35 pub json: bool,
36
37 #[arg(long, short = 'v')]
39 pub verbose: bool,
40}
41
42impl OnCommand {
43 pub fn execute(&self) -> Result<()> {
45 let targets = parse_targets(&self.target);
46
47 if targets.len() == 1 {
49 return match parse_target(&targets[0]) {
50 TargetType::Port(port) => self.show_process_on_port(port),
51 TargetType::Pid(pid) => self.show_ports_for_pid(pid),
52 TargetType::Name(name) => self.show_ports_for_name(&name),
53 };
54 }
55
56 let mut not_found = Vec::new();
58
59 for target in &targets {
60 match parse_target(target) {
61 TargetType::Port(port) => {
62 if let Err(e) = self.show_process_on_port(port) {
63 if !self.json {
64 println!("{} Port {}: {}", "⚠".yellow(), port, e);
65 }
66 not_found.push(target.clone());
67 }
68 }
69 TargetType::Pid(pid) => {
70 if let Err(e) = self.show_ports_for_pid(pid) {
71 if !self.json {
72 println!("{} PID {}: {}", "⚠".yellow(), pid, e);
73 }
74 not_found.push(target.clone());
75 }
76 }
77 TargetType::Name(ref name) => {
78 if let Err(e) = self.show_ports_for_name(name) {
79 if !self.json {
80 println!("{} '{}': {}", "⚠".yellow(), name, e);
81 }
82 not_found.push(target.clone());
83 }
84 }
85 }
86 }
87
88 Ok(())
89 }
90
91 fn resolve_in_dir(&self) -> Option<PathBuf> {
93 self.in_dir.as_ref().map(|p| {
94 if p == "." {
95 std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
96 } else {
97 let path = PathBuf::from(p);
98 if path.is_relative() {
99 std::env::current_dir()
100 .unwrap_or_else(|_| PathBuf::from("."))
101 .join(path)
102 } else {
103 path
104 }
105 }
106 })
107 }
108
109 fn matches_in_filter(&self, proc: &Process) -> bool {
111 if let Some(ref dir_path) = self.resolve_in_dir() {
112 if let Some(ref proc_cwd) = proc.cwd {
113 let proc_path = PathBuf::from(proc_cwd);
114 proc_path.starts_with(dir_path)
115 } else {
116 false
117 }
118 } else {
119 true
120 }
121 }
122
123 fn matches_by_filter(&self, proc: &Process) -> bool {
125 if let Some(ref name) = self.by_name {
126 proc.name.to_lowercase().contains(&name.to_lowercase())
127 } else {
128 true
129 }
130 }
131
132 fn matches_filters(&self, proc: &Process) -> bool {
134 self.matches_in_filter(proc) && self.matches_by_filter(proc)
135 }
136
137 fn show_process_on_port(&self, port: u16) -> Result<()> {
139 let port_info = match PortInfo::find_by_port(port)? {
140 Some(info) => info,
141 None => return Err(ProcError::PortNotFound(port)),
142 };
143
144 let process = Process::find_by_pid(port_info.pid)?;
145
146 if let Some(ref proc) = process {
148 if !self.matches_filters(proc) {
149 return Err(ProcError::ProcessNotFound(format!(
150 "port {} (process not in specified directory)",
151 port
152 )));
153 }
154 }
155
156 if self.json {
157 let output = PortLookupOutput {
158 action: "on",
159 query_type: "port_to_process",
160 success: true,
161 port: Some(port_info.port),
162 protocol: Some(format!("{:?}", port_info.protocol).to_lowercase()),
163 address: port_info.address.clone(),
164 process: process.as_ref(),
165 ports: None,
166 };
167 println!("{}", serde_json::to_string_pretty(&output)?);
168 } else {
169 self.print_process_on_port(&port_info, process.as_ref());
170 }
171
172 Ok(())
173 }
174
175 fn show_ports_for_pid(&self, pid: u32) -> Result<()> {
177 let process = Process::find_by_pid(pid)?
178 .ok_or_else(|| ProcError::ProcessNotFound(pid.to_string()))?;
179
180 if !self.matches_filters(&process) {
182 return Err(ProcError::ProcessNotFound(format!(
183 "PID {} (not in specified directory)",
184 pid
185 )));
186 }
187
188 let ports = find_ports_for_pid(pid)?;
189
190 if self.json {
191 let output = PortLookupOutput {
192 action: "on",
193 query_type: "process_to_ports",
194 success: true,
195 port: None,
196 protocol: None,
197 address: None,
198 process: Some(&process),
199 ports: Some(&ports),
200 };
201 println!("{}", serde_json::to_string_pretty(&output)?);
202 } else {
203 self.print_ports_for_process(&process, &ports);
204 }
205
206 Ok(())
207 }
208
209 fn show_ports_for_name(&self, name: &str) -> Result<()> {
211 let mut processes = resolve_target(name)?;
212
213 if processes.is_empty() {
214 return Err(ProcError::ProcessNotFound(name.to_string()));
215 }
216
217 if self.in_dir.is_some() || self.by_name.is_some() {
219 processes.retain(|p| self.matches_filters(p));
220 if processes.is_empty() {
221 return Err(ProcError::ProcessNotFound(format!(
222 "'{}' (no matches with specified filters)",
223 name
224 )));
225 }
226 }
227
228 let mut all_results: Vec<(Process, Vec<PortInfo>)> = Vec::new();
229
230 for proc in processes {
231 let ports = find_ports_for_pid(proc.pid)?;
232 all_results.push((proc, ports));
233 }
234
235 if self.json {
236 let output: Vec<_> = all_results
237 .iter()
238 .map(|(proc, ports)| ProcessPortsJson {
239 process: proc,
240 ports,
241 })
242 .collect();
243 println!("{}", serde_json::to_string_pretty(&output)?);
244 } else {
245 for (proc, ports) in &all_results {
246 self.print_ports_for_process(proc, ports);
247 }
248 }
249
250 Ok(())
251 }
252
253 fn print_process_on_port(&self, port_info: &PortInfo, process: Option<&Process>) {
254 println!(
255 "{} Port {} is used by:",
256 "✓".green().bold(),
257 port_info.port.to_string().cyan().bold()
258 );
259 println!();
260
261 println!(
262 " {} {} (PID {})",
263 "Process:".bright_black(),
264 port_info.process_name.white().bold(),
265 port_info.pid.to_string().cyan()
266 );
267
268 if let Some(proc) = process {
269 if let Some(ref path) = proc.exe_path {
270 println!(" {} {}", "Path:".bright_black(), path.bright_black());
271 }
272 }
273
274 let addr = port_info.address.as_deref().unwrap_or("*");
275 println!(
276 " {} {} on {}",
277 "Listening:".bright_black(),
278 format!("{:?}", port_info.protocol).to_uppercase(),
279 addr
280 );
281
282 if let Some(proc) = process {
283 println!(
284 " {} {:.1}% CPU, {:.1} MB",
285 "Resources:".bright_black(),
286 proc.cpu_percent,
287 proc.memory_mb
288 );
289
290 if let Some(start_time) = proc.start_time {
291 let uptime = std::time::SystemTime::now()
292 .duration_since(std::time::UNIX_EPOCH)
293 .map(|d| d.as_secs().saturating_sub(start_time))
294 .unwrap_or(0);
295 println!(" {} {}", "Uptime:".bright_black(), format_duration(uptime));
296 }
297
298 if self.verbose {
299 if let Some(ref cmd) = proc.command {
300 println!(" {} {}", "Command:".bright_black(), cmd.bright_black());
301 }
302 }
303 }
304
305 println!();
306 }
307
308 fn print_ports_for_process(&self, process: &Process, ports: &[PortInfo]) {
309 println!(
310 "{} {} (PID {}) is listening on:",
311 "✓".green().bold(),
312 process.name.white().bold(),
313 process.pid.to_string().cyan().bold()
314 );
315 println!();
316
317 if ports.is_empty() {
318 println!(" {} No listening ports", "ℹ".blue());
319 } else {
320 for port_info in ports {
321 let addr = port_info.address.as_deref().unwrap_or("*");
322 println!(
323 " {} :{} ({} on {})",
324 "→".bright_black(),
325 port_info.port.to_string().cyan(),
326 format!("{:?}", port_info.protocol).to_uppercase(),
327 addr
328 );
329 }
330 }
331
332 if self.verbose {
333 if let Some(ref path) = process.exe_path {
334 println!();
335 println!(" {} {}", "Path:".bright_black(), path.bright_black());
336 }
337 if let Some(ref cmd) = process.command {
338 println!(" {} {}", "Command:".bright_black(), cmd.bright_black());
339 }
340 }
341
342 println!();
343 }
344}
345
346fn format_duration(secs: u64) -> String {
347 if secs < 60 {
348 format!("{}s", secs)
349 } else if secs < 3600 {
350 format!("{}m {}s", secs / 60, secs % 60)
351 } else if secs < 86400 {
352 format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
353 } else {
354 format!("{}d {}h", secs / 86400, (secs % 86400) / 3600)
355 }
356}
357
358#[derive(Serialize)]
359struct PortLookupOutput<'a> {
360 action: &'static str,
361 query_type: &'static str,
362 success: bool,
363 #[serde(skip_serializing_if = "Option::is_none")]
364 port: Option<u16>,
365 #[serde(skip_serializing_if = "Option::is_none")]
366 protocol: Option<String>,
367 #[serde(skip_serializing_if = "Option::is_none")]
368 address: Option<String>,
369 #[serde(skip_serializing_if = "Option::is_none")]
370 process: Option<&'a Process>,
371 #[serde(skip_serializing_if = "Option::is_none")]
372 ports: Option<&'a [PortInfo]>,
373}
374
375#[derive(Serialize)]
376struct ProcessPortsJson<'a> {
377 process: &'a Process,
378 ports: &'a [PortInfo],
379}