Skip to main content

proc_tree/
ops.rs

1//! Process tree operations: snapshot, resolve, queries, display.
2//!
3//! All functions are generic over [`ProcessStore`] so they work with any storage backend.
4
5use std::fmt::Write;
6
7use crate::traits::ProcessStore;
8use crate::tree::{ExitedProcess, ProcEvent, ProcessLink};
9use crate::types::ProcessInfo;
10
11const UNKNOWN: &str = "unknown";
12
13/// Snapshot all running processes from `/proc`.
14///
15/// Populates the store. Call once at startup before processing events.
16///
17/// # Errors
18///
19/// Returns an error if `/proc` cannot be read.
20///
21/// # Example
22///
23/// ```no_run
24/// use proc_tree::{DefaultStore, snapshot, ProcessStore};
25///
26/// let store = DefaultStore::new(600);
27/// snapshot(&store).expect("failed to read /proc");
28///
29/// // PID 1 should always exist on Linux
30/// assert!(store.get_process(1).is_some());
31/// ```
32pub fn snapshot(store: &impl ProcessStore) -> Result<(), std::io::Error> {
33    let dir = std::fs::read_dir("/proc")?;
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(info) = crate::proc::parse_proc_entry(pid) {
42            store.insert_process(pid, info);
43        }
44    }
45    Ok(())
46}
47
48/// Resolve a PID to its process info.
49///
50/// Checks the store first, then falls back to reading `/proc` directly.
51///
52/// ```no_run
53/// use proc_tree::{DefaultStore, snapshot, resolve, ProcessStore};
54///
55/// let store = DefaultStore::new(600);
56/// snapshot(&store).expect("failed to read /proc");
57///
58/// let info = resolve(&store, 1).unwrap();
59/// assert!(!info.cmd().is_empty());
60/// ```
61pub fn resolve(store: &impl ProcessStore, pid: u32) -> Option<ProcessInfo> {
62    let info = resolve_process_info(store, pid);
63    // Populate store for future lookups if we got info from /proc
64    if let Some(ref info) = info
65        && store.get_process(pid).is_none()
66    {
67        store.insert_process(pid, info.clone());
68    }
69    info
70}
71
72/// Internal helper to resolve process info with fallback chain.
73///
74/// Tries store first, then falls back to reading `/proc` directly.
75/// Returns `None` if the process doesn't exist.
76fn resolve_process_info(store: &impl ProcessStore, pid: u32) -> Option<ProcessInfo> {
77    store
78        .get_process(pid)
79        .or_else(|| crate::proc::parse_proc_entry(pid))
80}
81
82/// Handle a batch of process lifecycle events.
83///
84/// Returns [`ExitedProcess`] handles for Exit events. The process info **stays in the store**
85/// — callers decide when to remove it via [`ExitedProcess::remove`].
86///
87/// This is critical for event-driven systems where file events (fanotify)
88/// may arrive after the proc connector exit event but still need to look
89/// up process info.
90///
91/// # Example
92///
93/// ```
94/// use proc_tree::{DefaultStore, handle_events, ProcEvent, ProcessStore};
95///
96/// let store = DefaultStore::new(0);
97///
98/// // Fork creates a process
99/// let exited = handle_events(&store, &[
100///     ProcEvent::Fork { child_pid: 200, parent_pid: 100, timestamp_ns: 0 },
101/// ]);
102/// assert!(exited.is_empty());
103///
104/// // Exit returns ExitedProcess handle — process info stays in store
105/// let exited = handle_events(&store, &[
106///     ProcEvent::Exit { pid: 200 },
107/// ]);
108/// assert_eq!(exited[0].pid, 200);
109/// assert!(store.get_process(200).is_some());
110///
111/// // Caller explicitly removes when done
112/// for ep in exited {
113///     ep.remove(&store);
114/// }
115/// assert!(store.get_process(200).is_none());
116/// ```
117#[must_use = "call .remove(&store) on each ExitedProcess after processing related events"]
118pub fn handle_events(store: &impl ProcessStore, events: &[ProcEvent]) -> Vec<ExitedProcess> {
119    let mut exited = Vec::new();
120    for event in events {
121        if let Some(ep) = handle_event(store, event) {
122            exited.push(ep);
123        }
124    }
125    exited
126}
127
128/// Handle a single process lifecycle event.
129///
130/// Returns [`ExitedProcess`] for Exit events, `None` for other events.
131/// The process info **stays in the store** — callers decide when to remove it
132/// via [`ExitedProcess::remove`].
133///
134/// # Example
135///
136/// ```
137/// use proc_tree::{DefaultStore, handle_event, ProcEvent, ProcessStore};
138///
139/// let store = DefaultStore::new(0);
140///
141/// // Create a process
142/// handle_event(&store, &ProcEvent::Fork {
143///     child_pid: 100,
144///     parent_pid: 1,
145///     timestamp_ns: 0,
146/// });
147///
148/// // Exit returns ExitedProcess handle — process info stays in store
149/// let exited = handle_event(&store, &ProcEvent::Exit { pid: 100 }).unwrap();
150/// assert_eq!(exited.pid, 100);
151/// assert!(store.get_process(100).is_some());
152///
153/// // Caller explicitly removes when done
154/// exited.remove(&store);
155/// assert!(store.get_process(100).is_none());
156/// ```
157#[must_use = "call .remove(&store) after processing related events"]
158pub fn handle_event(store: &impl ProcessStore, event: &ProcEvent) -> Option<ExitedProcess> {
159    match event {
160        ProcEvent::Fork {
161            child_pid,
162            parent_pid,
163            ..
164        } => {
165            store.insert_process(
166                *child_pid,
167                ProcessInfo::new(String::new(), String::new(), *parent_pid, 0, 0),
168            );
169        }
170        ProcEvent::Exec { pid, .. } => {
171            let info = crate::proc::parse_proc_entry(*pid).unwrap_or_else(|| {
172                ProcessInfo::new(UNKNOWN.to_string(), UNKNOWN.to_string(), 0, 0, 0)
173            });
174            // Keep start_time_ns from /proc, don't overwrite with event timestamp
175            store.insert_process(*pid, info);
176        }
177        ProcEvent::Exit { pid } => {
178            // Orphan children to init (PID 1)
179            store.for_each_child(*pid, &mut |child_pid| {
180                if let Some(mut info) = store.get_process(child_pid) {
181                    info = ProcessInfo::new(
182                        info.cmd().to_string(),
183                        info.user().to_string(),
184                        1,
185                        info.tgid(),
186                        info.start_time_ns(),
187                    );
188                    store.insert_process(child_pid, info);
189                }
190            });
191            return Some(ExitedProcess { pid: *pid });
192        }
193    }
194    None
195}
196
197/// Check if `pid` is a descendant of any process whose cmd == `target_cmd`.
198///
199/// ```
200/// use proc_tree::{DefaultStore, is_descendant, ProcessStore, ProcessInfo};
201///
202/// let store = DefaultStore::new(0);
203/// store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
204/// store.insert_process(100, ProcessInfo::new("sshd".into(), "root".into(), 1, 100, 0));
205/// store.insert_process(200, ProcessInfo::new("bash".into(), "root".into(), 100, 200, 0));
206///
207/// assert!(is_descendant(&store, 200, "sshd"));
208/// assert!(is_descendant(&store, 200, "init"));
209/// assert!(!is_descendant(&store, 200, "nginx"));
210/// assert!(!is_descendant(&store, 1, "sshd")); // init is not a descendant of sshd
211/// ```
212pub fn is_descendant(store: &impl ProcessStore, pid: u32, target_cmd: &str) -> bool {
213    walk_ancestors(store, pid, |info| info.cmd() == target_cmd)
214}
215
216/// Walk up the process tree from `pid`, calling `predicate` on each ancestor.
217///
218/// Returns `true` if the predicate matches any ancestor, `false` otherwise.
219/// Handles cycles by tracking visited PIDs.
220fn walk_ancestors<P>(store: &impl ProcessStore, pid: u32, predicate: P) -> bool
221where
222    P: Fn(&ProcessInfo) -> bool,
223{
224    let mut current = pid;
225    let mut visited = std::collections::HashSet::new();
226    while let Some(info) = store.get_process(current) {
227        if !visited.insert(current) {
228            break;
229        }
230        if predicate(&info) {
231            return true;
232        }
233        if info.ppid() == 0 || current == info.ppid() {
234            break;
235        }
236        current = info.ppid();
237    }
238    false
239}
240
241/// Build a chain of ProcessLink from the process tree.
242pub fn build_chain_links(store: &impl ProcessStore, pid: u32) -> Vec<ProcessLink> {
243    let mut parts = Vec::new();
244    let mut current = pid;
245    let mut visited = std::collections::HashSet::new();
246    loop {
247        if !visited.insert(current) {
248            break;
249        }
250        match resolve_process_info(store, current) {
251            Some(info) => {
252                parts.push(ProcessLink::new(
253                    current,
254                    info.cmd().to_string(),
255                    info.user().to_string(),
256                ));
257                if info.ppid() == 0 || current == info.ppid() {
258                    break;
259                }
260                current = info.ppid();
261            }
262            None => {
263                parts.push(ProcessLink::new(
264                    current,
265                    UNKNOWN.to_string(),
266                    UNKNOWN.to_string(),
267                ));
268                break;
269            }
270        }
271    }
272    parts
273}
274
275/// Build a chain string from the process tree.
276///
277/// Format: `"102|touch|root;101|sh|root;100|openclaw|root;1|systemd|root"`
278///
279/// ```
280/// use proc_tree::{DefaultStore, build_chain_string, ProcessStore, ProcessInfo};
281///
282/// let store = DefaultStore::new(0);
283///
284/// store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
285/// store.insert_process(100, ProcessInfo::new("sshd".into(), "root".into(), 1, 100, 0));
286/// store.insert_process(200, ProcessInfo::new("bash".into(), "root".into(), 100, 200, 0));
287///
288/// let chain = build_chain_string(&store, 200);
289/// assert_eq!(chain, "200|bash|root;100|sshd|root;1|init|root");
290/// ```
291pub fn build_chain_string(store: &impl ProcessStore, pid: u32) -> String {
292    let links = build_chain_links(store, pid);
293    if links.is_empty() {
294        return String::new();
295    }
296    let mut result = String::new();
297    for (i, link) in links.iter().enumerate() {
298        if i > 0 {
299            result.push(';');
300        }
301        // SAFETY: writing to String never fails
302        let _ = write!(result, "{link}");
303    }
304    result
305}
306
307/// Get direct children of a PID.
308///
309/// ```
310/// use proc_tree::{DefaultStore, children, ProcessStore, ProcessInfo};
311///
312/// let store = DefaultStore::new(0);
313/// store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
314/// store.insert_process(100, ProcessInfo::new("a".into(), "root".into(), 1, 100, 0));
315/// store.insert_process(200, ProcessInfo::new("b".into(), "root".into(), 1, 200, 0));
316/// store.insert_process(300, ProcessInfo::new("c".into(), "root".into(), 100, 300, 0));
317///
318/// let mut kids = children(&store, 1);
319/// kids.sort();
320/// assert_eq!(kids, vec![100, 200]);
321/// assert_eq!(children(&store, 100), vec![300]);
322/// assert!(children(&store, 999).is_empty());
323/// ```
324pub fn children(store: &impl ProcessStore, pid: u32) -> Vec<u32> {
325    store.children_of(pid)
326}
327
328/// Get all descendants of a PID (BFS traversal).
329///
330/// ```
331/// use proc_tree::{DefaultStore, descendants, ProcessStore, ProcessInfo};
332///
333/// let store = DefaultStore::new(0);
334/// store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
335/// store.insert_process(100, ProcessInfo::new("a".into(), "root".into(), 1, 100, 0));
336/// store.insert_process(200, ProcessInfo::new("b".into(), "root".into(), 100, 200, 0));
337/// store.insert_process(300, ProcessInfo::new("c".into(), "root".into(), 200, 300, 0));
338///
339/// let mut desc = descendants(&store, 1);
340/// desc.sort();
341/// assert_eq!(desc, vec![100, 200, 300]);
342/// assert_eq!(descendants(&store, 300), Vec::<u32>::new());
343/// ```
344pub fn descendants(store: &impl ProcessStore, pid: u32) -> Vec<u32> {
345    let mut result = Vec::new();
346    let mut queue = std::collections::VecDeque::new();
347    queue.push_back(pid);
348    while let Some(current) = queue.pop_front() {
349        let kids = children(store, current);
350        for kid in kids {
351            result.push(kid);
352            queue.push_back(kid);
353        }
354    }
355    result
356}
357
358/// Get siblings of a PID (processes with the same parent).
359///
360/// Excludes the given pid itself.
361///
362/// ```
363/// use proc_tree::{DefaultStore, siblings, ProcessStore, ProcessInfo};
364///
365/// let store = DefaultStore::new(0);
366/// store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
367/// store.insert_process(100, ProcessInfo::new("a".into(), "root".into(), 1, 100, 0));
368/// store.insert_process(200, ProcessInfo::new("b".into(), "root".into(), 1, 200, 0));
369/// store.insert_process(300, ProcessInfo::new("c".into(), "root".into(), 1, 300, 0));
370///
371/// let mut sibs = siblings(&store, 100);
372/// sibs.sort();
373/// assert_eq!(sibs, vec![200, 300]);
374/// assert!(siblings(&store, 1).is_empty()); // init has no siblings
375/// ```
376pub fn siblings(store: &impl ProcessStore, pid: u32) -> Vec<u32> {
377    let ppid = match store.get_process(pid) {
378        Some(info) => info.ppid(),
379        None => return Vec::new(),
380    };
381    children(store, ppid)
382        .into_iter()
383        .filter(|&c| c != pid)
384        .collect()
385}
386
387/// Find all PIDs whose cmd matches the given string.
388///
389/// ```
390/// use proc_tree::{DefaultStore, find_by_cmd, ProcessStore, ProcessInfo};
391///
392/// let store = DefaultStore::new(0);
393/// store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
394/// store.insert_process(100, ProcessInfo::new("sshd".into(), "root".into(), 1, 100, 0));
395/// store.insert_process(200, ProcessInfo::new("sshd".into(), "root".into(), 1, 200, 0));
396/// store.insert_process(300, ProcessInfo::new("bash".into(), "root".into(), 1, 300, 0));
397///
398/// let mut sshds = find_by_cmd(&store, "sshd");
399/// sshds.sort();
400/// assert_eq!(sshds, vec![100, 200]);
401/// assert_eq!(find_by_cmd(&store, "nginx"), Vec::<u32>::new());
402/// ```
403pub fn find_by_cmd(store: &impl ProcessStore, target_cmd: &str) -> Vec<u32> {
404    find_by(
405        store,
406        |pid| {
407            store
408                .get_process(pid)
409                .map(|info| info.cmd().to_string())
410                .filter(|c| !c.is_empty())
411                .or_else(|| crate::proc::read_proc_comm(pid))
412        },
413        target_cmd,
414    )
415}
416
417/// Find all PIDs whose user matches the given string.
418///
419/// ```
420/// use proc_tree::{DefaultStore, find_by_user, ProcessStore, ProcessInfo};
421///
422/// let store = DefaultStore::new(0);
423///
424/// store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
425/// store.insert_process(100, ProcessInfo::new("bash".into(), "alice".into(), 1, 100, 0));
426///
427/// assert_eq!(find_by_user(&store, "root"), vec![1]);
428/// assert_eq!(find_by_user(&store, "alice"), vec![100]);
429/// assert_eq!(find_by_user(&store, "nobody"), Vec::<u32>::new());
430/// ```
431pub fn find_by_user(store: &impl ProcessStore, target_user: &str) -> Vec<u32> {
432    find_by(
433        store,
434        |pid| {
435            store
436                .get_process(pid)
437                .map(|info| info.user().to_string())
438                .or_else(|| crate::proc::parse_proc_entry(pid).map(|info| info.user().to_string()))
439        },
440        target_user,
441    )
442}
443
444/// Generic find function that filters PIDs by a value extractor.
445///
446/// This is a helper to reduce code duplication between `find_by_cmd` and `find_by_user`.
447fn find_by<F>(store: &impl ProcessStore, extract: F, target: &str) -> Vec<u32>
448where
449    F: Fn(u32) -> Option<String>,
450{
451    store
452        .all_pids()
453        .into_iter()
454        .filter(|&pid| extract(pid).as_deref() == Some(target))
455        .collect()
456}
457
458/// Render a pstree-style display starting from the given root PID.
459///
460/// ```
461/// use proc_tree::{DefaultStore, display, ProcessStore, ProcessInfo};
462///
463/// let store = DefaultStore::new(0);
464/// store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
465/// store.insert_process(100, ProcessInfo::new("sshd".into(), "root".into(), 1, 100, 0));
466/// store.insert_process(200, ProcessInfo::new("cron".into(), "root".into(), 1, 200, 0));
467///
468/// let output = display(&store, 1);
469/// assert!(output.starts_with("init"));
470/// assert!(output.contains("sshd"));
471/// assert!(output.contains("cron"));
472/// ```
473pub fn display(store: &impl ProcessStore, root_pid: u32) -> String {
474    render_tree(store, root_pid, true)
475}
476
477/// Recursive tree renderer.
478///
479/// `is_root` controls the rendering style:
480/// - Root node: first child attaches with "─"
481/// - Non-root nodes: all children use tree prefixes
482fn render_tree(store: &impl ProcessStore, pid: u32, is_root: bool) -> String {
483    let cmd = get_cmd(store, pid);
484    let kids = children(store, pid);
485    if kids.is_empty() {
486        return cmd;
487    }
488    let mut output = cmd;
489    for (i, &kid) in kids.iter().enumerate() {
490        let is_last = i == kids.len() - 1;
491        let prefix = if is_last { "└─" } else { "├─" };
492        let continuation = if is_last { "  " } else { "│ " };
493        let sub = render_tree(store, kid, false);
494        let mut lines = sub.lines();
495        if let Some(first) = lines.next() {
496            if is_root && i == 0 {
497                output.push('─');
498                output.push_str(first);
499            } else {
500                output.push('\n');
501                output.push_str(prefix);
502                output.push_str(first);
503            }
504            for line in lines {
505                output.push('\n');
506                output.push_str(continuation);
507                output.push_str(line);
508            }
509        }
510    }
511    output
512}
513
514/// Get command name for a PID, with fallback chain: store -> /proc -> "unknown"
515fn get_cmd(store: &impl ProcessStore, pid: u32) -> String {
516    resolve_process_info(store, pid)
517        .map(|info| info.cmd().to_string())
518        .filter(|c| !c.is_empty())
519        .unwrap_or_else(|| UNKNOWN.to_string())
520}
521
522/// Get the number of entries in the store.
523///
524/// ```
525/// use proc_tree::{DefaultStore, tree_len, ProcessStore, ProcessInfo};
526///
527/// let store = DefaultStore::new(0);
528/// assert_eq!(tree_len(&store), 0);
529///
530/// store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
531/// store.insert_process(2, ProcessInfo::new("bash".into(), "root".into(), 1, 2, 0));
532/// assert_eq!(tree_len(&store), 2);
533/// ```
534pub fn tree_len(store: &impl ProcessStore) -> usize {
535    store.all_pids().len()
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541    use crate::default_store::DefaultStore;
542
543    #[test]
544    fn display_single_node() {
545        let store = DefaultStore::new(0);
546        store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
547        assert_eq!(display(&store, 1), "init");
548    }
549
550    #[test]
551    fn display_root_with_children() {
552        let store = DefaultStore::new(0);
553        store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
554        store.insert_process(100, ProcessInfo::new("a".into(), "root".into(), 1, 100, 0));
555        store.insert_process(200, ProcessInfo::new("b".into(), "root".into(), 1, 200, 0));
556        let d = display(&store, 1);
557        assert!(d.starts_with("init"));
558        assert!(d.contains("a"));
559        assert!(d.contains("b"));
560    }
561}