proc_cli/commands/
ports.rs1use crate::core::{resolve_in_dir, PortInfo, PortSortKey, Process};
11use crate::error::Result;
12use crate::ui::{truncate_string, OutputFormat, Printer};
13use clap::Args;
14use colored::*;
15use serde::Serialize;
16use std::collections::HashMap;
17use std::path::PathBuf;
18
19#[derive(Args, Debug)]
21pub struct PortsCommand {
22 #[arg(long = "by", short = 'b')]
24 pub by_name: Option<String>,
25
26 #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
28 pub in_dir: Option<String>,
29
30 #[arg(long, short = 'e')]
32 pub exposed: bool,
33
34 #[arg(long, short = 'l')]
36 pub local: bool,
37
38 #[arg(long, short = 'j')]
40 pub json: bool,
41
42 #[arg(long, short = 'v')]
44 pub verbose: bool,
45
46 #[arg(long, short = 's', value_enum, default_value_t = PortSortKey::Port)]
48 pub sort: PortSortKey,
49
50 #[arg(long, short = 'n')]
52 pub limit: Option<usize>,
53
54 #[arg(long, short = 'r')]
56 pub range: Option<String>,
57}
58
59impl PortsCommand {
60 pub fn execute(&self) -> Result<()> {
62 let mut ports = PortInfo::get_all_listening()?;
63
64 if let Some(ref name) = self.by_name {
66 let name_lower = name.to_lowercase();
67 ports.retain(|p| p.process_name.to_lowercase().contains(&name_lower));
68 }
69
70 if let Some(ref _dir) = self.in_dir {
72 let in_dir_filter = resolve_in_dir(&self.in_dir);
73 if let Some(ref dir_path) = in_dir_filter {
74 ports.retain(|p| {
75 if let Ok(Some(proc)) = Process::find_by_pid(p.pid) {
76 if let Some(ref cwd) = proc.cwd {
77 return PathBuf::from(cwd).starts_with(dir_path);
78 }
79 }
80 false
81 });
82 }
83 }
84
85 if self.exposed {
87 ports.retain(|p| {
88 p.address
89 .as_ref()
90 .map(|a| a == "0.0.0.0" || a == "::" || a == "*")
91 .unwrap_or(true)
92 });
93 }
94
95 if self.local {
96 ports.retain(|p| {
97 p.address
98 .as_ref()
99 .map(|a| a == "127.0.0.1" || a == "::1" || a.starts_with("[::1]"))
100 .unwrap_or(false)
101 });
102 }
103
104 if let Some(ref range) = self.range {
106 if let Some((start, end)) = range.split_once('-') {
107 if let (Ok(start), Ok(end)) =
108 (start.trim().parse::<u16>(), end.trim().parse::<u16>())
109 {
110 ports.retain(|p| p.port >= start && p.port <= end);
111 }
112 }
113 }
114
115 match self.sort {
117 PortSortKey::Port => ports.sort_by_key(|p| p.port),
118 PortSortKey::Pid => ports.sort_by_key(|p| p.pid),
119 PortSortKey::Name => ports.sort_by(|a, b| {
120 a.process_name
121 .to_lowercase()
122 .cmp(&b.process_name.to_lowercase())
123 }),
124 }
125
126 if let Some(limit) = self.limit {
128 ports.truncate(limit);
129 }
130
131 let process_map: HashMap<u32, Process> = if self.verbose {
133 let mut map = HashMap::new();
134 for port in &ports {
135 if let std::collections::hash_map::Entry::Vacant(e) = map.entry(port.pid) {
136 if let Ok(Some(proc)) = Process::find_by_pid(port.pid) {
137 e.insert(proc);
138 }
139 }
140 }
141 map
142 } else {
143 HashMap::new()
144 };
145
146 if self.json {
147 self.print_json(&ports, &process_map);
148 } else {
149 self.print_human(&ports, &process_map);
150 }
151
152 Ok(())
153 }
154
155 fn print_human(&self, ports: &[PortInfo], process_map: &HashMap<u32, Process>) {
156 if ports.is_empty() {
157 println!("{} No listening ports found", "⚠".yellow().bold());
158 return;
159 }
160
161 println!(
162 "{} Found {} listening port{}",
163 "✓".green().bold(),
164 ports.len().to_string().cyan().bold(),
165 if ports.len() == 1 { "" } else { "s" }
166 );
167 println!();
168
169 println!(
171 "{:<8} {:<10} {:<8} {:<20} {:<15}",
172 "PORT".bright_blue().bold(),
173 "PROTO".bright_blue().bold(),
174 "PID".bright_blue().bold(),
175 "PROCESS".bright_blue().bold(),
176 "ADDRESS".bright_blue().bold()
177 );
178 println!("{}", "─".repeat(65).bright_black());
179
180 for port in ports {
181 let addr = port.address.as_deref().unwrap_or("*");
182 let proto = format!("{:?}", port.protocol).to_uppercase();
183
184 println!(
185 "{:<8} {:<10} {:<8} {:<20} {:<15}",
186 port.port.to_string().cyan().bold(),
187 proto.white(),
188 port.pid.to_string().cyan(),
189 truncate_string(&port.process_name, 19).white(),
190 addr.bright_black()
191 );
192
193 if self.verbose {
195 if let Some(proc) = process_map.get(&port.pid) {
196 if let Some(ref path) = proc.exe_path {
197 println!(
198 " {} {}",
199 "↳".bright_black(),
200 truncate_string(path, 55).bright_black()
201 );
202 }
203 }
204 }
205 }
206 println!();
207 }
208
209 fn print_json(&self, ports: &[PortInfo], process_map: &HashMap<u32, Process>) {
210 let printer = Printer::new(OutputFormat::Json, self.verbose);
211
212 #[derive(Serialize)]
213 struct PortWithProcess<'a> {
214 #[serde(flatten)]
215 port: &'a PortInfo,
216 #[serde(skip_serializing_if = "Option::is_none")]
217 exe_path: Option<&'a str>,
218 }
219
220 let enriched: Vec<PortWithProcess> = ports
221 .iter()
222 .map(|p| PortWithProcess {
223 port: p,
224 exe_path: process_map
225 .get(&p.pid)
226 .and_then(|proc| proc.exe_path.as_deref()),
227 })
228 .collect();
229
230 #[derive(Serialize)]
231 struct Output<'a> {
232 action: &'static str,
233 success: bool,
234 count: usize,
235 ports: Vec<PortWithProcess<'a>>,
236 }
237
238 printer.print_json(&Output {
239 action: "ports",
240 success: true,
241 count: ports.len(),
242 ports: enriched,
243 });
244 }
245}