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