i3status_rs/blocks/
disk_iostats.rs

1//! Disk I/O statistics
2//!
3//! # Configuration
4//!
5//! Key | Values | Default
6//! ----|--------|--------
7//! `device` | Block device or partition 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//! Use labeled Games partition via persistent device names from /dev/disk/by-*/
29//!
30//! ```toml
31//! [[block]]
32//! block = "disk_iostats"
33//! device = "disk/by-partlabel/Games"
34//! format = " $icon $speed_write.eng(prefix:K) "
35//! ```
36//!
37//! # Icons Used
38//!
39//! - `disk_drive`
40
41use super::prelude::*;
42use crate::util::read_file;
43use libc::c_ulong;
44use std::ops;
45use std::path::Path;
46use std::time::Instant;
47use tokio::fs::read_dir;
48use tokio::fs::read_link;
49
50/// Path for block devices
51const BLOCK_DEVICES_PATH: &str = "/sys/class/block";
52
53#[derive(Deserialize, Debug, SmartDefault)]
54#[serde(deny_unknown_fields, default)]
55pub struct Config {
56    pub device: Option<String>,
57    #[default(2.into())]
58    pub interval: Seconds,
59    pub format: FormatConfig,
60    pub missing_format: FormatConfig,
61}
62
63pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
64    let format = config
65        .format
66        .with_default(" $icon $speed_read.eng(prefix:K) $speed_write.eng(prefix:K) ")?;
67    let missing_format = config.missing_format.with_default(" × ")?;
68
69    let mut timer = config.interval.timer();
70    let mut old_stats = None;
71    let mut stats_timer = Instant::now();
72
73    loop {
74        let mut device = config.device.clone();
75        if device.is_none() {
76            device = find_device().await?;
77        }
78        match device {
79            None => {
80                api.set_widget(Widget::new().with_format(missing_format.clone()))?;
81            }
82            Some(mut device) => {
83                let mut widget = Widget::new();
84
85                widget.set_format(format.clone());
86
87                if let Ok(link) = read_link(Path::new("/dev/").join(&device)).await
88                    && let Some(name) = link.file_name()
89                    && let Ok(target) = name.to_os_string().into_string()
90                {
91                    device = target;
92                }
93
94                let new_stats = read_stats(&device).await?;
95                let sector_size = read_sector_size(&device).await?;
96
97                let mut speed_read = 0.0;
98                let mut speed_write = 0.0;
99                if let Some(old_stats) = old_stats {
100                    let diff = new_stats - old_stats;
101                    let elapsed = stats_timer.elapsed().as_secs_f64();
102                    stats_timer = Instant::now();
103                    let size_read = diff.sectors_read as u64 * sector_size;
104                    let size_written = diff.sectors_written as u64 * sector_size;
105                    speed_read = size_read as f64 / elapsed;
106                    speed_write = size_written as f64 / elapsed;
107                };
108                old_stats = Some(new_stats);
109
110                widget.set_values(map! {
111                    "icon" => Value::icon("disk_drive"),
112                    "speed_read" => Value::bytes(speed_read),
113                    "speed_write" => Value::bytes(speed_write),
114                    "device" => Value::text(device),
115                });
116
117                api.set_widget(widget)?;
118            }
119        }
120
121        select! {
122            _ = timer.tick() => continue,
123            _ = api.wait_for_update_request() => continue,
124        }
125    }
126}
127
128async fn find_device() -> Result<Option<String>> {
129    let mut sysfs_dir = read_dir(BLOCK_DEVICES_PATH)
130        .await
131        .error("Failed to open /sys/class/block directory")?;
132    while let Some(dir) = sysfs_dir
133        .next_entry()
134        .await
135        .error("Failed to read /sys/class/block directory")?
136    {
137        let path = dir.path();
138        if path.join("device").exists() {
139            return Ok(Some(
140                dir.file_name()
141                    .into_string()
142                    .map_err(|_| Error::new("Invalid device filename"))?,
143            ));
144        }
145    }
146
147    Ok(None)
148}
149
150#[derive(Debug, Default, Clone, Copy)]
151struct Stats {
152    sectors_read: c_ulong,
153    sectors_written: c_ulong,
154}
155
156impl ops::Sub for Stats {
157    type Output = Self;
158
159    fn sub(mut self, rhs: Self) -> Self::Output {
160        self.sectors_read = self.sectors_read.wrapping_sub(rhs.sectors_read);
161        self.sectors_written = self.sectors_written.wrapping_sub(rhs.sectors_written);
162        self
163    }
164}
165
166async fn read_stats(device: &str) -> Result<Stats> {
167    let raw = read_file(Path::new(BLOCK_DEVICES_PATH).join(device).join("stat"))
168        .await
169        .error("Failed to read stat file")?;
170    let fields: Vec<&str> = raw.split_whitespace().collect();
171    Ok(Stats {
172        sectors_read: fields
173            .get(2)
174            .error("Missing sectors read field")?
175            .parse()
176            .error("Failed to parse sectors read")?,
177        sectors_written: fields
178            .get(6)
179            .error("Missing sectors written field")?
180            .parse()
181            .error("Failed to parse sectors written")?,
182    })
183}
184
185async fn read_sector_size(device: &str) -> Result<u64> {
186    if let Ok(raw) = read_file(
187        Path::new(BLOCK_DEVICES_PATH)
188            .join(device)
189            .join("queue/hw_sector_size"),
190    )
191    .await
192    {
193        raw.parse::<u64>().error("Failed to parse HW sector size")
194    } else {
195        // queue/hw_sector_size didn't exist in device
196        // check for a device entry that has named device as a
197        // sub-directory and use that one instead for partitions
198        let mut sysfs_dir = read_dir(BLOCK_DEVICES_PATH)
199            .await
200            .error("Failed to open /sys/class/block directory")?;
201        while let Some(dir) = sysfs_dir
202            .next_entry()
203            .await
204            .error("Failed to read /sys/class/block directory")?
205        {
206            let path = dir.path();
207            if path.join(device).exists() {
208                let raw = read_file(path.join("queue/hw_sector_size"))
209                    .await
210                    .error("Failed to read partition HW sector size")?;
211                return raw
212                    .parse::<u64>()
213                    .error("Failed to parse partition HW sector size");
214            }
215        }
216        Err(Error::new(
217            "Failed to find device for partition HW sector size",
218        ))
219    }
220}