From eb95d9593a4c44007e3df187c3e6ca26f16a00bc Mon Sep 17 00:00:00 2001 From: ddidderr Date: Thu, 13 Mar 2025 23:23:36 +0100 Subject: [PATCH] [feat] config file. also: extreme over-engineering --- src/cfg.rs | 64 +++++++++++++++++++++++++++++ src/macros.rs | 57 ++++++++++++++++++++++++++ src/main.rs | 106 ++++++++++++------------------------------------- src/math.rs | 10 +++++ src/measure.rs | 82 ++++++++++++++++++++++++++++++++++++++ src/sysfs.rs | 40 +++++++++++++++++++ 6 files changed, 279 insertions(+), 80 deletions(-) create mode 100644 src/cfg.rs create mode 100644 src/macros.rs create mode 100644 src/math.rs create mode 100644 src/measure.rs create mode 100644 src/sysfs.rs diff --git a/src/cfg.rs b/src/cfg.rs new file mode 100644 index 0000000..e795f1e --- /dev/null +++ b/src/cfg.rs @@ -0,0 +1,64 @@ +use std::{ + env, + fs::File, + io::{BufRead as _, BufReader}, + path::PathBuf, + time::Duration, +}; + +use eyre::ContextCompat as _; + +pub(crate) struct Config { + pub(crate) netdev: Option, + pub(crate) interval: Duration, +} + +impl Config { + const CONFIG_FILE: &'static str = ".config/pfs/linkspeed"; + + pub(crate) fn from_file() -> eyre::Result { + let config_file = PathBuf::from(env::var("HOME")?).join(Self::CONFIG_FILE); + + let mut config = Self::default(); + + if let Ok(file) = File::open(&config_file) { + let file = BufReader::new(file); + + for line in file.lines() { + let line = line?; + + match &line { + line if line.starts_with("netdev") => { + config.netdev = Some(Self::read_config_value(line)?.to_string()); + } + line if line.starts_with("interval") => { + let interval = Self::read_config_value(line)?.parse::()?; + config.interval = Duration::from_secs_f64(interval); + } + _ => {} + } + } + } + + Ok(config) + } + + fn read_config_value(line: &str) -> eyre::Result<&str> { + let val = line + .split('=') + .nth(1) + .wrap_err("failed to parse value".to_string())? + .trim(); + + Ok(val) + } +} + +impl Default for Config { + fn default() -> Self { + Config { + netdev: None, + interval: Duration::from_secs(1), + } + } +} diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..5966668 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,57 @@ +mod byte_conversion { + #[macro_export] + macro_rules! kbit { + ($bytes:expr) => { + kbyte!($bytes) * 8.0 + }; + } + + #[macro_export] + macro_rules! mbit { + ($bytes:expr) => { + mbyte!($bytes) * 8.0 + }; + } + + #[macro_export] + macro_rules! gbit { + ($bytes:expr) => { + gbyte!($bytes) * 8.0 + }; + } + + #[macro_export] + macro_rules! tbit { + ($bytes:expr) => { + tbyte!($bytes) * 8.0 + }; + } + + #[macro_export] + macro_rules! kbyte { + ($bytes:expr) => { + $bytes / 1024.0 + }; + } + + #[macro_export] + macro_rules! mbyte { + ($bytes:expr) => { + kbyte!($bytes) / 1024.0 + }; + } + + #[macro_export] + macro_rules! gbyte { + ($bytes:expr) => { + mbyte!($bytes) / 1024.0 + }; + } + + #[macro_export] + macro_rules! tbyte { + ($bytes:expr) => { + gbyte!($bytes) / 1024.0 + }; + } +} diff --git a/src/main.rs b/src/main.rs index 19bc9a8..1cb5203 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,92 +1,38 @@ -use std::{ - env, - fs::File, - io::{self, Read, Seek, SeekFrom}, - thread::sleep, - time::{Duration, Instant}, -}; +mod cfg; +mod macros; +mod math; +mod measure; +mod sysfs; -struct LinkSpeed { - rx: File, - tx: File, - rx_bytes: u64, - tx_bytes: u64, - time: Instant, -} +use std::{env, thread::sleep}; -impl LinkSpeed { - pub fn new(dev: String) -> io::Result { - let rx_bytes_file = File::open(format!("/sys/class/net/{dev}/statistics/rx_bytes"))?; - let tx_bytes_file = File::open(format!("/sys/class/net/{dev}/statistics/tx_bytes"))?; +use crate::{cfg::Config, measure::LinkSpeed}; - Ok(Self { - rx: rx_bytes_file, - tx: tx_bytes_file, - rx_bytes: 0, - tx_bytes: 0, - time: Instant::now(), - }) - } +fn main() -> eyre::Result<()> { + let config = Config::from_file()?; - fn read_bytes(file: &mut File) -> io::Result { - file.seek(SeekFrom::Start(0))?; - let mut buf = String::new(); - file.read_to_string(&mut buf)?; - Ok(buf.trim().parse().unwrap_or(0)) - } + let arg_netdev = env::args().nth(1); - fn update(&mut self) -> io::Result<(u64, u64)> { - let rx_bytes = Self::read_bytes(&mut self.rx)?; - let tx_bytes = Self::read_bytes(&mut self.tx)?; + // prefer given argument over the config file + #[allow(clippy::match_same_arms)] + let netdev_name = match (config.netdev, arg_netdev) { + (Some(_), Some(na)) => na, + (Some(nc), None) => nc, + (None, Some(na)) => na, + (None, None) => eyre::bail!("No network device specified"), + }; - let mut rx_diff = rx_bytes - self.rx_bytes; - let mut tx_diff = tx_bytes - self.tx_bytes; + let link_speed = LinkSpeed::new(&netdev_name)?; - if self.rx_bytes == 0 { - self.rx_bytes = rx_bytes; - rx_diff = 0; - } - - if self.tx_bytes == 0 { - self.tx_bytes = tx_bytes; - tx_diff = 0; - } - - self.rx_bytes = rx_bytes; - self.tx_bytes = tx_bytes; - - Ok((rx_diff, tx_diff)) - } - - pub fn get_measurement(&mut self) -> (f64, f64) { - let elapsed = self.time.elapsed().as_secs_f64(); - let (rx, tx) = self.update().unwrap(); - let rx_speed = rx as f64 / elapsed; - let tx_speed = tx as f64 / elapsed; - self.time = Instant::now(); - (rx_speed, tx_speed) - } -} - -impl Iterator for LinkSpeed { - type Item = (f64, f64); - - fn next(&mut self) -> Option { - Some(self.get_measurement()) - } -} - -fn main() { - let netdev_name = env::args().nth(1).expect("No network device provided"); - - let link_speed = LinkSpeed::new(netdev_name).expect("Failed to create LinkSpeed object"); - - link_speed.for_each(|(rx_speed, tx_speed)| { + link_speed.for_each(|measurement| { println!( "RX: {:.0} MBit/s, TX: {:.0} MBit/s", - rx_speed / 1024.0 / 1024.0 * 8.0, - tx_speed / 1024.0 / 1024.0 * 8.0 + mbit!(measurement.rx), + mbit!(measurement.tx) ); - sleep(Duration::from_millis(1000)); + + sleep(config.interval); }); + + Ok(()) } diff --git a/src/math.rs b/src/math.rs new file mode 100644 index 0000000..e38572e --- /dev/null +++ b/src/math.rs @@ -0,0 +1,10 @@ +pub(crate) fn safe_u64_to_f64(value: u64) -> eyre::Result { + const MAX_SAFE_INT: u64 = 1 << 53; + + if value < MAX_SAFE_INT { + #[allow(clippy::cast_precision_loss)] + Ok(value as f64) + } else { + eyre::bail!("Value {value} exceeds maximum safe integer, potential precision loss") + } +} diff --git a/src/measure.rs b/src/measure.rs new file mode 100644 index 0000000..2ce9d74 --- /dev/null +++ b/src/measure.rs @@ -0,0 +1,82 @@ +use std::time::Instant; + +use crate::{math::safe_u64_to_f64, sysfs::Netdev}; + +pub(crate) struct LinkSpeed { + netdev: Netdev, + rx_bytes: u64, + tx_bytes: u64, + time: Instant, +} + +pub(crate) struct MeasurementSpeed { + pub(crate) rx: f64, + pub(crate) tx: f64, +} +struct MeasurementDiff { + rx: u64, + tx: u64, +} + +impl LinkSpeed { + pub fn new(name: &str) -> eyre::Result { + let netdev = Netdev::from_name(name)?; + + Ok(Self { + netdev, + rx_bytes: 0, + tx_bytes: 0, + time: Instant::now(), + }) + } + + fn update(&mut self) -> eyre::Result { + let rx_bytes = self.netdev.rx_bytes()?; + let tx_bytes = self.netdev.tx_bytes()?; + + let mut rx_diff = rx_bytes - self.rx_bytes; + let mut tx_diff = tx_bytes - self.tx_bytes; + + if self.rx_bytes == 0 { + self.rx_bytes = rx_bytes; + rx_diff = 0; + } + + if self.tx_bytes == 0 { + self.tx_bytes = tx_bytes; + tx_diff = 0; + } + + self.rx_bytes = rx_bytes; + self.tx_bytes = tx_bytes; + + Ok(MeasurementDiff { + rx: rx_diff, + tx: tx_diff, + }) + } + + pub fn get_measurement(&mut self) -> eyre::Result { + let elapsed = self.time.elapsed().as_secs_f64(); + let diff = self.update()?; + let rx_speed = safe_u64_to_f64(diff.rx)? / elapsed; + let tx_speed = safe_u64_to_f64(diff.tx)? / elapsed; + self.time = Instant::now(); + Ok(MeasurementSpeed { + rx: rx_speed, + tx: tx_speed, + }) + } +} + +impl Iterator for LinkSpeed { + type Item = MeasurementSpeed; + + fn next(&mut self) -> Option { + let measurement = self.get_measurement(); + match measurement { + Ok(measurement) => Some(measurement), + Err(e) => panic!("Error getting measurement: {e}"), + } + } +} diff --git a/src/sysfs.rs b/src/sysfs.rs new file mode 100644 index 0000000..0b395ae --- /dev/null +++ b/src/sysfs.rs @@ -0,0 +1,40 @@ +use std::{ + fs::File, + io::{Read as _, Seek as _, SeekFrom}, +}; + +use eyre::Context as _; + +pub(crate) struct Netdev { + rx_bytes: File, + tx_bytes: File, +} + +impl Netdev { + pub fn from_name(name: &str) -> eyre::Result { + let rx_bytes = Self::open_netdev(name, "rx_bytes")?; + let tx_bytes = Self::open_netdev(name, "tx_bytes")?; + + Ok(Self { rx_bytes, tx_bytes }) + } + + fn open_netdev(dev: &str, stat: &str) -> eyre::Result { + let path = format!("/sys/class/net/{dev}/statistics/{stat}"); + File::open(&path).wrap_err(format!("Failed to open netdev '{dev}'")) + } + + fn read_bytes(file: &mut File) -> eyre::Result { + file.seek(SeekFrom::Start(0))?; + let mut buf = String::new(); + file.read_to_string(&mut buf)?; + Ok(buf.trim().parse().unwrap_or(0)) + } + + pub fn rx_bytes(&mut self) -> eyre::Result { + Self::read_bytes(&mut self.rx_bytes) + } + + pub fn tx_bytes(&mut self) -> eyre::Result { + Self::read_bytes(&mut self.tx_bytes) + } +}