1use crate::core::{
13 classify, matches_by_filter, matches_in_filter, parse_target, resolve_in_dir, resolve_target,
14 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 kept: Vec<&Process> = target_processes
265 .into_iter()
266 .filter(|p| in_and_by(p))
267 .collect();
268 if kept.is_empty() {
272 return Err(ProcError::FilteredOut {
273 target: target.clone(),
274 reason: "no matches with specified filters".to_string(),
275 });
276 }
277 kept
278 } else {
279 target_processes
280 };
281
282 let matches_filters = |p: &Process| self.matches_filters(p);
284
285 let has_filters = in_dir_filter.is_some()
287 || self.by_name.is_some()
288 || self.min_cpu.is_some()
289 || self.min_mem.is_some()
290 || self.status.is_some()
291 || self.min_uptime.is_some();
292
293 if self.json {
294 let tree_nodes = if self.target.is_some() {
295 target_processes
296 .iter()
297 .filter(|p| matches_filters(p))
298 .map(|p| self.build_tree_node(p, &children_map, 0))
299 .collect()
300 } else if has_filters {
301 let matches = all_processes
303 .iter()
304 .filter(|p| in_and_by(p) && matches_filters(p))
305 .collect();
306 self.topmost(matches, &pid_map)
307 .into_iter()
308 .map(|p| self.build_tree_node(p, &children_map, 0))
309 .collect()
310 } else {
311 all_processes
313 .iter()
314 .filter(|p| p.parent_pid.is_none() || p.parent_pid == Some(0))
315 .map(|p| self.build_tree_node(p, &children_map, 0))
316 .collect()
317 };
318
319 printer.print_json(&TreeOutput {
320 action: "tree",
321 success: true,
322 tree: tree_nodes,
323 });
324 } else if self.target.is_some() {
325 let filtered: Vec<_> = target_processes
326 .into_iter()
327 .filter(|p| matches_filters(p))
328 .collect();
329 if filtered.is_empty() {
330 let target = self.target.as_ref().unwrap();
334 printer.warning(&format!(
335 "No processes for '{target}' match the specified filters"
336 ));
337 return Ok(());
338 }
339
340 out_println!(
341 "{} Process tree for '{}':\n",
342 "✓".green().bold(),
343 self.target.as_ref().unwrap().cyan()
344 );
345
346 for proc in &filtered {
347 self.print_tree(proc, &children_map, "", true, 0);
348 out_println!();
349 }
350 } else if has_filters {
351 let filtered: Vec<_> = all_processes
352 .iter()
353 .filter(|p| in_and_by(p) && matches_filters(p))
354 .collect();
355 if filtered.is_empty() {
356 printer.warning("No processes match the specified filters");
357 return Ok(());
358 }
359
360 out_println!(
361 "{} {} process{} matching filters:\n",
362 "✓".green().bold(),
363 filtered.len().to_string().cyan().bold(),
364 plural(filtered.len())
365 );
366
367 let roots = self.topmost(filtered, &pid_map);
368 for (i, proc) in roots.iter().enumerate() {
369 let is_last = i == roots.len() - 1;
370 self.print_tree(proc, &children_map, "", is_last, 0);
371 }
372 } else {
373 out_println!("{} Process tree:\n", "✓".green().bold());
374
375 let display_roots: Vec<&Process> = all_processes
377 .iter()
378 .filter(|p| p.parent_pid.is_none() || p.parent_pid == Some(0))
379 .collect();
380
381 for (i, proc) in display_roots.iter().enumerate() {
382 let is_last = i == display_roots.len() - 1;
383 self.print_tree(proc, &children_map, "", is_last, 0);
384 }
385 }
386
387 Ok(())
388 }
389
390 fn matches_filters(&self, p: &Process) -> bool {
392 if let Some(min_cpu) = self.min_cpu {
393 if p.cpu_percent < min_cpu {
394 return false;
395 }
396 }
397 if let Some(min_mem) = self.min_mem {
398 if p.memory_mb < min_mem {
399 return false;
400 }
401 }
402 if let Some(status) = self.status {
403 if !status.matches(p.status) {
404 return false;
405 }
406 }
407 if let Some(min_uptime) = self.min_uptime {
408 if let Some(start_time) = p.start_time {
409 let now = std::time::SystemTime::now()
410 .duration_since(std::time::UNIX_EPOCH)
411 .map(|d| d.as_secs())
412 .unwrap_or(0);
413 if now.saturating_sub(start_time) < min_uptime {
414 return false;
415 }
416 } else {
417 return false;
418 }
419 }
420 true
421 }
422
423 fn print_tree(
424 &self,
425 proc: &Process,
426 children_map: &HashMap<u32, Vec<&Process>>,
427 prefix: &str,
428 is_last: bool,
429 depth: usize,
430 ) {
431 if depth > self.depth {
432 return;
433 }
434
435 let connector = if is_last { "└── " } else { "├── " };
436
437 if self.compact {
438 out_println!(
439 "{}{}{}",
440 prefix.bright_black(),
441 connector.bright_black(),
442 proc.pid.to_string().cyan()
443 );
444 } else {
445 let status_indicator = status_indicator(proc.status);
446
447 out_println!(
448 "{}{}{} {} [{}] {:.1}% {}{}",
449 prefix.bright_black(),
450 connector.bright_black(),
451 status_indicator,
452 proc.name.white().bold(),
453 proc.pid.to_string().cyan(),
454 proc.cpu_percent,
455 format_memory(proc.memory_mb),
456 uptime_label(proc.uptime_secs).bright_black()
457 );
458 }
459
460 let child_prefix = if is_last {
461 format!("{} ", prefix)
462 } else {
463 format!("{}│ ", prefix)
464 };
465
466 if let Some(children) = children_map.get(&proc.pid) {
468 for (i, child) in children.iter().enumerate() {
469 let child_is_last = i == children.len() - 1;
470 self.print_tree(child, children_map, &child_prefix, child_is_last, depth + 1);
471 }
472 }
473 }
474
475 fn build_tree_node(
476 &self,
477 proc: &Process,
478 children_map: &HashMap<u32, Vec<&Process>>,
479 depth: usize,
480 ) -> TreeNode {
481 let children = if depth < self.depth {
482 children_map
483 .get(&proc.pid)
484 .map(|kids| {
485 kids.iter()
486 .map(|p| self.build_tree_node(p, children_map, depth + 1))
487 .collect()
488 })
489 .unwrap_or_default()
490 } else {
491 Vec::new()
492 };
493
494 TreeNode {
495 pid: proc.pid,
496 name: proc.name.clone(),
497 cpu_percent: proc.cpu_percent,
498 memory_mb: proc.memory_mb,
499 status: proc.status,
500 uptime_secs: proc.uptime_secs,
501 children,
502 }
503 }
504
505 fn show_ancestors(
508 &self,
509 printer: &Printer,
510 target: &str,
511 pid_map: &HashMap<u32, &Process>,
512 all_processes: &[Process],
513 in_and_by: &dyn Fn(&Process) -> bool,
514 ) -> Result<()> {
515 use crate::core::{parse_target, resolve_target, TargetType};
516
517 let mode = NameMatch::from_flag(self.cmd);
518
519 let target_processes: Vec<Process> = match parse_target(target) {
521 TargetType::Port(_) | TargetType::Pid(_) => resolve_target(target, mode)?,
522 TargetType::Name(ref pattern) => {
523 let ancestor_pids = Self::ancestor_pids(all_processes);
524 let (named, others) =
525 Self::named(all_processes.iter(), &ancestor_pids, pattern, mode);
526 if named.is_empty() {
527 return Err(ProcError::name_not_found(pattern, others));
528 }
529 named.into_iter().cloned().collect()
530 }
531 };
532 let target_processes: Vec<Process> = target_processes
534 .into_iter()
535 .filter(|p| in_and_by(p))
536 .collect();
537 if target_processes.is_empty() {
538 return Err(ProcError::FilteredOut {
539 target: target.to_string(),
540 reason: "no matches with specified filters".to_string(),
541 });
542 }
543
544 if self.json {
545 let ancestry_output: Vec<AncestryNode> = target_processes
546 .iter()
547 .map(|proc| self.build_ancestry_node(proc, pid_map))
548 .collect();
549 printer.print_json(&AncestryOutput {
550 action: "tree",
551 success: true,
552 ancestry: ancestry_output,
553 });
554 } else {
555 out_println!("{} Ancestry for '{}':\n", "✓".green().bold(), target.cyan());
556
557 for proc in &target_processes {
558 self.print_ancestry(proc, pid_map);
559 out_println!();
560 }
561 }
562
563 Ok(())
564 }
565
566 fn print_ancestry(&self, target: &Process, pid_map: &HashMap<u32, &Process>) {
568 let chain = ancestor_chain(target, pid_map);
570
571 for (i, proc) in chain.iter().enumerate() {
573 let is_target = proc.pid == target.pid;
574 let indent = " ".repeat(i);
575 let connector = if i == 0 { "" } else { "└── " };
576
577 let status_indicator = status_indicator(proc.status);
578
579 if is_target {
580 out_println!(
582 "{}{}{} {} [{}] {:.1}% {}{} {}",
583 indent.bright_black(),
584 connector.bright_black(),
585 status_indicator,
586 proc.name.cyan().bold(),
587 proc.pid.to_string().cyan().bold(),
588 proc.cpu_percent,
589 format_memory(proc.memory_mb),
590 uptime_label(proc.uptime_secs).bright_black(),
591 "← target".yellow()
592 );
593 } else {
594 out_println!(
595 "{}{}{} {} [{}] {:.1}% {}{}",
596 indent.bright_black(),
597 connector.bright_black(),
598 status_indicator,
599 proc.name.white(),
600 proc.pid.to_string().cyan(),
601 proc.cpu_percent,
602 format_memory(proc.memory_mb),
603 uptime_label(proc.uptime_secs).bright_black()
604 );
605 }
606 }
607 }
608
609 fn build_ancestry_node(
611 &self,
612 target: &Process,
613 pid_map: &HashMap<u32, &Process>,
614 ) -> AncestryNode {
615 let chain: Vec<ProcessInfo> = ancestor_chain(target, pid_map)
616 .into_iter()
617 .map(|proc| ProcessInfo {
618 pid: proc.pid,
619 name: proc.name.clone(),
620 cpu_percent: proc.cpu_percent,
621 memory_mb: proc.memory_mb,
622 status: proc.status,
623 uptime_secs: proc.uptime_secs,
624 })
625 .collect();
626
627 AncestryNode {
628 target_pid: target.pid,
629 target_name: target.name.clone(),
630 depth: chain.len(),
631 chain,
632 }
633 }
634}
635
636fn status_indicator(status: ProcessStatus) -> ColoredString {
638 match status {
639 ProcessStatus::Running => "●".green(),
640 ProcessStatus::Sleeping => "○".blue(),
641 ProcessStatus::Stopped => "◐".yellow(),
642 ProcessStatus::Zombie => "✗".red(),
643 _ => "?".white(),
644 }
645}
646
647fn ancestor_chain<'a>(
652 target: &'a Process,
653 pid_map: &HashMap<u32, &'a Process>,
654) -> Vec<&'a Process> {
655 let mut chain: Vec<&Process> = Vec::new();
656 let mut current_pid = Some(target.pid);
657
658 while let Some(pid) = current_pid {
659 let Some(proc) = pid_map.get(&pid) else { break };
660 chain.push(proc);
661 current_pid = proc.parent_pid;
662 if chain.len() > 100 {
663 break;
664 }
665 }
666
667 chain.reverse();
668 chain
669}
670
671#[derive(Serialize)]
672struct AncestryOutput {
673 action: &'static str,
674 success: bool,
675 ancestry: Vec<AncestryNode>,
676}
677
678#[derive(Serialize)]
679struct AncestryNode {
680 target_pid: u32,
681 target_name: String,
682 depth: usize,
683 chain: Vec<ProcessInfo>,
684}
685
686#[derive(Serialize)]
687struct ProcessInfo {
688 pid: u32,
689 name: String,
690 cpu_percent: f32,
691 memory_mb: f64,
692 status: ProcessStatus,
693 #[serde(skip_serializing_if = "Option::is_none")]
694 uptime_secs: Option<u64>,
695}
696
697#[derive(Serialize)]
698struct TreeOutput {
699 action: &'static str,
700 success: bool,
701 tree: Vec<TreeNode>,
702}
703
704#[derive(Serialize)]
705struct TreeNode {
706 pid: u32,
707 name: String,
708 cpu_percent: f32,
709 memory_mb: f64,
710 status: ProcessStatus,
711 #[serde(skip_serializing_if = "Option::is_none")]
712 uptime_secs: Option<u64>,
713 children: Vec<TreeNode>,
714}
715
716#[cfg(test)]
717mod tests {
718 use super::*;
719
720 fn command() -> TreeCommand {
721 TreeCommand {
722 target: None,
723 ancestors: false,
724 verbose: false,
725 json: false,
726 depth: 10,
727 compact: false,
728 min_cpu: None,
729 min_mem: None,
730 status: None,
731 min_uptime: None,
732 in_dir: None,
733 by_name: None,
734 cmd: false,
735 }
736 }
737
738 fn process(pid: u32, parent_pid: Option<u32>) -> Process {
740 Process {
741 pid,
742 name: format!("p{pid}"),
743 exe_path: None,
744 cwd: None,
745 command: None,
746 argv0: None,
747 raw: None,
748 cpu_percent: 0.0,
749 memory_mb: 0.0,
750 status: ProcessStatus::Sleeping,
751 user: None,
752 parent_pid,
753 start_time: None,
754 uptime_secs: None,
755 }
756 }
757
758 #[test]
759 fn a_name_matches_whole_and_counts_the_processes_only_cmd_would_add() {
760 let mut node = process(1, None);
761 node.name = "node".to_string();
762 let mut editor = process(2, None);
763 editor.name = "vim".to_string();
764 editor.command = Some("vim node-notes.md".to_string());
765 let mut shell = process(3, None);
766 shell.name = "node".to_string();
767 let all = [node, editor, shell];
768 let skipped: std::collections::HashSet<u32> = [3].into();
769
770 let (named, others) = TreeCommand::named(all.iter(), &skipped, "node", NameMatch::Exact);
771 assert_eq!(named.iter().map(|p| p.pid).collect::<Vec<_>>(), vec![1]);
772 assert_eq!(others, 1);
773
774 let (named, others) = TreeCommand::named(all.iter(), &skipped, "node", NameMatch::Command);
775 assert_eq!(named.iter().map(|p| p.pid).collect::<Vec<_>>(), vec![1, 2]);
776 assert_eq!(others, 0);
777
778 let (named, others) = TreeCommand::named(all.iter(), &skipped, "python", NameMatch::Exact);
779 assert!(named.is_empty());
780 assert_eq!(others, 0);
781 }
782
783 #[test]
784 fn a_process_with_no_start_time_fails_min_uptime() {
785 let cmd = TreeCommand {
786 min_uptime: Some(0),
787 ..command()
788 };
789 assert!(!cmd.matches_filters(&process(1, None)));
790 assert!(command().matches_filters(&process(1, None)));
791 }
792
793 #[test]
794 fn every_status_has_its_own_marker() {
795 let color = |status| status_indicator(status).fgcolor;
796 let text = |status| status_indicator(status).trim().to_string();
797 assert_eq!(text(ProcessStatus::Running), "●");
798 assert_eq!(text(ProcessStatus::Sleeping), "○");
799 assert_eq!(text(ProcessStatus::Stopped), "◐");
800 assert_eq!(text(ProcessStatus::Zombie), "✗");
801 assert_eq!(text(ProcessStatus::Dead), "?");
802 assert_eq!(text(ProcessStatus::Unknown), "?");
803 assert_eq!(color(ProcessStatus::Running), Some(Color::Green));
804 assert_eq!(color(ProcessStatus::Sleeping), Some(Color::Blue));
805 assert_eq!(color(ProcessStatus::Stopped), Some(Color::Yellow));
806 assert_eq!(color(ProcessStatus::Zombie), Some(Color::Red));
807 assert_eq!(color(ProcessStatus::Unknown), Some(Color::White));
808 }
809
810 #[test]
811 fn the_chain_runs_from_the_root_down_to_the_target() {
812 let (root, middle, leaf) = (
813 process(1, Some(0)),
814 process(2, Some(1)),
815 process(3, Some(2)),
816 );
817 let map: HashMap<u32, &Process> = [&root, &middle, &leaf]
818 .iter()
819 .map(|p| (p.pid, *p))
820 .collect();
821 let chain: Vec<u32> = ancestor_chain(&leaf, &map).iter().map(|p| p.pid).collect();
822 assert_eq!(chain, vec![1, 2, 3]);
823 }
824
825 #[test]
826 fn a_loop_in_the_parent_links_is_cut() {
827 let (a, b) = (process(1, Some(2)), process(2, Some(1)));
828 let map: HashMap<u32, &Process> = [&a, &b].iter().map(|p| (p.pid, *p)).collect();
829 assert_eq!(ancestor_chain(&a, &map).len(), 101);
830 }
831
832 #[test]
833 fn a_process_that_passes_every_filter_is_kept() {
834 let mut p = process(1, None);
835 p.start_time = Some(0);
836 let cmd = TreeCommand {
837 min_cpu: Some(0.0),
838 min_mem: Some(0.0),
839 status: Some(StatusFilter::Sleeping),
840 min_uptime: Some(1),
841 ..command()
842 };
843 assert!(cmd.matches_filters(&p));
844 let other_status = TreeCommand {
845 status: Some(StatusFilter::Running),
846 ..command()
847 };
848 assert!(!other_status.matches_filters(&p));
849 }
850}