1use crate::core::{
10 find_ports_for_pid, parse_target, parse_targets, resolve_target, PortInfo, Process,
11 ProcessStatus, Protocol, TargetType,
12};
13use crate::error::{ExitCode, ProcError, Result};
14use crate::ui::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 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 let mut json_results: Vec<WhyOutput> = Vec::new();
48 let mut found_any = false;
49 let mut first_error: Option<ProcError> = None;
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 first_error.get_or_insert(e);
73 continue;
74 }
75 };
76
77 if target_processes.is_empty() {
78 printer.warning(&format!("No process found for '{}'", target_str));
79 first_error.get_or_insert(ProcError::ProcessNotFound(target_str.clone()));
80 continue;
81 }
82 found_any = true;
83
84 if self.json {
85 json_results.extend(target_processes.iter().map(|proc| {
86 let chain = self.build_ancestry_chain(proc, &pid_map);
87 let ports = find_ports_for_pid(proc.pid).unwrap_or_default();
88 WhyOutput {
89 target: target_str.clone(),
90 port: port_context.as_ref().map(|p| p.port),
91 protocol: port_context.as_ref().map(|p| p.protocol),
92 process: WhyProcessInfo {
93 pid: proc.pid,
94 name: proc.name.clone(),
95 command: proc.command.clone(),
96 cwd: proc.cwd.clone(),
97 status: proc.status,
98 },
99 ports,
100 ancestry: chain,
101 }
102 }));
103 } else {
104 if let Some(ref port_info) = port_context {
106 println!(
107 "{} Port {} ({}):",
108 "✓".green().bold(),
109 port_info.port.to_string().cyan().bold(),
110 format!("{:?}", port_info.protocol).to_uppercase()
111 );
112 } else {
113 println!(
114 "{} Ancestry for '{}':",
115 "✓".green().bold(),
116 target_str.cyan()
117 );
118 }
119 println!();
120
121 for proc in &target_processes {
122 self.print_ancestry_with_context(proc, &pid_map);
123 println!();
124 }
125 }
126 }
127
128 if !found_any {
129 let error =
130 first_error.unwrap_or_else(|| ProcError::ProcessNotFound(self.target.clone()));
131 if self.json {
132 return Err(error);
133 }
134 return Err(ProcError::reported(
136 ExitCode::from(&error),
137 error.to_string(),
138 ));
139 }
140
141 if self.json {
142 printer.print_json(&WhyEnvelope {
143 action: "why",
144 success: true,
145 count: json_results.len(),
146 results: json_results,
147 });
148 }
149
150 Ok(())
151 }
152
153 fn build_ancestry_chain(
155 &self,
156 target: &Process,
157 pid_map: &HashMap<u32, &Process>,
158 ) -> Vec<AncestryEntry> {
159 let mut chain: Vec<AncestryEntry> = Vec::new();
160 let mut current_pid = Some(target.pid);
161
162 while let Some(pid) = current_pid {
163 if let Some(proc) = pid_map.get(&pid) {
164 chain.push(AncestryEntry {
165 pid: proc.pid,
166 name: proc.name.clone(),
167 command: proc.command.clone(),
168 cwd: proc.cwd.clone(),
169 status: proc.status,
170 is_target: proc.pid == target.pid,
171 });
172 current_pid = proc.parent_pid;
173 if chain.len() > 100 {
174 break;
175 }
176 } else {
177 break;
178 }
179 }
180
181 chain.reverse();
182 chain
183 }
184
185 fn print_ancestry_with_context(&self, target: &Process, pid_map: &HashMap<u32, &Process>) {
187 let mut chain: Vec<&Process> = Vec::new();
189 let mut current_pid = Some(target.pid);
190
191 while let Some(pid) = current_pid {
192 if let Some(proc) = pid_map.get(&pid) {
193 chain.push(proc);
194 current_pid = proc.parent_pid;
195 if chain.len() > 100 {
196 break;
197 }
198 } else {
199 break;
200 }
201 }
202
203 chain.reverse();
205
206 for (i, proc) in chain.iter().enumerate() {
207 let is_target = proc.pid == target.pid;
208 let indent = " ".repeat(i);
209 let connector = if i == 0 { "" } else { "└── " };
210
211 let status_indicator = match proc.status {
212 ProcessStatus::Running => "●".green(),
213 ProcessStatus::Sleeping => "○".blue(),
214 ProcessStatus::Stopped => "◐".yellow(),
215 ProcessStatus::Zombie => "✗".red(),
216 _ => "?".white(),
217 };
218
219 let cmd_summary = proc
221 .command
222 .as_ref()
223 .and_then(|c| {
224 let parts: Vec<&str> = c.split_whitespace().collect();
225 if parts.len() > 1 {
226 Some(parts[1..].join(" "))
227 } else {
228 None
229 }
230 })
231 .unwrap_or_default();
232
233 if is_target {
234 let cmd_part = if cmd_summary.is_empty() {
235 String::new()
236 } else {
237 format!(" {}", cmd_summary)
238 };
239 println!(
240 "{}{}{} {} [{}]{} {}",
241 indent.bright_black(),
242 connector.bright_black(),
243 status_indicator,
244 proc.name.cyan().bold(),
245 proc.pid.to_string().cyan().bold(),
246 cmd_part,
247 "← target".yellow()
248 );
249 if let Some(ref cwd) = proc.cwd {
251 let dir_indent = " ".repeat(i + 1);
252 println!(
253 "{}{}",
254 dir_indent.bright_black(),
255 format!("dir: {}", cwd).bright_black()
256 );
257 }
258 } else {
259 let cmd_part = if cmd_summary.is_empty() {
260 String::new()
261 } else {
262 format!(" {}", cmd_summary)
263 };
264 println!(
265 "{}{}{} {} [{}]{}",
266 indent.bright_black(),
267 connector.bright_black(),
268 status_indicator,
269 proc.name.white(),
270 proc.pid.to_string().cyan(),
271 cmd_part.bright_black()
272 );
273 }
274 }
275 }
276}
277
278#[derive(Serialize)]
279struct WhyEnvelope {
280 action: &'static str,
281 success: bool,
282 count: usize,
283 results: Vec<WhyOutput>,
284}
285
286#[derive(Serialize)]
287struct WhyOutput {
288 target: String,
289 #[serde(skip_serializing_if = "Option::is_none")]
290 port: Option<u16>,
291 #[serde(skip_serializing_if = "Option::is_none")]
292 protocol: Option<Protocol>,
293 process: WhyProcessInfo,
294 ports: Vec<PortInfo>,
295 ancestry: Vec<AncestryEntry>,
296}
297
298#[derive(Serialize)]
299struct WhyProcessInfo {
300 pid: u32,
301 name: String,
302 #[serde(skip_serializing_if = "Option::is_none")]
303 command: Option<String>,
304 #[serde(skip_serializing_if = "Option::is_none")]
305 cwd: Option<String>,
306 status: ProcessStatus,
307}
308
309#[derive(Serialize)]
310struct AncestryEntry {
311 pid: u32,
312 name: String,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 command: Option<String>,
315 #[serde(skip_serializing_if = "Option::is_none")]
316 cwd: Option<String>,
317 status: ProcessStatus,
318 is_target: bool,
319}