Skip to main content

proc_tree/
default_store.rs

1//! Default storage implementation using standard library types.
2//!
3//! [`DefaultStore`] is a `HashMap<Mutex>` store with optional TTL-based eviction.
4//!
5//! # Example
6//!
7//! ```rust
8//! use proc_tree::{DefaultStore, snapshot};
9//!
10//! let store = DefaultStore::new(600);
11//! snapshot(&store).expect("failed to read /proc");
12//! ```
13
14use std::collections::HashMap;
15use std::sync::{Arc, Mutex};
16use std::time::{Duration, Instant};
17
18use crate::traits::ProcessStore;
19use crate::types::ProcessInfo;
20
21// ---- Internal entry with optional TTL ----
22
23struct Entry {
24    value: ProcessInfo,
25    inserted_at: Instant,
26}
27
28impl Clone for Entry {
29    fn clone(&self) -> Self {
30        Self {
31            value: self.value.clone(),
32            inserted_at: self.inserted_at,
33        }
34    }
35}
36
37// ---- Shared inner ----
38
39type Inner = Arc<Mutex<HashMap<u32, Entry>>>;
40
41fn get_inner(
42    inner: &Inner,
43    pid: u32,
44    ttl: Duration,
45    children_index: &Arc<Mutex<HashMap<u32, Vec<u32>>>>,
46) -> Option<ProcessInfo> {
47    let mut map = inner.lock().expect("lock poisoned");
48    let entry = map.get(&pid)?;
49    if !ttl.is_zero() && entry.inserted_at.elapsed() >= ttl {
50        let info = map.remove(&pid).expect("entry should exist").value;
51        // Update children index
52        let mut index = children_index.lock().expect("lock poisoned");
53        if let Some(children) = index.get_mut(&info.ppid()) {
54            children.retain(|&c| c != pid);
55        }
56        // Clean up this process's own children index entry
57        index.remove(&pid);
58        return None;
59    }
60    Some(entry.value.clone())
61}
62
63fn insert_inner(inner: &Inner, pid: u32, value: ProcessInfo) {
64    let mut map = inner.lock().expect("lock poisoned");
65    map.insert(
66        pid,
67        Entry {
68            value,
69            inserted_at: Instant::now(),
70        },
71    );
72}
73
74// ---- DefaultStore ----
75
76/// Process tree store backed by `HashMap<Mutex>` with optional TTL eviction.
77///
78/// Thread-safe via `Arc<Mutex<...>>`. Cloning shares the same data.
79pub struct DefaultStore {
80    inner: Inner,
81    children_index: Arc<Mutex<HashMap<u32, Vec<u32>>>>,
82    ttl: Duration,
83}
84
85impl DefaultStore {
86    /// Create a new store with the given TTL in seconds.
87    /// `ttl_secs = 0` means no expiration.
88    pub fn new(ttl_secs: u64) -> Self {
89        Self {
90            inner: Arc::new(Mutex::new(HashMap::new())),
91            children_index: Arc::new(Mutex::new(HashMap::new())),
92            ttl: Duration::from_secs(ttl_secs),
93        }
94    }
95
96    /// Number of entries (including possibly-expired ones not yet evicted).
97    pub fn len(&self) -> usize {
98        self.inner.lock().expect("lock poisoned").len()
99    }
100
101    /// Returns `true` if the store contains no entries.
102    pub fn is_empty(&self) -> bool {
103        self.len() == 0
104    }
105
106    /// Check if a PID exists in the store.
107    ///
108    /// Does not trigger TTL eviction — use [`get_process`](ProcessStore::get_process)
109    /// to check existence with TTL enforcement.
110    pub fn contains_key(&self, pid: u32) -> bool {
111        self.inner.lock().expect("lock poisoned").contains_key(&pid)
112    }
113}
114
115impl Clone for DefaultStore {
116    fn clone(&self) -> Self {
117        Self {
118            inner: Arc::clone(&self.inner),
119            children_index: Arc::clone(&self.children_index),
120            ttl: self.ttl,
121        }
122    }
123}
124
125impl std::fmt::Debug for DefaultStore {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        f.debug_struct("DefaultStore")
128            .field("len", &self.len())
129            .field("ttl", &self.ttl)
130            .finish()
131    }
132}
133
134impl Default for DefaultStore {
135    /// Creates a store with no TTL.
136    fn default() -> Self {
137        Self::new(0)
138    }
139}
140
141impl ProcessStore for DefaultStore {
142    fn get_process(&self, pid: u32) -> Option<ProcessInfo> {
143        get_inner(&self.inner, pid, self.ttl, &self.children_index)
144    }
145
146    fn insert_process(&self, pid: u32, info: ProcessInfo) {
147        let new_ppid = info.ppid();
148
149        // Check if process already exists with different ppid
150        let old_ppid = {
151            let map = self.inner.lock().expect("lock poisoned");
152            map.get(&pid).map(|e| e.value.ppid())
153        };
154
155        // Insert the process
156        insert_inner(&self.inner, pid, info);
157
158        // Update children index
159        let mut index = self.children_index.lock().expect("lock poisoned");
160
161        // Remove from old parent's index if ppid changed
162        if let Some(old_ppid) = old_ppid
163            && old_ppid != new_ppid
164            && let Some(children) = index.get_mut(&old_ppid)
165        {
166            children.retain(|&c| c != pid);
167        }
168
169        // Add to new parent's index (avoid duplicates)
170        let children = index.entry(new_ppid).or_default();
171        if !children.contains(&pid) {
172            children.push(pid);
173        }
174    }
175
176    fn remove_process(&self, pid: u32) -> Option<ProcessInfo> {
177        // Remove from inner
178        let info = {
179            let mut map = self.inner.lock().expect("lock poisoned");
180            map.remove(&pid).map(|e| e.value)
181        };
182
183        // Remove from children index
184        if let Some(ref p) = info {
185            let mut index = self.children_index.lock().expect("lock poisoned");
186            // Remove from parent's index
187            if let Some(children) = index.get_mut(&p.ppid()) {
188                children.retain(|&c| c != pid);
189            }
190            // Clean up this process's own children index entry
191            index.remove(&pid);
192        }
193
194        info
195    }
196
197    fn all_pids(&self) -> Vec<u32> {
198        self.inner
199            .lock()
200            .expect("lock poisoned")
201            .keys()
202            .copied()
203            .collect()
204    }
205
206    fn for_each_child(&self, pid: u32, f: &mut dyn FnMut(u32)) {
207        // Collect children first, then release lock before calling f.
208        // f may call insert_process which also locks children_index.
209        let children: Vec<u32> = {
210            let index = self.children_index.lock().expect("lock poisoned");
211            index.get(&pid).cloned().unwrap_or_default()
212        };
213        for child in children {
214            f(child);
215        }
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn default_store_insert_get() {
225        let store = DefaultStore::new(0);
226        store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
227        let info = store.get_process(1).unwrap();
228        assert_eq!(info.ppid(), 0);
229        assert_eq!(info.cmd(), "init");
230    }
231
232    #[test]
233    fn default_store_ttl_expired() {
234        let store = DefaultStore::new(0); // ttl=0 means no expiry
235        store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
236        assert!(store.get_process(1).is_some());
237
238        // With ttl=1, entry expires after 1 second
239        let store = DefaultStore::new(1);
240        store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
241        assert!(store.get_process(1).is_some());
242        std::thread::sleep(Duration::from_millis(1100));
243        assert!(store.get_process(1).is_none());
244    }
245
246    #[test]
247    fn clone_shares_data() {
248        let store = DefaultStore::new(0);
249        store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
250        let store2 = store.clone();
251        assert!(store2.get_process(1).is_some());
252        store2.insert_process(2, ProcessInfo::new("bash".into(), "root".into(), 1, 2, 0));
253        assert!(store.get_process(2).is_some());
254    }
255
256    #[test]
257    fn len_and_contains() {
258        let store = DefaultStore::new(0);
259        assert_eq!(store.len(), 0);
260        store.insert_process(1, ProcessInfo::new("a".into(), "u".into(), 0, 1, 0));
261        assert_eq!(store.len(), 1);
262        assert!(store.contains_key(1));
263        assert!(!store.contains_key(999));
264    }
265
266    #[test]
267    fn is_empty_default() {
268        let store = DefaultStore::new(0);
269        assert!(store.is_empty());
270        store.insert_process(1, ProcessInfo::new("a".into(), "u".into(), 0, 1, 0));
271        assert!(!store.is_empty());
272    }
273
274    #[test]
275    fn all_pids_returns_keys() {
276        let store = DefaultStore::new(0);
277        store.insert_process(1, ProcessInfo::new("a".into(), "u".into(), 0, 1, 0));
278        store.insert_process(2, ProcessInfo::new("b".into(), "u".into(), 1, 2, 0));
279        store.insert_process(3, ProcessInfo::new("c".into(), "u".into(), 1, 3, 0));
280        let mut pids = store.all_pids();
281        pids.sort();
282        assert_eq!(pids, vec![1, 2, 3]);
283    }
284
285    #[test]
286    fn ttl_contains_key_does_not_expire() {
287        let store = DefaultStore::new(1);
288        store.insert_process(1, ProcessInfo::new("a".into(), "u".into(), 0, 1, 0));
289        assert!(store.contains_key(1));
290        std::thread::sleep(Duration::from_millis(1100));
291        // contains_key does not trigger TTL eviction
292        assert!(store.contains_key(1));
293        // but get_process does
294        assert!(store.get_process(1).is_none());
295    }
296
297    #[test]
298    fn remove_process() {
299        let store = DefaultStore::new(0);
300        store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
301        store.insert_process(2, ProcessInfo::new("bash".into(), "root".into(), 1, 2, 0));
302
303        assert_eq!(store.len(), 2);
304        assert!(store.contains_key(2));
305
306        let removed = store.remove_process(2);
307        assert!(removed.is_some());
308        assert_eq!(store.len(), 1);
309        assert!(!store.contains_key(2));
310    }
311
312    #[test]
313    fn children_of() {
314        let store = DefaultStore::new(0);
315        store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
316        store.insert_process(100, ProcessInfo::new("a".into(), "root".into(), 1, 100, 0));
317        store.insert_process(200, ProcessInfo::new("b".into(), "root".into(), 1, 200, 0));
318        store.insert_process(
319            300,
320            ProcessInfo::new("c".into(), "root".into(), 100, 300, 0),
321        );
322
323        let mut kids = store.children_of(1);
324        kids.sort();
325        assert_eq!(kids, vec![100, 200]);
326
327        let kids_100 = store.children_of(100);
328        assert_eq!(kids_100, vec![300]);
329
330        assert!(store.children_of(999).is_empty());
331    }
332
333    #[test]
334    fn insert_ppid_change_removes_from_old_parent() {
335        let store = DefaultStore::new(0);
336        store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
337        store.insert_process(
338            100,
339            ProcessInfo::new("other".into(), "root".into(), 0, 100, 0),
340        );
341        store.insert_process(
342            200,
343            ProcessInfo::new("child".into(), "root".into(), 100, 200, 0),
344        );
345
346        // child 200 is under parent 100
347        assert_eq!(store.children_of(100), vec![200]);
348        assert!(store.children_of(1).is_empty());
349
350        // Re-parent 200 from 100 to 1
351        store.insert_process(
352            200,
353            ProcessInfo::new("child".into(), "root".into(), 1, 200, 0),
354        );
355
356        // 200 should be removed from old parent's index
357        assert!(
358            store.children_of(100).is_empty(),
359            "old parent should have no children"
360        );
361        // 200 should be in new parent's index
362        assert_eq!(store.children_of(1), vec![200]);
363    }
364
365    #[test]
366    fn insert_same_ppid_no_duplicate() {
367        let store = DefaultStore::new(0);
368        store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
369        store.insert_process(100, ProcessInfo::new("a".into(), "root".into(), 1, 100, 0));
370        // Insert same pid with same ppid again
371        store.insert_process(100, ProcessInfo::new("a".into(), "root".into(), 1, 100, 0));
372        // Should not duplicate
373        assert_eq!(store.children_of(1), vec![100]);
374    }
375
376    #[test]
377    fn remove_process_cleans_own_children_index() {
378        let store = DefaultStore::new(0);
379        store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
380        store.insert_process(
381            100,
382            ProcessInfo::new("parent".into(), "root".into(), 1, 100, 0),
383        );
384        store.insert_process(
385            200,
386            ProcessInfo::new("child".into(), "root".into(), 100, 200, 0),
387        );
388
389        assert_eq!(store.children_of(100), vec![200]);
390
391        // Remove parent 100
392        store.remove_process(100);
393
394        // children_of(100) should return empty, not stale [200]
395        assert!(
396            store.children_of(100).is_empty(),
397            "removed process should have no children index"
398        );
399        // Child 200 still exists in store but parent is gone
400        assert!(store.get_process(200).is_some());
401    }
402
403    #[test]
404    fn ttl_expiration_cleans_own_children_index() {
405        let store = DefaultStore::new(1); // 1 second TTL
406        store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
407        store.insert_process(
408            100,
409            ProcessInfo::new("parent".into(), "root".into(), 1, 100, 0),
410        );
411        store.insert_process(
412            200,
413            ProcessInfo::new("child".into(), "root".into(), 100, 200, 0),
414        );
415
416        assert_eq!(store.children_of(100), vec![200]);
417
418        // Wait for parent to expire
419        std::thread::sleep(Duration::from_millis(1100));
420
421        // Accessing expired parent triggers eviction
422        assert!(store.get_process(100).is_none());
423
424        // children_of(100) should return empty, not stale [200]
425        assert!(
426            store.children_of(100).is_empty(),
427            "expired process should have no children index"
428        );
429    }
430}