Skip to main content

proc_cli/commands/
tree.rs

1//! Tree command - Show process tree
2//!
3//! Usage:
4//!   proc tree              # Full process tree
5//!   proc tree node         # Tree for node processes
6//!   proc tree :3000        # Tree for process on port 3000
7//!   proc tree 1234         # Tree for PID 1234
8//!   proc tree --min-cpu 10 # Only processes using >10% CPU
9//!   proc tree 1234 -a      # Show ancestry (path UP to root)
10
11use crate::core::{
12    matches_by_filter, parse_target, resolve_in_dir, resolve_target, Process, ProcessStatus,
13    StatusFilter, TargetType,
14};
15use crate::error::Result;
16use crate::ui::{format_memory, plural, uptime_label, Printer};
17use clap::Args;
18use colored::*;
19use serde::Serialize;
20use std::collections::HashMap;
21use std::path::PathBuf;
22
23/// Show process tree
24#[derive(Args, Debug)]
25pub struct TreeCommand {
26    /// Target: process name, :port, or PID (shows full tree if omitted)
27    target: Option<String>,
28
29    /// Show ancestry (path UP to root) instead of descendants
30    #[arg(long, short)]
31    ancestors: bool,
32
33    /// Show verbose output
34    #[arg(long, short = 'v')]
35    verbose: bool,
36
37    /// Output as JSON
38    #[arg(long, short = 'j')]
39    json: bool,
40
41    /// Maximum depth to display
42    #[arg(long, short, default_value = "10")]
43    depth: usize,
44
45    /// Show PIDs only (compact view)
46    #[arg(long, short = 'C')]
47    compact: bool,
48
49    /// Only show processes using more than this CPU %
50    #[arg(long)]
51    min_cpu: Option<f32>,
52
53    /// Only show processes using more than this memory (MB)
54    #[arg(long)]
55    min_mem: Option<f64>,
56
57    /// Filter by status: running, sleeping, stopped, zombie
58    #[arg(long, value_enum, ignore_case = true)]
59    status: Option<StatusFilter>,
60
61    /// Only show processes running longer than this (seconds)
62    #[arg(long)]
63    min_uptime: Option<u64>,
64
65    /// Filter by directory (defaults to current directory if no path given)
66    #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
67    pub in_dir: Option<String>,
68
69    /// Filter by process name
70    #[arg(long = "by", short = 'b')]
71    pub by_name: Option<String>,
72}
73
74impl TreeCommand {
75    /// Build the set of ancestor PIDs to exclude from name matching.
76    ///
77    /// Walks up the process tree from the current process to avoid matching
78    /// shells whose command lines contain proc's own arguments.
79    fn ancestor_pids(all_processes: &[Process]) -> std::collections::HashSet<u32> {
80        let mut pids = std::collections::HashSet::new();
81        let self_pid = std::process::id();
82        pids.insert(self_pid);
83        let mut current = self_pid;
84        for _ in 0..10 {
85            if let Some(parent_pid) = all_processes
86                .iter()
87                .find(|p| p.pid == current)
88                .and_then(|p| p.parent_pid)
89            {
90                pids.insert(parent_pid);
91                current = parent_pid;
92            } else {
93                break;
94            }
95        }
96        pids
97    }
98
99    /// Executes the tree command, displaying the process hierarchy.
100    pub fn execute(&self) -> Result<()> {
101        let printer = Printer::from_flags(self.json, self.verbose);
102
103        // Get all processes
104        let all_processes = Process::find_all()?;
105
106        // Build PID -> Process map for quick lookup
107        let pid_map: HashMap<u32, &Process> = all_processes.iter().map(|p| (p.pid, p)).collect();
108
109        // Build parent -> children map
110        let mut children_map: HashMap<u32, Vec<&Process>> = HashMap::new();
111
112        for proc in &all_processes {
113            if let Some(ppid) = proc.parent_pid {
114                children_map.entry(ppid).or_default().push(proc);
115            }
116        }
117
118        // Handle --ancestors mode
119        if self.ancestors {
120            return self.show_ancestors(&printer, &pid_map);
121        }
122
123        // Determine target processes
124        let target_processes: Vec<&Process> = if let Some(ref target) = self.target {
125            // Use unified target resolution
126            match parse_target(target) {
127                TargetType::Port(_) | TargetType::Pid(_) => {
128                    // For port or PID, resolve to specific process(es)
129                    let resolved = resolve_target(target)?;
130                    if resolved.is_empty() {
131                        printer.warning(&format!("No process found for '{}'", target));
132                        return Ok(());
133                    }
134                    // Find matching processes in all_processes
135                    let pids: Vec<u32> = resolved.iter().map(|p| p.pid).collect();
136                    all_processes
137                        .iter()
138                        .filter(|p| pids.contains(&p.pid))
139                        .collect()
140                }
141                TargetType::Name(ref pattern) => {
142                    // For name, do pattern matching (exclude self and parent to avoid
143                    // false positives from proc's own args in the shell command line)
144                    let ancestor_pids = Self::ancestor_pids(&all_processes);
145                    all_processes
146                        .iter()
147                        .filter(|p| {
148                            !ancestor_pids.contains(&p.pid) && matches_by_filter(p, pattern)
149                        })
150                        .collect()
151                }
152            }
153        } else {
154            Vec::new() // Will show full tree
155        };
156
157        // Apply --in and --by filters (only for targeted mode)
158        let target_processes = if self.target.is_some() {
159            let in_dir_filter = resolve_in_dir(&self.in_dir);
160            target_processes
161                .into_iter()
162                .filter(|p| {
163                    if let Some(ref dir_path) = in_dir_filter {
164                        if let Some(ref cwd) = p.cwd {
165                            if !PathBuf::from(cwd).starts_with(dir_path) {
166                                return false;
167                            }
168                        } else {
169                            return false;
170                        }
171                    }
172                    if let Some(ref name) = self.by_name {
173                        if !matches_by_filter(p, name) {
174                            return false;
175                        }
176                    }
177                    true
178                })
179                .collect()
180        } else {
181            target_processes
182        };
183
184        // Apply resource filters if specified
185        let matches_filters = |p: &Process| -> bool {
186            if let Some(min_cpu) = self.min_cpu {
187                if p.cpu_percent < min_cpu {
188                    return false;
189                }
190            }
191            if let Some(min_mem) = self.min_mem {
192                if p.memory_mb < min_mem {
193                    return false;
194                }
195            }
196            if let Some(status) = self.status {
197                if !status.matches(p.status) {
198                    return false;
199                }
200            }
201            if let Some(min_uptime) = self.min_uptime {
202                if let Some(start_time) = p.start_time {
203                    let now = std::time::SystemTime::now()
204                        .duration_since(std::time::UNIX_EPOCH)
205                        .map(|d| d.as_secs())
206                        .unwrap_or(0);
207                    if now.saturating_sub(start_time) < min_uptime {
208                        return false;
209                    }
210                } else {
211                    return false;
212                }
213            }
214            true
215        };
216
217        // Apply filters to target processes or find filtered roots
218        let has_filters = self.min_cpu.is_some()
219            || self.min_mem.is_some()
220            || self.status.is_some()
221            || self.min_uptime.is_some();
222
223        if self.json {
224            let tree_nodes = if self.target.is_some() {
225                target_processes
226                    .iter()
227                    .filter(|p| matches_filters(p))
228                    .map(|p| self.build_tree_node(p, &children_map, 0))
229                    .collect()
230            } else if has_filters {
231                // Show only processes matching filters
232                all_processes
233                    .iter()
234                    .filter(|p| matches_filters(p))
235                    .map(|p| self.build_tree_node(p, &children_map, 0))
236                    .collect()
237            } else {
238                // Show full tree from roots
239                all_processes
240                    .iter()
241                    .filter(|p| p.parent_pid.is_none() || p.parent_pid == Some(0))
242                    .map(|p| self.build_tree_node(p, &children_map, 0))
243                    .collect()
244            };
245
246            printer.print_json(&TreeOutput {
247                action: "tree",
248                success: true,
249                tree: tree_nodes,
250            });
251        } else if self.target.is_some() {
252            let filtered: Vec<_> = target_processes
253                .into_iter()
254                .filter(|p| matches_filters(p))
255                .collect();
256            if filtered.is_empty() {
257                printer.warning(&format!(
258                    "No processes found for '{}'",
259                    self.target.as_ref().unwrap()
260                ));
261                return Ok(());
262            }
263
264            println!(
265                "{} Process tree for '{}':\n",
266                "✓".green().bold(),
267                self.target.as_ref().unwrap().cyan()
268            );
269
270            for proc in &filtered {
271                self.print_tree(proc, &children_map, "", true, 0);
272                println!();
273            }
274        } else if has_filters {
275            let filtered: Vec<_> = all_processes
276                .iter()
277                .filter(|p| matches_filters(p))
278                .collect();
279            if filtered.is_empty() {
280                printer.warning("No processes match the specified filters");
281                return Ok(());
282            }
283
284            println!(
285                "{} {} process{} matching filters:\n",
286                "✓".green().bold(),
287                filtered.len().to_string().cyan().bold(),
288                plural(filtered.len())
289            );
290
291            for (i, proc) in filtered.iter().enumerate() {
292                let is_last = i == filtered.len() - 1;
293                self.print_tree(proc, &children_map, "", is_last, 0);
294            }
295        } else {
296            println!("{} Process tree:\n", "✓".green().bold());
297
298            // Find processes with PID 1 or no parent as roots
299            let display_roots: Vec<&Process> = all_processes
300                .iter()
301                .filter(|p| p.parent_pid.is_none() || p.parent_pid == Some(0))
302                .collect();
303
304            for (i, proc) in display_roots.iter().enumerate() {
305                let is_last = i == display_roots.len() - 1;
306                self.print_tree(proc, &children_map, "", is_last, 0);
307            }
308        }
309
310        Ok(())
311    }
312
313    fn print_tree(
314        &self,
315        proc: &Process,
316        children_map: &HashMap<u32, Vec<&Process>>,
317        prefix: &str,
318        is_last: bool,
319        depth: usize,
320    ) {
321        if depth > self.depth {
322            return;
323        }
324
325        let connector = if is_last { "└── " } else { "├── " };
326
327        if self.compact {
328            println!(
329                "{}{}{}",
330                prefix.bright_black(),
331                connector.bright_black(),
332                proc.pid.to_string().cyan()
333            );
334        } else {
335            let status_indicator = match proc.status {
336                crate::core::ProcessStatus::Running => "●".green(),
337                crate::core::ProcessStatus::Sleeping => "○".blue(),
338                crate::core::ProcessStatus::Stopped => "◐".yellow(),
339                crate::core::ProcessStatus::Zombie => "✗".red(),
340                _ => "?".white(),
341            };
342
343            println!(
344                "{}{}{} {} [{}] {:.1}% {}{}",
345                prefix.bright_black(),
346                connector.bright_black(),
347                status_indicator,
348                proc.name.white().bold(),
349                proc.pid.to_string().cyan(),
350                proc.cpu_percent,
351                format_memory(proc.memory_mb),
352                uptime_label(proc.uptime_secs).bright_black()
353            );
354        }
355
356        let child_prefix = if is_last {
357            format!("{}    ", prefix)
358        } else {
359            format!("{}│   ", prefix)
360        };
361
362        if let Some(children) = children_map.get(&proc.pid) {
363            let mut sorted_children: Vec<&&Process> = children.iter().collect();
364            sorted_children.sort_by_key(|p| p.pid);
365
366            for (i, child) in sorted_children.iter().enumerate() {
367                let child_is_last = i == sorted_children.len() - 1;
368                self.print_tree(child, children_map, &child_prefix, child_is_last, depth + 1);
369            }
370        }
371    }
372
373    fn build_tree_node(
374        &self,
375        proc: &Process,
376        children_map: &HashMap<u32, Vec<&Process>>,
377        depth: usize,
378    ) -> TreeNode {
379        let children = if depth < self.depth {
380            children_map
381                .get(&proc.pid)
382                .map(|kids| {
383                    kids.iter()
384                        .map(|p| self.build_tree_node(p, children_map, depth + 1))
385                        .collect()
386                })
387                .unwrap_or_default()
388        } else {
389            Vec::new()
390        };
391
392        TreeNode {
393            pid: proc.pid,
394            name: proc.name.clone(),
395            cpu_percent: proc.cpu_percent,
396            memory_mb: proc.memory_mb,
397            status: proc.status,
398            uptime_secs: proc.uptime_secs,
399            children,
400        }
401    }
402
403    /// Show ancestry (path UP to root) for target processes
404    fn show_ancestors(&self, printer: &Printer, pid_map: &HashMap<u32, &Process>) -> Result<()> {
405        use crate::core::{parse_target, resolve_target, TargetType};
406
407        let target = match &self.target {
408            Some(t) => t,
409            None => {
410                printer.warning("--ancestors requires a target (PID, :port, or name)");
411                return Ok(());
412            }
413        };
414
415        // Resolve target to processes
416        let target_processes = match parse_target(target) {
417            TargetType::Port(_) | TargetType::Pid(_) => resolve_target(target)?,
418            TargetType::Name(ref pattern) => {
419                let all_procs: Vec<Process> = pid_map.values().map(|p| (*p).clone()).collect();
420                let ancestor_pids = Self::ancestor_pids(&all_procs);
421                pid_map
422                    .values()
423                    .filter(|p| !ancestor_pids.contains(&p.pid) && matches_by_filter(p, pattern))
424                    .map(|p| (*p).clone())
425                    .collect()
426            }
427        };
428
429        if target_processes.is_empty() {
430            printer.warning(&format!("No process found for '{}'", target));
431            return Ok(());
432        }
433
434        if self.json {
435            let ancestry_output: Vec<AncestryNode> = target_processes
436                .iter()
437                .map(|proc| self.build_ancestry_node(proc, pid_map))
438                .collect();
439            printer.print_json(&AncestryOutput {
440                action: "tree",
441                success: true,
442                ancestry: ancestry_output,
443            });
444        } else {
445            println!("{} Ancestry for '{}':\n", "✓".green().bold(), target.cyan());
446
447            for proc in &target_processes {
448                self.print_ancestry(proc, pid_map);
449                println!();
450            }
451        }
452
453        Ok(())
454    }
455
456    /// Trace and print ancestry from root down to target
457    fn print_ancestry(&self, target: &Process, pid_map: &HashMap<u32, &Process>) {
458        // Build the ancestor chain (from target up to root)
459        let mut chain: Vec<&Process> = Vec::new();
460        let mut current_pid = Some(target.pid);
461
462        while let Some(pid) = current_pid {
463            if let Some(proc) = pid_map.get(&pid) {
464                chain.push(proc);
465                current_pid = proc.parent_pid;
466                // Prevent infinite loops
467                if chain.len() > 100 {
468                    break;
469                }
470            } else {
471                break;
472            }
473        }
474
475        // Reverse to print from root to target
476        chain.reverse();
477
478        // Print the chain
479        for (i, proc) in chain.iter().enumerate() {
480            let is_target = proc.pid == target.pid;
481            let indent = "    ".repeat(i);
482            let connector = if i == 0 { "" } else { "└── " };
483
484            let status_indicator = match proc.status {
485                ProcessStatus::Running => "●".green(),
486                ProcessStatus::Sleeping => "○".blue(),
487                ProcessStatus::Stopped => "◐".yellow(),
488                ProcessStatus::Zombie => "✗".red(),
489                _ => "?".white(),
490            };
491
492            if is_target {
493                // Highlight the target
494                println!(
495                    "{}{}{} {} [{}] {:.1}% {}{}  {}",
496                    indent.bright_black(),
497                    connector.bright_black(),
498                    status_indicator,
499                    proc.name.cyan().bold(),
500                    proc.pid.to_string().cyan().bold(),
501                    proc.cpu_percent,
502                    format_memory(proc.memory_mb),
503                    uptime_label(proc.uptime_secs).bright_black(),
504                    "← target".yellow()
505                );
506            } else {
507                println!(
508                    "{}{}{} {} [{}] {:.1}% {}{}",
509                    indent.bright_black(),
510                    connector.bright_black(),
511                    status_indicator,
512                    proc.name.white(),
513                    proc.pid.to_string().cyan(),
514                    proc.cpu_percent,
515                    format_memory(proc.memory_mb),
516                    uptime_label(proc.uptime_secs).bright_black()
517                );
518            }
519        }
520    }
521
522    /// Build ancestry node for JSON output
523    fn build_ancestry_node(
524        &self,
525        target: &Process,
526        pid_map: &HashMap<u32, &Process>,
527    ) -> AncestryNode {
528        let mut chain: Vec<ProcessInfo> = Vec::new();
529        let mut current_pid = Some(target.pid);
530
531        while let Some(pid) = current_pid {
532            if let Some(proc) = pid_map.get(&pid) {
533                chain.push(ProcessInfo {
534                    pid: proc.pid,
535                    name: proc.name.clone(),
536                    cpu_percent: proc.cpu_percent,
537                    memory_mb: proc.memory_mb,
538                    status: proc.status,
539                    uptime_secs: proc.uptime_secs,
540                });
541                current_pid = proc.parent_pid;
542                if chain.len() > 100 {
543                    break;
544                }
545            } else {
546                break;
547            }
548        }
549
550        chain.reverse();
551
552        AncestryNode {
553            target_pid: target.pid,
554            target_name: target.name.clone(),
555            depth: chain.len(),
556            chain,
557        }
558    }
559}
560
561#[derive(Serialize)]
562struct AncestryOutput {
563    action: &'static str,
564    success: bool,
565    ancestry: Vec<AncestryNode>,
566}
567
568#[derive(Serialize)]
569struct AncestryNode {
570    target_pid: u32,
571    target_name: String,
572    depth: usize,
573    chain: Vec<ProcessInfo>,
574}
575
576#[derive(Serialize)]
577struct ProcessInfo {
578    pid: u32,
579    name: String,
580    cpu_percent: f32,
581    memory_mb: f64,
582    status: ProcessStatus,
583    #[serde(skip_serializing_if = "Option::is_none")]
584    uptime_secs: Option<u64>,
585}
586
587#[derive(Serialize)]
588struct TreeOutput {
589    action: &'static str,
590    success: bool,
591    tree: Vec<TreeNode>,
592}
593
594#[derive(Serialize)]
595struct TreeNode {
596    pid: u32,
597    name: String,
598    cpu_percent: f32,
599    memory_mb: f64,
600    status: ProcessStatus,
601    #[serde(skip_serializing_if = "Option::is_none")]
602    uptime_secs: Option<u64>,
603    children: Vec<TreeNode>,
604}