Skip to main content

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}