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