1use crate::core::{
11 find_ports_for_pid, parse_target, parse_targets, resolve_in_dir, resolve_target, PortInfo,
12 Process, TargetType,
13};
14use crate::error::{ExitCode, ProcError, Result};
15use crate::ui::{format_duration, format_memory, Printer};
16use clap::Args;
17use colored::*;
18use serde::Serialize;
19use std::path::PathBuf;
20
21#[derive(Args, Debug)]
23pub struct OnCommand {
24 pub target: String,
26
27 #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
29 pub in_dir: Option<String>,
30
31 #[arg(long = "by", short = 'b')]
33 pub by_name: Option<String>,
34
35 #[arg(long, short = 'j')]
37 pub json: bool,
38
39 #[arg(long, short = 'v')]
41 pub verbose: bool,
42}
43
44impl OnCommand {
45 pub fn execute(&self) -> Result<()> {
47 let targets = parse_targets(&self.target);
48
49 if targets.len() == 1 {
51 return match parse_target(&targets[0]) {
52 TargetType::Port(port) => self.show_process_on_port(port),
53 TargetType::Pid(pid) => self.show_ports_for_pid(pid),
54 TargetType::Name(name) => self.show_ports_for_name(&name),
55 };
56 }
57
58 if self.json {
59 return self.show_multi_json(&targets);
60 }
61
62 let mut not_found = Vec::new();
64
65 for target in &targets {
66 match parse_target(target) {
67 TargetType::Port(port) => {
68 if let Err(e) = self.show_process_on_port(port) {
69 println!("{} Port {}: {}", "⚠".yellow(), port, e);
70 not_found.push(target.clone());
71 }
72 }
73 TargetType::Pid(pid) => {
74 if let Err(e) = self.show_ports_for_pid(pid) {
75 println!("{} PID {}: {}", "⚠".yellow(), pid, e);
76 not_found.push(target.clone());
77 }
78 }
79 TargetType::Name(ref name) => {
80 if let Err(e) = self.show_ports_for_name(name) {
81 println!("{} '{}': {}", "⚠".yellow(), name, e);
82 not_found.push(target.clone());
83 }
84 }
85 }
86 }
87
88 if not_found.len() == targets.len() {
89 return Err(ProcError::reported(
91 ExitCode::NotFound,
92 format!("No process found matching '{}'", self.target),
93 ));
94 }
95
96 Ok(())
97 }
98
99 fn show_multi_json(&self, targets: &[String]) -> Result<()> {
101 let mut results: Vec<TargetResult> = Vec::new();
102 let mut not_found: Vec<String> = Vec::new();
103
104 for target in targets {
105 let found = match parse_target(target) {
106 TargetType::Port(port) => {
107 self.lookup_port(port)
108 .map(|(port_info, process)| match process {
109 Some(process) => vec![(process, vec![port_info])],
110 None => Vec::new(),
111 })
112 }
113 TargetType::Pid(pid) => self.lookup_pid(pid).map(|found| vec![found]),
114 TargetType::Name(ref name) => self.lookup_name(name),
115 };
116
117 match found {
118 Ok(found) if !found.is_empty() => {
119 results.extend(found.into_iter().map(|(process, ports)| TargetResult {
120 target: target.clone(),
121 process,
122 ports,
123 }))
124 }
125 _ => not_found.push(target.clone()),
126 }
127 }
128
129 if results.is_empty() {
130 return Err(ProcError::ProcessNotFound(self.target.clone()));
131 }
132
133 Printer::from_flags(true, self.verbose).print_json(&MultiTargetOutput {
134 action: "on",
135 success: true,
136 count: results.len(),
137 results,
138 not_found,
139 });
140
141 Ok(())
142 }
143
144 fn matches_in_filter(&self, proc: &Process) -> bool {
146 if let Some(ref dir_path) = resolve_in_dir(&self.in_dir) {
147 if let Some(ref proc_cwd) = proc.cwd {
148 let proc_path = PathBuf::from(proc_cwd);
149 proc_path.starts_with(dir_path)
150 } else {
151 false
152 }
153 } else {
154 true
155 }
156 }
157
158 fn matches_by_filter(&self, proc: &Process) -> bool {
160 if let Some(ref name) = self.by_name {
161 crate::core::matches_by_filter(proc, name)
162 } else {
163 true
164 }
165 }
166
167 fn matches_filters(&self, proc: &Process) -> bool {
169 self.matches_in_filter(proc) && self.matches_by_filter(proc)
170 }
171
172 fn lookup_port(&self, port: u16) -> Result<(PortInfo, Option<Process>)> {
174 let port_info = match PortInfo::find_by_port(port)? {
175 Some(info) => info,
176 None => return Err(ProcError::PortNotFound(port)),
177 };
178
179 let process = Process::find_by_pid(port_info.pid)?;
180
181 if let Some(ref proc) = process {
183 if !self.matches_filters(proc) {
184 return Err(ProcError::ProcessNotFound(format!(
185 "port {} (process not in specified directory)",
186 port
187 )));
188 }
189 }
190
191 Ok((port_info, process))
192 }
193
194 fn show_process_on_port(&self, port: u16) -> Result<()> {
196 let (port_info, process) = self.lookup_port(port)?;
197
198 if self.json {
199 let printer = Printer::from_flags(true, self.verbose);
200 let output = PortLookupOutput {
201 action: "on",
202 query_type: "port_to_process",
203 success: true,
204 port: Some(port_info.port),
205 protocol: Some(format!("{:?}", port_info.protocol).to_lowercase()),
206 address: port_info.address.clone(),
207 process: process.as_ref(),
208 ports: None,
209 };
210 printer.print_json(&output);
211 } else {
212 self.print_process_on_port(&port_info, process.as_ref());
213 }
214
215 Ok(())
216 }
217
218 fn lookup_pid(&self, pid: u32) -> Result<(Process, Vec<PortInfo>)> {
220 let process = Process::find_by_pid(pid)?
221 .ok_or_else(|| ProcError::ProcessNotFound(pid.to_string()))?;
222
223 if !self.matches_filters(&process) {
225 return Err(ProcError::ProcessNotFound(format!(
226 "PID {} (not in specified directory)",
227 pid
228 )));
229 }
230
231 let ports = find_ports_for_pid(pid)?;
232 Ok((process, ports))
233 }
234
235 fn show_ports_for_pid(&self, pid: u32) -> Result<()> {
237 let (process, ports) = self.lookup_pid(pid)?;
238
239 if self.json {
240 let printer = Printer::from_flags(true, self.verbose);
241 let output = PortLookupOutput {
242 action: "on",
243 query_type: "process_to_ports",
244 success: true,
245 port: None,
246 protocol: None,
247 address: None,
248 process: Some(&process),
249 ports: Some(&ports),
250 };
251 printer.print_json(&output);
252 } else {
253 self.print_ports_for_process(&process, &ports);
254 }
255
256 Ok(())
257 }
258
259 fn lookup_name(&self, name: &str) -> Result<Vec<(Process, Vec<PortInfo>)>> {
261 let mut processes = resolve_target(name)?;
262
263 if processes.is_empty() {
264 return Err(ProcError::ProcessNotFound(name.to_string()));
265 }
266
267 if self.in_dir.is_some() || self.by_name.is_some() {
269 processes.retain(|p| self.matches_filters(p));
270 if processes.is_empty() {
271 return Err(ProcError::ProcessNotFound(format!(
272 "'{}' (no matches with specified filters)",
273 name
274 )));
275 }
276 }
277
278 let mut all_results: Vec<(Process, Vec<PortInfo>)> = Vec::new();
279
280 for proc in processes {
281 let ports = find_ports_for_pid(proc.pid)?;
282 all_results.push((proc, ports));
283 }
284
285 Ok(all_results)
286 }
287
288 fn show_ports_for_name(&self, name: &str) -> Result<()> {
290 let all_results = self.lookup_name(name)?;
291
292 if self.json {
293 let printer = Printer::from_flags(true, self.verbose);
294 let results: Vec<_> = all_results
295 .iter()
296 .map(|(proc, ports)| ProcessPortsJson {
297 process: proc,
298 ports,
299 })
300 .collect();
301 let output = MultiProcessPortsOutput {
302 action: "on",
303 success: true,
304 count: results.len(),
305 results: &results,
306 };
307 printer.print_json(&output);
308 } else {
309 for (proc, ports) in &all_results {
310 self.print_ports_for_process(proc, ports);
311 }
312 }
313
314 Ok(())
315 }
316
317 fn print_process_on_port(&self, port_info: &PortInfo, process: Option<&Process>) {
318 println!(
319 "{} Port {} is used by:",
320 "✓".green().bold(),
321 port_info.port.to_string().cyan().bold()
322 );
323 println!();
324
325 println!(
326 " {} {} (PID {})",
327 "Process:".bright_black(),
328 port_info.process_name.white().bold(),
329 port_info.pid.to_string().cyan()
330 );
331
332 if let Some(proc) = process {
333 if let Some(ref cwd) = proc.cwd {
334 println!(" {} {}", "Directory:".bright_black(), cwd);
335 }
336 if let Some(ref path) = proc.exe_path {
337 println!(" {} {}", "Path:".bright_black(), path.bright_black());
338 }
339 }
340
341 let addr = port_info.address.as_deref().unwrap_or("*");
342 println!(
343 " {} {} on {}",
344 "Listening:".bright_black(),
345 format!("{:?}", port_info.protocol).to_uppercase(),
346 addr
347 );
348
349 if let Some(proc) = process {
350 println!(
351 " {} {:.1}% CPU, {}",
352 "Resources:".bright_black(),
353 proc.cpu_percent,
354 format_memory(proc.memory_mb)
355 );
356
357 if let Some(start_time) = proc.start_time {
358 let uptime = std::time::SystemTime::now()
359 .duration_since(std::time::UNIX_EPOCH)
360 .map(|d| d.as_secs().saturating_sub(start_time))
361 .unwrap_or(0);
362 println!(" {} {}", "Uptime:".bright_black(), format_duration(uptime));
363 }
364
365 if self.verbose {
366 if let Some(ref cmd) = proc.command {
367 println!(" {} {}", "Command:".bright_black(), cmd.bright_black());
368 }
369 }
370 }
371
372 println!();
373 }
374
375 fn print_ports_for_process(&self, process: &Process, ports: &[PortInfo]) {
376 println!(
377 "{} {} (PID {}) is listening on:",
378 "✓".green().bold(),
379 process.name.white().bold(),
380 process.pid.to_string().cyan().bold()
381 );
382 println!();
383
384 if ports.is_empty() {
385 println!(" {} No listening ports", "ℹ".blue());
386 } else {
387 for port_info in ports {
388 let addr = port_info.address.as_deref().unwrap_or("*");
389 println!(
390 " {} :{} ({} on {})",
391 "→".bright_black(),
392 port_info.port.to_string().cyan(),
393 format!("{:?}", port_info.protocol).to_uppercase(),
394 addr
395 );
396 }
397 }
398
399 if let Some(ref cwd) = process.cwd {
400 println!();
401 println!(" {} {}", "Directory:".bright_black(), cwd);
402 }
403
404 if self.verbose {
405 if let Some(ref path) = process.exe_path {
406 println!(" {} {}", "Path:".bright_black(), path.bright_black());
407 }
408 if let Some(ref cmd) = process.command {
409 println!(" {} {}", "Command:".bright_black(), cmd.bright_black());
410 }
411 }
412
413 println!();
414 }
415}
416
417#[derive(Serialize)]
418struct PortLookupOutput<'a> {
419 action: &'static str,
420 query_type: &'static str,
421 success: bool,
422 #[serde(skip_serializing_if = "Option::is_none")]
423 port: Option<u16>,
424 #[serde(skip_serializing_if = "Option::is_none")]
425 protocol: Option<String>,
426 #[serde(skip_serializing_if = "Option::is_none")]
427 address: Option<String>,
428 #[serde(skip_serializing_if = "Option::is_none")]
429 process: Option<&'a Process>,
430 #[serde(skip_serializing_if = "Option::is_none")]
431 ports: Option<&'a [PortInfo]>,
432}
433
434#[derive(Serialize)]
435struct ProcessPortsJson<'a> {
436 process: &'a Process,
437 ports: &'a [PortInfo],
438}
439
440#[derive(Serialize)]
441struct TargetResult {
442 target: String,
443 process: Process,
444 ports: Vec<PortInfo>,
445}
446
447#[derive(Serialize)]
448struct MultiTargetOutput {
449 action: &'static str,
450 success: bool,
451 count: usize,
452 results: Vec<TargetResult>,
453 not_found: Vec<String>,
454}
455
456#[derive(Serialize)]
457struct MultiProcessPortsOutput<'a> {
458 action: &'static str,
459 success: bool,
460 count: usize,
461 results: &'a Vec<ProcessPortsJson<'a>>,
462}