1use crate::core::{
14 find_ports_for_pid, flatten_targets, parse_target, resolve_in_dir, resolve_target,
15 ruled_out_by, with_ports, Guards, NameMatch, PortInfo, Process, TargetType,
16};
17use crate::error::{ExitCode, ProcError, Result};
18use crate::out_println;
19use crate::ui::{format_duration, format_memory, out, Printer};
20use clap::Args;
21use colored::*;
22use serde::Serialize;
23use std::io::Write;
24
25#[derive(Args, Debug, Clone)]
27pub struct OnCommand {
28 #[arg(required = true, num_args = 1.., value_parser = crate::core::validate_target_arg)]
30 pub target: Vec<String>,
31
32 #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
35 pub in_dir: Option<String>,
36
37 #[arg(long = "by", short = 'b')]
39 pub by_name: Option<String>,
40
41 #[arg(long)]
43 pub cmd: bool,
44
45 #[arg(long, short = 'j')]
47 pub json: bool,
48
49 #[arg(long, short = 'v')]
51 pub verbose: bool,
52
53 #[arg(skip)]
55 in_dir_path: Option<std::path::PathBuf>,
56}
57
58impl OnCommand {
59 pub fn execute(&self) -> Result<()> {
61 let in_dir_path = resolve_in_dir(&self.in_dir)?;
66 crate::core::require_in_dir_live(&self.in_dir, &in_dir_path)?;
67 let resolved = OnCommand {
68 in_dir_path,
69 ..self.clone()
70 };
71 resolved.run()
72 }
73
74 fn run(&self) -> Result<()> {
76 let targets = flatten_targets(&self.target);
77
78 if targets.len() == 1 {
80 return match parse_target(&targets[0]) {
81 TargetType::Port(port) => self.show_process_on_port(port),
82 TargetType::Pid(pid) => self.show_ports_for_pid(pid),
83 TargetType::Name(name) => self.show_ports_for_name(&name),
84 };
85 }
86
87 if self.json {
88 return self.show_multi_json(&targets);
89 }
90
91 let mut not_found = Vec::new();
93
94 for target in &targets {
95 match parse_target(target) {
96 TargetType::Port(port) => {
97 if let Err(e) = self.show_process_on_port(port) {
98 out_println!("{} Port {}: {}", "⚠".yellow(), port, e);
99 not_found.push(target.clone());
100 }
101 }
102 TargetType::Pid(pid) => {
103 if let Err(e) = self.show_ports_for_pid(pid) {
104 out_println!("{} PID {}: {}", "⚠".yellow(), pid, e);
105 not_found.push(target.clone());
106 }
107 }
108 TargetType::Name(ref name) => {
109 if let Err(e) = self.show_ports_for_name(name) {
110 out_println!("{} '{}': {}", "⚠".yellow(), name, e);
111 not_found.push(target.clone());
112 }
113 }
114 }
115 }
116
117 if not_found.len() == targets.len() {
118 return Err(ProcError::reported(
120 ExitCode::NotFound,
121 format!("No process found matching '{}'", self.target.join(" ")),
122 ));
123 }
124
125 Ok(())
126 }
127
128 fn show_multi_json(&self, targets: &[String]) -> Result<()> {
130 let mut results: Vec<TargetResult> = Vec::new();
131 let mut not_found: Vec<String> = Vec::new();
132
133 for target in targets {
134 let found = match parse_target(target) {
135 TargetType::Port(port) => self.lookup_port(port).map(port_hits),
136 TargetType::Pid(pid) => self.lookup_pid(pid).map(|found| vec![found]),
137 TargetType::Name(ref name) => self.lookup_name(name),
138 };
139
140 match found {
141 Ok(found) if !found.is_empty() => {
142 results.extend(found.into_iter().map(|(process, ports)| TargetResult {
143 target: target.clone(),
144 process,
145 ports,
146 }))
147 }
148 _ => not_found.push(target.clone()),
149 }
150 }
151
152 if results.is_empty() {
153 return Err(ProcError::ProcessNotFound(self.target.join(" ")));
154 }
155
156 Printer::from_flags(true, self.verbose).print_json(&MultiTargetOutput {
157 action: "on",
158 success: true,
159 count: results.len(),
160 results,
161 not_found,
162 });
163
164 Ok(())
165 }
166
167 fn matches_in_filter(&self, proc: &Process) -> bool {
169 self.in_dir_path
170 .as_ref()
171 .is_none_or(|dir_path| crate::core::filters::matches_in_filter(proc, dir_path))
172 }
173
174 fn matches_by_filter(&self, proc: &Process) -> bool {
176 if let Some(ref name) = self.by_name {
177 crate::core::matches_by_filter(proc, name, self.mode())
178 } else {
179 true
180 }
181 }
182
183 fn mode(&self) -> NameMatch {
185 NameMatch::from_flag(self.cmd)
186 }
187
188 fn guards(&self) -> Guards {
190 Guards::new(&self.in_dir, &self.by_name, None)
191 }
192
193 fn matches_filters(&self, proc: &Process) -> bool {
195 self.matches_in_filter(proc) && self.matches_by_filter(proc)
196 }
197
198 fn lookup_port(&self, port: u16) -> Result<Vec<(PortInfo, Option<Process>)>> {
200 let listeners = PortInfo::holders_of(port)?;
201 if listeners.is_empty() {
202 return Err(ProcError::PortNotFound(port));
203 }
204
205 let mut holders = Vec::new();
206 for listener in listeners {
207 let process = Process::find_by_pid(listener.pid)?;
208 holders.push((listener, process));
209 }
210 self.filter_holders(port, holders)
211 }
212
213 fn filter_holders(
218 &self,
219 port: u16,
220 holders: Vec<(PortInfo, Option<Process>)>,
221 ) -> Result<Vec<(PortInfo, Option<Process>)>> {
222 let found: Vec<Process> = holders.iter().filter_map(|(_, p)| p.clone()).collect();
223 let mut kept: Vec<(PortInfo, Option<Process>)> = holders
224 .into_iter()
225 .filter(|(_, process)| process.as_ref().is_none_or(|p| self.matches_filters(p)))
226 .collect();
227 if kept.is_empty() {
228 return Err(ProcError::FilteredOut {
229 target: format!(":{port}"),
230 ruled_out_by: ruled_out_by(&found, &self.guards(), self.mode())?,
231 });
232 }
233
234 let mut live: Vec<Process> = kept.iter().filter_map(|(_, p)| p.clone()).collect();
236 Process::measure_cpu(&mut live);
237 let mut live = live.into_iter();
238 for (_, slot) in kept.iter_mut() {
239 if slot.is_some() {
240 *slot = live.next();
241 }
242 }
243 Ok(kept)
244 }
245
246 fn show_process_on_port(&self, port: u16) -> Result<()> {
248 let holders = self.lookup_port(port)?;
249
250 if self.json {
251 let printer = Printer::from_flags(true, self.verbose);
252 let (first, _) = &holders[0];
253 let processes: Vec<&Process> = holders.iter().filter_map(|(_, p)| p.as_ref()).collect();
254 let output = PortLookupOutput {
255 action: "on",
256 query_type: "port_to_process",
257 success: true,
258 port: Some(first.port),
259 protocol: Some(format!("{:?}", first.protocol).to_lowercase()),
260 address: first.address.clone(),
261 process: processes.first().copied(),
263 processes: Some(processes),
264 ports: None,
265 };
266 printer.print_json(&output);
267 } else {
268 out::recorded(self.print_process_on_port(&mut out::stdout(), port, &holders));
270 }
271
272 Ok(())
273 }
274
275 fn lookup_pid(&self, pid: u32) -> Result<(Process, Vec<PortInfo>)> {
277 let mut process = Process::find_by_pid(pid)?
278 .ok_or_else(|| ProcError::ProcessNotFound(pid.to_string()))?;
279 Process::measure_cpu(std::slice::from_mut(&mut process));
281
282 if !self.matches_filters(&process) {
284 return Err(ProcError::FilteredOut {
285 target: pid.to_string(),
286 ruled_out_by: ruled_out_by(&[process], &self.guards(), self.mode())?,
287 });
288 }
289
290 let ports = find_ports_for_pid(pid)?;
291 Ok((process, ports))
292 }
293
294 fn show_ports_for_pid(&self, pid: u32) -> Result<()> {
296 let (process, ports) = self.lookup_pid(pid)?;
297
298 if self.json {
299 let printer = Printer::from_flags(true, self.verbose);
300 let output = PortLookupOutput {
301 action: "on",
302 query_type: "process_to_ports",
303 success: true,
304 port: None,
305 protocol: None,
306 address: None,
307 process: Some(&process),
308 processes: None,
309 ports: Some(&ports),
310 };
311 printer.print_json(&output);
312 } else {
313 out::recorded(self.print_ports_for_process(&mut out::stdout(), &process, &ports));
314 }
315
316 Ok(())
317 }
318
319 fn lookup_name(&self, name: &str) -> Result<Vec<(Process, Vec<PortInfo>)>> {
321 let mut processes = resolve_target(name, NameMatch::from_flag(self.cmd))?;
323
324 if self.in_dir.is_some() || self.by_name.is_some() {
326 let found = processes.clone();
327 processes.retain(|p| self.matches_filters(p));
328 if processes.is_empty() {
329 return Err(ProcError::FilteredOut {
330 target: name.to_string(),
331 ruled_out_by: ruled_out_by(&found, &self.guards(), self.mode())?,
332 });
333 }
334 }
335
336 Process::measure_cpu(&mut processes);
338
339 with_ports(processes)
340 }
341
342 fn show_ports_for_name(&self, name: &str) -> Result<()> {
344 let all_results = self.lookup_name(name)?;
345
346 if self.json {
347 let printer = Printer::from_flags(true, self.verbose);
348 let results: Vec<_> = all_results
349 .iter()
350 .map(|(proc, ports)| ProcessPortsJson {
351 process: proc,
352 ports,
353 })
354 .collect();
355 let output = MultiProcessPortsOutput {
356 action: "on",
357 success: true,
358 count: results.len(),
359 results: &results,
360 };
361 printer.print_json(&output);
362 } else {
363 for (proc, ports) in &all_results {
364 out::recorded(self.print_ports_for_process(&mut out::stdout(), proc, ports));
365 }
366 }
367
368 Ok(())
369 }
370
371 fn print_process_on_port(
373 &self,
374 out: &mut impl Write,
375 port: u16,
376 holders: &[(PortInfo, Option<Process>)],
377 ) -> std::io::Result<()> {
378 let who = if holders.len() > 1 {
379 format!(" {} processes", holders.len())
380 } else {
381 String::new()
382 };
383 writeln!(
384 out,
385 "{} Port {} is used by{}:",
386 "✓".green().bold(),
387 port.to_string().cyan().bold(),
388 who
389 )?;
390 writeln!(out)?;
391
392 for (port_info, process) in holders {
393 self.print_holder(out, port_info, process.as_ref())?;
394 }
395 Ok(())
396 }
397
398 fn print_holder(
399 &self,
400 out: &mut impl Write,
401 port_info: &PortInfo,
402 process: Option<&Process>,
403 ) -> std::io::Result<()> {
404 writeln!(
405 out,
406 " {} {} (PID {})",
407 "Process:".bright_black(),
408 port_info.process_name.white().bold(),
409 port_info.pid.to_string().cyan()
410 )?;
411
412 if let Some(proc) = process {
413 if let Some(ref cwd) = proc.cwd {
414 writeln!(out, " {} {}", "Directory:".bright_black(), cwd)?;
415 }
416 if let Some(ref path) = proc.exe_path {
417 writeln!(out, " {} {}", "Path:".bright_black(), path.bright_black())?;
418 }
419 }
420
421 let addr = port_info.address.as_deref().unwrap_or("*");
422 writeln!(
423 out,
424 " {} {} on {}",
425 "Listening:".bright_black(),
426 format!("{:?}", port_info.protocol).to_uppercase(),
427 addr
428 )?;
429
430 if let Some(proc) = process {
431 writeln!(
432 out,
433 " {} {:.1}% CPU, {}",
434 "Resources:".bright_black(),
435 proc.cpu_percent,
436 format_memory(proc.memory_mb)
437 )?;
438
439 if let Some(uptime) = proc.uptime_secs {
440 writeln!(
441 out,
442 " {} {}",
443 "Uptime:".bright_black(),
444 format_duration(uptime)
445 )?;
446 }
447
448 if self.verbose {
449 if let Some(ref cmd) = proc.command {
450 writeln!(
451 out,
452 " {} {}",
453 "Command:".bright_black(),
454 cmd.bright_black()
455 )?;
456 }
457 }
458 }
459
460 writeln!(out)
461 }
462
463 fn print_ports_for_process(
464 &self,
465 out: &mut impl Write,
466 process: &Process,
467 ports: &[PortInfo],
468 ) -> std::io::Result<()> {
469 writeln!(
470 out,
471 "{} {} (PID {}) is listening on:",
472 "✓".green().bold(),
473 process.name.white().bold(),
474 process.pid.to_string().cyan().bold()
475 )?;
476 writeln!(out)?;
477
478 if ports.is_empty() {
479 writeln!(out, " {} No listening ports", "ℹ".blue())?;
480 } else {
481 for port_info in ports {
482 let addr = port_info.address.as_deref().unwrap_or("*");
483 writeln!(
484 out,
485 " {} :{} ({} on {})",
486 "→".bright_black(),
487 port_info.port.to_string().cyan(),
488 format!("{:?}", port_info.protocol).to_uppercase(),
489 addr
490 )?;
491 }
492 }
493
494 if process.cwd.is_some() || process.uptime_secs.is_some() {
495 writeln!(out)?;
496 }
497 if let Some(ref cwd) = process.cwd {
498 writeln!(out, " {} {}", "Directory:".bright_black(), cwd)?;
499 }
500 if let Some(uptime) = process.uptime_secs {
501 writeln!(
502 out,
503 " {} {}",
504 "Uptime:".bright_black(),
505 format_duration(uptime)
506 )?;
507 }
508
509 if self.verbose {
510 if let Some(ref path) = process.exe_path {
511 writeln!(out, " {} {}", "Path:".bright_black(), path.bright_black())?;
512 }
513 if let Some(ref cmd) = process.command {
514 writeln!(
515 out,
516 " {} {}",
517 "Command:".bright_black(),
518 cmd.bright_black()
519 )?;
520 }
521 }
522
523 writeln!(out)
524 }
525}
526
527fn port_hits(holders: Vec<(PortInfo, Option<Process>)>) -> Vec<(Process, Vec<PortInfo>)> {
531 holders
532 .into_iter()
533 .filter_map(|(port_info, process)| process.map(|p| (p, vec![port_info])))
534 .collect()
535}
536
537#[derive(Serialize)]
538struct PortLookupOutput<'a> {
539 action: &'static str,
540 query_type: &'static str,
541 success: bool,
542 #[serde(skip_serializing_if = "Option::is_none")]
543 port: Option<u16>,
544 #[serde(skip_serializing_if = "Option::is_none")]
545 protocol: Option<String>,
546 #[serde(skip_serializing_if = "Option::is_none")]
547 address: Option<String>,
548 #[serde(skip_serializing_if = "Option::is_none")]
549 process: Option<&'a Process>,
550 #[serde(skip_serializing_if = "Option::is_none")]
552 processes: Option<Vec<&'a Process>>,
553 #[serde(skip_serializing_if = "Option::is_none")]
554 ports: Option<&'a [PortInfo]>,
555}
556
557#[derive(Serialize)]
558struct ProcessPortsJson<'a> {
559 process: &'a Process,
560 ports: &'a [PortInfo],
561}
562
563#[derive(Serialize)]
564struct TargetResult {
565 target: String,
566 process: Process,
567 ports: Vec<PortInfo>,
568}
569
570#[derive(Serialize)]
571struct MultiTargetOutput {
572 action: &'static str,
573 success: bool,
574 count: usize,
575 results: Vec<TargetResult>,
576 not_found: Vec<String>,
577}
578
579#[derive(Serialize)]
580struct MultiProcessPortsOutput<'a> {
581 action: &'static str,
582 success: bool,
583 count: usize,
584 results: &'a Vec<ProcessPortsJson<'a>>,
585}
586
587#[cfg(test)]
588mod tests {
589 use super::*;
590 use crate::core::{ProcessStatus, Protocol};
591 use crate::test_support::assert_stops_at_a_failed_write;
592
593 fn command(in_dir: Option<&str>) -> OnCommand {
594 let in_dir = in_dir.map(String::from);
595 OnCommand {
596 target: vec!["x".to_string()],
597 in_dir_path: resolve_in_dir(&in_dir).unwrap(),
599 in_dir,
600 by_name: None,
601 cmd: false,
602 json: false,
603 verbose: false,
604 }
605 }
606
607 fn without_cwd() -> Process {
610 Process {
611 pid: 1,
612 name: "x".to_string(),
613 exe_path: None,
614 cwd: None,
615 command: None,
616 argv0: None,
617 raw: None,
618 cpu_percent: 0.0,
619 memory_mb: 0.0,
620 status: ProcessStatus::Sleeping,
621 user: None,
622 parent_pid: None,
623 start_time: None,
624 uptime_secs: None,
625 }
626 }
627
628 fn listener() -> PortInfo {
629 PortInfo {
630 port: 4000,
631 protocol: Protocol::Tcp,
632 pid: 1,
633 process_name: "x".to_string(),
634 address: None,
635 }
636 }
637
638 #[test]
639 fn a_process_with_no_working_directory_fails_in() {
640 assert!(!command(Some("/")).matches_in_filter(&without_cwd()));
641 assert!(command(None).matches_in_filter(&without_cwd()));
642 }
643
644 #[test]
645 fn a_port_whose_process_ended_has_no_process_to_filter() {
646 let holders = command(Some("/"))
647 .filter_holders(4000, vec![(listener(), None)])
648 .unwrap();
649 assert_eq!(holders.len(), 1);
650 assert_eq!(holders[0].0.port, 4000);
651 assert!(holders[0].1.is_none());
652 }
653
654 #[test]
655 fn the_filters_drop_the_holders_they_do_not_match() {
656 let mut elsewhere = without_cwd();
657 elsewhere.pid = 2;
658 elsewhere.cwd = Some("/definitely/not/here".to_string());
659 let mut here = without_cwd();
660 here.pid = 3;
661 here.cwd = Some(
662 std::env::current_dir()
663 .unwrap()
664 .to_string_lossy()
665 .into_owned(),
666 );
667
668 let holders = vec![
669 (listener(), Some(elsewhere)),
670 (listener(), Some(here.clone())),
671 (listener(), None),
672 ];
673 let kept = command(Some("."))
674 .filter_holders(4000, holders.clone())
675 .unwrap();
676 let pids: Vec<_> = kept
677 .iter()
678 .map(|(_, p)| p.as_ref().map(|p| p.pid))
679 .collect();
680 assert_eq!(pids, vec![Some(3), None]);
681
682 let none = command(Some("/definitely/not/here/either"))
684 .filter_holders(4000, holders[..1].to_vec());
685 assert!(
686 matches!(&none, Err(ProcError::FilteredOut { target, .. }) if target == ":4000"),
687 "{none:?}"
688 );
689 }
690
691 #[test]
692 fn a_port_whose_process_ended_adds_nothing_to_the_json_results() {
693 assert!(port_hits(vec![(listener(), None)]).is_empty());
694 let hits = port_hits(vec![
695 (listener(), Some(without_cwd())),
696 (listener(), None),
697 (listener(), Some(without_cwd())),
698 ]);
699 assert_eq!(hits.len(), 2);
700 }
701
702 fn printed(print: impl FnOnce(&mut Vec<u8>) -> std::io::Result<()>) -> String {
704 colored::control::set_override(false);
705 let mut out = Vec::new();
706 print(&mut out).unwrap();
707 String::from_utf8(out).unwrap()
708 }
709
710 #[test]
711 fn a_port_without_a_process_still_prints() {
712 let mut cmd = command(None);
714 cmd.verbose = true;
715 let out = printed(|w| cmd.print_process_on_port(w, 4000, &[(listener(), None)]));
716 assert_eq!(
717 out,
718 "✓ Port 4000 is used by:\n\n Process: x (PID 1)\n Listening: TCP on *\n\n"
719 );
720 }
721
722 #[test]
723 fn a_port_with_several_holders_says_so_and_lists_each() {
724 let mut second = listener();
725 second.pid = 2;
726 second.address = Some("::1".to_string());
727 let out = printed(|w| {
728 command(None).print_process_on_port(w, 4000, &[(listener(), None), (second, None)])
729 });
730 assert_eq!(
731 out,
732 "✓ Port 4000 is used by 2 processes:\n\n\
733 \x20 Process: x (PID 1)\n Listening: TCP on *\n\n\
734 \x20 Process: x (PID 2)\n Listening: TCP on ::1\n\n"
735 );
736 }
737
738 #[test]
739 fn a_process_missing_its_details_prints_without_them() {
740 let mut cmd = command(None);
741 cmd.verbose = true;
742
743 let out =
744 printed(|w| cmd.print_process_on_port(w, 4000, &[(listener(), Some(without_cwd()))]));
745 assert!(out.contains("Listening: TCP on *\n"), "{out}");
746 assert!(out.contains("Resources: 0.0% CPU"), "{out}");
747 for label in ["Directory:", "Path:", "Uptime:", "Command:"] {
748 assert!(!out.contains(label), "{label} printed for nothing: {out}");
749 }
750
751 let out = printed(|w| cmd.print_ports_for_process(w, &without_cwd(), &[listener()]));
752 assert_eq!(
753 out,
754 "✓ x (PID 1) is listening on:\n\n → :4000 (TCP on *)\n\n"
755 );
756 }
757
758 fn detailed() -> Process {
760 let mut p = without_cwd();
761 p.cwd = Some("/work/app".to_string());
762 p.exe_path = Some("/usr/bin/x".to_string());
763 p.command = Some("x --serve".to_string());
764 p.uptime_secs = Some(90);
765 p
766 }
767
768 #[test]
769 fn a_failed_write_ends_the_port_lookup() {
770 let mut cmd = command(None);
771 cmd.verbose = true;
772 let p = detailed();
773 let held = [(listener(), Some(p))];
774 assert_stops_at_a_failed_write(|w| cmd.print_process_on_port(w, 4000, &held));
775 assert_stops_at_a_failed_write(|w| {
776 cmd.print_process_on_port(w, 4000, &[(listener(), None)])
777 });
778 }
779
780 #[test]
781 fn a_failed_write_ends_the_process_lookup() {
782 let mut cmd = command(None);
783 cmd.verbose = true;
784 let p = detailed();
785 assert_stops_at_a_failed_write(|w| cmd.print_ports_for_process(w, &p, &[listener()]));
786 assert_stops_at_a_failed_write(|w| cmd.print_ports_for_process(w, &p, &[]));
787 }
788}