1use serde::Serialize;
2use std::sync::Mutex;
3use std::time::{Duration, Instant};
4use sysinfo::Networks;
5
6#[derive(Debug, Clone, Serialize)]
7pub struct NetworkStats {
8 pub upload_mbps: f64,
9 pub download_mbps: f64,
10 pub upload_active: bool,
11 pub download_active: bool,
12 pub available: bool,
13}
14
15struct MonitorState {
18 networks: Networks,
19 selected_interface: Option<String>,
20 last_update: Instant,
22 last_stats: (u64, u64),
24 last_rates: (f64, f64),
26 last_active: (Instant, Instant),
28 last_rate_update: (Instant, Instant),
30 last_rate_stats: (u64, u64),
32}
33
34pub struct NetworkMonitor {
35 state: Mutex<MonitorState>,
36}
37
38impl NetworkMonitor {
39 const REFRESH_INTERVAL: Duration = Duration::from_millis(100);
40 const RATE_INTERVAL: Duration = Duration::from_millis(400);
41 const ACTIVE_HOLD: Duration = Duration::from_millis(200);
42
43 pub fn new() -> Self {
44 let mut networks = Networks::new_with_refreshed_list();
45 networks.refresh(true);
46
47 let (rx, tx, _) = sum_traffic(&networks, None);
48 let now = Instant::now();
49 let inactive = now - Self::ACTIVE_HOLD - Self::ACTIVE_HOLD;
50
51 Self {
52 state: Mutex::new(MonitorState {
53 networks,
54 selected_interface: None,
55 last_update: now,
56 last_stats: (rx, tx),
57 last_rates: (0.0, 0.0),
58 last_active: (inactive, inactive),
59 last_rate_update: (now, now),
60 last_rate_stats: (rx, tx),
61 }),
62 }
63 }
64
65 fn lock(&self) -> std::sync::MutexGuard<'_, MonitorState> {
66 self.state.lock().unwrap_or_else(|e| e.into_inner())
67 }
68
69 pub fn set_interface(&self, interface: Option<String>) {
70 let mut s = self.lock();
71 s.selected_interface = interface;
72 s.networks.refresh(true);
73 let (rx, tx, _) = sum_traffic(&s.networks, s.selected_interface.as_deref());
74
75 let now = Instant::now();
76 let inactive = now - Self::ACTIVE_HOLD - Self::ACTIVE_HOLD;
77
78 s.last_update = now;
79 s.last_stats = (rx, tx);
80 s.last_rates = (0.0, 0.0);
81 s.last_active = (inactive, inactive);
82 s.last_rate_update = (now, now);
83 s.last_rate_stats = (rx, tx);
84 }
85
86 pub fn selected_interface(&self) -> Option<String> {
87 self.lock().selected_interface.clone()
88 }
89
90 pub fn available_interfaces(&self) -> Vec<String> {
91 let mut s = self.lock();
92 s.networks.refresh(true);
93 let mut names: Vec<String> = s.networks.keys().map(|n| n.to_string()).collect();
94 names.sort_unstable();
95 names
96 }
97
98 pub fn get_stats(&self) -> NetworkStats {
99 let mut s = self.lock();
100 let now = Instant::now();
101 let elapsed = now.duration_since(s.last_update);
102
103 if elapsed < Self::REFRESH_INTERVAL {
106 let (upload_mbps, download_mbps) = s.last_rates;
107 return NetworkStats {
108 upload_mbps,
109 download_mbps,
110 upload_active: now.duration_since(s.last_active.0) < Self::ACTIVE_HOLD,
111 download_active: now.duration_since(s.last_active.1) < Self::ACTIVE_HOLD,
112 available: true,
113 };
114 }
115
116 s.networks.refresh(true);
117 let (current_rx, current_tx, available) =
118 sum_traffic(&s.networks, s.selected_interface.as_deref());
119 if !available {
120 return NetworkStats {
121 upload_mbps: 0.0,
122 download_mbps: 0.0,
123 upload_active: false,
124 download_active: false,
125 available: false,
126 };
127 }
128
129 let (seen_rx, seen_tx) = s.last_stats;
131 if current_tx.saturating_sub(seen_tx) > 0 {
132 s.last_active.0 = now;
133 }
134 if current_rx.saturating_sub(seen_rx) > 0 {
135 s.last_active.1 = now;
136 }
137 s.last_stats = (current_rx, current_tx);
138
139 let (mut upload_mbps, mut download_mbps) = s.last_rates;
142 let (mut last_upload, mut last_download) = s.last_rate_update;
143 let (mut rate_rx, mut rate_tx) = s.last_rate_stats;
144
145 if now.duration_since(last_upload) >= Self::RATE_INTERVAL {
146 upload_mbps = bytes_to_mbps(
147 current_tx.saturating_sub(rate_tx),
148 now.duration_since(last_upload),
149 );
150 rate_tx = current_tx;
151 last_upload = now;
152 }
153 if now.duration_since(last_download) >= Self::RATE_INTERVAL {
154 download_mbps = bytes_to_mbps(
155 current_rx.saturating_sub(rate_rx),
156 now.duration_since(last_download),
157 );
158 rate_rx = current_rx;
159 last_download = now;
160 }
161
162 s.last_update = now;
163 s.last_rate_update = (last_upload, last_download);
164 s.last_rate_stats = (rate_rx, rate_tx);
165 s.last_rates = (upload_mbps, download_mbps);
166
167 NetworkStats {
168 upload_mbps,
169 download_mbps,
170 upload_active: now.duration_since(s.last_active.0) < Self::ACTIVE_HOLD,
171 download_active: now.duration_since(s.last_active.1) < Self::ACTIVE_HOLD,
172 available: true,
173 }
174 }
175}
176
177impl Default for NetworkMonitor {
178 fn default() -> Self {
179 Self::new()
180 }
181}
182
183fn sum_traffic(networks: &Networks, selected: Option<&str>) -> (u64, u64, bool) {
184 if let Some(selected) = selected {
185 for (name, network) in networks {
186 if name == selected {
187 return (network.total_received(), network.total_transmitted(), true);
188 }
189 }
190 return (0, 0, false);
191 }
192
193 let mut rx = 0;
194 let mut tx = 0;
195 let mut any = false;
196 for (_, network) in networks {
197 any = true;
198 rx += network.total_received();
199 tx += network.total_transmitted();
200 }
201 (rx, tx, any)
202}
203
204fn bytes_to_mbps(bytes: u64, elapsed: Duration) -> f64 {
211 let secs = elapsed.as_secs_f64();
212 if secs <= 0.0 {
213 return 0.0;
214 }
215 (bytes as f64 * 8.0) / (1_000_000.0 * secs)
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn bytes_to_mbps_typical() {
224 assert!((bytes_to_mbps(1_000_000, Duration::from_secs(1)) - 8.0).abs() < 1e-6);
226 }
227
228 #[test]
229 fn bytes_to_mbps_zero_elapsed_safe() {
230 assert_eq!(bytes_to_mbps(1_000_000, Duration::from_secs(0)), 0.0);
231 }
232
233 #[test]
234 fn bytes_to_mbps_reports_true_value_for_gbe() {
235 let actual = bytes_to_mbps(1_250_000_000, Duration::from_secs(1));
238 assert!(
239 (actual - 10_000.0).abs() < 1.0,
240 "expected ~10_000 Mbps, got {actual}"
241 );
242 }
243
244 #[test]
245 fn new_monitor_reports_available_on_refresh() {
246 let monitor = NetworkMonitor::new();
250 let stats = monitor.get_stats();
251 assert!(stats.upload_mbps >= 0.0 && stats.download_mbps >= 0.0);
254 }
255
256 #[test]
257 fn selected_interface_round_trips() {
258 let monitor = NetworkMonitor::new();
259 assert_eq!(monitor.selected_interface(), None);
260 monitor.set_interface(Some("lo0".to_string()));
261 assert_eq!(monitor.selected_interface(), Some("lo0".to_string()));
262 monitor.set_interface(None);
263 assert_eq!(monitor.selected_interface(), None);
264 }
265}