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    TargetType,
14};
15use crate::error::Result;
16use crate::ui::{format_memory, plural, 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)]
59    status: Option<String>,
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(ref status) = self.status {
197                let status_match = match status.to_lowercase().as_str() {
198                    "running" => matches!(p.status, ProcessStatus::Running),
199                    "sleeping" | "sleep" => matches!(p.status, ProcessStatus::Sleeping),
200                    "stopped" | "stop" => matches!(p.status, ProcessStatus::Stopped),
201                    "zombie" => matches!(p.status, ProcessStatus::Zombie),
202                    _ => true,
203                };
204                if !status_match {
205                    return false;
206                }
207            }
208            if let Some(min_uptime) = self.min_uptime {
209                if let Some(start_time) = p.start_time {
210                    let now = std::time::SystemTime::now()
211                        .duration_since(std::time::UNIX_EPOCH)
212                        .map(|d| d.as_secs())
213                        .unwrap_or(0);
214                    if now.saturating_sub(start_time) < min_uptime {
215                        return false;
216                    }
217                } else {
218                    return false;
219                }
220            }
221            true
222        };
223
224        // Apply filters to target processes or find filtered roots
225        let has_filters = self.min_cpu.is_some()
226            || self.min_mem.is_some()
227            || self.status.is_some()
228            || self.min_uptime.is_some();
229
230        if self.json {
231            let tree_nodes = if self.target.is_some() {
232                target_processes
233                    .iter()
234                    .filter(|p| matches_filters(p))
235                    .map(|p| self.build_tree_node(p, &children_map, 0))
236                    .collect()
237            } else if has_filters {
238                // Show only processes matching filters
239                all_processes
240                    .iter()
241                    .filter(|p| matches_filters(p))
242                    .map(|p| self.build_tree_node(p, &children_map, 0))
243                    .collect()
244            } else {
245                // Show full tree from roots
246                all_processes
247                    .iter()
248                    .filter(|p| p.parent_pid.is_none() || p.parent_pid == Some(0))
249                    .map(|p| self.build_tree_node(p, &children_map, 0))
250                    .collect()
251            };
252
253            printer.print_json(&TreeOutput {
254                action: "tree",
255                success: true,
256                tree: tree_nodes,
257            });
258        } else if self.target.is_some() {
259            let filtered: Vec<_> = target_processes
260                .into_iter()
261                .filter(|p| matches_filters(p))
262                .collect();
263            if filtered.is_empty() {
264                printer.warning(&format!(
265                    "No processes found for '{}'",
266                    self.target.as_ref().unwrap()
267                ));
268                return Ok(());
269            }
270
271            println!(
272                "{} Process tree for '{}':\n",
273                "✓".green().bold(),
274                self.target.as_ref().unwrap().cyan()
275            );
276
277            for proc in &filtered {
278                self.print_tree(proc, &children_map, "", true, 0);
279                println!();
280            }
281        } else if has_filters {
282            let filtered: Vec<_> = all_processes
283                .iter()
284                .filter(|p| matches_filters(p))
285                .collect();
286            if filtered.is_empty() {
287                printer.warning("No processes match the specified filters");
288                return Ok(());
289            }
290
291            println!(
292                "{} {} process{} matching filters:\n",
293                "✓".green().bold(),
294                filtered.len().to_string().cyan().bold(),
295                plural(filtered.len())
296            );
297
298            for (i, proc) in filtered.iter().enumerate() {
299                let is_last = i == filtered.len() - 1;
300                self.print_tree(proc, &children_map, "", is_last, 0);
301            }
302        } else {
303            println!("{} Process tree:\n", "✓".green().bold());
304
305            // Find processes with PID 1 or no parent as roots
306            let display_roots: Vec<&Process> = all_processes
307                .iter()
308                .filter(|p| p.parent_pid.is_none() || p.parent_pid == Some(0))
309                .collect();
310
311            for (i, proc) in display_roots.iter().enumerate() {
312                let is_last = i == display_roots.len() - 1;
313                self.print_tree(proc, &children_map, "", is_last, 0);
314            }
315        }
316
317        Ok(())
318    }
319
320    fn print_tree(
321        &self,
322        proc: &Process,
323        children_map: &HashMap<u32, Vec<&Process>>,
324        prefix: &str,
325        is_last: bool,
326        depth: usize,
327    ) {
328        if depth > self.depth {
329            return;
330        }
331
332        let connector = if is_last { "└── " } else { "├── " };
333
334        if self.compact {
335            println!(
336                "{}{}{}",
337                prefix.bright_black(),
338                connector.bright_black(),
339                proc.pid.to_string().cyan()
340            );
341        } else {
342            let status_indicator = match proc.status {
343                crate::core::ProcessStatus::Running => "●".green(),
344                crate::core::ProcessStatus::Sleeping => "○".blue(),
345                crate::core::ProcessStatus::Stopped => "◐".yellow(),
346                crate::core::ProcessStatus::Zombie => "✗".red(),
347                _ => "?".white(),
348            };
349
350            println!(
351                "{}{}{} {} [{}] {:.1}% {}",
352                prefix.bright_black(),
353                connector.bright_black(),
354                status_indicator,
355                proc.name.white().bold(),
356                proc.pid.to_string().cyan(),
357                proc.cpu_percent,
358                format_memory(proc.memory_mb)
359            );
360        }
361
362        let child_prefix = if is_last {
363            format!("{}    ", prefix)
364        } else {
365            format!("{}│   ", prefix)
366        };
367
368        if let Some(children) = children_map.get(&proc.pid) {
369            let mut sorted_children: Vec<&&Process> = children.iter().collect();
370            sorted_children.sort_by_key(|p| p.pid);
371
372            for (i, child) in sorted_children.iter().enumerate() {
373                let child_is_last = i == sorted_children.len() - 1;
374                self.print_tree(child, children_map, &child_prefix, child_is_last, depth + 1);
375            }
376        }
377    }
378
379    fn build_tree_node(
380        &self,
381        proc: &Process,
382        children_map: &HashMap<u32, Vec<&Process>>,
383        depth: usize,
384    ) -> TreeNode {
385        let children = if depth < self.depth {
386            children_map
387                .get(&proc.pid)
388                .map(|kids| {
389                    kids.iter()
390                        .map(|p| self.build_tree_node(p, children_map, depth + 1))
391                        .collect()
392                })
393                .unwrap_or_default()
394        } else {
395            Vec::new()
396        };
397
398        TreeNode {
399            pid: proc.pid,
400            name: proc.name.clone(),
401            cpu_percent: proc.cpu_percent,
402            memory_mb: proc.memory_mb,
403            status: format!("{:?}", proc.status),
404            children,
405        }
406    }
407
408    /// Show ancestry (path UP to root) for target processes
409    fn show_ancestors(&self, printer: &Printer, pid_map: &HashMap<u32, &Process>) -> Result<()> {
410        use crate::core::{parse_target, resolve_target, TargetType};
411
412        let target = match &self.target {
413            Some(t) => t,
414            None => {
415                printer.warning("--ancestors requires a target (PID, :port, or name)");
416                return Ok(());
417            }
418        };
419
420        // Resolve target to processes
421        let target_processes = match parse_target(target) {
422            TargetType::Port(_) | TargetType::Pid(_) => resolve_target(target)?,
423            TargetType::Name(ref pattern) => {
424                let all_procs: Vec<Process> = pid_map.values().map(|p| (*p).clone()).collect();
425                let ancestor_pids = Self::ancestor_pids(&all_procs);
426                pid_map
427                    .values()
428                    .filter(|p| !ancestor_pids.contains(&p.pid) && matches_by_filter(p, pattern))
429                    .map(|p| (*p).clone())
430                    .collect()
431            }
432        };
433
434        if target_processes.is_empty() {
435            printer.warning(&format!("No process found for '{}'", target));
436            return Ok(());
437        }
438
439        if self.json {
440            let ancestry_output: Vec<AncestryNode> = target_processes
441                .iter()
442                .map(|proc| self.build_ancestry_node(proc, pid_map))
443                .collect();
444            printer.print_json(&AncestryOutput {
445                action: "tree",
446                success: true,
447                ancestry: ancestry_output,
448            });
449        } else {
450            println!("{} Ancestry for '{}':\n", "✓".green().bold(), target.cyan());
451
452            for proc in &target_processes {
453                self.print_ancestry(proc, pid_map);
454                println!();
455            }
456        }
457
458        Ok(())
459    }
460
461    /// Trace and print ancestry from root down to target
462    fn print_ancestry(&self, target: &Process, pid_map: &HashMap<u32, &Process>) {
463        // Build the ancestor chain (from target up to root)
464        let mut chain: Vec<&Process> = Vec::new();
465        let mut current_pid = Some(target.pid);
466
467        while let Some(pid) = current_pid {
468            if let Some(proc) = pid_map.get(&pid) {
469                chain.push(proc);
470                current_pid = proc.parent_pid;
471                // Prevent infinite loops
472                if chain.len() > 100 {
473                    break;
474                }
475            } else {
476                break;
477            }
478        }
479
480        // Reverse to print from root to target
481        chain.reverse();
482
483        // Print the chain
484        for (i, proc) in chain.iter().enumerate() {
485            let is_target = proc.pid == target.pid;
486            let indent = "    ".repeat(i);
487            let connector = if i == 0 { "" } else { "└── " };
488
489            let status_indicator = match proc.status {
490                ProcessStatus::Running => "●".green(),
491                ProcessStatus::Sleeping => "○".blue(),
492                ProcessStatus::Stopped => "◐".yellow(),
493                ProcessStatus::Zombie => "✗".red(),
494                _ => "?".white(),
495            };
496
497            if is_target {
498                // Highlight the target
499                println!(
500                    "{}{}{} {} [{}] {:.1}% {}  {}",
501                    indent.bright_black(),
502                    connector.bright_black(),
503                    status_indicator,
504                    proc.name.cyan().bold(),
505                    proc.pid.to_string().cyan().bold(),
506                    proc.cpu_percent,
507                    format_memory(proc.memory_mb),
508                    "← target".yellow()
509                );
510            } else {
511                println!(
512                    "{}{}{} {} [{}] {:.1}% {}",
513                    indent.bright_black(),
514                    connector.bright_black(),
515                    status_indicator,
516                    proc.name.white(),
517                    proc.pid.to_string().cyan(),
518                    proc.cpu_percent,
519                    format_memory(proc.memory_mb)
520                );
521            }
522        }
523    }
524
525    /// Build ancestry node for JSON output
526    fn build_ancestry_node(
527        &self,
528        target: &Process,
529        pid_map: &HashMap<u32, &Process>,
530    ) -> AncestryNode {
531        let mut chain: Vec<ProcessInfo> = Vec::new();
532        let mut current_pid = Some(target.pid);
533
534        while let Some(pid) = current_pid {
535            if let Some(proc) = pid_map.get(&pid) {
536                chain.push(ProcessInfo {
537                    pid: proc.pid,
538                    name: proc.name.clone(),
539                    cpu_percent: proc.cpu_percent,
540                    memory_mb: proc.memory_mb,
541                    status: format!("{:?}", proc.status),
542                });
543                current_pid = proc.parent_pid;
544                if chain.len() > 100 {
545                    break;
546                }
547            } else {
548                break;
549            }
550        }
551
552        chain.reverse();
553
554        AncestryNode {
555            target_pid: target.pid,
556            target_name: target.name.clone(),
557            depth: chain.len(),
558            chain,
559        }
560    }
561}
562
563#[derive(Serialize)]
564struct AncestryOutput {
565    action: &'static str,
566    success: bool,
567    ancestry: Vec<AncestryNode>,
568}
569
570#[derive(Serialize)]
571struct AncestryNode {
572    target_pid: u32,
573    target_name: String,
574    depth: usize,
575    chain: Vec<ProcessInfo>,
576}
577
578#[derive(Serialize)]
579struct ProcessInfo {
580    pid: u32,
581    name: String,
582    cpu_percent: f32,
583    memory_mb: f64,
584    status: String,
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: String,
601    children: Vec<TreeNode>,
602}