1use crate::core::{
11 find_ports_for_pid, parse_target, parse_targets, resolve_in_dir, resolve_target, PortInfo,
12 Process, TargetType,
13};
14use crate::error::{ProcError, Result};
15use crate::ui::{format_duration, format_memory};
16use clap::Args;
17use colored::*;
18use serde::Serialize;
19use std::path::PathBuf;
20
21#[derive(Args, Debug)]
23pub struct OnCommand {
24 pub target: String,
26
27 #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
29 pub in_dir: Option<String>,
30
31 #[arg(long = "by", short = 'b')]
33 pub by_name: Option<String>,
34
35 #[arg(long, short = 'j')]
37 pub json: bool,
38
39 #[arg(long, short = 'v')]
41 pub verbose: bool,
42}
43
44impl OnCommand {
45 pub fn execute(&self) -> Result<()> {
47 let targets = parse_targets(&self.target);
48
49 if targets.len() == 1 {
51 return match parse_target(&targets[0]) {
52 TargetType::Port(port) => self.show_process_on_port(port),
53 TargetType::Pid(pid) => self.show_ports_for_pid(pid),
54 TargetType::Name(name) => self.show_ports_for_name(&name),
55 };
56 }
57
58 let mut not_found = Vec::new();
60
61 for target in &targets {
62 match parse_target(target) {
63 TargetType::Port(port) => {
64 if let Err(e) = self.show_process_on_port(port) {
65 if !self.json {
66 println!("{} Port {}: {}", "⚠".yellow(), port, e);
67 }
68 not_found.push(target.clone());
69 }
70 }
71 TargetType::Pid(pid) => {
72 if let Err(e) = self.show_ports_for_pid(pid) {
73 if !self.json {
74 println!("{} PID {}: {}", "⚠".yellow(), pid, e);
75 }
76 not_found.push(target.clone());
77 }
78 }
79 TargetType::Name(ref name) => {
80 if let Err(e) = self.show_ports_for_name(name) {
81 if !self.json {
82 println!("{} '{}': {}", "⚠".yellow(), name, e);
83 }
84 not_found.push(target.clone());
85 }
86 }
87 }
88 }
89
90 Ok(())
91 }
92
93 fn matches_in_filter(&self, proc: &Process) -> bool {
95 if let Some(ref dir_path) = resolve_in_dir(&self.in_dir) {
96 if let Some(ref proc_cwd) = proc.cwd {
97 let proc_path = PathBuf::from(proc_cwd);
98 proc_path.starts_with(dir_path)
99 } else {
100 false
101 }
102 } else {
103 true
104 }
105 }
106
107 fn matches_by_filter(&self, proc: &Process) -> bool {
109 if let Some(ref name) = self.by_name {
110 proc.name.to_lowercase().contains(&name.to_lowercase())
111 } else {
112 true
113 }
114 }
115
116 fn matches_filters(&self, proc: &Process) -> bool {
118 self.matches_in_filter(proc) && self.matches_by_filter(proc)
119 }
120
121 fn show_process_on_port(&self, port: u16) -> Result<()> {
123 let port_info = match PortInfo::find_by_port(port)? {
124 Some(info) => info,
125 None => return Err(ProcError::PortNotFound(port)),
126 };
127
128 let process = Process::find_by_pid(port_info.pid)?;
129
130 if let Some(ref proc) = process {
132 if !self.matches_filters(proc) {
133 return Err(ProcError::ProcessNotFound(format!(
134 "port {} (process not in specified directory)",
135 port
136 )));
137 }
138 }
139
140 if self.json {
141 let output = PortLookupOutput {
142 action: "on",
143 query_type: "port_to_process",
144 success: true,
145 port: Some(port_info.port),
146 protocol: Some(format!("{:?}", port_info.protocol).to_lowercase()),
147 address: port_info.address.clone(),
148 process: process.as_ref(),
149 ports: None,
150 };
151 println!("{}", serde_json::to_string_pretty(&output)?);
152 } else {
153 self.print_process_on_port(&port_info, process.as_ref());
154 }
155
156 Ok(())
157 }
158
159 fn show_ports_for_pid(&self, pid: u32) -> Result<()> {
161 let process = Process::find_by_pid(pid)?
162 .ok_or_else(|| ProcError::ProcessNotFound(pid.to_string()))?;
163
164 if !self.matches_filters(&process) {
166 return Err(ProcError::ProcessNotFound(format!(
167 "PID {} (not in specified directory)",
168 pid
169 )));
170 }
171
172 let ports = find_ports_for_pid(pid)?;
173
174 if self.json {
175 let output = PortLookupOutput {
176 action: "on",
177 query_type: "process_to_ports",
178 success: true,
179 port: None,
180 protocol: None,
181 address: None,
182 process: Some(&process),
183 ports: Some(&ports),
184 };
185 println!("{}", serde_json::to_string_pretty(&output)?);
186 } else {
187 self.print_ports_for_process(&process, &ports);
188 }
189
190 Ok(())
191 }
192
193 fn show_ports_for_name(&self, name: &str) -> Result<()> {
195 let mut processes = resolve_target(name)?;
196
197 if processes.is_empty() {
198 return Err(ProcError::ProcessNotFound(name.to_string()));
199 }
200
201 if self.in_dir.is_some() || self.by_name.is_some() {
203 processes.retain(|p| self.matches_filters(p));
204 if processes.is_empty() {
205 return Err(ProcError::ProcessNotFound(format!(
206 "'{}' (no matches with specified filters)",
207 name
208 )));
209 }
210 }
211
212 let mut all_results: Vec<(Process, Vec<PortInfo>)> = Vec::new();
213
214 for proc in processes {
215 let ports = find_ports_for_pid(proc.pid)?;
216 all_results.push((proc, ports));
217 }
218
219 if self.json {
220 let output: Vec<_> = all_results
221 .iter()
222 .map(|(proc, ports)| ProcessPortsJson {
223 process: proc,
224 ports,
225 })
226 .collect();
227 println!("{}", serde_json::to_string_pretty(&output)?);
228 } else {
229 for (proc, ports) in &all_results {
230 self.print_ports_for_process(proc, ports);
231 }
232 }
233
234 Ok(())
235 }
236
237 fn print_process_on_port(&self, port_info: &PortInfo, process: Option<&Process>) {
238 println!(
239 "{} Port {} is used by:",
240 "✓".green().bold(),
241 port_info.port.to_string().cyan().bold()
242 );
243 println!();
244
245 println!(
246 " {} {} (PID {})",
247 "Process:".bright_black(),
248 port_info.process_name.white().bold(),
249 port_info.pid.to_string().cyan()
250 );
251
252 if let Some(proc) = process {
253 if let Some(ref cwd) = proc.cwd {
254 println!(" {} {}", "Directory:".bright_black(), cwd);
255 }
256 if let Some(ref path) = proc.exe_path {
257 println!(" {} {}", "Path:".bright_black(), path.bright_black());
258 }
259 }
260
261 let addr = port_info.address.as_deref().unwrap_or("*");
262 println!(
263 " {} {} on {}",
264 "Listening:".bright_black(),
265 format!("{:?}", port_info.protocol).to_uppercase(),
266 addr
267 );
268
269 if let Some(proc) = process {
270 println!(
271 " {} {:.1}% CPU, {}",
272 "Resources:".bright_black(),
273 proc.cpu_percent,
274 format_memory(proc.memory_mb)
275 );
276
277 if let Some(start_time) = proc.start_time {
278 let uptime = std::time::SystemTime::now()
279 .duration_since(std::time::UNIX_EPOCH)
280 .map(|d| d.as_secs().saturating_sub(start_time))
281 .unwrap_or(0);
282 println!(" {} {}", "Uptime:".bright_black(), format_duration(uptime));
283 }
284
285 if self.verbose {
286 if let Some(ref cmd) = proc.command {
287 println!(" {} {}", "Command:".bright_black(), cmd.bright_black());
288 }
289 }
290 }
291
292 println!();
293 }
294
295 fn print_ports_for_process(&self, process: &Process, ports: &[PortInfo]) {
296 println!(
297 "{} {} (PID {}) is listening on:",
298 "✓".green().bold(),
299 process.name.white().bold(),
300 process.pid.to_string().cyan().bold()
301 );
302 println!();
303
304 if ports.is_empty() {
305 println!(" {} No listening ports", "ℹ".blue());
306 } else {
307 for port_info in ports {
308 let addr = port_info.address.as_deref().unwrap_or("*");
309 println!(
310 " {} :{} ({} on {})",
311 "→".bright_black(),
312 port_info.port.to_string().cyan(),
313 format!("{:?}", port_info.protocol).to_uppercase(),
314 addr
315 );
316 }
317 }
318
319 if let Some(ref cwd) = process.cwd {
320 println!();
321 println!(" {} {}", "Directory:".bright_black(), cwd);
322 }
323
324 if self.verbose {
325 if let Some(ref path) = process.exe_path {
326 println!(" {} {}", "Path:".bright_black(), path.bright_black());
327 }
328 if let Some(ref cmd) = process.command {
329 println!(" {} {}", "Command:".bright_black(), cmd.bright_black());
330 }
331 }
332
333 println!();
334 }
335}
336
337#[derive(Serialize)]
338struct PortLookupOutput<'a> {
339 action: &'static str,
340 query_type: &'static str,
341 success: bool,
342 #[serde(skip_serializing_if = "Option::is_none")]
343 port: Option<u16>,
344 #[serde(skip_serializing_if = "Option::is_none")]
345 protocol: Option<String>,
346 #[serde(skip_serializing_if = "Option::is_none")]
347 address: Option<String>,
348 #[serde(skip_serializing_if = "Option::is_none")]
349 process: Option<&'a Process>,
350 #[serde(skip_serializing_if = "Option::is_none")]
351 ports: Option<&'a [PortInfo]>,
352}
353
354#[derive(Serialize)]
355struct ProcessPortsJson<'a> {
356 process: &'a Process,
357 ports: &'a [PortInfo],
358}