1use crate::core::{
10 find_ports_for_pid, parse_target, parse_targets, resolve_target, PortInfo, Process,
11 ProcessStatus, TargetType,
12};
13use crate::error::Result;
14use crate::ui::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 for (i, target_str) in targets.iter().enumerate() {
47 if i > 0 && !self.json {
48 println!();
49 }
50
51 let parsed = parse_target(target_str);
52
53 let port_context: Option<PortInfo> = if let TargetType::Port(port) = &parsed {
55 PortInfo::find_by_port(*port).ok().flatten()
56 } else {
57 None
58 };
59
60 let target_processes = match resolve_target(target_str) {
62 Ok(procs) => procs,
63 Err(e) => {
64 if !self.json {
65 printer.warning(&format!("{}", e));
66 }
67 continue;
68 }
69 };
70
71 if target_processes.is_empty() {
72 printer.warning(&format!("No process found for '{}'", target_str));
73 continue;
74 }
75
76 if self.json {
77 let results: Vec<WhyOutput> = target_processes
78 .iter()
79 .map(|proc| {
80 let chain = self.build_ancestry_chain(proc, &pid_map);
81 let ports = find_ports_for_pid(proc.pid).unwrap_or_default();
82 WhyOutput {
83 target: target_str.clone(),
84 port: port_context.as_ref().map(|p| p.port),
85 protocol: port_context
86 .as_ref()
87 .map(|p| format!("{:?}", p.protocol).to_uppercase()),
88 process: WhyProcessInfo {
89 pid: proc.pid,
90 name: proc.name.clone(),
91 command: proc.command.clone(),
92 cwd: proc.cwd.clone(),
93 status: format!("{:?}", proc.status),
94 },
95 ports,
96 ancestry: chain,
97 }
98 })
99 .collect();
100 printer.print_json(&WhyEnvelope {
101 action: "why",
102 success: true,
103 count: results.len(),
104 results,
105 });
106 } else {
107 if let Some(ref port_info) = port_context {
109 println!(
110 "{} Port {} ({}):",
111 "✓".green().bold(),
112 port_info.port.to_string().cyan().bold(),
113 format!("{:?}", port_info.protocol).to_uppercase()
114 );
115 } else {
116 println!(
117 "{} Ancestry for '{}':",
118 "✓".green().bold(),
119 target_str.cyan()
120 );
121 }
122 println!();
123
124 for proc in &target_processes {
125 self.print_ancestry_with_context(proc, &pid_map);
126 println!();
127 }
128 }
129 }
130
131 Ok(())
132 }
133
134 fn build_ancestry_chain(
136 &self,
137 target: &Process,
138 pid_map: &HashMap<u32, &Process>,
139 ) -> Vec<AncestryEntry> {
140 let mut chain: Vec<AncestryEntry> = Vec::new();
141 let mut current_pid = Some(target.pid);
142
143 while let Some(pid) = current_pid {
144 if let Some(proc) = pid_map.get(&pid) {
145 chain.push(AncestryEntry {
146 pid: proc.pid,
147 name: proc.name.clone(),
148 command: proc.command.clone(),
149 cwd: proc.cwd.clone(),
150 status: format!("{:?}", proc.status),
151 is_target: proc.pid == target.pid,
152 });
153 current_pid = proc.parent_pid;
154 if chain.len() > 100 {
155 break;
156 }
157 } else {
158 break;
159 }
160 }
161
162 chain.reverse();
163 chain
164 }
165
166 fn print_ancestry_with_context(&self, target: &Process, pid_map: &HashMap<u32, &Process>) {
168 let mut chain: Vec<&Process> = Vec::new();
170 let mut current_pid = Some(target.pid);
171
172 while let Some(pid) = current_pid {
173 if let Some(proc) = pid_map.get(&pid) {
174 chain.push(proc);
175 current_pid = proc.parent_pid;
176 if chain.len() > 100 {
177 break;
178 }
179 } else {
180 break;
181 }
182 }
183
184 chain.reverse();
186
187 for (i, proc) in chain.iter().enumerate() {
188 let is_target = proc.pid == target.pid;
189 let indent = " ".repeat(i);
190 let connector = if i == 0 { "" } else { "└── " };
191
192 let status_indicator = match proc.status {
193 ProcessStatus::Running => "●".green(),
194 ProcessStatus::Sleeping => "○".blue(),
195 ProcessStatus::Stopped => "◐".yellow(),
196 ProcessStatus::Zombie => "✗".red(),
197 _ => "?".white(),
198 };
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 println!(
221 "{}{}{} {} [{}]{} {}",
222 indent.bright_black(),
223 connector.bright_black(),
224 status_indicator,
225 proc.name.cyan().bold(),
226 proc.pid.to_string().cyan().bold(),
227 cmd_part,
228 "← target".yellow()
229 );
230 if let Some(ref cwd) = proc.cwd {
232 let dir_indent = " ".repeat(i + 1);
233 println!(
234 "{}{}",
235 dir_indent.bright_black(),
236 format!("dir: {}", cwd).bright_black()
237 );
238 }
239 } else {
240 let cmd_part = if cmd_summary.is_empty() {
241 String::new()
242 } else {
243 format!(" {}", cmd_summary)
244 };
245 println!(
246 "{}{}{} {} [{}]{}",
247 indent.bright_black(),
248 connector.bright_black(),
249 status_indicator,
250 proc.name.white(),
251 proc.pid.to_string().cyan(),
252 cmd_part.bright_black()
253 );
254 }
255 }
256 }
257}
258
259#[derive(Serialize)]
260struct WhyEnvelope {
261 action: &'static str,
262 success: bool,
263 count: usize,
264 results: Vec<WhyOutput>,
265}
266
267#[derive(Serialize)]
268struct WhyOutput {
269 target: String,
270 #[serde(skip_serializing_if = "Option::is_none")]
271 port: Option<u16>,
272 #[serde(skip_serializing_if = "Option::is_none")]
273 protocol: Option<String>,
274 process: WhyProcessInfo,
275 ports: Vec<PortInfo>,
276 ancestry: Vec<AncestryEntry>,
277}
278
279#[derive(Serialize)]
280struct WhyProcessInfo {
281 pid: u32,
282 name: String,
283 #[serde(skip_serializing_if = "Option::is_none")]
284 command: Option<String>,
285 #[serde(skip_serializing_if = "Option::is_none")]
286 cwd: Option<String>,
287 status: String,
288}
289
290#[derive(Serialize)]
291struct AncestryEntry {
292 pid: u32,
293 name: String,
294 #[serde(skip_serializing_if = "Option::is_none")]
295 command: Option<String>,
296 #[serde(skip_serializing_if = "Option::is_none")]
297 cwd: Option<String>,
298 status: String,
299 is_target: bool,
300}