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(dt: f64, nets: &sysinfo::Networks, sys: &sysinfo::System) -> Value {
51 use sysinfo::{Components, Disks, System};
52 let mut d = serde_json::Map::new();
53 d.insert("cpu".into(), json!(sys.global_cpu_usage().round() as i64));
54 let (mu, mt) = (sys.used_memory(), sys.total_memory());
55 d.insert(
56 "mem".into(),
57 json!({"u": mu, "t": mt, "p": (mu * 100).checked_div(mt).unwrap_or(0)}),
58 );
59 let (su, st) = (sys.used_swap(), sys.total_swap());
60 if st > 0 {
61 d.insert("swap".into(), json!({"u": su, "t": st, "p": su * 100 / st}));
62 }
63 let la = System::load_average();
64 d.insert(
65 "load".into(),
66 json!([round2(la.one), round2(la.five), round2(la.fifteen)]),
67 );
68 d.insert("uptime".into(), json!(System::uptime()));
69
70 let disks = Disks::new_with_refreshed_list();
71 if let Some(root) = disks.iter().find(|k| k.mount_point() == Path::new("/")) {
72 let (t, a) = (root.total_space(), root.available_space());
73 if t > 0 {
74 d.insert(
75 "disk".into(),
76 json!({"u": t - a, "t": t, "p": (t - a) * 100 / t}),
77 );
78 }
79 }
80 let (mut up, mut down) = (0u64, 0u64);
81 for (_, n) in nets {
82 up += n.transmitted();
83 down += n.received();
84 }
85 d.insert(
86 "net".into(),
87 json!({"up": (up as f64 / dt) as u64, "down": (down as f64 / dt) as u64}),
88 );
89
90 let comps = Components::new_with_refreshed_list();
91 let mut tmax = f32::MIN;
92 for c in &comps {
93 if let Some(t) = c.temperature() {
94 if t > tmax {
95 tmax = t;
96 }
97 }
98 }
99 if tmax > f32::MIN {
100 d.insert("temp".into(), json!(tmax.round() as i64));
101 }
102 if let Some(h) = System::host_name() {
103 d.insert("host".into(), json!(h.split('.').next().unwrap_or(&h)));
104 }
105 if let Some(ip) = local_ip() {
106 d.insert("lip".into(), json!(ip));
107 }
108 if let Some(b) = battery() {
109 d.insert("batt".into(), b);
110 }
111 Value::Object(d)
112}
113
114fn stream(out: &Out, stop: &AtomicBool, interval: Duration, id: &str) {
115 use sysinfo::{Networks, System};
116 let mut sys = System::new();
117 let mut nets = Networks::new_with_refreshed_list();
118 let mut pip = public_ip();
119 let mut last = Instant::now();
120 let mut ticks: u64 = 0;
121 while !stop.load(Ordering::Relaxed) {
122 sys.refresh_cpu_usage();
123 sys.refresh_memory();
124 nets.refresh(true);
125 let dt = last.elapsed().as_secs_f64().max(0.1);
126 last = Instant::now();
127
128 let mut snap = snapshot(dt, &nets, &sys);
129 if let Some(ref p) = pip {
130 if let Some(obj) = snap.as_object_mut() {
131 obj.insert("pip".into(), json!(p));
132 }
133 }
134 let mut frame = json!({ "sys": snap });
135 if !id.is_empty() {
136 frame["id"] = json!(id);
137 }
138 if send_msg(out, &frame).is_err() {
139 return; }
141 ticks += 1;
142 if ticks.is_multiple_of(60) {
143 if let Some(p) = public_ip() {
144 pip = Some(p);
145 }
146 }
147 let mut slept = Duration::ZERO;
149 while slept < interval && !stop.load(Ordering::Relaxed) {
150 let slice = Duration::from_millis(100).min(interval - slept);
151 std::thread::sleep(slice);
152 slept += slice;
153 }
154 }
155}
156
157fn round2(x: f64) -> f64 {
158 (x * 100.0).round() / 100.0
159}
160
161use crate::osops::local_ip;
162
163fn public_ip() -> Option<String> {
164 use std::io::{Read, Write};
165 let mut s = std::net::TcpStream::connect("api.ipify.org:80").ok()?;
166 s.set_read_timeout(Some(Duration::from_secs(3))).ok();
167 s.write_all(b"GET / HTTP/1.0\r\nHost: api.ipify.org\r\nConnection: close\r\n\r\n")
168 .ok()?;
169 let mut buf = String::new();
170 s.read_to_string(&mut buf).ok();
171 buf.rsplit("\r\n\r\n")
172 .next()
173 .map(|b| b.trim().to_string())
174 .filter(|b| !b.is_empty())
175}
176
177#[cfg(target_os = "macos")]
178fn battery() -> Option<Value> {
179 let out = std::process::Command::new("pmset")
180 .args(["-g", "batt"])
181 .output()
182 .ok()?;
183 let s = String::from_utf8_lossy(&out.stdout);
184 let idx = s.find('%')?;
185 let start = s[..idx]
186 .rfind(|c: char| !c.is_ascii_digit())
187 .map(|i| i + 1)
188 .unwrap_or(0);
189 let pct: i64 = s[start..idx].parse().ok()?;
190 let c = s.contains("AC Power") || s.contains("charging") || s.contains("charged");
191 Some(json!({"p": pct, "c": c}))
192}
193#[cfg(not(target_os = "macos"))]
194fn battery() -> Option<Value> {
195 None
196}