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, Printer};
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 crate::core::matches_by_filter(proc, name)
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 printer = Printer::from_flags(true, self.verbose);
142 let output = PortLookupOutput {
143 action: "on",
144 query_type: "port_to_process",
145 success: true,
146 port: Some(port_info.port),
147 protocol: Some(format!("{:?}", port_info.protocol).to_lowercase()),
148 address: port_info.address.clone(),
149 process: process.as_ref(),
150 ports: None,
151 };
152 printer.print_json(&output);
153 } else {
154 self.print_process_on_port(&port_info, process.as_ref());
155 }
156
157 Ok(())
158 }
159
160 fn show_ports_for_pid(&self, pid: u32) -> Result<()> {
162 let process = Process::find_by_pid(pid)?
163 .ok_or_else(|| ProcError::ProcessNotFound(pid.to_string()))?;
164
165 if !self.matches_filters(&process) {
167 return Err(ProcError::ProcessNotFound(format!(
168 "PID {} (not in specified directory)",
169 pid
170 )));
171 }
172
173 let ports = find_ports_for_pid(pid)?;
174
175 if self.json {
176 let printer = Printer::from_flags(true, self.verbose);
177 let output = PortLookupOutput {
178 action: "on",
179 query_type: "process_to_ports",
180 success: true,
181 port: None,
182 protocol: None,
183 address: None,
184 process: Some(&process),
185 ports: Some(&ports),
186 };
187 printer.print_json(&output);
188 } else {
189 self.print_ports_for_process(&process, &ports);
190 }
191
192 Ok(())
193 }
194
195 fn show_ports_for_name(&self, name: &str) -> Result<()> {
197 let mut processes = resolve_target(name)?;
198
199 if processes.is_empty() {
200 return Err(ProcError::ProcessNotFound(name.to_string()));
201 }
202
203 if self.in_dir.is_some() || self.by_name.is_some() {
205 processes.retain(|p| self.matches_filters(p));
206 if processes.is_empty() {
207 return Err(ProcError::ProcessNotFound(format!(
208 "'{}' (no matches with specified filters)",
209 name
210 )));
211 }
212 }
213
214 let mut all_results: Vec<(Process, Vec<PortInfo>)> = Vec::new();
215
216 for proc in processes {
217 let ports = find_ports_for_pid(proc.pid)?;
218 all_results.push((proc, ports));
219 }
220
221 if self.json {
222 let printer = Printer::from_flags(true, self.verbose);
223 let results: Vec<_> = all_results
224 .iter()
225 .map(|(proc, ports)| ProcessPortsJson {
226 process: proc,
227 ports,
228 })
229 .collect();
230 let output = MultiProcessPortsOutput {
231 action: "on",
232 success: true,
233 count: results.len(),
234 results: &results,
235 };
236 printer.print_json(&output);
237 } else {
238 for (proc, ports) in &all_results {
239 self.print_ports_for_process(proc, ports);
240 }
241 }
242
243 Ok(())
244 }
245
246 fn print_process_on_port(&self, port_info: &PortInfo, process: Option<&Process>) {
247 println!(
248 "{} Port {} is used by:",
249 "✓".green().bold(),
250 port_info.port.to_string().cyan().bold()
251 );
252 println!();
253
254 println!(
255 " {} {} (PID {})",
256 "Process:".bright_black(),
257 port_info.process_name.white().bold(),
258 port_info.pid.to_string().cyan()
259 );
260
261 if let Some(proc) = process {
262 if let Some(ref cwd) = proc.cwd {
263 println!(" {} {}", "Directory:".bright_black(), cwd);
264 }
265 if let Some(ref path) = proc.exe_path {
266 println!(" {} {}", "Path:".bright_black(), path.bright_black());
267 }
268 }
269
270 let addr = port_info.address.as_deref().unwrap_or("*");
271 println!(
272 " {} {} on {}",
273 "Listening:".bright_black(),
274 format!("{:?}", port_info.protocol).to_uppercase(),
275 addr
276 );
277
278 if let Some(proc) = process {
279 println!(
280 " {} {:.1}% CPU, {}",
281 "Resources:".bright_black(),
282 proc.cpu_percent,
283 format_memory(proc.memory_mb)
284 );
285
286 if let Some(start_time) = proc.start_time {
287 let uptime = std::time::SystemTime::now()
288 .duration_since(std::time::UNIX_EPOCH)
289 .map(|d| d.as_secs().saturating_sub(start_time))
290 .unwrap_or(0);
291 println!(" {} {}", "Uptime:".bright_black(), format_duration(uptime));
292 }
293
294 if self.verbose {
295 if let Some(ref cmd) = proc.command {
296 println!(" {} {}", "Command:".bright_black(), cmd.bright_black());
297 }
298 }
299 }
300
301 println!();
302 }
303
304 fn print_ports_for_process(&self, process: &Process, ports: &[PortInfo]) {
305 println!(
306 "{} {} (PID {}) is listening on:",
307 "✓".green().bold(),
308 process.name.white().bold(),
309 process.pid.to_string().cyan().bold()
310 );
311 println!();
312
313 if ports.is_empty() {
314 println!(" {} No listening ports", "ℹ".blue());
315 } else {
316 for port_info in ports {
317 let addr = port_info.address.as_deref().unwrap_or("*");
318 println!(
319 " {} :{} ({} on {})",
320 "→".bright_black(),
321 port_info.port.to_string().cyan(),
322 format!("{:?}", port_info.protocol).to_uppercase(),
323 addr
324 );
325 }
326 }
327
328 if let Some(ref cwd) = process.cwd {
329 println!();
330 println!(" {} {}", "Directory:".bright_black(), cwd);
331 }
332
333 if self.verbose {
334 if let Some(ref path) = process.exe_path {
335 println!(" {} {}", "Path:".bright_black(), path.bright_black());
336 }
337 if let Some(ref cmd) = process.command {
338 println!(" {} {}", "Command:".bright_black(), cmd.bright_black());
339 }
340 }
341
342 println!();
343 }
344}
345
346#[derive(Serialize)]
347struct PortLookupOutput<'a> {
348 action: &'static str,
349 query_type: &'static str,
350 success: bool,
351 #[serde(skip_serializing_if = "Option::is_none")]
352 port: Option<u16>,
353 #[serde(skip_serializing_if = "Option::is_none")]
354 protocol: Option<String>,
355 #[serde(skip_serializing_if = "Option::is_none")]
356 address: Option<String>,
357 #[serde(skip_serializing_if = "Option::is_none")]
358 process: Option<&'a Process>,
359 #[serde(skip_serializing_if = "Option::is_none")]
360 ports: Option<&'a [PortInfo]>,
361}
362
363#[derive(Serialize)]
364struct ProcessPortsJson<'a> {
365 process: &'a Process,
366 ports: &'a [PortInfo],
367}
368
369#[derive(Serialize)]
370struct MultiProcessPortsOutput<'a> {
371 action: &'static str,
372 success: bool,
373 count: usize,
374 results: &'a Vec<ProcessPortsJson<'a>>,
375}