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