1use crate::sys::utils::{get_all_utf8_data, to_cpath};
4use crate::{Disk, DiskKind, DiskRefreshKind, DiskUsage};
5
6use libc::statvfs;
7use std::collections::HashMap;
8use std::ffi::{OsStr, OsString};
9use std::fs;
10use std::mem::MaybeUninit;
11use std::os::unix::ffi::OsStrExt;
12use std::path::{Path, PathBuf};
13use std::str::FromStr;
14
15const SECTOR_SIZE: u64 = 512;
30
31macro_rules! cast {
32 ($x:expr) => {
33 u64::from($x)
34 };
35}
36
37pub(crate) struct DiskInner {
38 type_: DiskKind,
39 device_name: OsString,
40 actual_device_name: Option<String>,
41 file_system: OsString,
42 mount_point: PathBuf,
43 total_space: u64,
44 available_space: u64,
45 is_removable: bool,
46 is_read_only: bool,
47 old_written_bytes: u64,
48 old_read_bytes: u64,
49 written_bytes: u64,
50 read_bytes: u64,
51 updated: bool,
52}
53
54impl DiskInner {
55 pub(crate) fn kind(&self) -> DiskKind {
56 self.type_
57 }
58
59 pub(crate) fn name(&self) -> &OsStr {
60 &self.device_name
61 }
62
63 pub(crate) fn file_system(&self) -> &OsStr {
64 &self.file_system
65 }
66
67 pub(crate) fn mount_point(&self) -> &Path {
68 &self.mount_point
69 }
70
71 pub(crate) fn total_space(&self) -> u64 {
72 self.total_space
73 }
74
75 pub(crate) fn available_space(&self) -> u64 {
76 self.available_space
77 }
78
79 pub(crate) fn is_removable(&self) -> bool {
80 self.is_removable
81 }
82
83 pub(crate) fn is_read_only(&self) -> bool {
84 self.is_read_only
85 }
86
87 pub(crate) fn refresh_specifics(&mut self, refresh_kind: DiskRefreshKind) -> bool {
88 self.efficient_refresh(refresh_kind, &disk_stats(&refresh_kind), false)
89 }
90
91 fn efficient_refresh(
92 &mut self,
93 refresh_kind: DiskRefreshKind,
94 procfs_disk_stats: &HashMap<String, DiskStat>,
95 first: bool,
96 ) -> bool {
97 if refresh_kind.io_usage() {
98 if self.actual_device_name.is_none() {
99 self.actual_device_name = Some(get_actual_device_name(&self.device_name));
100 }
101 if let Some(stat) = self
102 .actual_device_name
103 .as_ref()
104 .and_then(|actual_device_name| procfs_disk_stats.get(actual_device_name))
105 {
106 self.old_read_bytes = self.read_bytes;
107 self.old_written_bytes = self.written_bytes;
108 self.read_bytes = stat.sectors_read * SECTOR_SIZE;
109 self.written_bytes = stat.sectors_written * SECTOR_SIZE;
110 } else {
111 sysinfo_debug!("Failed to update disk i/o stats");
112 }
113 }
114
115 if refresh_kind.kind() && self.type_ == DiskKind::Unknown(-1) {
116 self.type_ = find_type_for_device_name(&self.device_name);
117 }
118
119 if refresh_kind.storage() {
120 if let Some((total_space, available_space, is_read_only)) =
121 unsafe { load_statvfs_values(&self.mount_point) }
122 {
123 self.total_space = total_space;
124 self.available_space = available_space;
125 if first {
126 self.is_read_only = is_read_only;
127 }
128 }
129 }
130
131 true
132 }
133
134 pub(crate) fn usage(&self) -> DiskUsage {
135 DiskUsage {
136 read_bytes: self.read_bytes.saturating_sub(self.old_read_bytes),
137 total_read_bytes: self.read_bytes,
138 written_bytes: self.written_bytes.saturating_sub(self.old_written_bytes),
139 total_written_bytes: self.written_bytes,
140 }
141 }
142}
143
144impl crate::DisksInner {
145 pub(crate) fn new() -> Self {
146 Self {
147 disks: Vec::with_capacity(2),
148 }
149 }
150
151 pub(crate) fn refresh_specifics(
152 &mut self,
153 remove_not_listed_disks: bool,
154 refresh_kind: DiskRefreshKind,
155 ) {
156 get_all_list(
157 &mut self.disks,
158 &get_all_utf8_data("/proc/mounts", 16_385).unwrap_or_default(),
159 refresh_kind,
160 );
161
162 if remove_not_listed_disks {
163 self.disks.retain_mut(|disk| {
164 if !disk.inner.updated {
165 return false;
166 }
167 disk.inner.updated = false;
168 true
169 });
170 } else {
171 for c in self.disks.iter_mut() {
172 c.inner.updated = false;
173 }
174 }
175 }
176
177 pub(crate) fn list(&self) -> &[Disk] {
178 &self.disks
179 }
180
181 pub(crate) fn list_mut(&mut self) -> &mut [Disk] {
182 &mut self.disks
183 }
184}
185
186fn get_actual_device_name(device: &OsStr) -> String {
194 let device_path = PathBuf::from(device);
195
196 std::fs::canonicalize(&device_path)
197 .ok()
198 .and_then(|path| path.strip_prefix("/dev").ok().map(Path::to_path_buf))
199 .unwrap_or(device_path)
200 .to_str()
201 .map(str::to_owned)
202 .unwrap_or_default()
203}
204
205unsafe fn load_statvfs_values(mount_point: &Path) -> Option<(u64, u64, bool)> {
206 let mount_point_cpath = to_cpath(mount_point);
207 let mut stat: MaybeUninit<statvfs> = MaybeUninit::uninit();
208 if retry_eintr!(statvfs(
209 mount_point_cpath.as_ptr() as *const _,
210 stat.as_mut_ptr()
211 )) == 0
212 {
213 let stat = stat.assume_init();
214
215 let bsize = cast!(stat.f_bsize);
216 let blocks = cast!(stat.f_blocks);
217 let bavail = cast!(stat.f_bavail);
218 let total = bsize.saturating_mul(blocks);
219 if total == 0 {
220 return None;
221 }
222 let available = bsize.saturating_mul(bavail);
223 let is_read_only = (stat.f_flag & libc::ST_RDONLY) != 0;
224
225 Some((total, available, is_read_only))
226 } else {
227 None
228 }
229}
230
231fn new_disk(
232 device_name: &OsStr,
233 mount_point: &Path,
234 file_system: &OsStr,
235 removable_entries: &[PathBuf],
236 procfs_disk_stats: &HashMap<String, DiskStat>,
237 refresh_kind: DiskRefreshKind,
238) -> Disk {
239 let is_removable = removable_entries
240 .iter()
241 .any(|e| e.as_os_str() == device_name);
242
243 let mut disk = Disk {
244 inner: DiskInner {
245 type_: DiskKind::Unknown(-1),
246 device_name: device_name.to_owned(),
247 actual_device_name: None,
248 file_system: file_system.to_owned(),
249 mount_point: mount_point.to_owned(),
250 total_space: 0,
251 available_space: 0,
252 is_removable,
253 is_read_only: false,
254 old_read_bytes: 0,
255 old_written_bytes: 0,
256 read_bytes: 0,
257 written_bytes: 0,
258 updated: true,
259 },
260 };
261 disk.inner
262 .efficient_refresh(refresh_kind, procfs_disk_stats, true);
263 disk
264}
265
266#[allow(clippy::manual_range_contains)]
267fn find_type_for_device_name(device_name: &OsStr) -> DiskKind {
268 let device_name_path = device_name.to_str().unwrap_or_default();
279 let real_path = fs::canonicalize(device_name).unwrap_or_else(|_| PathBuf::from(device_name));
280 let mut real_path = real_path.to_str().unwrap_or_default();
281 if device_name_path.starts_with("/dev/mapper/") {
282 if real_path != device_name_path {
284 return find_type_for_device_name(OsStr::new(&real_path));
285 }
286 } else if device_name_path.starts_with("/dev/sd") || device_name_path.starts_with("/dev/vd") {
287 real_path = real_path.trim_start_matches("/dev/");
289 real_path = real_path.trim_end_matches(|c| c >= '0' && c <= '9');
290 } else if device_name_path.starts_with("/dev/nvme") {
291 real_path = match real_path.find('p') {
293 Some(idx) => &real_path["/dev/".len()..idx],
294 None => &real_path["/dev/".len()..],
295 };
296 } else if device_name_path.starts_with("/dev/root") {
297 if real_path != device_name_path {
299 return find_type_for_device_name(OsStr::new(&real_path));
300 }
301 } else if device_name_path.starts_with("/dev/mmcblk") {
302 real_path = match real_path.find('p') {
304 Some(idx) => &real_path["/dev/".len()..idx],
305 None => &real_path["/dev/".len()..],
306 };
307 } else {
308 real_path = real_path.trim_start_matches("/dev/");
311 }
312
313 let trimmed: &OsStr = OsStrExt::from_bytes(real_path.as_bytes());
314
315 let path = Path::new("/sys/block/")
316 .to_owned()
317 .join(trimmed)
318 .join("queue/rotational");
319 match get_all_utf8_data(path, 8)
321 .unwrap_or_default()
322 .trim()
323 .parse()
324 .ok()
325 {
326 Some(1) => DiskKind::HDD,
328 Some(0) => DiskKind::SSD,
330 Some(x) => DiskKind::Unknown(x),
332 None => DiskKind::Unknown(-1),
334 }
335}
336
337fn get_all_list(container: &mut Vec<Disk>, content: &str, refresh_kind: DiskRefreshKind) {
338 let removable_entries = match fs::read_dir("/dev/disk/by-id/") {
341 Ok(r) => r
342 .filter_map(|res| Some(res.ok()?.path()))
343 .filter_map(|e| {
344 if e.file_name()
345 .and_then(|x| Some(x.to_str()?.starts_with("usb-")))
346 .unwrap_or_default()
347 {
348 e.canonicalize().ok()
349 } else {
350 None
351 }
352 })
353 .collect::<Vec<PathBuf>>(),
354 _ => Vec::new(),
355 };
356
357 let procfs_disk_stats = disk_stats(&refresh_kind);
358
359 for (fs_spec, fs_file, fs_vfstype) in content
360 .lines()
361 .map(|line| {
362 let line = line.trim_start();
363 let mut fields = line.split_whitespace();
367 let fs_spec = fields.next().unwrap_or("");
368 let fs_file = fields
369 .next()
370 .unwrap_or("")
371 .replace("\\134", "\\")
372 .replace("\\040", " ")
373 .replace("\\011", "\t")
374 .replace("\\012", "\n");
375 let fs_vfstype = fields.next().unwrap_or("");
376 (fs_spec, fs_file, fs_vfstype)
377 })
378 .filter(|(fs_spec, fs_file, fs_vfstype)| {
379 let filtered = match *fs_vfstype {
381 "rootfs" | "sysfs" | "proc" | "devtmpfs" |
385 "cgroup" |
386 "cgroup2" |
387 "pstore" | "squashfs" | "rpc_pipefs" | "iso9660" | "devpts" | "hugetlbfs" | "mqueue" => true,
395 "tmpfs" => !cfg!(feature = "linux-tmpfs"),
396 "cifs" | "nfs" | "nfs4" | "autofs" => !cfg!(feature = "linux-netdevs"),
398 _ => false,
399 };
400
401 !(filtered ||
402 fs_file.starts_with("/sys") || fs_file.starts_with("/proc") ||
404 (fs_file.starts_with("/run") && !fs_file.starts_with("/run/media")) ||
405 fs_spec.starts_with("sunrpc"))
406 })
407 {
408 let mount_point = Path::new(&fs_file);
409 if let Some(disk) = container.iter_mut().find(|d| {
410 d.inner.mount_point == mount_point
411 && d.inner.device_name == fs_spec
412 && d.inner.file_system == fs_vfstype
413 }) {
414 disk.inner
415 .efficient_refresh(refresh_kind, &procfs_disk_stats, false);
416 disk.inner.updated = true;
417 continue;
418 }
419 container.push(new_disk(
420 fs_spec.as_ref(),
421 mount_point,
422 fs_vfstype.as_ref(),
423 &removable_entries,
424 &procfs_disk_stats,
425 refresh_kind,
426 ));
427 }
428}
429
430#[derive(Debug, PartialEq)]
457struct DiskStat {
458 sectors_read: u64,
459 sectors_written: u64,
460}
461
462impl DiskStat {
463 fn new_from_line(line: &str) -> Option<(String, Self)> {
465 let mut iter = line.split_whitespace();
466 let name = iter.nth(2).map(ToString::to_string)?;
468 let sectors_read = iter.nth(2).and_then(|v| u64::from_str(v).ok()).unwrap_or(0);
470 let sectors_written = iter.nth(3).and_then(|v| u64::from_str(v).ok()).unwrap_or(0);
472 Some((
473 name,
474 Self {
475 sectors_read,
476 sectors_written,
477 },
478 ))
479 }
480}
481
482fn disk_stats(refresh_kind: &DiskRefreshKind) -> HashMap<String, DiskStat> {
483 if refresh_kind.io_usage() {
484 let path = "/proc/diskstats";
485 match fs::read_to_string(path) {
486 Ok(content) => disk_stats_inner(&content),
487 Err(_error) => {
488 sysinfo_debug!("failed to read {path:?}: {_error:?}");
489 HashMap::new()
490 }
491 }
492 } else {
493 Default::default()
494 }
495}
496
497fn disk_stats_inner(content: &str) -> HashMap<String, DiskStat> {
499 let mut data = HashMap::new();
500
501 for line in content.lines() {
502 let line = line.trim();
503 if line.is_empty() {
504 continue;
505 }
506 if let Some((name, stats)) = DiskStat::new_from_line(line) {
507 data.insert(name, stats);
508 }
509 }
510 data
511}
512
513#[cfg(test)]
514mod test {
515 use super::{disk_stats_inner, DiskStat};
516 use std::collections::HashMap;
517
518 #[test]
519 fn test_disk_stat_parsing() {
520 let file_content = "\
522 259 0 nvme0n1 571695 101559 38943220 165643 9824246 1076193 462375378 4140037 0 1038904 4740493 254020 0 1436922320 68519 306875 366293
523 259 1 nvme0n1p1 240 2360 15468 48 2 0 2 0 0 21 50 8 0 2373552 2 0 0
524 259 2 nvme0n1p2 243 10 11626 26 63 39 616 125 0 84 163 44 0 1075280 11 0 0
525 259 3 nvme0n1p3 571069 99189 38910302 165547 9824180 1076154 462374760 4139911 0 1084855 4373964 253968 0 1433473488 68505 0 0
526 253 0 dm-0 670206 0 38909056 259490 10900330 0 462374760 12906518 0 1177098 13195902 253968 0 1433473488 29894 0 0
527 252 0 zram0 2382 0 20984 11 260261 0 2082088 2063 0 1964 2074 0 0 0 0 0 0
528 1 2 bla 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
529";
530
531 let data = disk_stats_inner(file_content);
532 let expected_data: HashMap<String, DiskStat> = HashMap::from([
533 (
534 "nvme0n1".to_string(),
535 DiskStat {
536 sectors_read: 38943220,
537 sectors_written: 462375378,
538 },
539 ),
540 (
541 "nvme0n1p1".to_string(),
542 DiskStat {
543 sectors_read: 15468,
544 sectors_written: 2,
545 },
546 ),
547 (
548 "nvme0n1p2".to_string(),
549 DiskStat {
550 sectors_read: 11626,
551 sectors_written: 616,
552 },
553 ),
554 (
555 "nvme0n1p3".to_string(),
556 DiskStat {
557 sectors_read: 38910302,
558 sectors_written: 462374760,
559 },
560 ),
561 (
562 "dm-0".to_string(),
563 DiskStat {
564 sectors_read: 38909056,
565 sectors_written: 462374760,
566 },
567 ),
568 (
569 "zram0".to_string(),
570 DiskStat {
571 sectors_read: 20984,
572 sectors_written: 2082088,
573 },
574 ),
575 (
577 "bla".to_string(),
578 DiskStat {
579 sectors_read: 6,
580 sectors_written: 10,
581 },
582 ),
583 ]);
584
585 assert_eq!(data, expected_data);
586 }
587
588 #[test]
589 fn disk_entry_with_less_information() {
590 let file_content = "\
591 systemd-1 /efi autofs rw,relatime,fd=181,pgrp=1,timeout=120,minproto=5,maxproto=5,direct,pipe_ino=8311 0 0
592 /dev/nvme0n1p1 /efi vfat rw,nosuid,nodev,noexec,relatime,nosymfollow,fmask=0077,dmask=0077 0 0
593";
594
595 let data = disk_stats_inner(file_content);
596 let expected_data: HashMap<String, DiskStat> = HashMap::from([
597 (
598 "autofs".to_string(),
599 DiskStat {
600 sectors_read: 0,
601 sectors_written: 0,
602 },
603 ),
604 (
605 "vfat".to_string(),
606 DiskStat {
607 sectors_read: 0,
608 sectors_written: 0,
609 },
610 ),
611 ]);
612
613 assert_eq!(data, expected_data);
614 }
615}