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::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 printer = Printer::from_flags(self.json, self.verbose);
40
41        let all_processes = Process::find_all()?;
42        let pid_map: HashMap<u32, &Process> = all_processes.iter().map(|p| (p.pid, p)).collect();
43
44        let targets = parse_targets(&self.target);
45
46        for (i, target_str) in targets.iter().enumerate() {
47            if i > 0 && !self.json {
48                println!();
49            }
50
51            let parsed = parse_target(target_str);
52
53            // Collect port context for port targets
54            let port_context: Option<PortInfo> = if let TargetType::Port(port) = &parsed {
55                PortInfo::find_by_port(*port).ok().flatten()
56            } else {
57                None
58            };
59
60            // Resolve to processes
61            let target_processes = match resolve_target(target_str) {
62                Ok(procs) => procs,
63                Err(e) => {
64                    if !self.json {
65                        printer.warning(&format!("{}", e));
66                    }
67                    continue;
68                }
69            };
70
71            if target_processes.is_empty() {
72                printer.warning(&format!("No process found for '{}'", target_str));
73                continue;
74            }
75
76            if self.json {
77                let results: Vec<WhyOutput> = target_processes
78                    .iter()
79                    .map(|proc| {
80                        let chain = self.build_ancestry_chain(proc, &pid_map);
81                        let ports = find_ports_for_pid(proc.pid).unwrap_or_default();
82                        WhyOutput {
83                            target: target_str.clone(),
84                            port: port_context.as_ref().map(|p| p.port),
85                            protocol: port_context
86                                .as_ref()
87                                .map(|p| format!("{:?}", p.protocol).to_uppercase()),
88                            process: WhyProcessInfo {
89                                pid: proc.pid,
90                                name: proc.name.clone(),
91                                command: proc.command.clone(),
92                                cwd: proc.cwd.clone(),
93                                status: format!("{:?}", proc.status),
94                            },
95                            ports,
96                            ancestry: chain,
97                        }
98                    })
99                    .collect();
100                printer.print_json(&WhyEnvelope {
101                    action: "why",
102                    success: true,
103                    count: results.len(),
104                    results,
105                });
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 WhyEnvelope {
261    action: &'static str,
262    success: bool,
263    count: usize,
264    results: Vec<WhyOutput>,
265}
266
267#[derive(Serialize)]
268struct WhyOutput {
269    target: String,
270    #[serde(skip_serializing_if = "Option::is_none")]
271    port: Option<u16>,
272    #[serde(skip_serializing_if = "Option::is_none")]
273    protocol: Option<String>,
274    process: WhyProcessInfo,
275    ports: Vec<PortInfo>,
276    ancestry: Vec<AncestryEntry>,
277}
278
279#[derive(Serialize)]
280struct WhyProcessInfo {
281    pid: u32,
282    name: String,
283    #[serde(skip_serializing_if = "Option::is_none")]
284    command: Option<String>,
285    #[serde(skip_serializing_if = "Option::is_none")]
286    cwd: Option<String>,
287    status: String,
288}
289
290#[derive(Serialize)]
291struct AncestryEntry {
292    pid: u32,
293    name: String,
294    #[serde(skip_serializing_if = "Option::is_none")]
295    command: Option<String>,
296    #[serde(skip_serializing_if = "Option::is_none")]
297    cwd: Option<String>,
298    status: String,
299    is_target: bool,
300}