proc_cli/commands/
ports.rs1use crate::core::{PortInfo, Process};
11use crate::error::Result;
12use crate::ui::{OutputFormat, Printer};
13use clap::Args;
14use colored::*;
15use serde::Serialize;
16use std::collections::HashMap;
17
18#[derive(Args, Debug)]
20pub struct PortsCommand {
21 #[arg(long, short = 'f')]
23 pub filter: Option<String>,
24
25 #[arg(long, short = 'e')]
27 pub exposed: bool,
28
29 #[arg(long, short = 'l')]
31 pub local: bool,
32
33 #[arg(long, short = 'j')]
35 pub json: bool,
36
37 #[arg(long, short = 'v')]
39 pub verbose: bool,
40
41 #[arg(long, short = 's', default_value = "port")]
43 pub sort: String,
44}
45
46impl PortsCommand {
47 pub fn execute(&self) -> Result<()> {
49 let mut ports = PortInfo::get_all_listening()?;
50
51 if let Some(ref filter) = self.filter {
53 let filter_lower = filter.to_lowercase();
54 ports.retain(|p| p.process_name.to_lowercase().contains(&filter_lower));
55 }
56
57 if self.exposed {
59 ports.retain(|p| {
60 p.address
61 .as_ref()
62 .map(|a| a == "0.0.0.0" || a == "::" || a == "*")
63 .unwrap_or(true)
64 });
65 }
66
67 if self.local {
68 ports.retain(|p| {
69 p.address
70 .as_ref()
71 .map(|a| a == "127.0.0.1" || a == "::1" || a.starts_with("[::1]"))
72 .unwrap_or(false)
73 });
74 }
75
76 match self.sort.to_lowercase().as_str() {
78 "port" => ports.sort_by_key(|p| p.port),
79 "pid" => ports.sort_by_key(|p| p.pid),
80 "name" => ports.sort_by(|a, b| {
81 a.process_name
82 .to_lowercase()
83 .cmp(&b.process_name.to_lowercase())
84 }),
85 _ => ports.sort_by_key(|p| p.port),
86 }
87
88 let process_map: HashMap<u32, Process> = if self.verbose {
90 let mut map = HashMap::new();
91 for port in &ports {
92 if let std::collections::hash_map::Entry::Vacant(e) = map.entry(port.pid) {
93 if let Ok(Some(proc)) = Process::find_by_pid(port.pid) {
94 e.insert(proc);
95 }
96 }
97 }
98 map
99 } else {
100 HashMap::new()
101 };
102
103 if self.json {
104 self.print_json(&ports, &process_map);
105 } else {
106 self.print_human(&ports, &process_map);
107 }
108
109 Ok(())
110 }
111
112 fn print_human(&self, ports: &[PortInfo], process_map: &HashMap<u32, Process>) {
113 if ports.is_empty() {
114 println!("{} No listening ports found", "⚠".yellow().bold());
115 return;
116 }
117
118 println!(
119 "{} Found {} listening port{}",
120 "✓".green().bold(),
121 ports.len().to_string().cyan().bold(),
122 if ports.len() == 1 { "" } else { "s" }
123 );
124 println!();
125
126 println!(
128 "{:<8} {:<10} {:<8} {:<20} {:<15}",
129 "PORT".bright_blue().bold(),
130 "PROTO".bright_blue().bold(),
131 "PID".bright_blue().bold(),
132 "PROCESS".bright_blue().bold(),
133 "ADDRESS".bright_blue().bold()
134 );
135 println!("{}", "─".repeat(65).bright_black());
136
137 for port in ports {
138 let addr = port.address.as_deref().unwrap_or("*");
139 let proto = format!("{:?}", port.protocol).to_uppercase();
140
141 println!(
142 "{:<8} {:<10} {:<8} {:<20} {:<15}",
143 port.port.to_string().cyan().bold(),
144 proto.white(),
145 port.pid.to_string().cyan(),
146 truncate_string(&port.process_name, 19).white(),
147 addr.bright_black()
148 );
149
150 if self.verbose {
152 if let Some(proc) = process_map.get(&port.pid) {
153 if let Some(ref path) = proc.exe_path {
154 println!(
155 " {} {}",
156 "↳".bright_black(),
157 truncate_string(path, 55).bright_black()
158 );
159 }
160 }
161 }
162 }
163 println!();
164 }
165
166 fn print_json(&self, ports: &[PortInfo], process_map: &HashMap<u32, Process>) {
167 let printer = Printer::new(OutputFormat::Json, self.verbose);
168
169 #[derive(Serialize)]
170 struct PortWithProcess<'a> {
171 #[serde(flatten)]
172 port: &'a PortInfo,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 exe_path: Option<&'a str>,
175 }
176
177 let enriched: Vec<PortWithProcess> = ports
178 .iter()
179 .map(|p| PortWithProcess {
180 port: p,
181 exe_path: process_map
182 .get(&p.pid)
183 .and_then(|proc| proc.exe_path.as_deref()),
184 })
185 .collect();
186
187 #[derive(Serialize)]
188 struct Output<'a> {
189 action: &'static str,
190 success: bool,
191 count: usize,
192 ports: Vec<PortWithProcess<'a>>,
193 }
194
195 printer.print_json(&Output {
196 action: "ports",
197 success: true,
198 count: ports.len(),
199 ports: enriched,
200 });
201 }
202}
203
204fn truncate_string(s: &str, max_len: usize) -> String {
205 if s.len() <= max_len {
206 s.to_string()
207 } else {
208 format!("{}...", &s[..max_len.saturating_sub(3)])
209 }
210}