openentropy_core/sources/
vmstat.rs1use std::collections::HashMap;
5use std::thread;
6use std::time::Duration;
7
8use crate::source::{EntropySource, SourceCategory, SourceInfo};
9
10use super::helpers::{extract_delta_bytes_i64, run_command};
11
12const SNAPSHOT_DELAY: Duration = Duration::from_millis(50);
14
15const NUM_ROUNDS: usize = 4;
17
18pub struct VmstatSource {
19 info: SourceInfo,
20}
21
22impl VmstatSource {
23 pub fn new() -> Self {
24 Self {
25 info: SourceInfo {
26 name: "vmstat_deltas",
27 description: "Samples macOS vm_stat counters and extracts entropy from memory management deltas",
28 physics: "Samples macOS vm_stat counters (page faults, pageins, pageouts, \
29 compressions, decompressions, swap activity). These track physical memory \
30 management \u{2014} each counter changes when hardware page table walks, TLB \
31 misses, or memory pressure triggers compressor/swap.",
32 category: SourceCategory::System,
33 platform_requirements: &["macos"],
34 entropy_rate_estimate: 1000.0,
35 composite: false,
36 },
37 }
38 }
39}
40
41impl Default for VmstatSource {
42 fn default() -> Self {
43 Self::new()
44 }
45}
46
47fn vm_stat_path() -> Option<String> {
49 let standard = "/usr/bin/vm_stat";
50 if std::path::Path::new(standard).exists() {
51 return Some(standard.to_string());
52 }
53
54 let path = run_command("which", &["vm_stat"])?;
56 let path = path.trim().to_string();
57 if !path.is_empty() {
58 return Some(path);
59 }
60
61 None
62}
63
64fn snapshot_vmstat(path: &str) -> Option<HashMap<String, i64>> {
75 let stdout = run_command(path, &[])?;
76 let mut map = HashMap::new();
77
78 for line in stdout.lines() {
79 if line.starts_with("Mach") || line.is_empty() {
81 continue;
82 }
83
84 if let Some(colon_idx) = line.rfind(':') {
86 let key = line[..colon_idx].trim().to_string();
87 let val_str = line[colon_idx + 1..].trim().trim_end_matches('.');
88
89 if let Ok(v) = val_str.parse::<i64>() {
90 map.insert(key, v);
91 }
92 }
93 }
94
95 Some(map)
96}
97
98impl EntropySource for VmstatSource {
99 fn info(&self) -> &SourceInfo {
100 &self.info
101 }
102
103 fn is_available(&self) -> bool {
104 vm_stat_path().is_some()
105 }
106
107 fn collect(&self, n_samples: usize) -> Vec<u8> {
108 let path = match vm_stat_path() {
109 Some(p) => p,
110 None => return Vec::new(),
111 };
112
113 let mut snapshots: Vec<HashMap<String, i64>> = Vec::with_capacity(NUM_ROUNDS);
115
116 for i in 0..NUM_ROUNDS {
117 if i > 0 {
118 thread::sleep(SNAPSHOT_DELAY);
119 }
120 match snapshot_vmstat(&path) {
121 Some(snap) => snapshots.push(snap),
122 None => return Vec::new(),
123 }
124 }
125
126 let mut all_deltas: Vec<i64> = Vec::new();
128
129 for pair in snapshots.windows(2) {
130 let prev = &pair[0];
131 let curr = &pair[1];
132
133 for (key, curr_val) in curr {
134 if let Some(prev_val) = prev.get(key) {
135 let delta = curr_val.wrapping_sub(*prev_val);
136 if delta != 0 {
137 all_deltas.push(delta);
138 }
139 }
140 }
141 }
142
143 extract_delta_bytes_i64(&all_deltas, n_samples)
144 }
145}