1use std::collections::{BTreeMap, BTreeSet};
31use std::sync::Mutex;
32
33use serde_json::{json, Value};
34use sysinfo::{
35 Components, MemoryRefreshKind, Pid, ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System,
36};
37
38static HARN_OWNED_PIDS: Mutex<BTreeMap<u32, usize>> = Mutex::new(BTreeMap::new());
44
45#[must_use = "dropping the registration stops claiming the pid as Harn-owned"]
50pub struct HarnOwnedPidRegistration {
51 pid: u32,
52}
53
54impl Drop for HarnOwnedPidRegistration {
55 fn drop(&mut self) {
56 unregister_harn_owned_pid(self.pid);
57 }
58}
59
60pub fn register_harn_owned_pid(pid: u32) -> HarnOwnedPidRegistration {
62 let mut set = HARN_OWNED_PIDS
63 .lock()
64 .unwrap_or_else(std::sync::PoisonError::into_inner);
65 *set.entry(pid).or_default() += 1;
66 HarnOwnedPidRegistration { pid }
67}
68
69fn unregister_harn_owned_pid(pid: u32) {
70 let mut set = HARN_OWNED_PIDS
71 .lock()
72 .unwrap_or_else(std::sync::PoisonError::into_inner);
73 match set.get_mut(&pid) {
74 Some(claims) if *claims > 1 => *claims -= 1,
75 Some(_) => {
76 set.remove(&pid);
77 }
78 None => {}
79 }
80}
81
82fn harn_owned_pids_snapshot() -> BTreeSet<u32> {
83 HARN_OWNED_PIDS
84 .lock()
85 .unwrap_or_else(std::sync::PoisonError::into_inner)
86 .keys()
87 .copied()
88 .collect()
89}
90
91pub fn cpu_snapshot() -> Value {
95 let mut sys = System::new_with_specifics(
96 RefreshKind::nothing().with_cpu(
97 sysinfo::CpuRefreshKind::nothing()
98 .with_cpu_usage()
99 .with_frequency(),
100 ),
101 );
102 sys.refresh_cpu_all();
103 let cpus = sys.cpus();
104 let count = cpus.len();
105 let physical_count = System::physical_core_count();
106 let (model, frequency_mhz) = match cpus.first() {
107 Some(cpu) => {
108 let brand = cpu.brand().trim().to_string();
109 (
110 if brand.is_empty() { None } else { Some(brand) },
111 Some(cpu.frequency()),
112 )
113 }
114 None => (None, None),
115 };
116 let cpu_usage = if cpus.is_empty() {
117 None
118 } else {
119 let total: f32 = cpus.iter().map(|c| c.cpu_usage()).sum();
120 Some(total as f64 / cpus.len() as f64)
121 };
122 json!({
123 "count": count,
124 "physical_count": physical_count,
125 "model": model,
126 "frequency_mhz": frequency_mhz,
127 "usage_pct": cpu_usage,
128 })
129}
130
131pub fn memory_snapshot() -> Value {
134 let mut sys = System::new_with_specifics(
135 RefreshKind::nothing().with_memory(MemoryRefreshKind::everything()),
136 );
137 sys.refresh_memory();
138 let total = sys.total_memory();
139 let used = sys.used_memory();
140 let available = sys.available_memory();
141 let total_gb = bytes_to_gb(total);
142 let used_gb = bytes_to_gb(used);
143 let available_gb = bytes_to_gb(available);
144 let pressure = if total == 0 {
145 "unknown"
146 } else {
147 let ratio = used as f64 / total as f64;
148 if ratio >= 0.85 {
149 "high"
150 } else if ratio >= 0.6 {
151 "medium"
152 } else {
153 "low"
154 }
155 };
156 json!({
157 "total_bytes": total,
158 "used_bytes": used,
159 "available_bytes": available,
160 "total_gb": total_gb,
161 "used_gb": used_gb,
162 "available_gb": available_gb,
163 "pressure": pressure,
164 })
165}
166
167pub fn current_process_memory_bytes() -> Option<u64> {
174 let pid = Pid::from_u32(std::process::id());
175 let mut sys = System::new();
176 sys.refresh_processes_specifics(
177 ProcessesToUpdate::Some(&[pid]),
178 false,
179 ProcessRefreshKind::nothing().with_memory(),
180 );
181 let reported = sys.process(pid).map(|process| process.memory());
182 reported.filter(|bytes| *bytes > 0).or_else(linux_peak_rss)
183}
184
185#[cfg(target_os = "linux")]
186fn linux_peak_rss() -> Option<u64> {
187 use std::mem::MaybeUninit;
188
189 unsafe {
192 let mut usage = MaybeUninit::<libc::rusage>::zeroed();
193 if libc::getrusage(libc::RUSAGE_SELF, usage.as_mut_ptr()) != 0 {
194 return None;
195 }
196 u64::try_from(usage.assume_init().ru_maxrss)
197 .ok()?
198 .checked_mul(1024)
199 }
200}
201
202#[cfg(not(target_os = "linux"))]
203fn linux_peak_rss() -> Option<u64> {
204 None
205}
206
207pub fn gpus_snapshot() -> Value {
212 Value::Array(Vec::new())
213}
214
215pub fn temperature_snapshot() -> Value {
221 let components = Components::new_with_refreshed_list();
222 let mut entries = Vec::new();
223 for component in &components {
224 entries.push(json!({
225 "label": component.label(),
226 "celsius": component.temperature(),
227 "max_celsius": component.max(),
228 "critical_celsius": component.critical(),
229 }));
230 }
231 json!({
232 "components": entries,
233 })
234}
235
236pub fn platform_snapshot() -> Value {
238 json!({
239 "os": canonical_os(),
242 "arch": std::env::consts::ARCH,
243 "version": System::os_version(),
244 "kernel": System::kernel_version(),
245 "long_os_version": System::long_os_version(),
246 "hostname": System::host_name(),
247 })
248}
249
250fn canonical_os() -> &'static str {
251 if cfg!(target_os = "macos") {
252 "darwin"
253 } else {
254 std::env::consts::OS
255 }
256}
257
258pub fn identity_snapshot() -> Value {
260 json!({
261 "username": std::env::var("USER")
262 .or_else(|_| std::env::var("USERNAME"))
263 .unwrap_or_default(),
264 "hostname": System::host_name(),
265 "pid": std::process::id(),
266 })
267}
268
269pub fn processes_snapshot() -> Value {
277 let mut sys = System::new();
278 sys.refresh_processes_specifics(
279 ProcessesToUpdate::All,
280 false,
281 ProcessRefreshKind::nothing()
282 .with_cpu()
283 .with_memory()
284 .with_exe(sysinfo::UpdateKind::OnlyIfNotSet),
285 );
286 let our_pid = std::process::id();
287 let our_pid_sys = Pid::from_u32(our_pid);
288 let registry = harn_owned_pids_snapshot();
289
290 let mut entries = Vec::new();
291 for (pid, process) in sys.processes() {
292 let pid_u32 = pid.as_u32();
293 let parent_u32 = process.parent().map(|p| p.as_u32());
294 let is_harn_owned =
295 pid_u32 == our_pid || registry.contains(&pid_u32) || parent_u32 == Some(our_pid);
296 if !is_harn_owned {
297 entries.push(json!({
302 "pid": pid_u32,
303 "name": process.name().to_string_lossy(),
304 "is_harn_owned": false,
305 }));
306 continue;
307 }
308 entries.push(json!({
309 "pid": pid_u32,
310 "parent_pid": parent_u32,
311 "name": process.name().to_string_lossy(),
312 "cpu_pct": process.cpu_usage(),
313 "mem_bytes": process.memory(),
314 "is_harn_owned": true,
315 "is_self": pid_u32 == our_pid,
316 }));
317 }
318
319 entries.sort_by(|a, b| {
321 let a_owned = a
322 .get("is_harn_owned")
323 .and_then(Value::as_bool)
324 .unwrap_or(false);
325 let b_owned = b
326 .get("is_harn_owned")
327 .and_then(Value::as_bool)
328 .unwrap_or(false);
329 b_owned.cmp(&a_owned).then_with(|| {
330 a.get("pid")
331 .and_then(Value::as_u64)
332 .cmp(&b.get("pid").and_then(Value::as_u64))
333 })
334 });
335
336 if !entries
341 .iter()
342 .any(|entry| entry.get("pid").and_then(Value::as_u64).map(|p| p as u32) == Some(our_pid))
343 {
344 entries.insert(
345 0,
346 json!({
347 "pid": our_pid,
348 "parent_pid": Value::Null,
349 "name": current_process_name(&sys, our_pid_sys),
350 "cpu_pct": 0.0,
351 "mem_bytes": 0,
352 "is_harn_owned": true,
353 "is_self": true,
354 }),
355 );
356 }
357
358 Value::Array(entries)
359}
360
361fn current_process_name(sys: &System, pid: Pid) -> String {
362 sys.process(pid)
363 .map(|process| process.name().to_string_lossy().into_owned())
364 .unwrap_or_else(|| "harn".to_string())
365}
366
367fn bytes_to_gb(bytes: u64) -> f64 {
368 bytes as f64 / 1_073_741_824.0
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374
375 #[test]
376 fn cpu_snapshot_reports_nonzero_count() {
377 let snapshot = cpu_snapshot();
378 let count = snapshot
379 .get("count")
380 .and_then(Value::as_u64)
381 .expect("count present");
382 assert!(count >= 1, "expected at least one logical cpu, got {count}");
383 }
384
385 #[test]
386 fn memory_snapshot_has_nonzero_total() {
387 let snapshot = memory_snapshot();
388 let total = snapshot
389 .get("total_bytes")
390 .and_then(Value::as_u64)
391 .expect("total_bytes present");
392 assert!(total > 0, "total memory should be non-zero, got {total}");
393 let pressure = snapshot
394 .get("pressure")
395 .and_then(Value::as_str)
396 .expect("pressure present");
397 assert!(
398 matches!(pressure, "low" | "medium" | "high" | "unknown"),
399 "pressure should be a known bucket, got {pressure:?}"
400 );
401 }
402
403 #[test]
404 fn gpus_snapshot_returns_list() {
405 let snapshot = gpus_snapshot();
406 assert!(snapshot.is_array(), "gpus snapshot is a list");
407 }
408
409 #[test]
410 fn temperature_snapshot_returns_components_field() {
411 let snapshot = temperature_snapshot();
412 assert!(
413 snapshot.get("components").is_some(),
414 "components field present"
415 );
416 assert!(
417 snapshot.get("components").unwrap().is_array(),
418 "components is array"
419 );
420 }
421
422 #[test]
423 fn platform_snapshot_includes_arch() {
424 let snapshot = platform_snapshot();
425 assert_eq!(
426 snapshot.get("arch").and_then(Value::as_str),
427 Some(std::env::consts::ARCH)
428 );
429 }
430
431 #[test]
432 fn processes_snapshot_includes_self() {
433 let snapshot = processes_snapshot();
434 let entries = snapshot.as_array().expect("array");
435 let our_pid = std::process::id() as u64;
436 let self_entry = entries
437 .iter()
438 .find(|entry| entry.get("pid").and_then(Value::as_u64) == Some(our_pid))
439 .expect("self entry present");
440 assert_eq!(
441 self_entry.get("is_harn_owned").and_then(Value::as_bool),
442 Some(true),
443 "self entry must be harn-owned"
444 );
445 }
446
447 #[test]
448 fn current_process_memory_bytes_reports_self_when_available() {
449 if let Some(bytes) = current_process_memory_bytes() {
450 assert!(bytes > 0, "current process memory should be non-zero");
451 }
452 }
453
454 #[cfg(target_os = "linux")]
455 #[test]
456 fn linux_peak_rss_reports_self() {
457 assert!(linux_peak_rss().is_some_and(|bytes| bytes > 0));
458 }
459
460 #[test]
461 fn harn_owned_pid_process_global_lifetime_waits_for_every_owner() {
462 let fake = u32::MAX - 1;
464 let first = register_harn_owned_pid(fake);
465 let second = register_harn_owned_pid(fake);
466 assert!(harn_owned_pids_snapshot().contains(&fake));
467 drop(first);
468 assert!(
469 harn_owned_pids_snapshot().contains(&fake),
470 "one owner dropping must not erase another owner's live claim"
471 );
472 drop(second);
473 assert!(!harn_owned_pids_snapshot().contains(&fake));
474 }
475}