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 path) = proc.exe_path {
254 println!(" {} {}", "Path:".bright_black(), path.bright_black());
255 }
256 }
257
258 let addr = port_info.address.as_deref().unwrap_or("*");
259 println!(
260 " {} {} on {}",
261 "Listening:".bright_black(),
262 format!("{:?}", port_info.protocol).to_uppercase(),
263 addr
264 );
265
266 if let Some(proc) = process {
267 println!(
268 " {} {:.1}% CPU, {}",
269 "Resources:".bright_black(),
270 proc.cpu_percent,
271 format_memory(proc.memory_mb)
272 );
273
274 if let Some(start_time) = proc.start_time {
275 let uptime = std::time::SystemTime::now()
276 .duration_since(std::time::UNIX_EPOCH)
277 .map(|d| d.as_secs().saturating_sub(start_time))
278 .unwrap_or(0);
279 println!(" {} {}", "Uptime:".bright_black(), format_duration(uptime));
280 }
281
282 if self.verbose {
283 if let Some(ref cmd) = proc.command {
284 println!(" {} {}", "Command:".bright_black(), cmd.bright_black());
285 }
286 }
287 }
288
289 println!();
290 }
291
292 fn print_ports_for_process(&self, process: &Process, ports: &[PortInfo]) {
293 println!(
294 "{} {} (PID {}) is listening on:",
295 "✓".green().bold(),
296 process.name.white().bold(),
297 process.pid.to_string().cyan().bold()
298 );
299 println!();
300
301 if ports.is_empty() {
302 println!(" {} No listening ports", "ℹ".blue());
303 } else {
304 for port_info in ports {
305 let addr = port_info.address.as_deref().unwrap_or("*");
306 println!(
307 " {} :{} ({} on {})",
308 "→".bright_black(),
309 port_info.port.to_string().cyan(),
310 format!("{:?}", port_info.protocol).to_uppercase(),
311 addr
312 );
313 }
314 }
315
316 if self.verbose {
317 if let Some(ref path) = process.exe_path {
318 println!();
319 println!(" {} {}", "Path:".bright_black(), path.bright_black());
320 }
321 if let Some(ref cmd) = process.command {
322 println!(" {} {}", "Command:".bright_black(), cmd.bright_black());
323 }
324 }
325
326 println!();
327 }
328}
329
330#[derive(Serialize)]
331struct PortLookupOutput<'a> {
332 action: &'static str,
333 query_type: &'static str,
334 success: bool,
335 #[serde(skip_serializing_if = "Option::is_none")]
336 port: Option<u16>,
337 #[serde(skip_serializing_if = "Option::is_none")]
338 protocol: Option<String>,
339 #[serde(skip_serializing_if = "Option::is_none")]
340 address: Option<String>,
341 #[serde(skip_serializing_if = "Option::is_none")]
342 process: Option<&'a Process>,
343 #[serde(skip_serializing_if = "Option::is_none")]
344 ports: Option<&'a [PortInfo]>,
345}
346
347#[derive(Serialize)]
348struct ProcessPortsJson<'a> {
349 process: &'a Process,
350 ports: &'a [PortInfo],
351}