1use crate::core::{
14 flatten_targets, parse_target, resolve_target, NameMatch, PortInfo, Process, ProcessStatus,
15 Protocol, TargetType,
16};
17use crate::error::{ExitCode, ProcError, Result};
18use crate::out_println;
19use crate::ui::{uptime_label, Printer};
20use clap::Args;
21use colored::*;
22use serde::Serialize;
23use std::collections::HashMap;
24
25#[derive(Args, Debug)]
27pub struct WhyCommand {
28 #[arg(required = true, num_args = 1.., value_parser = crate::core::validate_target_arg)]
30 pub target: Vec<String>,
31
32 #[arg(long)]
34 pub cmd: bool,
35
36 #[arg(long, short = 'j')]
38 pub json: bool,
39
40 #[arg(long, short = 'v')]
42 pub verbose: bool,
43}
44
45impl WhyCommand {
46 pub fn execute(&self) -> Result<()> {
48 let printer = Printer::from_flags(self.json, self.verbose);
49
50 let all_processes = Process::find_all()?;
51 let pid_map: HashMap<u32, &Process> = all_processes.iter().map(|p| (p.pid, p)).collect();
52
53 let targets = flatten_targets(&self.target);
54 let mode = NameMatch::from_flag(self.cmd);
55
56 let mut json_results: Vec<WhyOutput> = Vec::new();
58 let mut found_any = false;
59 let mut first_error: Option<ProcError> = None;
60 let mut not_found: Vec<String> = Vec::new();
61 let listening = std::cell::OnceCell::new();
63
64 for (i, target_str) in targets.iter().enumerate() {
65 if i > 0 && !self.json {
66 out_println!();
67 }
68
69 let parsed = parse_target(target_str);
70
71 let port_context: Option<PortInfo> = if let TargetType::Port(port) = &parsed {
74 PortInfo::holders_of(*port)
75 .ok()
76 .and_then(|holders| holders.into_iter().next())
77 } else {
78 None
79 };
80
81 let target_processes = match found_processes(target_str, |t| resolve_target(t, mode)) {
83 Ok(procs) => procs,
84 Err((error, warning)) => {
85 printer.warning(&warning);
86 first_error.get_or_insert(error);
87 not_found.push(target_str.clone());
88 continue;
89 }
90 };
91 found_any = true;
92
93 if self.json {
94 json_results.extend(target_processes.iter().map(|proc| {
95 let chain = self.build_ancestry_chain(proc, &pid_map);
96 let ports = listening
97 .get_or_init(|| PortInfo::get_all_listening().unwrap_or_default())
98 .iter()
99 .filter(|p| p.pid == proc.pid)
100 .cloned()
101 .collect();
102 WhyOutput {
103 target: target_str.clone(),
104 port: port_context.as_ref().map(|p| p.port),
105 protocol: port_context.as_ref().map(|p| p.protocol),
106 process: WhyProcessInfo {
107 pid: proc.pid,
108 name: proc.name.clone(),
109 command: proc.command.clone(),
110 cwd: proc.cwd.clone(),
111 status: proc.status,
112 uptime_secs: proc.uptime_secs,
113 },
114 ports,
115 ancestry: chain,
116 }
117 }));
118 } else {
119 if let Some(ref port_info) = port_context {
121 out_println!(
122 "{} Port {} ({}):",
123 "✓".green().bold(),
124 port_info.port.to_string().cyan().bold(),
125 format!("{:?}", port_info.protocol).to_uppercase()
126 );
127 } else {
128 out_println!(
129 "{} Ancestry for '{}':",
130 "✓".green().bold(),
131 target_str.cyan()
132 );
133 }
134 out_println!();
135
136 for proc in &target_processes {
137 self.print_ancestry_with_context(proc, &pid_map);
138 out_println!();
139 }
140 }
141 }
142
143 if !found_any {
144 let error =
145 first_error.unwrap_or_else(|| ProcError::ProcessNotFound(self.target.join(" ")));
146 if self.json {
147 return Err(error);
148 }
149 return Err(ProcError::reported(
151 ExitCode::from(&error),
152 error.to_string(),
153 ));
154 }
155
156 if self.json {
157 printer.print_json(&WhyEnvelope {
158 action: "why",
159 success: true,
160 count: json_results.len(),
161 not_found,
162 results: json_results,
163 });
164 }
165
166 Ok(())
167 }
168
169 fn build_ancestry_chain(
171 &self,
172 target: &Process,
173 pid_map: &HashMap<u32, &Process>,
174 ) -> Vec<AncestryEntry> {
175 ancestor_chain(target, pid_map)
176 .into_iter()
177 .map(|proc| AncestryEntry {
178 pid: proc.pid,
179 name: proc.name.clone(),
180 command: proc.command.clone(),
181 cwd: proc.cwd.clone(),
182 status: proc.status,
183 uptime_secs: proc.uptime_secs,
184 is_target: proc.pid == target.pid,
185 })
186 .collect()
187 }
188
189 fn print_ancestry_with_context(&self, target: &Process, pid_map: &HashMap<u32, &Process>) {
191 let chain = ancestor_chain(target, pid_map);
192
193 for (i, proc) in chain.iter().enumerate() {
194 let is_target = proc.pid == target.pid;
195 let indent = " ".repeat(i);
196 let connector = if i == 0 { "" } else { "└── " };
197
198 let symbol = status_indicator(proc.status);
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 out_println!(
221 "{}{}{} {} [{}]{}{} {}",
222 indent.bright_black(),
223 connector.bright_black(),
224 symbol,
225 proc.name.cyan().bold(),
226 proc.pid.to_string().cyan().bold(),
227 cmd_part,
228 uptime_label(proc.uptime_secs).bright_black(),
229 "← target".yellow()
230 );
231 if let Some(ref cwd) = proc.cwd {
233 let dir_indent = " ".repeat(i + 1);
234 out_println!(
235 "{}{}",
236 dir_indent.bright_black(),
237 format!("dir: {}", cwd).bright_black()
238 );
239 }
240 } else {
241 let cmd_part = if cmd_summary.is_empty() {
242 String::new()
243 } else {
244 format!(" {}", cmd_summary)
245 };
246 out_println!(
247 "{}{}{} {} [{}]{}{}",
248 indent.bright_black(),
249 connector.bright_black(),
250 symbol,
251 proc.name.white(),
252 proc.pid.to_string().cyan(),
253 cmd_part.bright_black(),
254 uptime_label(proc.uptime_secs).bright_black()
255 );
256 }
257 }
258 }
259}
260
261fn found_processes(
265 target: &str,
266 resolve: impl Fn(&str) -> Result<Vec<Process>>,
267) -> std::result::Result<Vec<Process>, (ProcError, String)> {
268 match resolve(target) {
269 Ok(processes) if processes.is_empty() => Err((
270 ProcError::ProcessNotFound(target.to_string()),
271 format!("No process found for '{}'", target),
272 )),
273 Ok(processes) => Ok(processes),
274 Err(e) => {
275 let warning = e.to_string();
276 Err((e, warning))
277 }
278 }
279}
280
281fn ancestor_chain<'a>(target: &Process, pid_map: &HashMap<u32, &'a Process>) -> Vec<&'a Process> {
285 let mut chain: Vec<&Process> = Vec::new();
286 let mut current_pid = Some(target.pid);
287
288 while let Some(pid) = current_pid {
289 let Some(proc) = pid_map.get(&pid) else {
290 break;
291 };
292 chain.push(proc);
293 current_pid = proc.parent_pid;
294 if chain.len() > 100 {
295 break;
296 }
297 }
298
299 chain.reverse();
300 chain
301}
302
303fn status_indicator(status: ProcessStatus) -> ColoredString {
305 match status {
306 ProcessStatus::Running => "●".green(),
307 ProcessStatus::Sleeping => "○".blue(),
308 ProcessStatus::Stopped => "◐".yellow(),
309 ProcessStatus::Zombie => "✗".red(),
310 _ => "?".white(),
311 }
312}
313
314#[derive(Serialize)]
315struct WhyEnvelope {
316 action: &'static str,
317 success: bool,
318 count: usize,
319 not_found: Vec<String>,
321 results: Vec<WhyOutput>,
322}
323
324#[derive(Serialize)]
325struct WhyOutput {
326 target: String,
327 #[serde(skip_serializing_if = "Option::is_none")]
328 port: Option<u16>,
329 #[serde(skip_serializing_if = "Option::is_none")]
330 protocol: Option<Protocol>,
331 process: WhyProcessInfo,
332 ports: Vec<PortInfo>,
333 ancestry: Vec<AncestryEntry>,
334}
335
336#[derive(Serialize)]
337struct WhyProcessInfo {
338 pid: u32,
339 name: String,
340 #[serde(skip_serializing_if = "Option::is_none")]
341 command: Option<String>,
342 #[serde(skip_serializing_if = "Option::is_none")]
343 cwd: Option<String>,
344 status: ProcessStatus,
345 #[serde(skip_serializing_if = "Option::is_none")]
346 uptime_secs: Option<u64>,
347}
348
349#[derive(Serialize)]
350struct AncestryEntry {
351 pid: u32,
352 name: String,
353 #[serde(skip_serializing_if = "Option::is_none")]
354 command: Option<String>,
355 #[serde(skip_serializing_if = "Option::is_none")]
356 cwd: Option<String>,
357 status: ProcessStatus,
358 #[serde(skip_serializing_if = "Option::is_none")]
359 uptime_secs: Option<u64>,
360 is_target: bool,
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 fn process(pid: u32, parent_pid: Option<u32>) -> Process {
368 Process {
369 pid,
370 name: format!("p{pid}"),
371 exe_path: None,
372 cwd: None,
373 command: None,
374 argv0: None,
375 raw: None,
376 cpu_percent: 0.0,
377 memory_mb: 0.0,
378 status: ProcessStatus::Sleeping,
379 user: None,
380 parent_pid,
381 start_time: None,
382 uptime_secs: None,
383 }
384 }
385
386 fn pids(chain: &[&Process]) -> Vec<u32> {
387 chain.iter().map(|p| p.pid).collect()
388 }
389
390 #[test]
391 fn a_chain_runs_from_the_root_down_to_the_target() {
392 let (root, mid, leaf) = (process(1, None), process(2, Some(1)), process(3, Some(2)));
393 let map: HashMap<u32, &Process> =
394 [&root, &mid, &leaf].iter().map(|p| (p.pid, *p)).collect();
395 assert_eq!(pids(&ancestor_chain(&leaf, &map)), vec![1, 2, 3]);
396 }
397
398 #[test]
399 fn a_chain_stops_at_a_parent_that_is_not_listed() {
400 let (mid, leaf) = (process(2, Some(1)), process(3, Some(2)));
402 let map: HashMap<u32, &Process> = [&mid, &leaf].iter().map(|p| (p.pid, *p)).collect();
403 assert_eq!(pids(&ancestor_chain(&leaf, &map)), vec![2, 3]);
404 }
405
406 #[test]
407 fn a_target_that_is_not_listed_has_no_chain() {
408 let map: HashMap<u32, &Process> = HashMap::new();
409 assert!(ancestor_chain(&process(3, Some(2)), &map).is_empty());
410 }
411
412 #[test]
413 fn a_loop_in_the_parent_links_ends_after_101_entries() {
414 let (a, b) = (process(1, Some(2)), process(2, Some(1)));
415 let map: HashMap<u32, &Process> = [&a, &b].iter().map(|p| (p.pid, *p)).collect();
416 let chain = ancestor_chain(&a, &map);
417 assert_eq!(chain.len(), 101);
418 assert_eq!(chain.last().map(|p| p.pid), Some(1));
420 }
421
422 #[test]
423 fn each_status_has_its_own_symbol() {
424 let symbol = |status| (*status_indicator(status)).to_string();
425 assert_eq!(symbol(ProcessStatus::Running), "●");
426 assert_eq!(symbol(ProcessStatus::Sleeping), "○");
427 assert_eq!(symbol(ProcessStatus::Stopped), "◐");
428 assert_eq!(symbol(ProcessStatus::Zombie), "✗");
429 assert_eq!(symbol(ProcessStatus::Dead), "?");
430 assert_eq!(symbol(ProcessStatus::Unknown), "?");
431 }
432
433 #[test]
434 fn a_target_that_resolves_to_nothing_is_not_found() {
435 let (error, warning) = found_processes("ghost", |_| Ok(Vec::new())).unwrap_err();
436 assert!(
437 matches!(&error, ProcError::ProcessNotFound(t) if t == "ghost"),
438 "{error:?}"
439 );
440 assert_eq!(warning, "No process found for 'ghost'");
441 }
442
443 #[test]
444 fn a_target_that_fails_to_resolve_warns_with_the_error() {
445 let (error, warning) =
446 found_processes("ghost", |_| Err(ProcError::PortNotFound(1))).unwrap_err();
447 assert!(matches!(error, ProcError::PortNotFound(1)), "{error:?}");
448 assert_eq!(warning, ProcError::PortNotFound(1).to_string());
449 }
450
451 #[test]
452 fn a_target_that_resolves_gives_its_processes() {
453 let found = found_processes("p", |_| Ok(vec![process(5, None)])).unwrap();
454 assert_eq!(found.len(), 1);
455 assert_eq!(found[0].pid, 5);
456 }
457}