Skip to main content

proc_cli/commands/
why.rs

1//! `proc why` - Explain why a port is busy or trace process ancestry
2//!
3//! Examples:
4//!   proc why :3000              # Why is port 3000 busy? (ancestry + port context)
5//!   proc why node               # Show ancestry for node processes
6//!   proc why 1234               # Show ancestry for PID 1234
7//!   proc why :3000 --json       # JSON output with port context
8
9use crate::core::{
10    find_ports_for_pid, parse_target, parse_targets, resolve_target, PortInfo, Process,
11    ProcessStatus, TargetType,
12};
13use crate::error::Result;
14use crate::ui::{OutputFormat, Printer};
15use clap::Args;
16use colored::*;
17use serde::Serialize;
18use std::collections::HashMap;
19
20/// Trace why a port is busy or show process ancestry
21#[derive(Args, Debug)]
22pub struct WhyCommand {
23    /// Target(s): :port, PID, or process name (comma-separated for multiple)
24    #[arg(required = true)]
25    pub target: String,
26
27    /// Output as JSON
28    #[arg(long, short = 'j')]
29    pub json: bool,
30
31    /// Show verbose output
32    #[arg(long, short = 'v')]
33    pub verbose: bool,
34}
35
36impl WhyCommand {
37    /// Executes the why command, showing ancestry and port context.
38    pub fn execute(&self) -> Result<()> {
39        let format = if self.json {
40            OutputFormat::Json
41        } else {
42            OutputFormat::Human
43        };
44        let printer = Printer::new(format, self.verbose);
45
46        let all_processes = Process::find_all()?;
47        let pid_map: HashMap<u32, &Process> = all_processes.iter().map(|p| (p.pid, p)).collect();
48
49        let targets = parse_targets(&self.target);
50
51        for (i, target_str) in targets.iter().enumerate() {
52            if i > 0 && !self.json {
53                println!();
54            }
55
56            let parsed = parse_target(target_str);
57
58            // Collect port context for port targets
59            let port_context: Option<PortInfo> = if let TargetType::Port(port) = &parsed {
60                PortInfo::find_by_port(*port).ok().flatten()
61            } else {
62                None
63            };
64
65            // Resolve to processes
66            let target_processes = match resolve_target(target_str) {
67                Ok(procs) => procs,
68                Err(e) => {
69                    if !self.json {
70                        printer.warning(&format!("{}", e));
71                    }
72                    continue;
73                }
74            };
75
76            if target_processes.is_empty() {
77                printer.warning(&format!("No process found for '{}'", target_str));
78                continue;
79            }
80
81            if self.json {
82                let output: Vec<WhyOutput> = target_processes
83                    .iter()
84                    .map(|proc| {
85                        let chain = self.build_ancestry_chain(proc, &pid_map);
86                        let ports = find_ports_for_pid(proc.pid).unwrap_or_default();
87                        WhyOutput {
88                            target: target_str.clone(),
89                            port: port_context.as_ref().map(|p| p.port),
90                            protocol: port_context
91                                .as_ref()
92                                .map(|p| format!("{:?}", p.protocol).to_uppercase()),
93                            process: WhyProcessInfo {
94                                pid: proc.pid,
95                                name: proc.name.clone(),
96                                command: proc.command.clone(),
97                                cwd: proc.cwd.clone(),
98                                status: format!("{:?}", proc.status),
99                            },
100                            ports,
101                            ancestry: chain,
102                        }
103                    })
104                    .collect();
105                printer.print_json(&output);
106            } else {
107                // Print port header if applicable
108                if let Some(ref port_info) = port_context {
109                    println!(
110                        "{} Port {} ({}):",
111                        "✓".green().bold(),
112                        port_info.port.to_string().cyan().bold(),
113                        format!("{:?}", port_info.protocol).to_uppercase()
114                    );
115                } else {
116                    println!(
117                        "{} Ancestry for '{}':",
118                        "✓".green().bold(),
119                        target_str.cyan()
120                    );
121                }
122                println!();
123
124                for proc in &target_processes {
125                    self.print_ancestry_with_context(proc, &pid_map);
126                    println!();
127                }
128            }
129        }
130
131        Ok(())
132    }
133
134    /// Build the ancestor chain from root down to target
135    fn build_ancestry_chain(
136        &self,
137        target: &Process,
138        pid_map: &HashMap<u32, &Process>,
139    ) -> Vec<AncestryEntry> {
140        let mut chain: Vec<AncestryEntry> = Vec::new();
141        let mut current_pid = Some(target.pid);
142
143        while let Some(pid) = current_pid {
144            if let Some(proc) = pid_map.get(&pid) {
145                chain.push(AncestryEntry {
146                    pid: proc.pid,
147                    name: proc.name.clone(),
148                    command: proc.command.clone(),
149                    cwd: proc.cwd.clone(),
150                    status: format!("{:?}", proc.status),
151                    is_target: proc.pid == target.pid,
152                });
153                current_pid = proc.parent_pid;
154                if chain.len() > 100 {
155                    break;
156                }
157            } else {
158                break;
159            }
160        }
161
162        chain.reverse();
163        chain
164    }
165
166    /// Print ancestry tree with working directory and command context
167    fn print_ancestry_with_context(&self, target: &Process, pid_map: &HashMap<u32, &Process>) {
168        // Build the ancestor chain (from target up to root)
169        let mut chain: Vec<&Process> = Vec::new();
170        let mut current_pid = Some(target.pid);
171
172        while let Some(pid) = current_pid {
173            if let Some(proc) = pid_map.get(&pid) {
174                chain.push(proc);
175                current_pid = proc.parent_pid;
176                if chain.len() > 100 {
177                    break;
178                }
179            } else {
180                break;
181            }
182        }
183
184        // Reverse to print from root to target
185        chain.reverse();
186
187        for (i, proc) in chain.iter().enumerate() {
188            let is_target = proc.pid == target.pid;
189            let indent = "    ".repeat(i);
190            let connector = if i == 0 { "" } else { "└── " };
191
192            let status_indicator = match proc.status {
193                ProcessStatus::Running => "●".green(),
194                ProcessStatus::Sleeping => "○".blue(),
195                ProcessStatus::Stopped => "◐".yellow(),
196                ProcessStatus::Zombie => "✗".red(),
197                _ => "?".white(),
198            };
199
200            // Build the command summary (skip exe name, show args)
201            let cmd_summary = proc
202                .command
203                .as_ref()
204                .and_then(|c| {
205                    let parts: Vec<&str> = c.split_whitespace().collect();
206                    if parts.len() > 1 {
207                        Some(parts[1..].join(" "))
208                    } else {
209                        None
210                    }
211                })
212                .unwrap_or_default();
213
214            if is_target {
215                let cmd_part = if cmd_summary.is_empty() {
216                    String::new()
217                } else {
218                    format!(" {}", cmd_summary)
219                };
220                println!(
221                    "{}{}{} {} [{}]{}  {}",
222                    indent.bright_black(),
223                    connector.bright_black(),
224                    status_indicator,
225                    proc.name.cyan().bold(),
226                    proc.pid.to_string().cyan().bold(),
227                    cmd_part,
228                    "← target".yellow()
229                );
230                // Show working directory for the target
231                if let Some(ref cwd) = proc.cwd {
232                    let dir_indent = "    ".repeat(i + 1);
233                    println!(
234                        "{}{}",
235                        dir_indent.bright_black(),
236                        format!("dir: {}", cwd).bright_black()
237                    );
238                }
239            } else {
240                let cmd_part = if cmd_summary.is_empty() {
241                    String::new()
242                } else {
243                    format!(" {}", cmd_summary)
244                };
245                println!(
246                    "{}{}{} {} [{}]{}",
247                    indent.bright_black(),
248                    connector.bright_black(),
249                    status_indicator,
250                    proc.name.white(),
251                    proc.pid.to_string().cyan(),
252                    cmd_part.bright_black()
253                );
254            }
255        }
256    }
257}
258
259#[derive(Serialize)]
260struct WhyOutput {
261    target: String,
262    #[serde(skip_serializing_if = "Option::is_none")]
263    port: Option<u16>,
264    #[serde(skip_serializing_if = "Option::is_none")]
265    protocol: Option<String>,
266    process: WhyProcessInfo,
267    ports: Vec<PortInfo>,
268    ancestry: Vec<AncestryEntry>,
269}
270
271#[derive(Serialize)]
272struct WhyProcessInfo {
273    pid: u32,
274    name: String,
275    #[serde(skip_serializing_if = "Option::is_none")]
276    command: Option<String>,
277    #[serde(skip_serializing_if = "Option::is_none")]
278    cwd: Option<String>,
279    status: String,
280}
281
282#[derive(Serialize)]
283struct AncestryEntry {
284    pid: u32,
285    name: String,
286    #[serde(skip_serializing_if = "Option::is_none")]
287    command: Option<String>,
288    #[serde(skip_serializing_if = "Option::is_none")]
289    cwd: Option<String>,
290    status: String,
291    is_target: bool,
292}