sysinfo/unix/linux/
disk.rs

1// Take a look at the license at the top of the repository in the LICENSE file.
2
3use crate::sys::utils::{get_all_data, to_cpath};
4use crate::{Disk, DiskKind};
5
6use libc::statvfs;
7use std::ffi::{OsStr, OsString};
8use std::fs;
9use std::mem;
10use std::os::unix::ffi::OsStrExt;
11use std::path::{Path, PathBuf};
12
13macro_rules! cast {
14    ($x:expr) => {
15        u64::from($x)
16    };
17}
18
19pub(crate) struct DiskInner {
20    type_: DiskKind,
21    device_name: OsString,
22    file_system: OsString,
23    mount_point: PathBuf,
24    total_space: u64,
25    available_space: u64,
26    is_removable: bool,
27}
28
29impl DiskInner {
30    pub(crate) fn kind(&self) -> DiskKind {
31        self.type_
32    }
33
34    pub(crate) fn name(&self) -> &OsStr {
35        &self.device_name
36    }
37
38    pub(crate) fn file_system(&self) -> &OsStr {
39        &self.file_system
40    }
41
42    pub(crate) fn mount_point(&self) -> &Path {
43        &self.mount_point
44    }
45
46    pub(crate) fn total_space(&self) -> u64 {
47        self.total_space
48    }
49
50    pub(crate) fn available_space(&self) -> u64 {
51        self.available_space
52    }
53
54    pub(crate) fn is_removable(&self) -> bool {
55        self.is_removable
56    }
57
58    pub(crate) fn refresh(&mut self) -> bool {
59        unsafe {
60            let mut stat: statvfs = mem::zeroed();
61            let mount_point_cpath = to_cpath(&self.mount_point);
62            if retry_eintr!(statvfs(mount_point_cpath.as_ptr() as *const _, &mut stat)) == 0 {
63                let tmp = cast!(stat.f_bsize).saturating_mul(cast!(stat.f_bavail));
64                self.available_space = cast!(tmp);
65                true
66            } else {
67                false
68            }
69        }
70    }
71}
72
73impl crate::DisksInner {
74    pub(crate) fn new() -> Self {
75        Self {
76            disks: Vec::with_capacity(2),
77        }
78    }
79
80    pub(crate) fn refresh_list(&mut self) {
81        get_all_list(
82            &mut self.disks,
83            &get_all_data("/proc/mounts", 16_385).unwrap_or_default(),
84        )
85    }
86
87    pub(crate) fn list(&self) -> &[Disk] {
88        &self.disks
89    }
90
91    pub(crate) fn list_mut(&mut self) -> &mut [Disk] {
92        &mut self.disks
93    }
94}
95
96fn new_disk(
97    device_name: &OsStr,
98    mount_point: &Path,
99    file_system: &OsStr,
100    removable_entries: &[PathBuf],
101) -> Option<Disk> {
102    let mount_point_cpath = to_cpath(mount_point);
103    let type_ = find_type_for_device_name(device_name);
104    let mut total = 0;
105    let mut available = 0;
106    unsafe {
107        let mut stat: statvfs = mem::zeroed();
108        if retry_eintr!(statvfs(mount_point_cpath.as_ptr() as *const _, &mut stat)) == 0 {
109            let bsize = cast!(stat.f_bsize);
110            let blocks = cast!(stat.f_blocks);
111            let bavail = cast!(stat.f_bavail);
112            total = bsize.saturating_mul(blocks);
113            available = bsize.saturating_mul(bavail);
114        }
115        if total == 0 {
116            return None;
117        }
118        let mount_point = mount_point.to_owned();
119        let is_removable = removable_entries
120            .iter()
121            .any(|e| e.as_os_str() == device_name);
122        Some(Disk {
123            inner: DiskInner {
124                type_,
125                device_name: device_name.to_owned(),
126                file_system: file_system.to_owned(),
127                mount_point,
128                total_space: cast!(total),
129                available_space: cast!(available),
130                is_removable,
131            },
132        })
133    }
134}
135
136#[allow(clippy::manual_range_contains)]
137fn find_type_for_device_name(device_name: &OsStr) -> DiskKind {
138    // The format of devices are as follows:
139    //  - device_name is symbolic link in the case of /dev/mapper/
140    //     and /dev/root, and the target is corresponding device under
141    //     /sys/block/
142    //  - In the case of /dev/sd, the format is /dev/sd[a-z][1-9],
143    //     corresponding to /sys/block/sd[a-z]
144    //  - In the case of /dev/nvme, the format is /dev/nvme[0-9]n[0-9]p[0-9],
145    //     corresponding to /sys/block/nvme[0-9]n[0-9]
146    //  - In the case of /dev/mmcblk, the format is /dev/mmcblk[0-9]p[0-9],
147    //     corresponding to /sys/block/mmcblk[0-9]
148    let device_name_path = device_name.to_str().unwrap_or_default();
149    let real_path = fs::canonicalize(device_name).unwrap_or_else(|_| PathBuf::from(device_name));
150    let mut real_path = real_path.to_str().unwrap_or_default();
151    if device_name_path.starts_with("/dev/mapper/") {
152        // Recursively solve, for example /dev/dm-0
153        if real_path != device_name_path {
154            return find_type_for_device_name(OsStr::new(&real_path));
155        }
156    } else if device_name_path.starts_with("/dev/sd") || device_name_path.starts_with("/dev/vd") {
157        // Turn "sda1" into "sda" or "vda1" into "vda"
158        real_path = real_path.trim_start_matches("/dev/");
159        real_path = real_path.trim_end_matches(|c| c >= '0' && c <= '9');
160    } else if device_name_path.starts_with("/dev/nvme") {
161        // Turn "nvme0n1p1" into "nvme0n1"
162        real_path = match real_path.find('p') {
163            Some(idx) => &real_path["/dev/".len()..idx],
164            None => &real_path["/dev/".len()..],
165        };
166    } else if device_name_path.starts_with("/dev/root") {
167        // Recursively solve, for example /dev/mmcblk0p1
168        if real_path != device_name_path {
169            return find_type_for_device_name(OsStr::new(&real_path));
170        }
171    } else if device_name_path.starts_with("/dev/mmcblk") {
172        // Turn "mmcblk0p1" into "mmcblk0"
173        real_path = match real_path.find('p') {
174            Some(idx) => &real_path["/dev/".len()..idx],
175            None => &real_path["/dev/".len()..],
176        };
177    } else {
178        // Default case: remove /dev/ and expects the name presents under /sys/block/
179        // For example, /dev/dm-0 to dm-0
180        real_path = real_path.trim_start_matches("/dev/");
181    }
182
183    let trimmed: &OsStr = OsStrExt::from_bytes(real_path.as_bytes());
184
185    let path = Path::new("/sys/block/")
186        .to_owned()
187        .join(trimmed)
188        .join("queue/rotational");
189    // Normally, this file only contains '0' or '1' but just in case, we get 8 bytes...
190    match get_all_data(path, 8)
191        .unwrap_or_default()
192        .trim()
193        .parse()
194        .ok()
195    {
196        // The disk is marked as rotational so it's a HDD.
197        Some(1) => DiskKind::HDD,
198        // The disk is marked as non-rotational so it's very likely a SSD.
199        Some(0) => DiskKind::SSD,
200        // Normally it shouldn't happen but welcome to the wonderful world of IT! :D
201        Some(x) => DiskKind::Unknown(x),
202        // The information isn't available...
203        None => DiskKind::Unknown(-1),
204    }
205}
206
207fn get_all_list(container: &mut Vec<Disk>, content: &str) {
208    container.clear();
209    // The goal of this array is to list all removable devices (the ones whose name starts with
210    // "usb-").
211    let removable_entries = match fs::read_dir("/dev/disk/by-id/") {
212        Ok(r) => r
213            .filter_map(|res| Some(res.ok()?.path()))
214            .filter_map(|e| {
215                if e.file_name()
216                    .and_then(|x| Some(x.to_str()?.starts_with("usb-")))
217                    .unwrap_or_default()
218                {
219                    e.canonicalize().ok()
220                } else {
221                    None
222                }
223            })
224            .collect::<Vec<PathBuf>>(),
225        _ => Vec::new(),
226    };
227
228    for disk in content
229        .lines()
230        .map(|line| {
231            let line = line.trim_start();
232            // mounts format
233            // http://man7.org/linux/man-pages/man5/fstab.5.html
234            // fs_spec<tab>fs_file<tab>fs_vfstype<tab>other fields
235            let mut fields = line.split_whitespace();
236            let fs_spec = fields.next().unwrap_or("");
237            let fs_file = fields
238                .next()
239                .unwrap_or("")
240                .replace("\\134", "\\")
241                .replace("\\040", " ")
242                .replace("\\011", "\t")
243                .replace("\\012", "\n");
244            let fs_vfstype = fields.next().unwrap_or("");
245            (fs_spec, fs_file, fs_vfstype)
246        })
247        .filter(|(fs_spec, fs_file, fs_vfstype)| {
248            // Check if fs_vfstype is one of our 'ignored' file systems.
249            let filtered = match *fs_vfstype {
250                "rootfs" | // https://www.kernel.org/doc/Documentation/filesystems/ramfs-rootfs-initramfs.txt
251                "sysfs" | // pseudo file system for kernel objects
252                "proc" |  // another pseudo file system
253                "devtmpfs" |
254                "cgroup" |
255                "cgroup2" |
256                "pstore" | // https://www.kernel.org/doc/Documentation/ABI/testing/pstore
257                "squashfs" | // squashfs is a compressed read-only file system (for snaps)
258                "rpc_pipefs" | // The pipefs pseudo file system service
259                "iso9660" // optical media
260                => true,
261                "tmpfs" => !cfg!(feature = "linux-tmpfs"),
262                // calling statvfs on a mounted CIFS or NFS may hang, when they are mounted with option: hard
263                "cifs" | "nfs" | "nfs4" => !cfg!(feature = "linux-netdevs"),
264                _ => false,
265            };
266
267            !(filtered ||
268               fs_file.starts_with("/sys") || // check if fs_file is an 'ignored' mount point
269               fs_file.starts_with("/proc") ||
270               (fs_file.starts_with("/run") && !fs_file.starts_with("/run/media")) ||
271               fs_spec.starts_with("sunrpc"))
272        })
273        .filter_map(|(fs_spec, fs_file, fs_vfstype)| {
274            new_disk(
275                fs_spec.as_ref(),
276                Path::new(&fs_file),
277                fs_vfstype.as_ref(),
278                &removable_entries,
279            )
280        })
281    {
282        container.push(disk);
283    }
284}
285
286// #[test]
287// fn check_all_list() {
288//     let disks = get_all_disks_inner(
289//         r#"tmpfs /proc tmpfs rw,seclabel,relatime 0 0
290// proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
291// systemd-1 /proc/sys/fs/binfmt_misc autofs rw,relatime,fd=29,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=17771 0 0
292// tmpfs /sys tmpfs rw,seclabel,relatime 0 0
293// sysfs /sys sysfs rw,seclabel,nosuid,nodev,noexec,relatime 0 0
294// securityfs /sys/kernel/security securityfs rw,nosuid,nodev,noexec,relatime 0 0
295// cgroup2 /sys/fs/cgroup cgroup2 rw,seclabel,nosuid,nodev,noexec,relatime,nsdelegate 0 0
296// pstore /sys/fs/pstore pstore rw,seclabel,nosuid,nodev,noexec,relatime 0 0
297// none /sys/fs/bpf bpf rw,nosuid,nodev,noexec,relatime,mode=700 0 0
298// configfs /sys/kernel/config configfs rw,nosuid,nodev,noexec,relatime 0 0
299// selinuxfs /sys/fs/selinux selinuxfs rw,relatime 0 0
300// debugfs /sys/kernel/debug debugfs rw,seclabel,nosuid,nodev,noexec,relatime 0 0
301// tmpfs /dev/shm tmpfs rw,seclabel,relatime 0 0
302// devpts /dev/pts devpts rw,seclabel,relatime,gid=5,mode=620,ptmxmode=666 0 0
303// tmpfs /sys/fs/selinux tmpfs rw,seclabel,relatime 0 0
304// /dev/vda2 /proc/filesystems xfs rw,seclabel,relatime,attr2,inode64,logbufs=8,logbsize=32k,noquota 0 0
305// "#,
306//     );
307//     assert_eq!(disks.len(), 1);
308//     assert_eq!(
309//         disks[0],
310//         Disk {
311//             type_: DiskType::Unknown(-1),
312//             name: OsString::from("devpts"),
313//             file_system: vec![100, 101, 118, 112, 116, 115],
314//             mount_point: PathBuf::from("/dev/pts"),
315//             total_space: 0,
316//             available_space: 0,
317//         }
318//     );
319// }