use crate::core::{
flatten_targets, parse_target, resolve_target, NameMatch, PortInfo, Process, ProcessStatus,
Protocol, TargetType,
};
use crate::error::{ExitCode, ProcError, Result};
use crate::out_println;
use crate::ui::{uptime_label, Printer};
use clap::Args;
use colored::*;
use serde::Serialize;
use std::collections::HashMap;
#[derive(Args, Debug)]
pub struct WhyCommand {
#[arg(required = true, num_args = 1.., value_parser = crate::core::validate_target_arg)]
pub target: Vec<String>,
#[arg(long)]
pub cmd: bool,
#[arg(long, short = 'j')]
pub json: bool,
#[arg(long, short = 'v')]
pub verbose: bool,
}
impl WhyCommand {
pub fn execute(&self) -> Result<()> {
let printer = Printer::from_flags(self.json, self.verbose);
let all_processes = Process::find_all()?;
let pid_map: HashMap<u32, &Process> = all_processes.iter().map(|p| (p.pid, p)).collect();
let targets = flatten_targets(&self.target);
let mode = NameMatch::from_flag(self.cmd);
let mut json_results: Vec<WhyOutput> = Vec::new();
let mut found_any = false;
let mut first_error: Option<ProcError> = None;
let mut not_found: Vec<String> = Vec::new();
let listening = std::cell::OnceCell::new();
for (i, target_str) in targets.iter().enumerate() {
if i > 0 && !self.json {
out_println!();
}
let parsed = parse_target(target_str);
let port_context: Option<PortInfo> = if let TargetType::Port(port) = &parsed {
PortInfo::holders_of(*port)
.ok()
.and_then(|holders| holders.into_iter().next())
} else {
None
};
let target_processes = match found_processes(target_str, |t| resolve_target(t, mode)) {
Ok(procs) => procs,
Err((error, warning)) => {
printer.warning(&warning);
first_error.get_or_insert(error);
not_found.push(target_str.clone());
continue;
}
};
found_any = true;
if self.json {
json_results.extend(target_processes.iter().map(|proc| {
let chain = self.build_ancestry_chain(proc, &pid_map);
let ports = listening
.get_or_init(|| PortInfo::get_all_listening().unwrap_or_default())
.iter()
.filter(|p| p.pid == proc.pid)
.cloned()
.collect();
WhyOutput {
target: target_str.clone(),
port: port_context.as_ref().map(|p| p.port),
protocol: port_context.as_ref().map(|p| p.protocol),
process: WhyProcessInfo {
pid: proc.pid,
name: proc.name.clone(),
command: proc.command.clone(),
cwd: proc.cwd.clone(),
status: proc.status,
uptime_secs: proc.uptime_secs,
},
ports,
ancestry: chain,
}
}));
} else {
if let Some(ref port_info) = port_context {
out_println!(
"{} Port {} ({}):",
"✓".green().bold(),
port_info.port.to_string().cyan().bold(),
format!("{:?}", port_info.protocol).to_uppercase()
);
} else {
out_println!(
"{} Ancestry for '{}':",
"✓".green().bold(),
target_str.cyan()
);
}
out_println!();
for proc in &target_processes {
self.print_ancestry_with_context(proc, &pid_map);
out_println!();
}
}
}
if !found_any {
let error =
first_error.unwrap_or_else(|| ProcError::ProcessNotFound(self.target.join(" ")));
if self.json {
return Err(error);
}
return Err(ProcError::reported(
ExitCode::from(&error),
error.to_string(),
));
}
if self.json {
printer.print_json(&WhyEnvelope {
action: "why",
success: true,
count: json_results.len(),
not_found,
results: json_results,
});
}
Ok(())
}
fn build_ancestry_chain(
&self,
target: &Process,
pid_map: &HashMap<u32, &Process>,
) -> Vec<AncestryEntry> {
ancestor_chain(target, pid_map)
.into_iter()
.map(|proc| AncestryEntry {
pid: proc.pid,
name: proc.name.clone(),
command: proc.command.clone(),
cwd: proc.cwd.clone(),
status: proc.status,
uptime_secs: proc.uptime_secs,
is_target: proc.pid == target.pid,
})
.collect()
}
fn print_ancestry_with_context(&self, target: &Process, pid_map: &HashMap<u32, &Process>) {
let chain = ancestor_chain(target, pid_map);
for (i, proc) in chain.iter().enumerate() {
let is_target = proc.pid == target.pid;
let indent = " ".repeat(i);
let connector = if i == 0 { "" } else { "└── " };
let symbol = status_indicator(proc.status);
let cmd_summary = proc
.command
.as_ref()
.and_then(|c| {
let parts: Vec<&str> = c.split_whitespace().collect();
if parts.len() > 1 {
Some(parts[1..].join(" "))
} else {
None
}
})
.unwrap_or_default();
if is_target {
let cmd_part = if cmd_summary.is_empty() {
String::new()
} else {
format!(" {}", cmd_summary)
};
out_println!(
"{}{}{} {} [{}]{}{} {}",
indent.bright_black(),
connector.bright_black(),
symbol,
proc.name.cyan().bold(),
proc.pid.to_string().cyan().bold(),
cmd_part,
uptime_label(proc.uptime_secs).bright_black(),
"← target".yellow()
);
if let Some(ref cwd) = proc.cwd {
let dir_indent = " ".repeat(i + 1);
out_println!(
"{}{}",
dir_indent.bright_black(),
format!("dir: {}", cwd).bright_black()
);
}
} else {
let cmd_part = if cmd_summary.is_empty() {
String::new()
} else {
format!(" {}", cmd_summary)
};
out_println!(
"{}{}{} {} [{}]{}{}",
indent.bright_black(),
connector.bright_black(),
symbol,
proc.name.white(),
proc.pid.to_string().cyan(),
cmd_part.bright_black(),
uptime_label(proc.uptime_secs).bright_black()
);
}
}
}
}
fn found_processes(
target: &str,
resolve: impl Fn(&str) -> Result<Vec<Process>>,
) -> std::result::Result<Vec<Process>, (ProcError, String)> {
match resolve(target) {
Ok(processes) if processes.is_empty() => Err((
ProcError::ProcessNotFound(target.to_string()),
format!("No process found for '{}'", target),
)),
Ok(processes) => Ok(processes),
Err(e) => {
let warning = e.to_string();
Err((e, warning))
}
}
}
fn ancestor_chain<'a>(target: &Process, pid_map: &HashMap<u32, &'a Process>) -> Vec<&'a Process> {
let mut chain: Vec<&Process> = Vec::new();
let mut current_pid = Some(target.pid);
while let Some(pid) = current_pid {
let Some(proc) = pid_map.get(&pid) else {
break;
};
chain.push(proc);
current_pid = proc.parent_pid;
if chain.len() > 100 {
break;
}
}
chain.reverse();
chain
}
fn status_indicator(status: ProcessStatus) -> ColoredString {
match status {
ProcessStatus::Running => "●".green(),
ProcessStatus::Sleeping => "○".blue(),
ProcessStatus::Stopped => "◐".yellow(),
ProcessStatus::Zombie => "✗".red(),
_ => "?".white(),
}
}
#[derive(Serialize)]
struct WhyEnvelope {
action: &'static str,
success: bool,
count: usize,
not_found: Vec<String>,
results: Vec<WhyOutput>,
}
#[derive(Serialize)]
struct WhyOutput {
target: String,
#[serde(skip_serializing_if = "Option::is_none")]
port: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
protocol: Option<Protocol>,
process: WhyProcessInfo,
ports: Vec<PortInfo>,
ancestry: Vec<AncestryEntry>,
}
#[derive(Serialize)]
struct WhyProcessInfo {
pid: u32,
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
command: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
cwd: Option<String>,
status: ProcessStatus,
#[serde(skip_serializing_if = "Option::is_none")]
uptime_secs: Option<u64>,
}
#[derive(Serialize)]
struct AncestryEntry {
pid: u32,
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
command: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
cwd: Option<String>,
status: ProcessStatus,
#[serde(skip_serializing_if = "Option::is_none")]
uptime_secs: Option<u64>,
is_target: bool,
}
#[cfg(test)]
mod tests {
use super::*;
fn process(pid: u32, parent_pid: Option<u32>) -> Process {
Process {
pid,
name: format!("p{pid}"),
exe_path: None,
cwd: None,
command: None,
argv0: None,
raw: None,
cpu_percent: 0.0,
memory_mb: 0.0,
status: ProcessStatus::Sleeping,
user: None,
parent_pid,
start_time: None,
uptime_secs: None,
}
}
fn pids(chain: &[&Process]) -> Vec<u32> {
chain.iter().map(|p| p.pid).collect()
}
#[test]
fn a_chain_runs_from_the_root_down_to_the_target() {
let (root, mid, leaf) = (process(1, None), process(2, Some(1)), process(3, Some(2)));
let map: HashMap<u32, &Process> =
[&root, &mid, &leaf].iter().map(|p| (p.pid, *p)).collect();
assert_eq!(pids(&ancestor_chain(&leaf, &map)), vec![1, 2, 3]);
}
#[test]
fn a_chain_stops_at_a_parent_that_is_not_listed() {
let (mid, leaf) = (process(2, Some(1)), process(3, Some(2)));
let map: HashMap<u32, &Process> = [&mid, &leaf].iter().map(|p| (p.pid, *p)).collect();
assert_eq!(pids(&ancestor_chain(&leaf, &map)), vec![2, 3]);
}
#[test]
fn a_target_that_is_not_listed_has_no_chain() {
let map: HashMap<u32, &Process> = HashMap::new();
assert!(ancestor_chain(&process(3, Some(2)), &map).is_empty());
}
#[test]
fn a_loop_in_the_parent_links_ends_after_101_entries() {
let (a, b) = (process(1, Some(2)), process(2, Some(1)));
let map: HashMap<u32, &Process> = [&a, &b].iter().map(|p| (p.pid, *p)).collect();
let chain = ancestor_chain(&a, &map);
assert_eq!(chain.len(), 101);
assert_eq!(chain.last().map(|p| p.pid), Some(1));
}
#[test]
fn each_status_has_its_own_symbol() {
let symbol = |status| (*status_indicator(status)).to_string();
assert_eq!(symbol(ProcessStatus::Running), "●");
assert_eq!(symbol(ProcessStatus::Sleeping), "○");
assert_eq!(symbol(ProcessStatus::Stopped), "◐");
assert_eq!(symbol(ProcessStatus::Zombie), "✗");
assert_eq!(symbol(ProcessStatus::Dead), "?");
assert_eq!(symbol(ProcessStatus::Unknown), "?");
}
#[test]
fn a_target_that_resolves_to_nothing_is_not_found() {
let (error, warning) = found_processes("ghost", |_| Ok(Vec::new())).unwrap_err();
assert!(
matches!(&error, ProcError::ProcessNotFound(t) if t == "ghost"),
"{error:?}"
);
assert_eq!(warning, "No process found for 'ghost'");
}
#[test]
fn a_target_that_fails_to_resolve_warns_with_the_error() {
let (error, warning) =
found_processes("ghost", |_| Err(ProcError::PortNotFound(1))).unwrap_err();
assert!(matches!(error, ProcError::PortNotFound(1)), "{error:?}");
assert_eq!(warning, ProcError::PortNotFound(1).to_string());
}
#[test]
fn a_target_that_resolves_gives_its_processes() {
let found = found_processes("p", |_| Ok(vec![process(5, None)])).unwrap();
assert_eq!(found.len(), 1);
assert_eq!(found[0].pid, 5);
}
}