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());
73 d.insert(
74 "swap".into(),
75 json!({"u": su, "t": st, "p": (su * 100).checked_div(st).unwrap_or(0)}),
76 );
77 let la = System::load_average();
78 d.insert(
79 "load".into(),
80 json!([round2(la.one), round2(la.five), round2(la.fifteen)]),
81 );
82 d.insert("uptime".into(), json!(System::uptime()));
83
84 if let Some(root) = disks.iter().find(|k| k.mount_point() == Path::new("/")) {
85 let (t, a) = (root.total_space(), root.available_space());
86 if t > 0 {
87 d.insert(
88 "disk".into(),
89 json!({"u": t - a, "t": t, "p": (t - a) * 100 / t}),
90 );
91 }
92 }
93 let (mut ior, mut iow) = (0u64, 0u64);
97 for k in disks.iter() {
98 let u = k.usage();
99 ior += u.read_bytes;
100 iow += u.written_bytes;
101 }
102 d.insert(
103 "io".into(),
104 json!({"r": (ior as f64 / dt) as u64, "w": (iow as f64 / dt) as u64}),
105 );
106
107 let (mut up, mut down) = (0u64, 0u64);
108 for (_, n) in nets {
109 up += n.transmitted();
110 down += n.received();
111 }
112 d.insert(
113 "net".into(),
114 json!({"up": (up as f64 / dt) as u64, "down": (down as f64 / dt) as u64}),
115 );
116
117 let comps = Components::new_with_refreshed_list();
118 let mut tmax = f32::MIN;
119 for c in &comps {
120 if let Some(t) = c.temperature() {
121 if t > tmax {
122 tmax = t;
123 }
124 }
125 }
126 if tmax > f32::MIN {
127 d.insert("temp".into(), json!(tmax.round() as i64));
128 }
129 if let Some(h) = System::host_name() {
130 d.insert("host".into(), json!(h.split('.').next().unwrap_or(&h)));
131 }
132 if let Some(ip) = local_ip() {
133 d.insert("lip".into(), json!(ip));
134 }
135 if let Some(b) = battery() {
136 d.insert("batt".into(), b);
137 }
138 Value::Object(d)
139}
140
141fn stream(out: &Out, stop: &AtomicBool, interval: Duration, id: &str) {
142 use sysinfo::{Disks, Networks, System};
143 let mut sys = System::new();
144 let mut nets = Networks::new_with_refreshed_list();
145 let mut disks = Disks::new_with_refreshed_list();
146 let mut pip = public_ip();
147 let mut last = Instant::now();
148 let mut ticks: u64 = 0;
149 while !stop.load(Ordering::Relaxed) {
150 sys.refresh_cpu_usage();
151 sys.refresh_memory();
152 nets.refresh(true);
153 disks.refresh(true);
154 let dt = last.elapsed().as_secs_f64().max(0.1);
155 last = Instant::now();
156
157 let mut snap = snapshot(dt, &nets, &disks, &sys);
158 if let Some(ref p) = pip {
159 if let Some(obj) = snap.as_object_mut() {
160 obj.insert("pip".into(), json!(p));
161 }
162 }
163 let mut frame = json!({ "sys": snap });
164 if !id.is_empty() {
165 frame["id"] = json!(id);
166 }
167 crate::hostlog::record("rx", &json!({ "cmd": "sysinfo" }), &frame);
171 if send_msg(out, &frame).is_err() {
172 return; }
174 ticks += 1;
175 if ticks.is_multiple_of(60) {
176 if let Some(p) = public_ip() {
177 pip = Some(p);
178 }
179 }
180 let mut slept = Duration::ZERO;
182 while slept < interval && !stop.load(Ordering::Relaxed) {
183 let slice = Duration::from_millis(100).min(interval - slept);
184 std::thread::sleep(slice);
185 slept += slice;
186 }
187 }
188}
189
190fn round2(x: f64) -> f64 {
191 (x * 100.0).round() / 100.0
192}
193
194use crate::osops::local_ip;
195
196fn public_ip() -> Option<String> {
197 use std::io::{Read, Write};
198 let mut s = std::net::TcpStream::connect("api.ipify.org:80").ok()?;
199 s.set_read_timeout(Some(Duration::from_secs(3))).ok();
200 s.write_all(b"GET / HTTP/1.0\r\nHost: api.ipify.org\r\nConnection: close\r\n\r\n")
201 .ok()?;
202 let mut buf = String::new();
203 s.read_to_string(&mut buf).ok();
204 buf.rsplit("\r\n\r\n")
205 .next()
206 .map(|b| b.trim().to_string())
207 .filter(|b| !b.is_empty())
208}
209
210#[cfg(target_os = "macos")]
214fn battery() -> Option<Value> {
215 let out = std::process::Command::new("pmset")
216 .args(["-g", "batt"])
217 .output()
218 .ok()?;
219 let s = String::from_utf8_lossy(&out.stdout);
220 let idx = s.find('%')?;
221 let start = s[..idx]
222 .rfind(|c: char| !c.is_ascii_digit())
223 .map(|i| i + 1)
224 .unwrap_or(0);
225 let pct: i64 = s[start..idx].parse().ok()?;
226 let c = s.contains("AC Power") || s.contains("charging") || s.contains("charged");
227 Some(json!({"p": pct, "c": c}))
228}
229
230#[cfg(target_os = "linux")]
234fn battery() -> Option<Value> {
235 use std::fs;
236 let dir = fs::read_dir("/sys/class/power_supply").ok()?;
237 let mut pct: Option<i64> = None;
238 let mut on_ac = false;
239 for entry in dir.flatten() {
240 let p = entry.path();
241 let kind = fs::read_to_string(p.join("type")).unwrap_or_default();
242 match kind.trim() {
243 "Battery" => {
244 if pct.is_none() {
245 pct = fs::read_to_string(p.join("capacity"))
246 .ok()
247 .and_then(|c| c.trim().parse().ok());
248 }
249 let status = fs::read_to_string(p.join("status")).unwrap_or_default();
250 if matches!(status.trim(), "Charging" | "Full") {
251 on_ac = true;
252 }
253 }
254 "Mains" if fs::read_to_string(p.join("online")).is_ok_and(|s| s.trim() == "1") => {
255 on_ac = true;
256 }
257 _ => {}
258 }
259 }
260 Some(json!({"p": pct?, "c": on_ac}))
261}
262
263#[cfg(target_os = "windows")]
266fn battery() -> Option<Value> {
267 #[repr(C)]
268 struct SystemPowerStatus {
269 ac_line_status: u8,
270 battery_flag: u8,
271 battery_life_percent: u8,
272 system_status_flag: u8,
273 battery_life_time: u32,
274 battery_full_life_time: u32,
275 }
276 #[link(name = "kernel32")]
277 extern "system" {
278 fn GetSystemPowerStatus(status: *mut SystemPowerStatus) -> i32;
279 }
280 let mut s = SystemPowerStatus {
281 ac_line_status: 0,
282 battery_flag: 0,
283 battery_life_percent: 0,
284 system_status_flag: 0,
285 battery_life_time: 0,
286 battery_full_life_time: 0,
287 };
288 if unsafe { GetSystemPowerStatus(&mut s) } == 0 {
290 return None;
291 }
292 if s.battery_flag == 128 || s.battery_life_percent == 255 {
293 return None; }
295 Some(json!({"p": s.battery_life_percent as i64, "c": s.ac_line_status == 1}))
296}
297
298#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
299fn battery() -> Option<Value> {
300 None
301}