i3status_rs/blocks/
disk_iostats.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
//! 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`
//! `missing_format` | Same as `format` but for when the device is missing | `" × "`
//!
//! 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_iostats"
//! 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<String>,
    #[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<Option<String>> {
    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<Stats> {
    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
            .get(2)
            .error("Missing sectors read field")?
            .parse()
            .error("Failed to parse sectors read")?,
        sectors_written: fields
            .get(6)
            .error("Missing sectors written field")?
            .parse()
            .error("Failed to parse sectors written")?,
    })
}

async fn read_sector_size(device: &str) -> Result<u64> {
    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::<u64>().error("Failed to parse HW sector size")
}