1use std::collections::HashMap;
9use std::process::Command;
10use std::time::{Duration, Instant};
11
12use super::{SysRoots, read_i64, read_trimmed};
13use crate::snapshot::{NetworkStats, WirelessLink};
14
15const WIRELESS_REFRESH: Duration = Duration::from_secs(10);
17
18#[derive(Debug, Default)]
20pub struct NetLinkReader {
21 wireless: HashMap<String, (Instant, Option<WirelessLink>)>,
23}
24
25impl NetLinkReader {
26 pub fn new() -> Self {
28 Self::default()
29 }
30
31 pub fn annotate(&mut self, roots: &SysRoots, networks: &mut [NetworkStats], now: Instant) {
33 for net in networks.iter_mut() {
34 let dir = roots.sys.join("class/net").join(&net.name);
35 net.link_speed_mbps = read_i64(&dir.join("speed"))
36 .filter(|s| *s > 0)
37 .and_then(|s| u32::try_from(s).ok());
38 net.link_up =
39 read_trimmed(&dir.join("operstate")).and_then(|state| match state.as_str() {
40 "up" => Some(true),
41 "down" | "lowerlayerdown" | "dormant" | "notpresent" => Some(false),
42 _ => None,
44 });
45 if dir.join("wireless").is_dir() {
46 net.wireless = self.wireless_link(&net.name, now);
47 }
48 }
49 self.wireless
51 .retain(|name, _| networks.iter().any(|n| &n.name == name));
52 }
53
54 fn wireless_link(&mut self, name: &str, now: Instant) -> Option<WirelessLink> {
55 if let Some((at, link)) = self.wireless.get(name)
56 && now.saturating_duration_since(*at) < WIRELESS_REFRESH
57 {
58 return link.clone();
59 }
60 let link = Command::new("iw")
61 .args(["dev", name, "link"])
62 .output()
63 .ok()
64 .filter(|out| out.status.success())
65 .and_then(|out| parse_iw_link(&String::from_utf8_lossy(&out.stdout)));
66 self.wireless.insert(name.to_owned(), (now, link.clone()));
67 link
68 }
69}
70
71pub(crate) fn parse_iw_link(text: &str) -> Option<WirelessLink> {
73 if !text.trim_start().starts_with("Connected") {
74 return None;
75 }
76 let mut link = WirelessLink::default();
77 for line in text.lines() {
78 let Some((key, value)) = line.trim().split_once(':') else {
79 continue;
80 };
81 let first = value.split_whitespace().next();
83 match key {
84 "rx bitrate" => link.rx_bitrate_mbps = first.and_then(|v| v.parse().ok()),
85 "tx bitrate" => link.tx_bitrate_mbps = first.and_then(|v| v.parse().ok()),
86 "signal" => link.signal_dbm = first.and_then(|v| v.parse().ok()),
87 _ => {}
88 }
89 }
90 Some(link)
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use crate::linux::testutil::write;
97
98 const IW_CONNECTED: &str = "\
99Connected to 04:42:1a:e4:c4:34 (on wlp7s0)
100\tSSID: example
101\tfreq: 5500.0
102\tsignal: -74 dBm
103\trx bitrate: 576.4 MBit/s 160MHz HE-MCS 5 HE-NSS 1 HE-GI 0 HE-DCM 0
104\ttx bitrate: 490.0 MBit/s 160MHz HE-MCS 3 HE-NSS 2 HE-GI 2 HE-DCM 0
105";
106
107 #[test]
108 fn parses_iw_bitrates_and_signal() {
109 let link = parse_iw_link(IW_CONNECTED).unwrap();
110 assert_eq!(link.rx_bitrate_mbps, Some(576.4));
111 assert_eq!(link.tx_bitrate_mbps, Some(490.0));
112 assert_eq!(link.signal_dbm, Some(-74));
113 assert!(parse_iw_link("Not connected.\n").is_none());
114 }
115
116 #[test]
117 fn reads_wired_speed_and_operstate() {
118 let tmp = tempfile::tempdir().unwrap();
119 let root = tmp.path();
120 write(root, "sys/class/net/enp9s0/speed", "10000\n");
121 write(root, "sys/class/net/enp9s0/operstate", "up\n");
122 write(root, "sys/class/net/enp8s0/speed", "-1\n");
124 write(root, "sys/class/net/enp8s0/operstate", "down\n");
125 write(root, "sys/class/net/tailscale0/operstate", "unknown\n");
126 let roots = SysRoots {
127 sys: root.join("sys"),
128 proc: root.join("proc"),
129 };
130 let mut networks: Vec<NetworkStats> = ["enp9s0", "enp8s0", "tailscale0"]
131 .into_iter()
132 .map(|name| NetworkStats {
133 name: name.to_owned(),
134 ..Default::default()
135 })
136 .collect();
137
138 NetLinkReader::new().annotate(&roots, &mut networks, Instant::now());
139 assert_eq!(networks[0].link_speed_mbps, Some(10_000));
140 assert_eq!(networks[0].link_up, Some(true));
141 assert_eq!(networks[1].link_speed_mbps, None);
142 assert_eq!(networks[1].link_up, Some(false));
143 assert_eq!(networks[2].link_up, None);
144 assert!(networks[0].wireless.is_none());
145 }
146}