use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use tokio::sync::broadcast;
use crate::system_profile::SystemProfile;
#[derive(Clone, Debug)]
pub enum ProfileChange {
Updated(Arc<SystemProfile>),
HardwareChange { description: String },
}
pub struct SystemProfileWatcher {
last_profile: Arc<Mutex<Option<Arc<SystemProfile>>>>,
tx: broadcast::Sender<ProfileChange>,
poll_interval: Duration,
}
impl SystemProfileWatcher {
pub fn new(poll_interval: Duration) -> Self {
let initial = Arc::new(SystemProfile::detect());
let (tx, _rx) = broadcast::channel(16);
Self {
last_profile: Arc::new(Mutex::new(Some(initial))),
tx,
poll_interval,
}
}
pub fn subscribe(&self) -> broadcast::Receiver<ProfileChange> {
let rx = self.tx.subscribe();
if let Some(profile) = self.current_profile() {
let _ = self.tx.send(ProfileChange::Updated(profile));
}
rx
}
pub fn current_profile(&self) -> Option<Arc<SystemProfile>> {
self.last_profile.lock().ok().and_then(|g| g.clone())
}
pub fn poll_once(&self) {
let fresh = Arc::new(SystemProfile::detect_fast());
let changed = {
let guard = self.last_profile.lock().ok();
match guard.and_then(|g| g.as_ref().map(|old| profiles_differ(old, &fresh))) {
Some(true) | None => true,
Some(false) => false,
}
};
if changed {
if let Ok(mut guard) = self.last_profile.lock() {
*guard = Some(fresh.clone());
}
let _ = self.tx.send(ProfileChange::Updated(fresh));
}
}
pub fn start_background(self: &Arc<Self>) -> tokio::task::JoinHandle<()> {
let this = self.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(this.poll_interval).await;
this.poll_once();
}
})
}
}
fn profiles_differ(a: &SystemProfile, b: &SystemProfile) -> bool {
if a.gpus.len() != b.gpus.len() {
return true;
}
for (ga, gb) in a.gpus.iter().zip(b.gpus.iter()) {
if ga.name != gb.name || (ga.vram_gb - gb.vram_gb).abs() > 0.1 || ga.backend != gb.backend {
return true;
}
}
if a.npus.len() != b.npus.len() {
return true;
}
if a.cpu.logical_cores != b.cpu.logical_cores {
return true;
}
if (a.memory.total_gb - b.memory.total_gb).abs() > 0.5 {
return true;
}
false
}
pub fn check_for_changes(last: &SystemProfile) -> Option<SystemProfile> {
let current = SystemProfile::detect_fast();
if profiles_differ(last, ¤t) {
Some(current)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn watcher_starts_with_valid_profile() {
let watcher = SystemProfileWatcher::new(Duration::from_secs(60));
let profile = watcher.current_profile();
assert!(profile.is_some());
assert!(!profile.unwrap().gpus.is_empty());
}
#[test]
fn watcher_subscribe_receives_initial() {
let watcher = SystemProfileWatcher::new(Duration::from_secs(60));
let mut rx = watcher.subscribe();
let event = rx.try_recv();
assert!(
event.is_ok(),
"should have initial event: {:?}",
event.err()
);
match event.unwrap() {
ProfileChange::Updated(_) => {} _ => panic!("expected Updated event"),
}
}
#[test]
fn poll_once_does_not_panic() {
let watcher = SystemProfileWatcher::new(Duration::from_secs(60));
watcher.poll_once(); }
#[test]
fn check_for_changes_returns_none_when_unchanged() {
let profile = SystemProfile::detect_fast();
let result = check_for_changes(&profile);
let _ = result;
}
#[test]
fn profiles_differ_detects_gpu_count_change() {
let a = SystemProfile::detect_fast();
let mut b = a.clone();
b.gpus.clear();
assert!(profiles_differ(&a, &b));
}
}