Skip to main content

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` (`$icon`)
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(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
64    BlockPlan::new(vec![
65        OutputPlan::new(
66            "main",
67            config
68                .format
69                .with_default(" $icon $speed_read.eng(prefix:K) $speed_write.eng(prefix:K) ")?,
70        )
71        .icon("icon", IconChoices::one(icons::DISK_DRIVE)),
72        // The "missing" output sets no values at all.
73        OutputPlan::new("missing", config.missing_format.with_default(" × ")?),
74    ])
75}
76
77pub(crate) async fn run(config: &Config, api: &CommonApi, plan: &Arc<BlockPlan>) -> Result<()> {
78    let output_main = plan.output("main")?;
79    let output_missing = plan.output("missing")?;
80
81    let mut timer = config.interval.timer();
82    let mut old_stats = None;
83    let mut stats_timer = Instant::now();
84
85    loop {
86        let mut device = config.device.clone();
87        if device.is_none() {
88            device = find_device().await?;
89        }
90        match device {
91            None => {
92                api.set_widget(output_missing.new_widget())?;
93            }
94            Some(mut device) => {
95                let mut widget = output_main.new_widget();
96
97                if let Ok(link) = read_link(Path::new("/dev/").join(&device)).await
98                    && let Some(name) = link.file_name()
99                    && let Ok(target) = name.to_os_string().into_string()
100                {
101                    device = target;
102                }
103
104                let new_stats = read_stats(&device).await?;
105                let sector_size = read_sector_size(&device).await?;
106
107                let mut speed_read = 0.0;
108                let mut speed_write = 0.0;
109                if let Some(old_stats) = old_stats {
110                    let diff = new_stats - old_stats;
111                    let elapsed = stats_timer.elapsed().as_secs_f64();
112                    stats_timer = Instant::now();
113                    let size_read = diff.sectors_read as u64 * sector_size;
114                    let size_written = diff.sectors_written as u64 * sector_size;
115                    speed_read = size_read as f64 / elapsed;
116                    speed_write = size_written as f64 / elapsed;
117                };
118                old_stats = Some(new_stats);
119
120                widget.set_values(map! {
121                    "icon" => Value::icon(icons::DISK_DRIVE),
122                    "speed_read" => Value::bytes(speed_read),
123                    "speed_write" => Value::bytes(speed_write),
124                    "device" => Value::text(device),
125                });
126
127                api.set_widget(widget)?;
128            }
129        }
130
131        select! {
132            _ = timer.tick() => continue,
133            _ = api.wait_for_update_request() => continue,
134        }
135    }
136}
137
138async fn find_device() -> Result<Option<String>> {
139    let mut sysfs_dir = read_dir(BLOCK_DEVICES_PATH)
140        .await
141        .error("Failed to open /sys/class/block directory")?;
142    while let Some(dir) = sysfs_dir
143        .next_entry()
144        .await
145        .error("Failed to read /sys/class/block directory")?
146    {
147        let path = dir.path();
148        if path.join("device").exists() {
149            return Ok(Some(
150                dir.file_name()
151                    .into_string()
152                    .map_err(|_| Error::new("Invalid device filename"))?,
153            ));
154        }
155    }
156
157    Ok(None)
158}
159
160#[derive(Debug, Default, Clone, Copy)]
161struct Stats {
162    sectors_read: c_ulong,
163    sectors_written: c_ulong,
164}
165
166impl ops::Sub for Stats {
167    type Output = Self;
168
169    fn sub(mut self, rhs: Self) -> Self::Output {
170        self.sectors_read = self.sectors_read.wrapping_sub(rhs.sectors_read);
171        self.sectors_written = self.sectors_written.wrapping_sub(rhs.sectors_written);
172        self
173    }
174}
175
176async fn read_stats(device: &str) -> Result<Stats> {
177    let raw = read_file(Path::new(BLOCK_DEVICES_PATH).join(device).join("stat"))
178        .await
179        .error("Failed to read stat file")?;
180    let fields: Vec<&str> = raw.split_whitespace().collect();
181    Ok(Stats {
182        sectors_read: fields
183            .get(2)
184            .error("Missing sectors read field")?
185            .parse()
186            .error("Failed to parse sectors read")?,
187        sectors_written: fields
188            .get(6)
189            .error("Missing sectors written field")?
190            .parse()
191            .error("Failed to parse sectors written")?,
192    })
193}
194
195async fn read_sector_size(device: &str) -> Result<u64> {
196    if let Ok(raw) = read_file(
197        Path::new(BLOCK_DEVICES_PATH)
198            .join(device)
199            .join("queue/hw_sector_size"),
200    )
201    .await
202    {
203        raw.parse::<u64>().error("Failed to parse HW sector size")
204    } else {
205        // queue/hw_sector_size didn't exist in device
206        // check for a device entry that has named device as a
207        // sub-directory and use that one instead for partitions
208        let mut sysfs_dir = read_dir(BLOCK_DEVICES_PATH)
209            .await
210            .error("Failed to open /sys/class/block directory")?;
211        while let Some(dir) = sysfs_dir
212            .next_entry()
213            .await
214            .error("Failed to read /sys/class/block directory")?
215        {
216            let path = dir.path();
217            if path.join(device).exists() {
218                let raw = read_file(path.join("queue/hw_sector_size"))
219                    .await
220                    .error("Failed to read partition HW sector size")?;
221                return raw
222                    .parse::<u64>()
223                    .error("Failed to parse partition HW sector size");
224            }
225        }
226        Err(Error::new(
227            "Failed to find device for partition HW sector size",
228        ))
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn plan_declares_main_and_missing() {
238        let plan = prepare(&Config::default()).unwrap();
239        let main = plan.output("main").unwrap();
240        assert_eq!(main.single_icon("icon").unwrap(), "disk_drive");
241        assert!(main.format().contains_key("speed_read"));
242        assert!(main.format().contains_key("speed_write"));
243        let missing = plan.output("missing").unwrap();
244        assert_eq!(missing.output().icon_placeholders().count(), 0);
245    }
246
247    #[test]
248    fn custom_missing_format_is_used() {
249        let config = Config {
250            missing_format: " $device gone ".parse().unwrap(),
251            ..Config::default()
252        };
253        let plan = prepare(&config).unwrap();
254        let missing = plan.output("missing").unwrap();
255        assert!(missing.format().contains_key("device"));
256    }
257}