Skip to main content

proc_cli/commands/
ports.rs

1//! `proc ports` - List all listening ports
2//!
3//! Examples:
4//!   proc ports              # Show all listening ports
5//!   proc ports --by node     # Filter by process name
6//!   proc ports --exposed    # Only network-accessible ports (0.0.0.0)
7//!   proc ports --local      # Only localhost ports (127.0.0.1)
8//!   proc ports -v           # Show with executable paths
9
10use 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/// List all listening ports
20#[derive(Args, Debug)]
21pub struct PortsCommand {
22    /// Filter by process name
23    #[arg(long = "by", short = 'b')]
24    pub by_name: Option<String>,
25
26    /// Filter by directory (defaults to current directory if no path given)
27    #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
28    pub in_dir: Option<String>,
29
30    /// Only show network-exposed ports (0.0.0.0, ::)
31    #[arg(long, short = 'e')]
32    pub exposed: bool,
33
34    /// Only show localhost ports (127.0.0.1, ::1)
35    #[arg(long, short = 'l')]
36    pub local: bool,
37
38    /// Output as JSON
39    #[arg(long, short = 'j')]
40    pub json: bool,
41
42    /// Show verbose output (includes executable path)
43    #[arg(long, short = 'v')]
44    pub verbose: bool,
45
46    /// Sort by: port, pid, name
47    #[arg(long, short = 's', value_enum, default_value_t = PortSortKey::Port)]
48    pub sort: PortSortKey,
49
50    /// Limit the number of results
51    #[arg(long, short = 'n')]
52    pub limit: Option<usize>,
53
54    /// Filter ports by range (e.g., 8000-9000)
55    #[arg(long, short = 'r')]
56    pub range: Option<String>,
57}
58
59impl PortsCommand {
60    /// Executes the ports command, listing all listening network ports.
61    pub fn execute(&self) -> Result<()> {
62        let mut ports = PortInfo::get_all_listening()?;
63
64        // Filter by process name if specified
65        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        // Filter by directory if specified
71        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        // Filter by address exposure
86        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        // Filter by port range (e.g., "3000-9000")
105        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        // Sort ports
116        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        // Apply limit if specified
127        if let Some(limit) = self.limit {
128            ports.truncate(limit);
129        }
130
131        // In verbose mode, fetch process info for paths
132        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        // Header
170        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            // In verbose mode, show path
194            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}