use std::{ fs::File, io::{BufRead, BufReader}, sync::{atomic::Ordering, mpsc::Sender}, thread::JoinHandle, time::Duration, }; use regex_lite::Regex; use time::OffsetDateTime; use crate::{griph, AwakeState}; pub struct Gatherer { state: AwakeState, hwnd: Option>, } impl Gatherer { pub fn new(state: AwakeState) -> Self { Self { state, hwnd: None } } pub fn start(&mut self) { let state = self.state.clone(); let hwnd = std::thread::spawn(|| task(state)); self.hwnd = Some(hwnd); } } fn task(state: AwakeState) { tracing::info!("starting gatherer thread"); // I just want a graph on first boot; don't care about divisions just yet make_mem_graph(&state); make_net_graph(&state); let mut last_netinfo: Option = None; // this is a `let _` because otherwise the attribute was // making the comiler mad #[rustfmt::skip] let _ = loop { tracing::debug!("collecting stats"); // Gather data let meminfo = Meminfo::current(); let netinfo = Netinfo::current(); // Print traces, y'know, for tracing tracing::trace!("memory: {}MB used / {}MB total", meminfo.usage() / 1000, meminfo.total / 1000); tracing::trace!("net: rx {} / tx {}", data_human_fmt(netinfo.rx_bytes), data_human_fmt(netinfo.tx_bytes)); if let Some(lni) = last_netinfo { let rx_delta = netinfo.rx_bytes - lni.rx_bytes; let tx_delta = netinfo.tx_bytes - lni.tx_bytes; state.database.insert_hostnet(60, rx_delta, tx_delta); } last_netinfo = Some(netinfo); // Store stats in database state.database.insert_host_meminfo(meminfo); // Only generate graphs every 15 minutes let now = OffsetDateTime::now_utc(); if now.minute() % 15 == 0 { make_mem_graph(&state); make_net_graph(&state); } std::thread::sleep(Duration::from_secs(60)); }; } pub fn make_mem_graph(state: &AwakeState) { tracing::debug!("generating meminfo graph"); let infos = state.database.get_last_n_host_meminfo(256); let max = infos[0].total_kb; let mut usages: Vec = infos.into_iter().map(|mi| mi.usage()).collect(); // Reversing here because we want latest valeus on on the // right side, so last in the array usages.reverse(); let gif = griph::make_1line(0, max, &usages); let path = state.cache_path.join("current_hostmeminfo.gif"); gif.save(path).unwrap(); } pub fn make_net_graph(state: &AwakeState) { tracing::debug!("generating netinfo graph"); let infos = state.database.get_last_n_hostnet(256); // 125 is (1000 / 8) so it converst Bytes to kiloBITS let mut rx_deltas: Vec = infos .iter() .map(|ni| ni.rx_bytes_per_sec() as usize / 124) .collect(); let mut tx_deltas: Vec = infos .iter() .map(|ni| ni.tx_bytes_per_sec() as usize / 125) .collect(); // Reversing to put latest values on the right side rx_deltas.reverse(); tx_deltas.reverse(); // Mixing the TX/RX delta so we can pick a range. let mut mixed = vec![0; 512]; mixed[..256].copy_from_slice(&rx_deltas); mixed[256..].copy_from_slice(&tx_deltas); mixed.sort(); let kinda_highest = mixed[511 - 32]; let high_bound = (kinda_highest as f32 / 256.0).ceil().min(1.0) as usize * 256; state .netinfo_upper_bound .store(high_bound, Ordering::Release); let gif = griph::make_2line(0, high_bound, &tx_deltas, &rx_deltas); let path = state.cache_path.join("current_hostnetinfo.gif"); gif.save(path).unwrap(); } pub struct Meminfo { pub total: usize, pub free: usize, pub avaialable: usize, } impl Meminfo { pub fn current() -> Self { let procinfo = File::open("/proc/meminfo").unwrap(); let bread = BufReader::new(procinfo); let mut meminfo = Meminfo { total: 0, free: 0, avaialable: 0, }; for line in bread.lines() { let line = line.unwrap(); if let Some((raw_key, raw_value_kb)) = line.split_once(':') { let value = if let Some(raw_value) = raw_value_kb.trim().strip_suffix(" kB") { if let Ok(parsed) = raw_value.parse() { parsed } else { continue; } } else { continue; }; match raw_key.trim() { "MemTotal" => meminfo.total = value, "MemFree" => meminfo.free = value, "MemAvailable" => meminfo.avaialable = value, _ => (), } } } meminfo } pub fn usage(&self) -> usize { self.total - self.avaialable } } pub struct Netinfo { rx_bytes: usize, tx_bytes: usize, } impl Netinfo { pub fn current() -> Self { let procinfo = File::open("/proc/net/dev").unwrap(); let bread = BufReader::new(procinfo); let mut netinfo = Self { rx_bytes: 0, tx_bytes: 0, }; let re = Regex::new(r"[ ]*(\d+)").unwrap(); let interface = "eth0:"; for line in bread.lines() { let line = line.unwrap(); let trim = line.trim(); let mut captures = if let Some(data) = trim.strip_prefix(interface) { re.captures_iter(data) } else { continue; }; netinfo.rx_bytes = captures .next() .unwrap() .get(1) .unwrap() .as_str() .parse() .unwrap(); netinfo.tx_bytes = captures .skip(7) .next() .unwrap() .get(1) .unwrap() .as_str() .parse() .unwrap(); break; } netinfo } } fn data_human_fmt(bytes: usize) -> String { let (num, unit) = data_human(bytes); format!("{num}{unit}") } fn data_human(bytes: usize) -> (f32, &'static str) { const UNITS: &[&str] = &["B", "kB", "MB", "GB", "TB"]; let mut wrk = bytes as f32; let mut unit_idx = 0; loop { if wrk < 1500.0 || unit_idx == UNITS.len() - 1 { return (wrk, UNITS[unit_idx]); } wrk /= 1000.0; unit_idx += 1; } }