1use std::collections::HashMap;
15use std::sync::{Arc, Mutex};
16use std::time::{Duration, Instant};
17
18use crate::traits::ProcessStore;
19use crate::types::ProcessInfo;
20
21struct 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
37type 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 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 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
74pub struct DefaultStore {
80 inner: Inner,
81 children_index: Arc<Mutex<HashMap<u32, Vec<u32>>>>,
82 ttl: Duration,
83}
84
85impl DefaultStore {
86 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 pub fn len(&self) -> usize {
98 self.inner.lock().expect("lock poisoned").len()
99 }
100
101 pub fn is_empty(&self) -> bool {
103 self.len() == 0
104 }
105
106 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 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 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_inner(&self.inner, pid, info);
157
158 let mut index = self.children_index.lock().expect("lock poisoned");
160
161 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 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 let info = {
179 let mut map = self.inner.lock().expect("lock poisoned");
180 map.remove(&pid).map(|e| e.value)
181 };
182
183 if let Some(ref p) = info {
185 let mut index = self.children_index.lock().expect("lock poisoned");
186 if let Some(children) = index.get_mut(&p.ppid()) {
188 children.retain(|&c| c != pid);
189 }
190 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 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); store.insert_process(1, ProcessInfo::new("init".into(), "root".into(), 0, 1, 0));
236 assert!(store.get_process(1).is_some());
237
238 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 assert!(store.contains_key(1));
293 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 assert_eq!(store.children_of(100), vec![200]);
348 assert!(store.children_of(1).is_empty());
349
350 store.insert_process(
352 200,
353 ProcessInfo::new("child".into(), "root".into(), 1, 200, 0),
354 );
355
356 assert!(
358 store.children_of(100).is_empty(),
359 "old parent should have no children"
360 );
361 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 store.insert_process(100, ProcessInfo::new("a".into(), "root".into(), 1, 100, 0));
372 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 store.remove_process(100);
393
394 assert!(
396 store.children_of(100).is_empty(),
397 "removed process should have no children index"
398 );
399 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); 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 std::thread::sleep(Duration::from_millis(1100));
420
421 assert!(store.get_process(100).is_none());
423
424 assert!(
426 store.children_of(100).is_empty(),
427 "expired process should have no children index"
428 );
429 }
430}