proc_tree/ops.rs
1//! Process tree operations: snapshot, resolve, queries, display.
2//!
3//! All functions are generic over [`TreeStore`] and [`CacheStore`] so they
4//! work with any storage backend.
5
6use crate::traits::{CacheStore, TreeStore};
7use crate::tree::{ProcEvent, ProcessLink};
8use crate::types::PidNode;
9use crate::types::ProcInfo;
10
11/// Snapshot all running processes from `/proc`.
12///
13/// Populates both the tree and cache. Call once at startup before
14/// processing events.
15///
16/// ```no_run
17/// use proc_tree::{DefaultTree, DefaultCache, snapshot, TreeStore};
18///
19/// let tree = DefaultTree::new(65536, 600);
20/// let cache = DefaultCache::new(65536, 600);
21/// snapshot(&tree, &cache);
22///
23/// // PID 1 should always exist on Linux
24/// assert!(tree.get_node(1).is_some());
25/// ```
26pub fn snapshot(tree: &impl TreeStore, cache: &impl CacheStore) {
27 let dir = match std::fs::read_dir("/proc") {
28 Ok(d) => d,
29 Err(e) => {
30 eprintln!("[WARNING] proc-tree: cannot read /proc: {e}");
31 return;
32 }
33 };
34 for entry in dir.flatten() {
35 let name = entry.file_name();
36 let name_str = name.to_string_lossy();
37 let pid: u32 = match name_str.parse() {
38 Ok(p) => p,
39 Err(_) => continue,
40 };
41 if let Some((node, info)) = crate::proc::parse_proc_entry(pid) {
42 tree.insert_node(pid, node);
43 cache.insert_info(pid, info);
44 }
45 }
46}
47
48/// Resolve a PID to its process info.
49///
50/// Checks the cache first, then falls back to reading `/proc` directly.
51///
52/// ```no_run
53/// use proc_tree::{DefaultTree, DefaultCache, snapshot, resolve, TreeStore};
54///
55/// let tree = DefaultTree::new(65536, 600);
56/// let cache = DefaultCache::new(65536, 600);
57/// snapshot(&tree, &cache);
58///
59/// let info = resolve(&cache, 1).unwrap();
60/// assert!(!info.cmd.is_empty());
61/// ```
62pub fn resolve(cache: &impl CacheStore, pid: u32) -> Option<ProcInfo> {
63 // Try cache first
64 if let Some(info) = cache.get_info(pid) {
65 return Some(info);
66 }
67 // Fallback: read /proc directly via parse_proc_entry
68 let (_node, info) = crate::proc::parse_proc_entry(pid)?;
69 // Populate cache for future lookups
70 cache.insert_info(pid, info.clone());
71 Some(info)
72}
73
74/// Handle a batch of process lifecycle events.
75///
76/// ```
77/// use proc_tree::{DefaultTree, DefaultCache, handle_events, ProcEvent, CacheStore, TreeStore};
78///
79/// let tree = DefaultTree::new(100, 0);
80/// let cache = DefaultCache::new(100, 0);
81///
82/// handle_events(&tree, &cache, &[
83/// ProcEvent::Fork { child_pid: 200, parent_pid: 100, timestamp_ns: 0 },
84/// ]);
85///
86/// let node = tree.get_node(200).unwrap();
87/// assert_eq!(node.ppid, 100);
88/// ```
89pub fn handle_events(tree: &impl TreeStore, cache: &impl CacheStore, events: &[ProcEvent]) {
90 for event in events {
91 handle_event(tree, cache, event);
92 }
93}
94
95/// Handle a single process lifecycle event.
96pub fn handle_event(tree: &impl TreeStore, cache: &impl CacheStore, event: &ProcEvent) {
97 match event {
98 ProcEvent::Fork {
99 child_pid,
100 parent_pid,
101 ..
102 } => {
103 tree.insert_node(
104 *child_pid,
105 PidNode {
106 ppid: *parent_pid,
107 cmd: String::new(),
108 },
109 );
110 }
111 ProcEvent::Exec { pid, timestamp_ns } => {
112 let (node, mut info) = crate::proc::parse_proc_entry(*pid).unwrap_or_else(|| {
113 let cmd = "unknown".to_string();
114 (
115 PidNode {
116 ppid: 0,
117 cmd: cmd.clone(),
118 },
119 ProcInfo {
120 cmd,
121 user: "unknown".to_string(),
122 ppid: 0,
123 tgid: 0,
124 start_time_ns: 0,
125 },
126 )
127 });
128 info.start_time_ns = *timestamp_ns;
129 tree.insert_node(*pid, node);
130 cache.insert_info(*pid, info);
131 }
132 ProcEvent::Exit { .. } => {
133 // Keep the node — still valid for historical chain lookups
134 }
135 }
136}
137
138/// Check if `pid` is a descendant of any process whose cmd == `target_cmd`.
139///
140/// ```
141/// use proc_tree::{DefaultTree, is_descendant, TreeStore, PidNode};
142///
143/// let tree = DefaultTree::new(100, 0);
144/// tree.insert_node(1, PidNode { ppid: 0, cmd: "init".into() });
145/// tree.insert_node(100, PidNode { ppid: 1, cmd: "sshd".into() });
146/// tree.insert_node(200, PidNode { ppid: 100, cmd: "bash".into() });
147///
148/// assert!(is_descendant(&tree, 200, "sshd"));
149/// assert!(is_descendant(&tree, 200, "init"));
150/// assert!(!is_descendant(&tree, 200, "nginx"));
151/// assert!(!is_descendant(&tree, 1, "sshd")); // init is not a descendant of sshd
152/// ```
153pub fn is_descendant(tree: &impl TreeStore, pid: u32, target_cmd: &str) -> bool {
154 let mut current = pid;
155 let mut visited = std::collections::HashSet::new();
156 while let Some(node) = tree.get_node(current) {
157 if !visited.insert(current) {
158 break;
159 }
160 if node.cmd == target_cmd {
161 return true;
162 }
163 if node.ppid == 0 || current == node.ppid {
164 break;
165 }
166 current = node.ppid;
167 }
168 false
169}
170
171/// Build a chain of ProcessLink from the process tree.
172pub fn build_chain_links(
173 tree: &impl TreeStore,
174 cache: &impl CacheStore,
175 pid: u32,
176) -> Vec<ProcessLink> {
177 let mut parts = Vec::new();
178 let mut current = pid;
179 let mut visited = std::collections::HashSet::new();
180 loop {
181 if !visited.insert(current) {
182 break;
183 }
184 let (ppid, cmd, user) = if let Some(node) = tree.get_node(current) {
185 let user = cache
186 .get_info(current)
187 .map(|info| info.user)
188 .unwrap_or_else(|| "unknown".to_string());
189 (node.ppid, node.cmd, user)
190 } else if let Some((node, info)) = crate::proc::parse_proc_entry(current) {
191 (node.ppid, node.cmd, info.user)
192 } else {
193 parts.push(ProcessLink {
194 pid: current,
195 cmd: "unknown".to_string(),
196 user: "unknown".to_string(),
197 });
198 break;
199 };
200 parts.push(ProcessLink {
201 pid: current,
202 cmd,
203 user,
204 });
205 if ppid == 0 || current == ppid {
206 break;
207 }
208 current = ppid;
209 }
210 parts
211}
212
213/// Build a chain string from the process tree.
214///
215/// Format: `"102|touch|root;101|sh|root;100|openclaw|root;1|systemd|root"`
216///
217/// ```
218/// use proc_tree::{DefaultTree, DefaultCache, build_chain_string, TreeStore, CacheStore, PidNode, ProcInfo};
219///
220/// let tree = DefaultTree::new(100, 0);
221/// let cache = DefaultCache::new(100, 0);
222///
223/// tree.insert_node(1, PidNode { ppid: 0, cmd: "init".into() });
224/// cache.insert_info(1, ProcInfo { cmd: "init".into(), user: "root".into(), ppid: 0, tgid: 1, start_time_ns: 0 });
225/// tree.insert_node(100, PidNode { ppid: 1, cmd: "sshd".into() });
226/// cache.insert_info(100, ProcInfo { cmd: "sshd".into(), user: "root".into(), ppid: 1, tgid: 100, start_time_ns: 0 });
227/// tree.insert_node(200, PidNode { ppid: 100, cmd: "bash".into() });
228/// cache.insert_info(200, ProcInfo { cmd: "bash".into(), user: "root".into(), ppid: 100, tgid: 200, start_time_ns: 0 });
229///
230/// let chain = build_chain_string(&tree, &cache, 200);
231/// assert_eq!(chain, "200|bash|root;100|sshd|root;1|init|root");
232/// ```
233pub fn build_chain_string(tree: &impl TreeStore, cache: &impl CacheStore, pid: u32) -> String {
234 build_chain_links(tree, cache, pid)
235 .iter()
236 .map(|l| l.to_string())
237 .collect::<Vec<_>>()
238 .join(";")
239}
240
241/// Get direct children of a PID.
242///
243/// ```
244/// use proc_tree::{DefaultTree, children, TreeStore, PidNode};
245///
246/// let tree = DefaultTree::new(100, 0);
247/// tree.insert_node(1, PidNode { ppid: 0, cmd: "init".into() });
248/// tree.insert_node(100, PidNode { ppid: 1, cmd: "a".into() });
249/// tree.insert_node(200, PidNode { ppid: 1, cmd: "b".into() });
250/// tree.insert_node(300, PidNode { ppid: 100, cmd: "c".into() });
251///
252/// let mut kids = children(&tree, 1);
253/// kids.sort();
254/// assert_eq!(kids, vec![100, 200]);
255/// assert_eq!(children(&tree, 100), vec![300]);
256/// assert!(children(&tree, 999).is_empty());
257/// ```
258pub fn children(tree: &impl TreeStore, pid: u32) -> Vec<u32> {
259 tree.all_pids()
260 .into_iter()
261 .filter(|&p| tree.get_node(p).map(|n| n.ppid == pid).unwrap_or(false))
262 .collect()
263}
264
265/// Get all descendants of a PID (BFS traversal).
266///
267/// ```
268/// use proc_tree::{DefaultTree, descendants, TreeStore, PidNode};
269///
270/// let tree = DefaultTree::new(100, 0);
271/// tree.insert_node(1, PidNode { ppid: 0, cmd: "init".into() });
272/// tree.insert_node(100, PidNode { ppid: 1, cmd: "a".into() });
273/// tree.insert_node(200, PidNode { ppid: 100, cmd: "b".into() });
274/// tree.insert_node(300, PidNode { ppid: 200, cmd: "c".into() });
275///
276/// let mut desc = descendants(&tree, 1);
277/// desc.sort();
278/// assert_eq!(desc, vec![100, 200, 300]);
279/// assert_eq!(descendants(&tree, 300), Vec::<u32>::new());
280/// ```
281pub fn descendants(tree: &impl TreeStore, pid: u32) -> Vec<u32> {
282 let mut result = Vec::new();
283 let mut queue = std::collections::VecDeque::new();
284 queue.push_back(pid);
285 while let Some(current) = queue.pop_front() {
286 let kids = children(tree, current);
287 for kid in kids {
288 result.push(kid);
289 queue.push_back(kid);
290 }
291 }
292 result
293}
294
295/// Get siblings of a PID (processes with the same parent).
296///
297/// Excludes the given pid itself.
298///
299/// ```
300/// use proc_tree::{DefaultTree, siblings, TreeStore, PidNode};
301///
302/// let tree = DefaultTree::new(100, 0);
303/// tree.insert_node(1, PidNode { ppid: 0, cmd: "init".into() });
304/// tree.insert_node(100, PidNode { ppid: 1, cmd: "a".into() });
305/// tree.insert_node(200, PidNode { ppid: 1, cmd: "b".into() });
306/// tree.insert_node(300, PidNode { ppid: 1, cmd: "c".into() });
307///
308/// let mut sibs = siblings(&tree, 100);
309/// sibs.sort();
310/// assert_eq!(sibs, vec![200, 300]);
311/// assert!(siblings(&tree, 1).is_empty()); // init has no siblings
312/// ```
313pub fn siblings(tree: &impl TreeStore, pid: u32) -> Vec<u32> {
314 let ppid = match tree.get_node(pid) {
315 Some(node) => node.ppid,
316 None => return Vec::new(),
317 };
318 children(tree, ppid)
319 .into_iter()
320 .filter(|&c| c != pid)
321 .collect()
322}
323
324/// Find all PIDs whose cmd matches the given string.
325///
326/// ```
327/// use proc_tree::{DefaultTree, find_by_cmd, TreeStore, PidNode};
328///
329/// let tree = DefaultTree::new(100, 0);
330/// tree.insert_node(1, PidNode { ppid: 0, cmd: "init".into() });
331/// tree.insert_node(100, PidNode { ppid: 1, cmd: "sshd".into() });
332/// tree.insert_node(200, PidNode { ppid: 1, cmd: "sshd".into() });
333/// tree.insert_node(300, PidNode { ppid: 1, cmd: "bash".into() });
334///
335/// let mut sshds = find_by_cmd(&tree, "sshd");
336/// sshds.sort();
337/// assert_eq!(sshds, vec![100, 200]);
338/// assert_eq!(find_by_cmd(&tree, "nginx"), Vec::<u32>::new());
339/// ```
340pub fn find_by_cmd(tree: &impl TreeStore, target_cmd: &str) -> Vec<u32> {
341 tree.all_pids()
342 .into_iter()
343 .filter(|&pid| {
344 let cmd = tree
345 .get_node(pid)
346 .map(|n| n.cmd)
347 .filter(|c| !c.is_empty())
348 .or_else(|| crate::proc::read_proc_comm(pid));
349 cmd.as_deref() == Some(target_cmd)
350 })
351 .collect()
352}
353
354/// Find all PIDs whose user matches the given string.
355///
356/// ```
357/// use proc_tree::{DefaultTree, DefaultCache, find_by_user, TreeStore, CacheStore, PidNode, ProcInfo};
358///
359/// let tree = DefaultTree::new(100, 0);
360/// let cache = DefaultCache::new(100, 0);
361///
362/// tree.insert_node(1, PidNode { ppid: 0, cmd: "init".into() });
363/// cache.insert_info(1, ProcInfo { cmd: "init".into(), user: "root".into(), ppid: 0, tgid: 1, start_time_ns: 0 });
364/// tree.insert_node(100, PidNode { ppid: 1, cmd: "bash".into() });
365/// cache.insert_info(100, ProcInfo { cmd: "bash".into(), user: "alice".into(), ppid: 1, tgid: 100, start_time_ns: 0 });
366///
367/// assert_eq!(find_by_user(&tree, &cache, "root"), vec![1]);
368/// assert_eq!(find_by_user(&tree, &cache, "alice"), vec![100]);
369/// assert_eq!(find_by_user(&tree, &cache, "nobody"), Vec::<u32>::new());
370/// ```
371pub fn find_by_user(tree: &impl TreeStore, cache: &impl CacheStore, target_user: &str) -> Vec<u32> {
372 tree.all_pids()
373 .into_iter()
374 .filter(|&pid| {
375 let user = cache
376 .get_info(pid)
377 .map(|info| info.user)
378 .or_else(|| crate::proc::parse_proc_entry(pid).map(|(_, info)| info.user));
379 user.as_deref() == Some(target_user)
380 })
381 .collect()
382}
383
384/// Render a pstree-style display starting from the given root PID.
385///
386/// ```
387/// use proc_tree::{DefaultTree, display, TreeStore, PidNode};
388///
389/// let tree = DefaultTree::new(100, 0);
390/// tree.insert_node(1, PidNode { ppid: 0, cmd: "init".into() });
391/// tree.insert_node(100, PidNode { ppid: 1, cmd: "sshd".into() });
392/// tree.insert_node(200, PidNode { ppid: 1, cmd: "cron".into() });
393///
394/// let output = display(&tree, 1);
395/// assert!(output.starts_with("init"));
396/// assert!(output.contains("sshd"));
397/// assert!(output.contains("cron"));
398/// ```
399pub fn display(tree: &impl TreeStore, root_pid: u32) -> String {
400 let cmd = get_cmd(tree, root_pid);
401 let kids = children(tree, root_pid);
402 if kids.is_empty() {
403 return cmd;
404 }
405 // Root node: first child attaches with "─", rest with tree prefixes
406 let mut output = cmd;
407 for (i, &kid) in kids.iter().enumerate() {
408 let is_last = i == kids.len() - 1;
409 let prefix = if is_last { "└─" } else { "├─" };
410 let continuation = if is_last { " " } else { "│ " };
411 let sub = display_subtree(tree, kid);
412 let lines: Vec<&str> = sub.lines().collect();
413 if i == 0 {
414 output.push_str(&format!("─{}", lines[0]));
415 } else {
416 output.push('\n');
417 output.push_str(prefix);
418 output.push_str(lines[0]);
419 }
420 for line in &lines[1..] {
421 output.push('\n');
422 output.push_str(continuation);
423 output.push_str(line);
424 }
425 }
426 output
427}
428
429/// Recursive helper for non-root subtrees.
430fn display_subtree(tree: &impl TreeStore, pid: u32) -> String {
431 let cmd = get_cmd(tree, pid);
432 let kids = children(tree, pid);
433 if kids.is_empty() {
434 return cmd;
435 }
436 let mut output = cmd;
437 for (i, &kid) in kids.iter().enumerate() {
438 let is_last = i == kids.len() - 1;
439 let prefix = if is_last { "└─" } else { "├─" };
440 let continuation = if is_last { " " } else { "│ " };
441 let sub = display_subtree(tree, kid);
442 let lines: Vec<&str> = sub.lines().collect();
443 output.push('\n');
444 output.push_str(prefix);
445 output.push_str(lines[0]);
446 for line in &lines[1..] {
447 output.push('\n');
448 output.push_str(continuation);
449 output.push_str(line);
450 }
451 }
452 output
453}
454
455/// Get command name for a PID, with fallback chain: tree -> /proc -> "unknown"
456fn get_cmd(tree: &impl TreeStore, pid: u32) -> String {
457 tree.get_node(pid)
458 .map(|n| n.cmd)
459 .filter(|c| !c.is_empty())
460 .or_else(|| crate::proc::read_proc_comm(pid))
461 .unwrap_or_else(|| "unknown".to_string())
462}
463
464/// Get the number of entries in the tree.
465///
466/// ```
467/// use proc_tree::{DefaultTree, tree_len, TreeStore, PidNode};
468///
469/// let tree = DefaultTree::new(100, 0);
470/// assert_eq!(tree_len(&tree), 0);
471///
472/// tree.insert_node(1, PidNode { ppid: 0, cmd: "init".into() });
473/// tree.insert_node(2, PidNode { ppid: 1, cmd: "bash".into() });
474/// assert_eq!(tree_len(&tree), 2);
475/// ```
476pub fn tree_len(tree: &impl TreeStore) -> u64 {
477 tree.all_pids().len() as u64
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483 use crate::default_store::DefaultTree;
484
485 #[test]
486 fn display_single_node() {
487 let tree = DefaultTree::new(100, 0);
488 tree.insert_node(
489 1,
490 PidNode {
491 ppid: 0,
492 cmd: "init".into(),
493 },
494 );
495 assert_eq!(display(&tree, 1), "init");
496 }
497
498 #[test]
499 fn display_root_with_children() {
500 let tree = DefaultTree::new(100, 0);
501 tree.insert_node(
502 1,
503 PidNode {
504 ppid: 0,
505 cmd: "init".into(),
506 },
507 );
508 tree.insert_node(
509 100,
510 PidNode {
511 ppid: 1,
512 cmd: "a".into(),
513 },
514 );
515 tree.insert_node(
516 200,
517 PidNode {
518 ppid: 1,
519 cmd: "b".into(),
520 },
521 );
522 let d = display(&tree, 1);
523 assert!(d.starts_with("init"));
524 assert!(d.contains("a"));
525 assert!(d.contains("b"));
526 }
527}