1use 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#[derive(Args, Debug)]
22pub struct WhyCommand {
23 #[arg(required = true)]
25 pub target: String,
26
27 #[arg(long, short = 'j')]
29 pub json: bool,
30
31 #[arg(long, short = 'v')]
33 pub verbose: bool,
34}
35
36impl WhyCommand {
37 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 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 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 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 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 fn print_ancestry_with_context(&self, target: &Process, pid_map: &HashMap<u32, &Process>) {
168 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 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 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 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}