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