use crate::core::{
find_ports_for_pid, parse_target, parse_targets, resolve_target, PortInfo, Process,
ProcessStatus, Protocol, TargetType,
};
use crate::error::{ExitCode, ProcError, Result};
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)]
pub target: String,
#[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 = parse_targets(&self.target);
let mut json_results: Vec<WhyOutput> = Vec::new();
let mut found_any = false;
let mut first_error: Option<ProcError> = None;
for (i, target_str) in targets.iter().enumerate() {
if i > 0 && !self.json {
println!();
}
let parsed = parse_target(target_str);
let port_context: Option<PortInfo> = if let TargetType::Port(port) = &parsed {
PortInfo::find_by_port(*port).ok().flatten()
} else {
None
};
let target_processes = match resolve_target(target_str) {
Ok(procs) => procs,
Err(e) => {
if !self.json {
printer.warning(&format!("{}", e));
}
first_error.get_or_insert(e);
continue;
}
};
if target_processes.is_empty() {
printer.warning(&format!("No process found for '{}'", target_str));
first_error.get_or_insert(ProcError::ProcessNotFound(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 = find_ports_for_pid(proc.pid).unwrap_or_default();
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 {
println!(
"{} Port {} ({}):",
"✓".green().bold(),
port_info.port.to_string().cyan().bold(),
format!("{:?}", port_info.protocol).to_uppercase()
);
} else {
println!(
"{} Ancestry for '{}':",
"✓".green().bold(),
target_str.cyan()
);
}
println!();
for proc in &target_processes {
self.print_ancestry_with_context(proc, &pid_map);
println!();
}
}
}
if !found_any {
let error =
first_error.unwrap_or_else(|| ProcError::ProcessNotFound(self.target.clone()));
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(),
results: json_results,
});
}
Ok(())
}
fn build_ancestry_chain(
&self,
target: &Process,
pid_map: &HashMap<u32, &Process>,
) -> Vec<AncestryEntry> {
let mut chain: Vec<AncestryEntry> = Vec::new();
let mut current_pid = Some(target.pid);
while let Some(pid) = current_pid {
if let Some(proc) = pid_map.get(&pid) {
chain.push(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,
});
current_pid = proc.parent_pid;
if chain.len() > 100 {
break;
}
} else {
break;
}
}
chain.reverse();
chain
}
fn print_ancestry_with_context(&self, target: &Process, pid_map: &HashMap<u32, &Process>) {
let mut chain: Vec<&Process> = Vec::new();
let mut current_pid = Some(target.pid);
while let Some(pid) = current_pid {
if let Some(proc) = pid_map.get(&pid) {
chain.push(proc);
current_pid = proc.parent_pid;
if chain.len() > 100 {
break;
}
} else {
break;
}
}
chain.reverse();
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 status_indicator = match proc.status {
ProcessStatus::Running => "●".green(),
ProcessStatus::Sleeping => "○".blue(),
ProcessStatus::Stopped => "◐".yellow(),
ProcessStatus::Zombie => "✗".red(),
_ => "?".white(),
};
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)
};
println!(
"{}{}{} {} [{}]{}{} {}",
indent.bright_black(),
connector.bright_black(),
status_indicator,
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);
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)
};
println!(
"{}{}{} {} [{}]{}{}",
indent.bright_black(),
connector.bright_black(),
status_indicator,
proc.name.white(),
proc.pid.to_string().cyan(),
cmd_part.bright_black(),
uptime_label(proc.uptime_secs).bright_black()
);
}
}
}
}
#[derive(Serialize)]
struct WhyEnvelope {
action: &'static str,
success: bool,
count: usize,
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,
}