//! Lightweight host metrics (CPU %, RAM, disk, battery, uptime) for the portal's //! live monitoring. Windows-only; sampled every few seconds by the engine and //! pushed to the server as a `host.telemetry` control message. #![cfg(windows)] use serde_json::{json, Value}; /// Keeps the previous CPU time sample so each `sample()` yields a real % over /// the interval since the last call. pub struct Telemetry { prev_idle: u64, prev_total: u64, } impl Telemetry { pub fn new() -> Self { Self { prev_idle: 0, prev_total: 0 } } pub fn sample(&mut self) -> Value { let cpu = self.cpu_percent(); let (ram_used, ram_total) = mem(); let (disk_used, disk_total) = disk(); let (battery, charging) = power(); json!({ "t": "host.telemetry", "cpu": cpu, "ram_used": ram_used, "ram_total": ram_total, "disk_used": disk_used, "disk_total": disk_total, "battery": battery, "charging": charging, "uptime": uptime_secs(), }) } fn cpu_percent(&mut self) -> u32 { use windows::Win32::Foundation::FILETIME; use windows::Win32::System::Threading::GetSystemTimes; unsafe { let mut idle = FILETIME::default(); let mut kern = FILETIME::default(); let mut user = FILETIME::default(); if GetSystemTimes(Some(&mut idle), Some(&mut kern), Some(&mut user)).is_err() { return 0; } let ft = |f: FILETIME| ((f.dwHighDateTime as u64) << 32) | f.dwLowDateTime as u64; let idle_t = ft(idle); // Kernel time INCLUDES idle, so kernel+user is the full busy+idle total. let total_t = ft(kern) + ft(user); let d_total = total_t.saturating_sub(self.prev_total); let d_idle = idle_t.saturating_sub(self.prev_idle); self.prev_total = total_t; self.prev_idle = idle_t; if d_total == 0 { return 0; } let busy = d_total.saturating_sub(d_idle); ((busy as f64 / d_total as f64) * 100.0).round().clamp(0.0, 100.0) as u32 } } } fn mem() -> (u64, u64) { use windows::Win32::System::SystemInformation::{GlobalMemoryStatusEx, MEMORYSTATUSEX}; unsafe { let mut m = MEMORYSTATUSEX { dwLength: std::mem::size_of::() as u32, ..Default::default() }; if GlobalMemoryStatusEx(&mut m).is_ok() { (m.ullTotalPhys.saturating_sub(m.ullAvailPhys), m.ullTotalPhys) } else { (0, 0) } } } fn disk() -> (u64, u64) { use windows::core::PCWSTR; use windows::Win32::Storage::FileSystem::GetDiskFreeSpaceExW; unsafe { let path: Vec = "C:\\".encode_utf16().chain(std::iter::once(0)).collect(); let mut free_avail = 0u64; let mut total = 0u64; let mut total_free = 0u64; if GetDiskFreeSpaceExW( PCWSTR(path.as_ptr()), Some(&mut free_avail), Some(&mut total), Some(&mut total_free), ) .is_ok() { (total.saturating_sub(total_free), total) } else { (0, 0) } } } fn power() -> (i32, bool) { use windows::Win32::System::Power::GetSystemPowerStatus; use windows::Win32::System::Power::SYSTEM_POWER_STATUS; unsafe { let mut s = SYSTEM_POWER_STATUS::default(); if GetSystemPowerStatus(&mut s).is_ok() { let pct = if s.BatteryLifePercent == 255 { -1 } else { s.BatteryLifePercent as i32 }; let charging = s.ACLineStatus == 1; // 1 = plugged in (pct, charging) } else { (-1, true) } } } fn uptime_secs() -> u64 { use windows::Win32::System::SystemInformation::GetTickCount64; unsafe { GetTickCount64() / 1000 } }