1use crate::proto::{send_msg, Out};
9use serde_json::{json, Value};
10use std::path::Path;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::sync::Arc;
13use std::thread::JoinHandle;
14use std::time::{Duration, Instant};
15
16pub struct Monitor {
18 stop: Arc<AtomicBool>,
19 handle: Option<JoinHandle<()>>,
20}
21
22impl Monitor {
23 pub fn start(out: &Out, interval_ms: u64, id: String) -> Monitor {
27 let stop = Arc::new(AtomicBool::new(false));
28 let s2 = stop.clone();
29 let o = out.clone();
30 let interval = Duration::from_millis(interval_ms.max(250));
31 let handle = std::thread::spawn(move || stream(&o, &s2, interval, &id));
32 Monitor {
33 stop,
34 handle: Some(handle),
35 }
36 }
37}
38
39impl Drop for Monitor {
40 fn drop(&mut self) {
41 self.stop.store(true, Ordering::Relaxed);
42 if let Some(h) = self.handle.take() {
43 let _ = h.join();
44 }
45 }
46}
47
48pub fn snapshot(
54 dt: f64,
55 nets: &sysinfo::Networks,
56 disks: &sysinfo::Disks,
57 sys: &sysinfo::System,
58) -> Value {
59 use sysinfo::{Components, System};
60 let mut d = serde_json::Map::new();
61 d.insert("cpu".into(), json!(sys.global_cpu_usage().round() as i64));
62 let (mu, mt) = (sys.used_memory(), sys.total_memory());
63 d.insert(
64 "mem".into(),
65 json!({"u": mu, "t": mt, "p": (mu * 100).checked_div(mt).unwrap_or(0)}),
66 );
67 let (su, st) = (sys.used_swap(), sys.total_swap());
68 if st > 0 {
69 d.insert("swap".into(), json!({"u": su, "t": st, "p": su * 100 / st}));
70 }
71 let la = System::load_average();
72 d.insert(
73 "load".into(),
74 json!([round2(la.one), round2(la.five), round2(la.fifteen)]),
75 );
76 d.insert("uptime".into(), json!(System::uptime()));
77
78 if let Some(root) = disks.iter().find(|k| k.mount_point() == Path::new("/")) {
79 let (t, a) = (root.total_space(), root.available_space());
80 if t > 0 {
81 d.insert(
82 "disk".into(),
83 json!({"u": t - a, "t": t, "p": (t - a) * 100 / t}),
84 );
85 }
86 }
87 let (mut ior, mut iow) = (0u64, 0u64);
91 for k in disks.iter() {
92 let u = k.usage();
93 ior += u.read_bytes;
94 iow += u.written_bytes;
95 }
96 d.insert(
97 "io".into(),
98 json!({"r": (ior as f64 / dt) as u64, "w": (iow as f64 / dt) as u64}),
99 );
100
101 let (mut up, mut down) = (0u64, 0u64);
102 for (_, n) in nets {
103 up += n.transmitted();
104 down += n.received();
105 }
106 d.insert(
107 "net".into(),
108 json!({"up": (up as f64 / dt) as u64, "down": (down as f64 / dt) as u64}),
109 );
110
111 let comps = Components::new_with_refreshed_list();
112 let mut tmax = f32::MIN;
113 for c in &comps {
114 if let Some(t) = c.temperature() {
115 if t > tmax {
116 tmax = t;
117 }
118 }
119 }
120 if tmax > f32::MIN {
121 d.insert("temp".into(), json!(tmax.round() as i64));
122 }
123 if let Some(h) = System::host_name() {
124 d.insert("host".into(), json!(h.split('.').next().unwrap_or(&h)));
125 }
126 if let Some(ip) = local_ip() {
127 d.insert("lip".into(), json!(ip));
128 }
129 if let Some(b) = battery() {
130 d.insert("batt".into(), b);
131 }
132 Value::Object(d)
133}
134
135fn stream(out: &Out, stop: &AtomicBool, interval: Duration, id: &str) {
136 use sysinfo::{Disks, Networks, System};
137 let mut sys = System::new();
138 let mut nets = Networks::new_with_refreshed_list();
139 let mut disks = Disks::new_with_refreshed_list();
140 let mut pip = public_ip();
141 let mut last = Instant::now();
142 let mut ticks: u64 = 0;
143 while !stop.load(Ordering::Relaxed) {
144 sys.refresh_cpu_usage();
145 sys.refresh_memory();
146 nets.refresh(true);
147 disks.refresh(true);
148 let dt = last.elapsed().as_secs_f64().max(0.1);
149 last = Instant::now();
150
151 let mut snap = snapshot(dt, &nets, &disks, &sys);
152 if let Some(ref p) = pip {
153 if let Some(obj) = snap.as_object_mut() {
154 obj.insert("pip".into(), json!(p));
155 }
156 }
157 let mut frame = json!({ "sys": snap });
158 if !id.is_empty() {
159 frame["id"] = json!(id);
160 }
161 if send_msg(out, &frame).is_err() {
162 return; }
164 ticks += 1;
165 if ticks.is_multiple_of(60) {
166 if let Some(p) = public_ip() {
167 pip = Some(p);
168 }
169 }
170 let mut slept = Duration::ZERO;
172 while slept < interval && !stop.load(Ordering::Relaxed) {
173 let slice = Duration::from_millis(100).min(interval - slept);
174 std::thread::sleep(slice);
175 slept += slice;
176 }
177 }
178}
179
180fn round2(x: f64) -> f64 {
181 (x * 100.0).round() / 100.0
182}
183
184use crate::osops::local_ip;
185
186fn public_ip() -> Option<String> {
187 use std::io::{Read, Write};
188 let mut s = std::net::TcpStream::connect("api.ipify.org:80").ok()?;
189 s.set_read_timeout(Some(Duration::from_secs(3))).ok();
190 s.write_all(b"GET / HTTP/1.0\r\nHost: api.ipify.org\r\nConnection: close\r\n\r\n")
191 .ok()?;
192 let mut buf = String::new();
193 s.read_to_string(&mut buf).ok();
194 buf.rsplit("\r\n\r\n")
195 .next()
196 .map(|b| b.trim().to_string())
197 .filter(|b| !b.is_empty())
198}
199
200#[cfg(target_os = "macos")]
204fn battery() -> Option<Value> {
205 let out = std::process::Command::new("pmset")
206 .args(["-g", "batt"])
207 .output()
208 .ok()?;
209 let s = String::from_utf8_lossy(&out.stdout);
210 let idx = s.find('%')?;
211 let start = s[..idx]
212 .rfind(|c: char| !c.is_ascii_digit())
213 .map(|i| i + 1)
214 .unwrap_or(0);
215 let pct: i64 = s[start..idx].parse().ok()?;
216 let c = s.contains("AC Power") || s.contains("charging") || s.contains("charged");
217 Some(json!({"p": pct, "c": c}))
218}
219
220#[cfg(target_os = "linux")]
224fn battery() -> Option<Value> {
225 use std::fs;
226 let dir = fs::read_dir("/sys/class/power_supply").ok()?;
227 let mut pct: Option<i64> = None;
228 let mut on_ac = false;
229 for entry in dir.flatten() {
230 let p = entry.path();
231 let kind = fs::read_to_string(p.join("type")).unwrap_or_default();
232 match kind.trim() {
233 "Battery" => {
234 if pct.is_none() {
235 pct = fs::read_to_string(p.join("capacity"))
236 .ok()
237 .and_then(|c| c.trim().parse().ok());
238 }
239 let status = fs::read_to_string(p.join("status")).unwrap_or_default();
240 if matches!(status.trim(), "Charging" | "Full") {
241 on_ac = true;
242 }
243 }
244 "Mains" if fs::read_to_string(p.join("online")).is_ok_and(|s| s.trim() == "1") => {
245 on_ac = true;
246 }
247 _ => {}
248 }
249 }
250 Some(json!({"p": pct?, "c": on_ac}))
251}
252
253#[cfg(target_os = "windows")]
256fn battery() -> Option<Value> {
257 #[repr(C)]
258 struct SystemPowerStatus {
259 ac_line_status: u8,
260 battery_flag: u8,
261 battery_life_percent: u8,
262 system_status_flag: u8,
263 battery_life_time: u32,
264 battery_full_life_time: u32,
265 }
266 #[link(name = "kernel32")]
267 extern "system" {
268 fn GetSystemPowerStatus(status: *mut SystemPowerStatus) -> i32;
269 }
270 let mut s = SystemPowerStatus {
271 ac_line_status: 0,
272 battery_flag: 0,
273 battery_life_percent: 0,
274 system_status_flag: 0,
275 battery_life_time: 0,
276 battery_full_life_time: 0,
277 };
278 if unsafe { GetSystemPowerStatus(&mut s) } == 0 {
280 return None;
281 }
282 if s.battery_flag == 128 || s.battery_life_percent == 255 {
283 return None; }
285 Some(json!({"p": s.battery_life_percent as i64, "c": s.ac_line_status == 1}))
286}
287
288#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
289fn battery() -> Option<Value> {
290 None
291}