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 crate::hostlog::record("rx", &json!({ "cmd": "sysinfo" }), &frame);
165 if send_msg(out, &frame).is_err() {
166 return; }
168 ticks += 1;
169 if ticks.is_multiple_of(60) {
170 if let Some(p) = public_ip() {
171 pip = Some(p);
172 }
173 }
174 let mut slept = Duration::ZERO;
176 while slept < interval && !stop.load(Ordering::Relaxed) {
177 let slice = Duration::from_millis(100).min(interval - slept);
178 std::thread::sleep(slice);
179 slept += slice;
180 }
181 }
182}
183
184fn round2(x: f64) -> f64 {
185 (x * 100.0).round() / 100.0
186}
187
188use crate::osops::local_ip;
189
190fn public_ip() -> Option<String> {
191 use std::io::{Read, Write};
192 let mut s = std::net::TcpStream::connect("api.ipify.org:80").ok()?;
193 s.set_read_timeout(Some(Duration::from_secs(3))).ok();
194 s.write_all(b"GET / HTTP/1.0\r\nHost: api.ipify.org\r\nConnection: close\r\n\r\n")
195 .ok()?;
196 let mut buf = String::new();
197 s.read_to_string(&mut buf).ok();
198 buf.rsplit("\r\n\r\n")
199 .next()
200 .map(|b| b.trim().to_string())
201 .filter(|b| !b.is_empty())
202}
203
204#[cfg(target_os = "macos")]
208fn battery() -> Option<Value> {
209 let out = std::process::Command::new("pmset")
210 .args(["-g", "batt"])
211 .output()
212 .ok()?;
213 let s = String::from_utf8_lossy(&out.stdout);
214 let idx = s.find('%')?;
215 let start = s[..idx]
216 .rfind(|c: char| !c.is_ascii_digit())
217 .map(|i| i + 1)
218 .unwrap_or(0);
219 let pct: i64 = s[start..idx].parse().ok()?;
220 let c = s.contains("AC Power") || s.contains("charging") || s.contains("charged");
221 Some(json!({"p": pct, "c": c}))
222}
223
224#[cfg(target_os = "linux")]
228fn battery() -> Option<Value> {
229 use std::fs;
230 let dir = fs::read_dir("/sys/class/power_supply").ok()?;
231 let mut pct: Option<i64> = None;
232 let mut on_ac = false;
233 for entry in dir.flatten() {
234 let p = entry.path();
235 let kind = fs::read_to_string(p.join("type")).unwrap_or_default();
236 match kind.trim() {
237 "Battery" => {
238 if pct.is_none() {
239 pct = fs::read_to_string(p.join("capacity"))
240 .ok()
241 .and_then(|c| c.trim().parse().ok());
242 }
243 let status = fs::read_to_string(p.join("status")).unwrap_or_default();
244 if matches!(status.trim(), "Charging" | "Full") {
245 on_ac = true;
246 }
247 }
248 "Mains" if fs::read_to_string(p.join("online")).is_ok_and(|s| s.trim() == "1") => {
249 on_ac = true;
250 }
251 _ => {}
252 }
253 }
254 Some(json!({"p": pct?, "c": on_ac}))
255}
256
257#[cfg(target_os = "windows")]
260fn battery() -> Option<Value> {
261 #[repr(C)]
262 struct SystemPowerStatus {
263 ac_line_status: u8,
264 battery_flag: u8,
265 battery_life_percent: u8,
266 system_status_flag: u8,
267 battery_life_time: u32,
268 battery_full_life_time: u32,
269 }
270 #[link(name = "kernel32")]
271 extern "system" {
272 fn GetSystemPowerStatus(status: *mut SystemPowerStatus) -> i32;
273 }
274 let mut s = SystemPowerStatus {
275 ac_line_status: 0,
276 battery_flag: 0,
277 battery_life_percent: 0,
278 system_status_flag: 0,
279 battery_life_time: 0,
280 battery_full_life_time: 0,
281 };
282 if unsafe { GetSystemPowerStatus(&mut s) } == 0 {
284 return None;
285 }
286 if s.battery_flag == 128 || s.battery_life_percent == 255 {
287 return None; }
289 Some(json!({"p": s.battery_life_percent as i64, "c": s.ac_line_status == 1}))
290}
291
292#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
293fn battery() -> Option<Value> {
294 None
295}