i3status_rs/blocks/
disk_iostats.rs1use 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
50const 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 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 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}