proc-tree 0.2.0

Linux process tree: snapshot, incremental maintenance via fork/exec events, ancestry chain queries, PID reuse detection
Documentation

proc-tree

Crates.io Docs.rs License: MIT CI

Linux process tree — snapshot from /proc, incremental maintenance via fork/exec/exit events, ancestry chain queries, PID reuse detection, and pstree-style display.

Installation

cargo add proc-tree

Minimum supported Rust version: 1.85 (edition 2024).


About this crate

A Linux process tree library with a unified storage interface.

Key design decisions:

  • Process tree only contains living processes — Exit removes the node, children are orphaned to init (PID 1)
  • Unified storage — single ProcessStore trait for both tree structure and process info
  • O(1) child lookupschildren_index maintained on insert/remove

Quick Start

One-shot snapshot

use proc_tree::{DefaultStore, snapshot, resolve, display};

let store = DefaultStore::new(600);  // TTL in seconds
snapshot(&store);

// Resolve any PID
let info = resolve(&store, 1).unwrap();
println!("PID 1: {} ({})", info.cmd, info.user);

// Render pstree-style tree
println!("{}", display(&store, 1));

Incremental maintenance

use proc_tree::{DefaultStore, snapshot, handle_events, ProcEvent};

let store = DefaultStore::new(600);
snapshot(&store);

// Events from proc-connector, audit, or any source
let exited = handle_events(&store, &[
    ProcEvent::Fork { child_pid: 5000, parent_pid: 1234, timestamp_ns: 0 },
    ProcEvent::Exec { pid: 5000, timestamp_ns: 1 },
    ProcEvent::Exit { pid: 5000 },  // Children orphaned to init
]);

// Caller explicitly removes when done processing related events
for ep in exited {
    ep.remove(&store);
}

Complete Example

use proc_tree::{
    DefaultStore, snapshot, resolve, handle_events,
    build_chain_string, is_descendant, children, descendants, siblings,
    find_by_cmd, find_by_user, display, ProcEvent, ProcessStore,
};

// Create store (TTL in seconds)
let store = DefaultStore::new(600);

// Seed from /proc
snapshot(&store);

// Resolve a PID
let info = resolve(&store, 1).unwrap();
println!("PID 1: cmd={}, user={}, tgid={}", info.cmd, info.user, info.tgid);

// Build ancestry chain: "200|bash|root;100|sshd|root;1|systemd|root"
let chain = build_chain_string(&store, std::process::id());

// Query relationships (O(1) for children)
let kids = children(&store, 1);          // direct children of PID 1
let all = descendants(&store, 1);        // all descendants (BFS)
let sibs = siblings(&store, std::process::id()); // same-parent processes

// Find by name or user (O(n) - requires full scan)
let sshds = find_by_cmd(&store, "sshd");
let roots = find_by_user(&store, "root");

// Ancestry check
let mine = std::process::id();
if is_descendant(&store, mine, "sshd") {
    println!("running under sshd");
}

// pstree-style display
println!("{}", display(&store, 1));

Types

ProcessInfo

pub struct ProcessInfo {
    pub ppid: u32,            // parent PID
    pub cmd: String,          // command name from /proc/{pid}/status
    pub user: String,         // username (from UID → /etc/passwd lookup)
    pub tgid: u32,            // thread group ID
    pub start_time_ns: u64,   // start time in nanoseconds since boot
}

ProcEvent

pub enum ProcEvent {
    Fork { child_pid: u32, parent_pid: u32, timestamp_ns: u64 },
    Exec { pid: u32, timestamp_ns: u64 },
    Exit { pid: u32 },
}
Variant Behavior
Fork Inserts a new process (child_pid → parent_pid), cmd left empty
Exec Reads /proc/{pid}/status to update cmd, user, ppid, tgid
Exit Returns ExitedProcess handle, orphans children to init (PID 1)

ProcessLink

pub struct ProcessLink {
    pub pid: u32,
    pub cmd: String,
    pub user: String,
}

Displayed as "pid|cmd|user". A chain is a Vec<ProcessLink> ordered from child to ancestor.

ExitedProcess

pub struct ExitedProcess {
    pub pid: u32,
}

Returned by handle_event / handle_events for Exit events. The process info stays in the store until remove() is called, allowing late-arriving events to still look up process info.

let exited = handle_event(&store, &ProcEvent::Exit { pid: 100 }).unwrap();
assert!(store.get_process(100).is_some());  // still accessible
exited.remove(&store);                       // explicitly remove when done
assert!(store.get_process(100).is_none());

Trait (custom backend)

Implement ProcessStore for any storage (Redis, moka, dashmap, ...):

pub trait ProcessStore {
    fn get_process(&self, pid: u32) -> Option<ProcessInfo>;
    fn insert_process(&self, pid: u32, info: ProcessInfo);
    fn remove_process(&self, pid: u32) -> Option<ProcessInfo>;
    fn all_pids(&self) -> Vec<u32>;
    fn children_of(&self, pid: u32) -> Vec<u32>;
}

All functions in ops are generic over this trait — bring your own storage.


Performance

Operation Complexity Notes
children(pid) O(1) Uses children_index
descendants(pid) O(k) k = number of descendants
build_chain_links(pid) O(d) d = depth of process
is_descendant(pid, cmd) O(d) d = depth of process
find_by_cmd(cmd) O(n) n = total processes
find_by_user(user) O(n) n = total processes
snapshot() O(n) n = total processes

PID Reuse Detection

When a process exits and its PID is reused, cached data becomes stale. The start_time_ns field (nanoseconds since boot, from /proc/{pid}/stat) lets implementations detect reuse by comparing cached vs. current values.


Thread Safety

DefaultStore is Arc<Mutex<HashMap>> — clone shares the same underlying data. Safe to pass across threads.


License

MIT License