diff --git a/src/blocks.rs b/src/blocks.rs index f00a29454..16b1cb750 100644 --- a/src/blocks.rs +++ b/src/blocks.rs @@ -156,6 +156,7 @@ define_blocks!( cpu, custom, custom_dbus, + disk_stats, disk_space, #[deprecated( since = "0.33.0", diff --git a/src/blocks/disk_stats.rs b/src/blocks/disk_stats.rs new file mode 100644 index 000000000..b99fa2c38 --- /dev/null +++ b/src/blocks/disk_stats.rs @@ -0,0 +1,168 @@ +//! Disk I/O statistics +//! +//! # Configuration +//! +//! Key | Values | Default +//! ----|--------|-------- +//! `device` | Block device name to monitor (as specified in `/dev/`) | If not set, device will be automatically selected every `interval` +//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $speed_read.eng(prefix:K) $speed_write.eng(prefix:K) "` +//! `interval` | Update interval in seconds | `2` +//! +//! Placeholder | Value | Type | Unit +//! ------------|-------|--------|------- +//! `icon` | A static icon | Icon | - +//! `device` | The name of device | Text | - +//! `speed_read` | Read speed | Number | Bytes per second +//! `speed_write` | Write speed | Number | Bytes per second +//! +//! # Examples +//! +//! ```toml +//! [[block]] +//! block = "disk_stats" +//! device = "sda" +//! format = " $icon $speed_write.eng(prefix:K) " +//! ``` +//! +//! # Icons Used +//! +//! - `disk_drive` + +use super::prelude::*; +use crate::util::read_file; +use libc::c_ulong; +use std::ops; +use std::path::Path; +use std::time::Instant; +use tokio::fs::read_dir; + +/// Path for block devices +const BLOCK_DEVICES_PATH: &str = "/sys/class/block"; + +#[derive(Deserialize, Debug, SmartDefault)] +#[serde(deny_unknown_fields, default)] +pub struct Config { + pub device: Option, + #[default(2.into())] + pub interval: Seconds, + pub format: FormatConfig, + pub missing_format: FormatConfig, +} + +pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { + let format = config + .format + .with_default(" $icon $speed_read.eng(prefix:K) $speed_write.eng(prefix:K) ")?; + let missing_format = config.missing_format.with_default(" × ")?; + + let mut timer = config.interval.timer(); + let mut old_stats = None; + let mut stats_timer = Instant::now(); + + loop { + let mut device = config.device.clone(); + if device.is_none() { + device = find_device().await?; + } + match device { + None => { + api.set_widget(Widget::new().with_format(missing_format.clone()))?; + } + Some(device) => { + let mut widget = Widget::new(); + + widget.set_format(format.clone()); + + let new_stats = read_stats(&device).await?; + let sector_size = read_sector_size(&device).await?; + + let mut speed_read = 0.0; + let mut speed_write = 0.0; + if let Some(old_stats) = old_stats { + let diff = new_stats - old_stats; + let elapsed = stats_timer.elapsed().as_secs_f64(); + stats_timer = Instant::now(); + let size_read = diff.sectors_read as u64 * sector_size; + let size_written = diff.sectors_written as u64 * sector_size; + speed_read = size_read as f64 / elapsed; + speed_write = size_written as f64 / elapsed; + }; + old_stats = Some(new_stats); + + widget.set_values(map! { + "icon" => Value::icon("disk_drive"), + "speed_read" => Value::bytes(speed_read), + "speed_write" => Value::bytes(speed_write), + "device" => Value::text(device), + }); + + api.set_widget(widget)?; + } + } + + select! { + _ = timer.tick() => continue, + _ = api.wait_for_update_request() => continue, + } + } +} + +async fn find_device() -> Result> { + let mut sysfs_dir = read_dir(BLOCK_DEVICES_PATH) + .await + .error("Failed to open /sys/class/block directory")?; + while let Some(dir) = sysfs_dir + .next_entry() + .await + .error("Failed to read /sys/class/block directory")? + { + let path = dir.path(); + if path.join("device").exists() { + return Ok(Some( + dir.file_name() + .into_string() + .map_err(|_| Error::new("Invalid device filename"))?, + )); + } + } + + Ok(None) +} + +#[derive(Debug, Default, Clone, Copy)] +struct Stats { + sectors_read: c_ulong, + sectors_written: c_ulong, +} + +impl ops::Sub for Stats { + type Output = Self; + + fn sub(mut self, rhs: Self) -> Self::Output { + self.sectors_read = self.sectors_read.wrapping_sub(rhs.sectors_read); + self.sectors_written = self.sectors_written.wrapping_sub(rhs.sectors_written); + self + } +} + +async fn read_stats(device: &str) -> Result { + let raw = read_file(Path::new(BLOCK_DEVICES_PATH).join(device).join("stat")) + .await + .error("Failed to read stat file")?; + let fields: Vec<&str> = raw.split_whitespace().collect(); + Ok(Stats { + sectors_read: fields[2].parse().error("Failed to parse sectors read")?, + sectors_written: fields[6].parse().error("Failed to parse sectors written")?, + }) +} + +async fn read_sector_size(device: &str) -> Result { + let raw = read_file( + Path::new(BLOCK_DEVICES_PATH) + .join(device) + .join("queue/hw_sector_size"), + ) + .await + .error("Failed to read HW sector size")?; + raw.parse::().error("Failed to parse HW sector size") +}