i3status_rs/blocks/
disk_iostats.rs

1//! Disk I/O statistics
2//!
3//! # Configuration
4//!
5//! Key | Values | Default
6//! ----|--------|--------
7//! `device` | Block device name to monitor (as specified in `/dev/`) | If not set, device will be automatically selected every `interval`
8//! `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) "`
9//! `interval` | Update interval in seconds | `2`
10//! `missing_format` | Same as `format` but for when the device is missing | `" × "`
11//!
12//! Placeholder | Value | Type   | Unit
13//! ------------|-------|--------|-------
14//! `icon` | A static icon | Icon | -
15//! `device` | The name of device | Text | -
16//! `speed_read` | Read speed | Number | Bytes per second
17//! `speed_write` | Write speed | Number | Bytes per second
18//!
19//! # Examples
20//!
21//! ```toml
22//! [[block]]
23//! block = "disk_iostats"
24//! device = "sda"
25//! format = " $icon $speed_write.eng(prefix:K) "
26//! ```
27//!
28//! # Icons Used
29//!
30//! - `disk_drive`
31
32use super::prelude::*;
33use crate::util::read_file;
34use libc::c_ulong;
35use std::ops;
36use std::path::Path;
37use std::time::Instant;
38use tokio::fs::read_dir;
39
40/// Path for block devices
41const BLOCK_DEVICES_PATH: &str = "/sys/class/block";
42
43#[derive(Deserialize, Debug, SmartDefault)]
44#[serde(deny_unknown_fields, default)]
45pub struct Config {
46    pub device: Option<String>,
47    #[default(2.into())]
48    pub interval: Seconds,
49    pub format: FormatConfig,
50    pub missing_format: FormatConfig,
51}
52
53pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
54    let format = config
55        .format
56        .with_default(" $icon $speed_read.eng(prefix:K) $speed_write.eng(prefix:K) ")?;
57    let missing_format = config.missing_format.with_default(" × ")?;
58
59    let mut timer = config.interval.timer();
60    let mut old_stats = None;
61    let mut stats_timer = Instant::now();
62
63    loop {
64        let mut device = config.device.clone();
65        if device.is_none() {
66            device = find_device().await?;
67        }
68        match device {
69            None => {
70                api.set_widget(Widget::new().with_format(missing_format.clone()))?;
71            }
72            Some(device) => {
73                let mut widget = Widget::new();
74
75                widget.set_format(format.clone());
76
77                let new_stats = read_stats(&device).await?;
78                let sector_size = read_sector_size(&device).await?;
79
80                let mut speed_read = 0.0;
81                let mut speed_write = 0.0;
82                if let Some(old_stats) = old_stats {
83                    let diff = new_stats - old_stats;
84                    let elapsed = stats_timer.elapsed().as_secs_f64();
85                    stats_timer = Instant::now();
86                    let size_read = diff.sectors_read as u64 * sector_size;
87                    let size_written = diff.sectors_written as u64 * sector_size;
88                    speed_read = size_read as f64 / elapsed;
89                    speed_write = size_written as f64 / elapsed;
90                };
91                old_stats = Some(new_stats);
92
93                widget.set_values(map! {
94                    "icon" => Value::icon("disk_drive"),
95                    "speed_read" => Value::bytes(speed_read),
96                    "speed_write" => Value::bytes(speed_write),
97                    "device" => Value::text(device),
98                });
99
100                api.set_widget(widget)?;
101            }
102        }
103
104        select! {
105            _ = timer.tick() => continue,
106            _ = api.wait_for_update_request() => continue,
107        }
108    }
109}
110
111async fn find_device() -> Result<Option<String>> {
112    let mut sysfs_dir = read_dir(BLOCK_DEVICES_PATH)
113        .await
114        .error("Failed to open /sys/class/block directory")?;
115    while let Some(dir) = sysfs_dir
116        .next_entry()
117        .await
118        .error("Failed to read /sys/class/block directory")?
119    {
120        let path = dir.path();
121        if path.join("device").exists() {
122            return Ok(Some(
123                dir.file_name()
124                    .into_string()
125                    .map_err(|_| Error::new("Invalid device filename"))?,
126            ));
127        }
128    }
129
130    Ok(None)
131}
132
133#[derive(Debug, Default, Clone, Copy)]
134struct Stats {
135    sectors_read: c_ulong,
136    sectors_written: c_ulong,
137}
138
139impl ops::Sub for Stats {
140    type Output = Self;
141
142    fn sub(mut self, rhs: Self) -> Self::Output {
143        self.sectors_read = self.sectors_read.wrapping_sub(rhs.sectors_read);
144        self.sectors_written = self.sectors_written.wrapping_sub(rhs.sectors_written);
145        self
146    }
147}
148
149async fn read_stats(device: &str) -> Result<Stats> {
150    let raw = read_file(Path::new(BLOCK_DEVICES_PATH).join(device).join("stat"))
151        .await
152        .error("Failed to read stat file")?;
153    let fields: Vec<&str> = raw.split_whitespace().collect();
154    Ok(Stats {
155        sectors_read: fields
156            .get(2)
157            .error("Missing sectors read field")?
158            .parse()
159            .error("Failed to parse sectors read")?,
160        sectors_written: fields
161            .get(6)
162            .error("Missing sectors written field")?
163            .parse()
164            .error("Failed to parse sectors written")?,
165    })
166}
167
168async fn read_sector_size(device: &str) -> Result<u64> {
169    let raw = read_file(
170        Path::new(BLOCK_DEVICES_PATH)
171            .join(device)
172            .join("queue/hw_sector_size"),
173    )
174    .await
175    .error("Failed to read HW sector size")?;
176    raw.parse::<u64>().error("Failed to parse HW sector size")
177}