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;
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', default_value = "port")]
48 pub sort: String,
49}
50
51impl PortsCommand {
52 pub fn execute(&self) -> Result<()> {
54 let mut ports = PortInfo::get_all_listening()?;
55
56 if let Some(ref name) = self.by_name {
58 let name_lower = name.to_lowercase();
59 ports.retain(|p| p.process_name.to_lowercase().contains(&name_lower));
60 }
61
62 if let Some(ref _dir) = self.in_dir {
64 let in_dir_filter = resolve_in_dir(&self.in_dir);
65 if let Some(ref dir_path) = in_dir_filter {
66 ports.retain(|p| {
67 if let Ok(Some(proc)) = Process::find_by_pid(p.pid) {
68 if let Some(ref cwd) = proc.cwd {
69 return PathBuf::from(cwd).starts_with(dir_path);
70 }
71 }
72 false
73 });
74 }
75 }
76
77 if self.exposed {
79 ports.retain(|p| {
80 p.address
81 .as_ref()
82 .map(|a| a == "0.0.0.0" || a == "::" || a == "*")
83 .unwrap_or(true)
84 });
85 }
86
87 if self.local {
88 ports.retain(|p| {
89 p.address
90 .as_ref()
91 .map(|a| a == "127.0.0.1" || a == "::1" || a.starts_with("[::1]"))
92 .unwrap_or(false)
93 });
94 }
95
96 match self.sort.to_lowercase().as_str() {
98 "port" => ports.sort_by_key(|p| p.port),
99 "pid" => ports.sort_by_key(|p| p.pid),
100 "name" => ports.sort_by(|a, b| {
101 a.process_name
102 .to_lowercase()
103 .cmp(&b.process_name.to_lowercase())
104 }),
105 _ => ports.sort_by_key(|p| p.port),
106 }
107
108 let process_map: HashMap<u32, Process> = if self.verbose {
110 let mut map = HashMap::new();
111 for port in &ports {
112 if let std::collections::hash_map::Entry::Vacant(e) = map.entry(port.pid) {
113 if let Ok(Some(proc)) = Process::find_by_pid(port.pid) {
114 e.insert(proc);
115 }
116 }
117 }
118 map
119 } else {
120 HashMap::new()
121 };
122
123 if self.json {
124 self.print_json(&ports, &process_map);
125 } else {
126 self.print_human(&ports, &process_map);
127 }
128
129 Ok(())
130 }
131
132 fn print_human(&self, ports: &[PortInfo], process_map: &HashMap<u32, Process>) {
133 if ports.is_empty() {
134 println!("{} No listening ports found", "⚠".yellow().bold());
135 return;
136 }
137
138 println!(
139 "{} Found {} listening port{}",
140 "✓".green().bold(),
141 ports.len().to_string().cyan().bold(),
142 if ports.len() == 1 { "" } else { "s" }
143 );
144 println!();
145
146 println!(
148 "{:<8} {:<10} {:<8} {:<20} {:<15}",
149 "PORT".bright_blue().bold(),
150 "PROTO".bright_blue().bold(),
151 "PID".bright_blue().bold(),
152 "PROCESS".bright_blue().bold(),
153 "ADDRESS".bright_blue().bold()
154 );
155 println!("{}", "─".repeat(65).bright_black());
156
157 for port in ports {
158 let addr = port.address.as_deref().unwrap_or("*");
159 let proto = format!("{:?}", port.protocol).to_uppercase();
160
161 println!(
162 "{:<8} {:<10} {:<8} {:<20} {:<15}",
163 port.port.to_string().cyan().bold(),
164 proto.white(),
165 port.pid.to_string().cyan(),
166 truncate_string(&port.process_name, 19).white(),
167 addr.bright_black()
168 );
169
170 if self.verbose {
172 if let Some(proc) = process_map.get(&port.pid) {
173 if let Some(ref path) = proc.exe_path {
174 println!(
175 " {} {}",
176 "↳".bright_black(),
177 truncate_string(path, 55).bright_black()
178 );
179 }
180 }
181 }
182 }
183 println!();
184 }
185
186 fn print_json(&self, ports: &[PortInfo], process_map: &HashMap<u32, Process>) {
187 let printer = Printer::new(OutputFormat::Json, self.verbose);
188
189 #[derive(Serialize)]
190 struct PortWithProcess<'a> {
191 #[serde(flatten)]
192 port: &'a PortInfo,
193 #[serde(skip_serializing_if = "Option::is_none")]
194 exe_path: Option<&'a str>,
195 }
196
197 let enriched: Vec<PortWithProcess> = ports
198 .iter()
199 .map(|p| PortWithProcess {
200 port: p,
201 exe_path: process_map
202 .get(&p.pid)
203 .and_then(|proc| proc.exe_path.as_deref()),
204 })
205 .collect();
206
207 #[derive(Serialize)]
208 struct Output<'a> {
209 action: &'static str,
210 success: bool,
211 count: usize,
212 ports: Vec<PortWithProcess<'a>>,
213 }
214
215 printer.print_json(&Output {
216 action: "ports",
217 success: true,
218 count: ports.len(),
219 ports: enriched,
220 });
221 }
222}
223
224fn truncate_string(s: &str, max_len: usize) -> String {
225 if s.len() <= max_len {
226 s.to_string()
227 } else {
228 format!("{}...", &s[..max_len.saturating_sub(3)])
229 }
230}
231
232fn resolve_in_dir(in_dir: &Option<String>) -> Option<PathBuf> {
233 in_dir.as_ref().map(|p| {
234 if p == "." {
235 std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
236 } else {
237 let path = PathBuf::from(p);
238 if path.is_relative() {
239 std::env::current_dir()
240 .unwrap_or_else(|_| PathBuf::from("."))
241 .join(path)
242 } else {
243 path
244 }
245 }
246 })
247}