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 Default for DefaultStore {
126 fn default() -> Self {
128 Self::new(0)
129 }
130}
131
132impl ProcessStore for DefaultStore {
133 fn get_process(&self, pid: u32) -> Option<ProcessInfo> {
134 get_inner(&self.inner, pid, self.ttl, &self.children_index)
135 }
136
137 fn insert_process(&self, pid: u32, info: ProcessInfo) {
138 let new_ppid = info.ppid;
139
140 let old_ppid = {
142 let map = self.inner.lock().expect("lock poisoned");
143 map.get(&pid).map(|e| e.value.ppid)
144 };
145
146 insert_inner(&self.inner, pid, info);
148
149 let mut index = self.children_index.lock().expect("lock poisoned");
151
152 if let Some(old_ppid) = old_ppid
154 && old_ppid != new_ppid
155 && let Some(children) = index.get_mut(&old_ppid)
156 {
157 children.retain(|&c| c != pid);
158 }
159
160 let children = index.entry(new_ppid).or_default();
162 if !children.contains(&pid) {
163 children.push(pid);
164 }
165 }
166
167 fn remove_process(&self, pid: u32) -> Option<ProcessInfo> {
168 let info = {
170 let mut map = self.inner.lock().expect("lock poisoned");
171 map.remove(&pid).map(|e| e.value)
172 };
173
174 if let Some(ref p) = info {
176 let mut index = self.children_index.lock().expect("lock poisoned");
177 if let Some(children) = index.get_mut(&p.ppid) {
179 children.retain(|&c| c != pid);
180 }
181 index.remove(&pid);
183 }
184
185 info
186 }
187
188 fn all_pids(&self) -> Vec<u32> {
189 self.inner
190 .lock()
191 .expect("lock poisoned")
192 .keys()
193 .copied()
194 .collect()
195 }
196
197 fn for_each_child(&self, pid: u32, f: &mut dyn FnMut(u32)) {
198 let children: Vec<u32> = {
201 let index = self.children_index.lock().expect("lock poisoned");
202 index.get(&pid).cloned().unwrap_or_default()
203 };
204 for child in children {
205 f(child);
206 }
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn default_store_insert_get() {
216 let store = DefaultStore::new(0);
217 store.insert_process(
218 1,
219 ProcessInfo {
220 ppid: 0,
221 cmd: "init".into(),
222 user: "root".into(),
223 tgid: 1,
224 start_time_ns: 0,
225 },
226 );
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(
236 1,
237 ProcessInfo {
238 ppid: 0,
239 cmd: "init".into(),
240 user: "root".into(),
241 tgid: 1,
242 start_time_ns: 0,
243 },
244 );
245 assert!(store.get_process(1).is_some());
246
247 let store = DefaultStore::new(1);
249 store.insert_process(
250 1,
251 ProcessInfo {
252 ppid: 0,
253 cmd: "init".into(),
254 user: "root".into(),
255 tgid: 1,
256 start_time_ns: 0,
257 },
258 );
259 assert!(store.get_process(1).is_some());
260 std::thread::sleep(Duration::from_millis(1100));
261 assert!(store.get_process(1).is_none());
262 }
263
264 #[test]
265 fn clone_shares_data() {
266 let store = DefaultStore::new(0);
267 store.insert_process(
268 1,
269 ProcessInfo {
270 ppid: 0,
271 cmd: "init".into(),
272 user: "root".into(),
273 tgid: 1,
274 start_time_ns: 0,
275 },
276 );
277 let store2 = store.clone();
278 assert!(store2.get_process(1).is_some());
279 store2.insert_process(
280 2,
281 ProcessInfo {
282 ppid: 1,
283 cmd: "bash".into(),
284 user: "root".into(),
285 tgid: 2,
286 start_time_ns: 0,
287 },
288 );
289 assert!(store.get_process(2).is_some());
290 }
291
292 #[test]
293 fn len_and_contains() {
294 let store = DefaultStore::new(0);
295 assert_eq!(store.len(), 0);
296 store.insert_process(
297 1,
298 ProcessInfo {
299 ppid: 0,
300 cmd: "a".into(),
301 user: "u".into(),
302 tgid: 1,
303 start_time_ns: 0,
304 },
305 );
306 assert_eq!(store.len(), 1);
307 assert!(store.contains_key(1));
308 assert!(!store.contains_key(999));
309 }
310
311 #[test]
312 fn is_empty_default() {
313 let store = DefaultStore::new(0);
314 assert!(store.is_empty());
315 store.insert_process(
316 1,
317 ProcessInfo {
318 ppid: 0,
319 cmd: "a".into(),
320 user: "u".into(),
321 tgid: 1,
322 start_time_ns: 0,
323 },
324 );
325 assert!(!store.is_empty());
326 }
327
328 #[test]
329 fn all_pids_returns_keys() {
330 let store = DefaultStore::new(0);
331 store.insert_process(
332 1,
333 ProcessInfo {
334 ppid: 0,
335 cmd: "a".into(),
336 user: "u".into(),
337 tgid: 1,
338 start_time_ns: 0,
339 },
340 );
341 store.insert_process(
342 2,
343 ProcessInfo {
344 ppid: 1,
345 cmd: "b".into(),
346 user: "u".into(),
347 tgid: 2,
348 start_time_ns: 0,
349 },
350 );
351 store.insert_process(
352 3,
353 ProcessInfo {
354 ppid: 1,
355 cmd: "c".into(),
356 user: "u".into(),
357 tgid: 3,
358 start_time_ns: 0,
359 },
360 );
361 let mut pids = store.all_pids();
362 pids.sort();
363 assert_eq!(pids, vec![1, 2, 3]);
364 }
365
366 #[test]
367 fn ttl_contains_key_does_not_expire() {
368 let store = DefaultStore::new(1);
369 store.insert_process(
370 1,
371 ProcessInfo {
372 ppid: 0,
373 cmd: "a".into(),
374 user: "u".into(),
375 tgid: 1,
376 start_time_ns: 0,
377 },
378 );
379 assert!(store.contains_key(1));
380 std::thread::sleep(Duration::from_millis(1100));
381 assert!(store.contains_key(1));
383 assert!(store.get_process(1).is_none());
385 }
386
387 #[test]
388 fn remove_process() {
389 let store = DefaultStore::new(0);
390 store.insert_process(
391 1,
392 ProcessInfo {
393 ppid: 0,
394 cmd: "init".into(),
395 user: "root".into(),
396 tgid: 1,
397 start_time_ns: 0,
398 },
399 );
400 store.insert_process(
401 2,
402 ProcessInfo {
403 ppid: 1,
404 cmd: "bash".into(),
405 user: "root".into(),
406 tgid: 2,
407 start_time_ns: 0,
408 },
409 );
410
411 assert_eq!(store.len(), 2);
412 assert!(store.contains_key(2));
413
414 let removed = store.remove_process(2);
415 assert!(removed.is_some());
416 assert_eq!(store.len(), 1);
417 assert!(!store.contains_key(2));
418 }
419
420 #[test]
421 fn children_of() {
422 let store = DefaultStore::new(0);
423 store.insert_process(
424 1,
425 ProcessInfo {
426 ppid: 0,
427 cmd: "init".into(),
428 user: "root".into(),
429 tgid: 1,
430 start_time_ns: 0,
431 },
432 );
433 store.insert_process(
434 100,
435 ProcessInfo {
436 ppid: 1,
437 cmd: "a".into(),
438 user: "root".into(),
439 tgid: 100,
440 start_time_ns: 0,
441 },
442 );
443 store.insert_process(
444 200,
445 ProcessInfo {
446 ppid: 1,
447 cmd: "b".into(),
448 user: "root".into(),
449 tgid: 200,
450 start_time_ns: 0,
451 },
452 );
453 store.insert_process(
454 300,
455 ProcessInfo {
456 ppid: 100,
457 cmd: "c".into(),
458 user: "root".into(),
459 tgid: 300,
460 start_time_ns: 0,
461 },
462 );
463
464 let mut kids = store.children_of(1);
465 kids.sort();
466 assert_eq!(kids, vec![100, 200]);
467
468 let kids_100 = store.children_of(100);
469 assert_eq!(kids_100, vec![300]);
470
471 assert!(store.children_of(999).is_empty());
472 }
473
474 #[test]
475 fn insert_ppid_change_removes_from_old_parent() {
476 let store = DefaultStore::new(0);
477 store.insert_process(
478 1,
479 ProcessInfo {
480 ppid: 0,
481 cmd: "init".into(),
482 user: "root".into(),
483 tgid: 1,
484 start_time_ns: 0,
485 },
486 );
487 store.insert_process(
488 100,
489 ProcessInfo {
490 ppid: 0,
491 cmd: "other".into(),
492 user: "root".into(),
493 tgid: 100,
494 start_time_ns: 0,
495 },
496 );
497 store.insert_process(
498 200,
499 ProcessInfo {
500 ppid: 100,
501 cmd: "child".into(),
502 user: "root".into(),
503 tgid: 200,
504 start_time_ns: 0,
505 },
506 );
507
508 assert_eq!(store.children_of(100), vec![200]);
510 assert!(store.children_of(1).is_empty());
511
512 store.insert_process(
514 200,
515 ProcessInfo {
516 ppid: 1,
517 cmd: "child".into(),
518 user: "root".into(),
519 tgid: 200,
520 start_time_ns: 0,
521 },
522 );
523
524 assert!(
526 store.children_of(100).is_empty(),
527 "old parent should have no children"
528 );
529 assert_eq!(store.children_of(1), vec![200]);
531 }
532
533 #[test]
534 fn insert_same_ppid_no_duplicate() {
535 let store = DefaultStore::new(0);
536 store.insert_process(
537 1,
538 ProcessInfo {
539 ppid: 0,
540 cmd: "init".into(),
541 user: "root".into(),
542 tgid: 1,
543 start_time_ns: 0,
544 },
545 );
546 store.insert_process(
547 100,
548 ProcessInfo {
549 ppid: 1,
550 cmd: "a".into(),
551 user: "root".into(),
552 tgid: 100,
553 start_time_ns: 0,
554 },
555 );
556 store.insert_process(
558 100,
559 ProcessInfo {
560 ppid: 1,
561 cmd: "a".into(),
562 user: "root".into(),
563 tgid: 100,
564 start_time_ns: 0,
565 },
566 );
567 assert_eq!(store.children_of(1), vec![100]);
569 }
570
571 #[test]
572 fn remove_process_cleans_own_children_index() {
573 let store = DefaultStore::new(0);
574 store.insert_process(
575 1,
576 ProcessInfo {
577 ppid: 0,
578 cmd: "init".into(),
579 user: "root".into(),
580 tgid: 1,
581 start_time_ns: 0,
582 },
583 );
584 store.insert_process(
585 100,
586 ProcessInfo {
587 ppid: 1,
588 cmd: "parent".into(),
589 user: "root".into(),
590 tgid: 100,
591 start_time_ns: 0,
592 },
593 );
594 store.insert_process(
595 200,
596 ProcessInfo {
597 ppid: 100,
598 cmd: "child".into(),
599 user: "root".into(),
600 tgid: 200,
601 start_time_ns: 0,
602 },
603 );
604
605 assert_eq!(store.children_of(100), vec![200]);
606
607 store.remove_process(100);
609
610 assert!(
612 store.children_of(100).is_empty(),
613 "removed process should have no children index"
614 );
615 assert!(store.get_process(200).is_some());
617 }
618
619 #[test]
620 fn ttl_expiration_cleans_own_children_index() {
621 let store = DefaultStore::new(1); store.insert_process(
623 1,
624 ProcessInfo {
625 ppid: 0,
626 cmd: "init".into(),
627 user: "root".into(),
628 tgid: 1,
629 start_time_ns: 0,
630 },
631 );
632 store.insert_process(
633 100,
634 ProcessInfo {
635 ppid: 1,
636 cmd: "parent".into(),
637 user: "root".into(),
638 tgid: 100,
639 start_time_ns: 0,
640 },
641 );
642 store.insert_process(
643 200,
644 ProcessInfo {
645 ppid: 100,
646 cmd: "child".into(),
647 user: "root".into(),
648 tgid: 200,
649 start_time_ns: 0,
650 },
651 );
652
653 assert_eq!(store.children_of(100), vec![200]);
654
655 std::thread::sleep(Duration::from_millis(1100));
657
658 assert!(store.get_process(100).is_none());
660
661 assert!(
663 store.children_of(100).is_empty(),
664 "expired process should have no children index"
665 );
666 }
667}