Compare commits
No commits in common. "main" and "v1.0.0" have entirely different histories.
27
Cargo.lock
generated
27
Cargo.lock
generated
@ -2,31 +2,6 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "eyre"
|
|
||||||
version = "0.6.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
|
|
||||||
dependencies = [
|
|
||||||
"indenter",
|
|
||||||
"once_cell",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indenter"
|
|
||||||
version = "0.3.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linkspeed"
|
name = "linkspeed"
|
||||||
version = "0.1.3"
|
version = "0.1.0"
|
||||||
dependencies = [
|
|
||||||
"eyre",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "once_cell"
|
|
||||||
version = "1.21.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
|
||||||
|
28
Cargo.toml
28
Cargo.toml
@ -1,35 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "linkspeed"
|
name = "linkspeed"
|
||||||
version = "0.1.3"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[lints.rust]
|
|
||||||
unsafe_code = "forbid"
|
|
||||||
|
|
||||||
[lints.clippy]
|
|
||||||
redundant_closure_for_method_calls = "allow"
|
|
||||||
similar_names = "allow"
|
|
||||||
|
|
||||||
pedantic = { level = "warn", priority = -1 }
|
|
||||||
todo = "warn"
|
|
||||||
unwrap_used = "warn"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
eyre = "0.6"
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
debug = true
|
|
||||||
debug-assertions = true
|
|
||||||
overflow-checks = true
|
|
||||||
strip = false
|
|
||||||
lto = false
|
|
||||||
panic = "unwind"
|
|
||||||
codegen-units = 1
|
|
||||||
|
|
||||||
[profile.release-lto]
|
|
||||||
inherits = "release"
|
|
||||||
lto = true
|
lto = true
|
||||||
debug = false
|
debug = false
|
||||||
debug-assertions = false
|
|
||||||
overflow-checks = false
|
|
||||||
strip = true
|
strip = true
|
||||||
|
panic = "unwind"
|
||||||
|
codegen-units = 1
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
group_imports = "StdExternalCrate"
|
|
||||||
imports_granularity = "Crate"
|
|
||||||
imports_layout = "HorizontalVertical"
|
|
64
src/cfg.rs
64
src/cfg.rs
@ -1,64 +0,0 @@
|
|||||||
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<String>,
|
|
||||||
pub(crate) interval: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
const CONFIG_FILE: &'static str = ".config/pfs/linkspeed";
|
|
||||||
|
|
||||||
pub(crate) fn from_file() -> eyre::Result<Self> {
|
|
||||||
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::<f64>()?;
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
109
src/main.rs
109
src/main.rs
@ -1,45 +1,92 @@
|
|||||||
mod cfg;
|
use std::{
|
||||||
mod macros;
|
env,
|
||||||
mod math;
|
fs::File,
|
||||||
mod measure;
|
io::{self, Read, Seek, SeekFrom},
|
||||||
mod sysfs;
|
thread::sleep,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
use std::{env, thread::sleep};
|
struct LinkSpeed {
|
||||||
|
rx: File,
|
||||||
|
tx: File,
|
||||||
|
rx_bytes: u64,
|
||||||
|
tx_bytes: u64,
|
||||||
|
time: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
use sysfs::NetdevError;
|
impl LinkSpeed {
|
||||||
|
pub fn new(dev: String) -> io::Result<Self> {
|
||||||
|
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<()> {
|
fn read_bytes(file: &mut File) -> io::Result<u64> {
|
||||||
let config = Config::from_file()?;
|
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
|
let mut rx_diff = rx_bytes - self.rx_bytes;
|
||||||
#[allow(clippy::match_same_arms)]
|
let mut tx_diff = tx_bytes - self.tx_bytes;
|
||||||
let netdev_name = match (config.netdev, arg_netdev) {
|
|
||||||
(Some(_), Some(na)) => na,
|
if self.rx_bytes == 0 {
|
||||||
(Some(nc), None) => nc,
|
self.rx_bytes = rx_bytes;
|
||||||
(None, Some(na)) => na,
|
rx_diff = 0;
|
||||||
(None, None) => {
|
|
||||||
eyre::bail!(
|
|
||||||
"No network device specified. Please provide a network device name as an argument or in the config file.\n\n{}",
|
|
||||||
NetdevError::available_netdevs_msg()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
let link_speed = LinkSpeed::new(&netdev_name)?;
|
if self.tx_bytes == 0 {
|
||||||
|
self.tx_bytes = tx_bytes;
|
||||||
|
tx_diff = 0;
|
||||||
|
}
|
||||||
|
|
||||||
link_speed.for_each(|measurement| {
|
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<Self::Item> {
|
||||||
|
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)| {
|
||||||
println!(
|
println!(
|
||||||
"RX: {:.0} MBit/s, TX: {:.0} MBit/s",
|
"RX: {:.0} MBit/s, TX: {:.0} MBit/s",
|
||||||
mbit!(measurement.rx),
|
rx_speed / 1024.0 / 1024.0 * 8.0,
|
||||||
mbit!(measurement.tx)
|
tx_speed / 1024.0 / 1024.0 * 8.0
|
||||||
);
|
);
|
||||||
|
sleep(Duration::from_millis(1000));
|
||||||
sleep(config.interval);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
10
src/math.rs
10
src/math.rs
@ -1,10 +0,0 @@
|
|||||||
pub(crate) fn safe_u64_to_f64(value: u64) -> eyre::Result<f64> {
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
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<Self> {
|
|
||||||
let netdev = Netdev::from_name(name)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
netdev,
|
|
||||||
rx_bytes: 0,
|
|
||||||
tx_bytes: 0,
|
|
||||||
time: Instant::now(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self) -> eyre::Result<MeasurementDiff> {
|
|
||||||
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<MeasurementSpeed> {
|
|
||||||
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<Self::Item> {
|
|
||||||
let measurement = self.get_measurement();
|
|
||||||
match measurement {
|
|
||||||
Ok(measurement) => Some(measurement),
|
|
||||||
Err(e) => panic!("Error getting measurement: {e}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
102
src/sysfs.rs
102
src/sysfs.rs
@ -1,102 +0,0 @@
|
|||||||
use std::{
|
|
||||||
fs::File,
|
|
||||||
io::{Read as _, Seek as _, SeekFrom},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct Netdev {
|
|
||||||
rx_bytes: File,
|
|
||||||
tx_bytes: File,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum NetdevError {
|
|
||||||
NotFound { name: String },
|
|
||||||
IoError { name: String, err: std::io::Error },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NetdevError {
|
|
||||||
pub fn available_netdevs_msg() -> String {
|
|
||||||
let list = Netdev::list()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_iter()
|
|
||||||
.map(|dev| format!(" {dev}"))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
format!("Available network devices:\n{list}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for NetdevError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
NetdevError::NotFound { name } => write!(
|
|
||||||
f,
|
|
||||||
"Netdev '{name}' not found.\n\n{}",
|
|
||||||
Self::available_netdevs_msg()
|
|
||||||
),
|
|
||||||
NetdevError::IoError { name, err } => {
|
|
||||||
write!(f, "I/O Error occurred for netdev '{name}': {err}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Netdev {
|
|
||||||
const SYS_CLASS_NET: &'static str = "/sys/class/net";
|
|
||||||
|
|
||||||
pub fn from_name(name: &str) -> eyre::Result<Self> {
|
|
||||||
let rx_bytes = Self::open_netdev_stat(name, "rx_bytes")?;
|
|
||||||
let tx_bytes = Self::open_netdev_stat(name, "tx_bytes")?;
|
|
||||||
|
|
||||||
Ok(Self { rx_bytes, tx_bytes })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_netdev_stat(name: &str, stat: &str) -> eyre::Result<File> {
|
|
||||||
let sys_class_net = Self::SYS_CLASS_NET;
|
|
||||||
let path = format!("{sys_class_net}/{name}/statistics/{stat}");
|
|
||||||
match File::open(&path) {
|
|
||||||
Ok(file) => Ok(file),
|
|
||||||
Err(e) => {
|
|
||||||
if e.kind() == std::io::ErrorKind::NotFound {
|
|
||||||
Err(eyre::eyre!(NetdevError::NotFound {
|
|
||||||
name: name.to_string(),
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
Err(eyre::eyre!(NetdevError::IoError {
|
|
||||||
name: name.to_string(),
|
|
||||||
err: e,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_bytes(file: &mut File) -> eyre::Result<u64> {
|
|
||||||
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<u64> {
|
|
||||||
Self::read_bytes(&mut self.rx_bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tx_bytes(&mut self) -> eyre::Result<u64> {
|
|
||||||
Self::read_bytes(&mut self.tx_bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list() -> eyre::Result<Vec<String>> {
|
|
||||||
let mut entries = std::fs::read_dir(Self::SYS_CLASS_NET)?
|
|
||||||
.filter_map(|entry| {
|
|
||||||
entry
|
|
||||||
.ok()
|
|
||||||
.and_then(|entry| entry.file_name().into_string().ok())
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
entries.sort();
|
|
||||||
Ok(entries)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user