ghostlink_core/
watcher.rs1use std::sync::Arc;
8use std::sync::Mutex;
9use std::time::Duration;
10use tokio::sync::broadcast;
11
12use crate::system_profile::SystemProfile;
13
14#[derive(Clone, Debug)]
16pub enum ProfileChange {
17 Updated(Arc<SystemProfile>),
19 HardwareChange { description: String },
21}
22
23pub struct SystemProfileWatcher {
28 last_profile: Arc<Mutex<Option<Arc<SystemProfile>>>>,
30 tx: broadcast::Sender<ProfileChange>,
32 poll_interval: Duration,
34}
35
36impl SystemProfileWatcher {
37 pub fn new(poll_interval: Duration) -> Self {
41 let initial = Arc::new(SystemProfile::detect());
42 let (tx, _rx) = broadcast::channel(16);
43
44 Self {
45 last_profile: Arc::new(Mutex::new(Some(initial))),
46 tx,
47 poll_interval,
48 }
49 }
50
51 pub fn subscribe(&self) -> broadcast::Receiver<ProfileChange> {
55 let rx = self.tx.subscribe();
56 if let Some(profile) = self.current_profile() {
59 let _ = self.tx.send(ProfileChange::Updated(profile));
60 }
61 rx
62 }
63
64 pub fn current_profile(&self) -> Option<Arc<SystemProfile>> {
66 self.last_profile.lock().ok().and_then(|g| g.clone())
67 }
68
69 pub fn poll_once(&self) {
71 let fresh = Arc::new(SystemProfile::detect_fast());
72 let changed = {
73 let guard = self.last_profile.lock().ok();
74 match guard.and_then(|g| g.as_ref().map(|old| profiles_differ(old, &fresh))) {
75 Some(true) | None => true,
76 Some(false) => false,
77 }
78 };
79
80 if changed {
81 if let Ok(mut guard) = self.last_profile.lock() {
82 *guard = Some(fresh.clone());
83 }
84 let _ = self.tx.send(ProfileChange::Updated(fresh));
85 }
86 }
87
88 pub fn start_background(self: &Arc<Self>) -> tokio::task::JoinHandle<()> {
90 let this = self.clone();
91 tokio::spawn(async move {
92 loop {
93 tokio::time::sleep(this.poll_interval).await;
94 this.poll_once();
95 }
96 })
97 }
98}
99
100fn profiles_differ(a: &SystemProfile, b: &SystemProfile) -> bool {
102 if a.gpus.len() != b.gpus.len() {
103 return true;
104 }
105 for (ga, gb) in a.gpus.iter().zip(b.gpus.iter()) {
106 if ga.name != gb.name || (ga.vram_gb - gb.vram_gb).abs() > 0.1 || ga.backend != gb.backend {
107 return true;
108 }
109 }
110 if a.npus.len() != b.npus.len() {
111 return true;
112 }
113 if a.cpu.logical_cores != b.cpu.logical_cores {
114 return true;
115 }
116 if (a.memory.total_gb - b.memory.total_gb).abs() > 0.5 {
117 return true;
118 }
119 false
120}
121
122pub fn check_for_changes(last: &SystemProfile) -> Option<SystemProfile> {
129 let current = SystemProfile::detect_fast();
130 if profiles_differ(last, ¤t) {
131 Some(current)
132 } else {
133 None
134 }
135}
136
137#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn watcher_starts_with_valid_profile() {
147 let watcher = SystemProfileWatcher::new(Duration::from_secs(60));
148 let profile = watcher.current_profile();
149 assert!(profile.is_some());
150 assert!(!profile.unwrap().gpus.is_empty());
151 }
152
153 #[test]
154 fn watcher_subscribe_receives_initial() {
155 let watcher = SystemProfileWatcher::new(Duration::from_secs(60));
156 let mut rx = watcher.subscribe();
157 let event = rx.try_recv();
159 assert!(
160 event.is_ok(),
161 "should have initial event: {:?}",
162 event.err()
163 );
164 match event.unwrap() {
165 ProfileChange::Updated(_) => {} _ => panic!("expected Updated event"),
167 }
168 }
169
170 #[test]
171 fn poll_once_does_not_panic() {
172 let watcher = SystemProfileWatcher::new(Duration::from_secs(60));
173 watcher.poll_once(); }
175
176 #[test]
177 fn check_for_changes_returns_none_when_unchanged() {
178 let profile = SystemProfile::detect_fast();
179 let result = check_for_changes(&profile);
180 let _ = result;
183 }
184
185 #[test]
186 fn profiles_differ_detects_gpu_count_change() {
187 let a = SystemProfile::detect_fast();
188 let mut b = a.clone();
189 b.gpus.clear();
190 assert!(profiles_differ(&a, &b));
191 }
192}