1use crate::core::{
13 classify, matches_by_filter, matches_in_filter, parse_target, resolve_in_dir, resolve_target,
14 ruled_out_by, Guards, Hit, NameMatch, Process, ProcessStatus, StatusFilter, TargetType,
15};
16use crate::error::{ProcError, Result};
17use crate::out_println;
18use crate::ui::{format_memory, plural, uptime_label, Printer};
19use clap::Args;
20use colored::*;
21use serde::Serialize;
22use std::collections::HashMap;
23
24#[derive(Args, Debug)]
26pub struct TreeCommand {
27 #[arg(value_parser = crate::core::validate_target_arg)]
29 target: Option<String>,
30
31 #[arg(long, short)]
33 ancestors: bool,
34
35 #[arg(long, short = 'v')]
37 verbose: bool,
38
39 #[arg(long, short = 'j')]
41 json: bool,
42
43 #[arg(long, short, default_value = "10")]
45 depth: usize,
46
47 #[arg(long, short = 'C')]
49 compact: bool,
50
51 #[arg(long)]
53 min_cpu: Option<f32>,
54
55 #[arg(long)]
57 min_mem: Option<f64>,
58
59 #[arg(long, value_enum, ignore_case = true)]
61 status: Option<StatusFilter>,
62
63 #[arg(long)]
65 min_uptime: Option<u64>,
66
67 #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
70 pub in_dir: Option<String>,
71
72 #[arg(long = "by", short = 'b')]
74 pub by_name: Option<String>,
75
76 #[arg(long)]
78 pub cmd: bool,
79}
80
81impl TreeCommand {
82 fn topmost<'a>(
89 &self,
90 matches: Vec<&'a Process>,
91 pid_map: &HashMap<u32, &Process>,
92 ) -> Vec<&'a Process> {
93 let above: Vec<(&Process, Vec<u32>)> = matches
95 .into_iter()
96 .map(|p| {
97 let up = std::iter::successors(p.parent_pid, |ppid| {
98 pid_map.get(ppid).and_then(|q| q.parent_pid)
99 })
100 .take(pid_map.len())
101 .collect();
102 (p, up)
103 })
104 .collect();
105 let mut top_down: Vec<&(&Process, Vec<u32>)> = above.iter().collect();
106 top_down.sort_by_key(|(_, up)| up.len());
107 let mut roots = std::collections::HashSet::new();
108 for (p, up) in top_down {
109 if !up.iter().take(self.depth).any(|pid| roots.contains(pid)) {
110 roots.insert(p.pid);
111 }
112 }
113 above
114 .into_iter()
115 .filter(|(p, _)| roots.contains(&p.pid))
116 .map(|(p, _)| p)
117 .collect()
118 }
119
120 fn ancestor_pids(all_processes: &[Process]) -> std::collections::HashSet<u32> {
126 let mut pids = std::collections::HashSet::new();
127 let self_pid = std::process::id();
128 pids.insert(self_pid);
129 let mut next = Process::find_by_pid(self_pid)
130 .ok()
131 .flatten()
132 .and_then(|me| me.parent_pid);
133 for _ in 0..10 {
134 let Some(parent_pid) = next else { break };
135 pids.insert(parent_pid);
136 next = all_processes
137 .iter()
138 .find(|p| p.pid == parent_pid)
139 .and_then(|p| p.parent_pid);
140 }
141 pids
142 }
143
144 fn named<'a>(
148 all: impl Iterator<Item = &'a Process>,
149 ancestors: &std::collections::HashSet<u32>,
150 pattern: &str,
151 mode: NameMatch,
152 ) -> (Vec<&'a Process>, usize) {
153 let mut others = 0;
154 let named = all
155 .filter(|p| !ancestors.contains(&p.pid))
156 .filter(|p| match classify(pattern, &p.names(), mode) {
157 Hit::Match => true,
158 Hit::CommandLineOnly => {
159 others += 1;
160 false
161 }
162 Hit::No => false,
163 })
164 .collect();
165 (named, others)
166 }
167
168 pub fn execute(&self) -> Result<()> {
170 let printer = Printer::from_flags(self.json, self.verbose);
171
172 let ancestry_of = match (self.ancestors, &self.target) {
175 (true, None) => {
176 return Err(ProcError::InvalidInput(
177 "--ancestors needs a target (PID, :port, or name)".to_string(),
178 ))
179 }
180 (true, Some(target)) => Some(target),
181 (false, _) => None,
182 };
183
184 let mut all_processes = Process::find_all()?;
186 Process::measure_cpu(&mut all_processes);
187
188 all_processes.sort_by_key(|p| p.pid);
193
194 let pid_map: HashMap<u32, &Process> = all_processes.iter().map(|p| (p.pid, p)).collect();
196
197 let mut children_map: HashMap<u32, Vec<&Process>> = HashMap::new();
199
200 for proc in &all_processes {
201 if let Some(ppid) = proc.parent_pid {
202 children_map.entry(ppid).or_default().push(proc);
203 }
204 }
205
206 let mode = NameMatch::from_flag(self.cmd);
210 let in_dir_filter = resolve_in_dir(&self.in_dir)?;
211 let in_dir_has_any = in_dir_filter
212 .as_ref()
213 .is_some_and(|dir_path| all_processes.iter().any(|p| matches_in_filter(p, dir_path)));
214 crate::core::require_in_dir(&self.in_dir, &in_dir_filter, in_dir_has_any)?;
215 let in_and_by = |p: &Process| {
216 in_dir_filter
217 .as_ref()
218 .is_none_or(|dir_path| matches_in_filter(p, dir_path))
219 && self
220 .by_name
221 .as_ref()
222 .is_none_or(|name| matches_by_filter(p, name, mode))
223 };
224
225 if let Some(target) = ancestry_of {
227 return self.show_ancestors(&printer, target, &pid_map, &all_processes, &in_and_by);
228 }
229
230 let target_processes: Vec<&Process> = if let Some(ref target) = self.target {
232 match parse_target(target) {
234 TargetType::Port(_) | TargetType::Pid(_) => {
235 let resolved = resolve_target(target, mode)?;
238 let pids: Vec<u32> = resolved.iter().map(|p| p.pid).collect();
240 all_processes
241 .iter()
242 .filter(|p| pids.contains(&p.pid))
243 .collect()
244 }
245 TargetType::Name(ref pattern) => {
246 let ancestor_pids = Self::ancestor_pids(&all_processes);
249 let (named, others) =
250 Self::named(all_processes.iter(), &ancestor_pids, pattern, mode);
251 if named.is_empty() {
254 return Err(ProcError::name_not_found(pattern, others));
255 }
256 named
257 }
258 }
259 } else {
260 Vec::new() };
262
263 let target_processes: Vec<&Process> = if let Some(target) = &self.target {
264 let found: Vec<Process> = target_processes.iter().map(|p| (*p).clone()).collect();
265 let kept: Vec<&Process> = target_processes
266 .into_iter()
267 .filter(|p| in_and_by(p))
268 .collect();
269 if kept.is_empty() {
273 let guards = Guards::new(&self.in_dir, &self.by_name, None);
274 return Err(ProcError::FilteredOut {
275 target: target.clone(),
276 ruled_out_by: ruled_out_by(&found, &guards, mode)?,
277 });
278 }
279 kept
280 } else {
281 target_processes
282 };
283
284 let matches_filters = |p: &Process| self.matches_filters(p);
286
287 let has_filters = in_dir_filter.is_some()
289 || self.by_name.is_some()
290 || self.min_cpu.is_some()
291 || self.min_mem.is_some()
292 || self.status.is_some()
293 || self.min_uptime.is_some();
294
295 if self.json {
296 let tree_nodes = if self.target.is_some() {
297 target_processes
298 .iter()
299 .filter(|p| matches_filters(p))
300 .map(|p| self.build_tree_node(p, &children_map, 0))
301 .collect()
302 } else if has_filters {
303 let matches = all_processes
305 .iter()
306 .filter(|p| in_and_by(p) && matches_filters(p))
307 .collect();
308 self.topmost(matches, &pid_map)
309 .into_iter()
310 .map(|p| self.build_tree_node(p, &children_map, 0))
311 .collect()
312 } else {
313 all_processes
315 .iter()
316 .filter(|p| p.parent_pid.is_none() || p.parent_pid == Some(0))
317 .map(|p| self.build_tree_node(p, &children_map, 0))
318 .collect()
319 };
320
321 printer.print_json(&TreeOutput {
322 action: "tree",
323 success: true,
324 tree: tree_nodes,
325 });
326 } else if self.target.is_some() {
327 let filtered: Vec<_> = target_processes
328 .into_iter()
329 .filter(|p| matches_filters(p))
330 .collect();
331 if filtered.is_empty() {
332 let target = self.target.as_ref().unwrap();
336 printer.warning(&format!(
337 "No processes for '{target}' match the specified filters"
338 ));
339 return Ok(());
340 }
341
342 out_println!(
343 "{} Process tree for '{}':\n",
344 "✓".green().bold(),
345 self.target.as_ref().unwrap().cyan()
346 );
347
348 for proc in &filtered {
349 self.print_tree(proc, &children_map, "", true, 0);
350 out_println!();
351 }
352 } else if has_filters {
353 let filtered: Vec<_> = all_processes
354 .iter()
355 .filter(|p| in_and_by(p) && matches_filters(p))
356 .collect();
357 if filtered.is_empty() {
358 printer.warning("No processes match the specified filters");
359 return Ok(());
360 }
361
362 out_println!(
363 "{} {} process{} matching filters:\n",
364 "✓".green().bold(),
365 filtered.len().to_string().cyan().bold(),
366 plural(filtered.len())
367 );
368
369 let roots = self.topmost(filtered, &pid_map);
370 for (i, proc) in roots.iter().enumerate() {
371 let is_last = i == roots.len() - 1;
372 self.print_tree(proc, &children_map, "", is_last, 0);
373 }
374 } else {
375 out_println!("{} Process tree:\n", "✓".green().bold());
376
377 let display_roots: Vec<&Process> = all_processes
379 .iter()
380 .filter(|p| p.parent_pid.is_none() || p.parent_pid == Some(0))
381 .collect();
382
383 for (i, proc) in display_roots.iter().enumerate() {
384 let is_last = i == display_roots.len() - 1;
385 self.print_tree(proc, &children_map, "", is_last, 0);
386 }
387 }
388
389 Ok(())
390 }
391
392 fn matches_filters(&self, p: &Process) -> bool {
394 if let Some(min_cpu) = self.min_cpu {
395 if p.cpu_percent < min_cpu {
396 return false;
397 }
398 }
399 if let Some(min_mem) = self.min_mem {
400 if p.memory_mb < min_mem {
401 return false;
402 }
403 }
404 if let Some(status) = self.status {
405 if !status.matches(p.status) {
406 return false;
407 }
408 }
409 if let Some(min_uptime) = self.min_uptime {
410 if let Some(start_time) = p.start_time {
411 let now = std::time::SystemTime::now()
412 .duration_since(std::time::UNIX_EPOCH)
413 .map(|d| d.as_secs())
414 .unwrap_or(0);
415 if now.saturating_sub(start_time) < min_uptime {
416 return false;
417 }
418 } else {
419 return false;
420 }
421 }
422 true
423 }
424
425 fn print_tree(
426 &self,
427 proc: &Process,
428 children_map: &HashMap<u32, Vec<&Process>>,
429 prefix: &str,
430 is_last: bool,
431 depth: usize,
432 ) {
433 if depth > self.depth {
434 return;
435 }
436
437 let connector = if is_last { "└── " } else { "├── " };
438
439 if self.compact {
440 out_println!(
441 "{}{}{}",
442 prefix.bright_black(),
443 connector.bright_black(),
444 proc.pid.to_string().cyan()
445 );
446 } else {
447 let status_indicator = status_indicator(proc.status);
448
449 out_println!(
450 "{}{}{} {} [{}] {:.1}% {}{}",
451 prefix.bright_black(),
452 connector.bright_black(),
453 status_indicator,
454 proc.name.white().bold(),
455 proc.pid.to_string().cyan(),
456 proc.cpu_percent,
457 format_memory(proc.memory_mb),
458 uptime_label(proc.uptime_secs).bright_black()
459 );
460 }
461
462 let child_prefix = if is_last {
463 format!("{} ", prefix)
464 } else {
465 format!("{}│ ", prefix)
466 };
467
468 if let Some(children) = children_map.get(&proc.pid) {
470 for (i, child) in children.iter().enumerate() {
471 let child_is_last = i == children.len() - 1;
472 self.print_tree(child, children_map, &child_prefix, child_is_last, depth + 1);
473 }
474 }
475 }
476
477 fn build_tree_node(
478 &self,
479 proc: &Process,
480 children_map: &HashMap<u32, Vec<&Process>>,
481 depth: usize,
482 ) -> TreeNode {
483 let children = if depth < self.depth {
484 children_map
485 .get(&proc.pid)
486 .map(|kids| {
487 kids.iter()
488 .map(|p| self.build_tree_node(p, children_map, depth + 1))
489 .collect()
490 })
491 .unwrap_or_default()
492 } else {
493 Vec::new()
494 };
495
496 TreeNode {
497 pid: proc.pid,
498 name: proc.name.clone(),
499 cpu_percent: proc.cpu_percent,
500 memory_mb: proc.memory_mb,
501 status: proc.status,
502 uptime_secs: proc.uptime_secs,
503 children,
504 }
505 }
506
507 fn show_ancestors(
510 &self,
511 printer: &Printer,
512 target: &str,
513 pid_map: &HashMap<u32, &Process>,
514 all_processes: &[Process],
515 in_and_by: &dyn Fn(&Process) -> bool,
516 ) -> Result<()> {
517 use crate::core::{parse_target, resolve_target, TargetType};
518
519 let mode = NameMatch::from_flag(self.cmd);
520
521 let target_processes: Vec<Process> = match parse_target(target) {
523 TargetType::Port(_) | TargetType::Pid(_) => resolve_target(target, mode)?,
524 TargetType::Name(ref pattern) => {
525 let ancestor_pids = Self::ancestor_pids(all_processes);
526 let (named, others) =
527 Self::named(all_processes.iter(), &ancestor_pids, pattern, mode);
528 if named.is_empty() {
529 return Err(ProcError::name_not_found(pattern, others));
530 }
531 named.into_iter().cloned().collect()
532 }
533 };
534 let found = target_processes.clone();
536 let target_processes: Vec<Process> = target_processes
537 .into_iter()
538 .filter(|p| in_and_by(p))
539 .collect();
540 if target_processes.is_empty() {
541 let guards = Guards::new(&self.in_dir, &self.by_name, None);
542 return Err(ProcError::FilteredOut {
543 target: target.to_string(),
544 ruled_out_by: ruled_out_by(&found, &guards, mode)?,
545 });
546 }
547
548 if self.json {
549 let ancestry_output: Vec<AncestryNode> = target_processes
550 .iter()
551 .map(|proc| self.build_ancestry_node(proc, pid_map))
552 .collect();
553 printer.print_json(&AncestryOutput {
554 action: "tree",
555 success: true,
556 ancestry: ancestry_output,
557 });
558 } else {
559 out_println!("{} Ancestry for '{}':\n", "✓".green().bold(), target.cyan());
560
561 for proc in &target_processes {
562 self.print_ancestry(proc, pid_map);
563 out_println!();
564 }
565 }
566
567 Ok(())
568 }
569
570 fn print_ancestry(&self, target: &Process, pid_map: &HashMap<u32, &Process>) {
572 let chain = ancestor_chain(target, pid_map);
574
575 for (i, proc) in chain.iter().enumerate() {
577 let is_target = proc.pid == target.pid;
578 let indent = " ".repeat(i);
579 let connector = if i == 0 { "" } else { "└── " };
580
581 let status_indicator = status_indicator(proc.status);
582
583 if is_target {
584 out_println!(
586 "{}{}{} {} [{}] {:.1}% {}{} {}",
587 indent.bright_black(),
588 connector.bright_black(),
589 status_indicator,
590 proc.name.cyan().bold(),
591 proc.pid.to_string().cyan().bold(),
592 proc.cpu_percent,
593 format_memory(proc.memory_mb),
594 uptime_label(proc.uptime_secs).bright_black(),
595 "← target".yellow()
596 );
597 } else {
598 out_println!(
599 "{}{}{} {} [{}] {:.1}% {}{}",
600 indent.bright_black(),
601 connector.bright_black(),
602 status_indicator,
603 proc.name.white(),
604 proc.pid.to_string().cyan(),
605 proc.cpu_percent,
606 format_memory(proc.memory_mb),
607 uptime_label(proc.uptime_secs).bright_black()
608 );
609 }
610 }
611 }
612
613 fn build_ancestry_node(
615 &self,
616 target: &Process,
617 pid_map: &HashMap<u32, &Process>,
618 ) -> AncestryNode {
619 let chain: Vec<ProcessInfo> = ancestor_chain(target, pid_map)
620 .into_iter()
621 .map(|proc| ProcessInfo {
622 pid: proc.pid,
623 name: proc.name.clone(),
624 cpu_percent: proc.cpu_percent,
625 memory_mb: proc.memory_mb,
626 status: proc.status,
627 uptime_secs: proc.uptime_secs,
628 })
629 .collect();
630
631 AncestryNode {
632 target_pid: target.pid,
633 target_name: target.name.clone(),
634 depth: chain.len(),
635 chain,
636 }
637 }
638}
639
640fn status_indicator(status: ProcessStatus) -> ColoredString {
642 match status {
643 ProcessStatus::Running => "●".green(),
644 ProcessStatus::Sleeping => "○".blue(),
645 ProcessStatus::Stopped => "◐".yellow(),
646 ProcessStatus::Zombie => "✗".red(),
647 _ => "?".white(),
648 }
649}
650
651fn ancestor_chain<'a>(
656 target: &'a Process,
657 pid_map: &HashMap<u32, &'a Process>,
658) -> Vec<&'a Process> {
659 let mut chain: Vec<&Process> = Vec::new();
660 let mut current_pid = Some(target.pid);
661
662 while let Some(pid) = current_pid {
663 let Some(proc) = pid_map.get(&pid) else { break };
664 chain.push(proc);
665 current_pid = proc.parent_pid;
666 if chain.len() > 100 {
667 break;
668 }
669 }
670
671 chain.reverse();
672 chain
673}
674
675#[derive(Serialize)]
676struct AncestryOutput {
677 action: &'static str,
678 success: bool,
679 ancestry: Vec<AncestryNode>,
680}
681
682#[derive(Serialize)]
683struct AncestryNode {
684 target_pid: u32,
685 target_name: String,
686 depth: usize,
687 chain: Vec<ProcessInfo>,
688}
689
690#[derive(Serialize)]
691struct ProcessInfo {
692 pid: u32,
693 name: String,
694 cpu_percent: f32,
695 memory_mb: f64,
696 status: ProcessStatus,
697 #[serde(skip_serializing_if = "Option::is_none")]
698 uptime_secs: Option<u64>,
699}
700
701#[derive(Serialize)]
702struct TreeOutput {
703 action: &'static str,
704 success: bool,
705 tree: Vec<TreeNode>,
706}
707
708#[derive(Serialize)]
709struct TreeNode {
710 pid: u32,
711 name: String,
712 cpu_percent: f32,
713 memory_mb: f64,
714 status: ProcessStatus,
715 #[serde(skip_serializing_if = "Option::is_none")]
716 uptime_secs: Option<u64>,
717 children: Vec<TreeNode>,
718}
719
720#[cfg(test)]
721mod tests {
722 use super::*;
723
724 fn command() -> TreeCommand {
725 TreeCommand {
726 target: None,
727 ancestors: false,
728 verbose: false,
729 json: false,
730 depth: 10,
731 compact: false,
732 min_cpu: None,
733 min_mem: None,
734 status: None,
735 min_uptime: None,
736 in_dir: None,
737 by_name: None,
738 cmd: false,
739 }
740 }
741
742 fn process(pid: u32, parent_pid: Option<u32>) -> Process {
744 Process {
745 pid,
746 name: format!("p{pid}"),
747 exe_path: None,
748 cwd: None,
749 command: None,
750 argv0: None,
751 raw: None,
752 cpu_percent: 0.0,
753 memory_mb: 0.0,
754 status: ProcessStatus::Sleeping,
755 user: None,
756 parent_pid,
757 start_time: None,
758 uptime_secs: None,
759 }
760 }
761
762 #[test]
763 fn a_name_matches_whole_and_counts_the_processes_only_cmd_would_add() {
764 let mut node = process(1, None);
765 node.name = "node".to_string();
766 let mut editor = process(2, None);
767 editor.name = "vim".to_string();
768 editor.command = Some("vim node-notes.md".to_string());
769 let mut shell = process(3, None);
770 shell.name = "node".to_string();
771 let all = [node, editor, shell];
772 let skipped: std::collections::HashSet<u32> = [3].into();
773
774 let (named, others) = TreeCommand::named(all.iter(), &skipped, "node", NameMatch::Exact);
775 assert_eq!(named.iter().map(|p| p.pid).collect::<Vec<_>>(), vec![1]);
776 assert_eq!(others, 1);
777
778 let (named, others) = TreeCommand::named(all.iter(), &skipped, "node", NameMatch::Command);
779 assert_eq!(named.iter().map(|p| p.pid).collect::<Vec<_>>(), vec![1, 2]);
780 assert_eq!(others, 0);
781
782 let (named, others) = TreeCommand::named(all.iter(), &skipped, "python", NameMatch::Exact);
783 assert!(named.is_empty());
784 assert_eq!(others, 0);
785 }
786
787 #[test]
788 fn a_process_with_no_start_time_fails_min_uptime() {
789 let cmd = TreeCommand {
790 min_uptime: Some(0),
791 ..command()
792 };
793 assert!(!cmd.matches_filters(&process(1, None)));
794 assert!(command().matches_filters(&process(1, None)));
795 }
796
797 #[test]
798 fn every_status_has_its_own_marker() {
799 let color = |status| status_indicator(status).fgcolor;
800 let text = |status| status_indicator(status).trim().to_string();
801 assert_eq!(text(ProcessStatus::Running), "●");
802 assert_eq!(text(ProcessStatus::Sleeping), "○");
803 assert_eq!(text(ProcessStatus::Stopped), "◐");
804 assert_eq!(text(ProcessStatus::Zombie), "✗");
805 assert_eq!(text(ProcessStatus::Dead), "?");
806 assert_eq!(text(ProcessStatus::Unknown), "?");
807 assert_eq!(color(ProcessStatus::Running), Some(Color::Green));
808 assert_eq!(color(ProcessStatus::Sleeping), Some(Color::Blue));
809 assert_eq!(color(ProcessStatus::Stopped), Some(Color::Yellow));
810 assert_eq!(color(ProcessStatus::Zombie), Some(Color::Red));
811 assert_eq!(color(ProcessStatus::Unknown), Some(Color::White));
812 }
813
814 #[test]
815 fn the_chain_runs_from_the_root_down_to_the_target() {
816 let (root, middle, leaf) = (
817 process(1, Some(0)),
818 process(2, Some(1)),
819 process(3, Some(2)),
820 );
821 let map: HashMap<u32, &Process> = [&root, &middle, &leaf]
822 .iter()
823 .map(|p| (p.pid, *p))
824 .collect();
825 let chain: Vec<u32> = ancestor_chain(&leaf, &map).iter().map(|p| p.pid).collect();
826 assert_eq!(chain, vec![1, 2, 3]);
827 }
828
829 #[test]
830 fn a_loop_in_the_parent_links_is_cut() {
831 let (a, b) = (process(1, Some(2)), process(2, Some(1)));
832 let map: HashMap<u32, &Process> = [&a, &b].iter().map(|p| (p.pid, *p)).collect();
833 assert_eq!(ancestor_chain(&a, &map).len(), 101);
834 }
835
836 #[test]
837 fn a_process_that_passes_every_filter_is_kept() {
838 let mut p = process(1, None);
839 p.start_time = Some(0);
840 let cmd = TreeCommand {
841 min_cpu: Some(0.0),
842 min_mem: Some(0.0),
843 status: Some(StatusFilter::Sleeping),
844 min_uptime: Some(1),
845 ..command()
846 };
847 assert!(cmd.matches_filters(&p));
848 let other_status = TreeCommand {
849 status: Some(StatusFilter::Running),
850 ..command()
851 };
852 assert!(!other_status.matches_filters(&p));
853 }
854}