use crate::sys::utils::{get_all_utf8_data, to_cpath};
use crate::{Disk, DiskKind, DiskRefreshKind, DiskUsage};
use libc::statvfs;
use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use std::fs;
use std::mem::MaybeUninit;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
const SECTOR_SIZE: u64 = 512;
macro_rules! cast {
($x:expr) => {
u64::from($x)
};
}
pub(crate) struct DiskInner {
type_: DiskKind,
device_name: OsString,
actual_device_name: Option<String>,
file_system: OsString,
mount_point: PathBuf,
total_space: u64,
available_space: u64,
is_removable: bool,
is_read_only: bool,
old_written_bytes: u64,
old_read_bytes: u64,
written_bytes: u64,
read_bytes: u64,
updated: bool,
}
impl DiskInner {
pub(crate) fn kind(&self) -> DiskKind {
self.type_
}
pub(crate) fn name(&self) -> &OsStr {
&self.device_name
}
pub(crate) fn file_system(&self) -> &OsStr {
&self.file_system
}
pub(crate) fn mount_point(&self) -> &Path {
&self.mount_point
}
pub(crate) fn total_space(&self) -> u64 {
self.total_space
}
pub(crate) fn available_space(&self) -> u64 {
self.available_space
}
pub(crate) fn is_removable(&self) -> bool {
self.is_removable
}
pub(crate) fn is_read_only(&self) -> bool {
self.is_read_only
}
pub(crate) fn refresh_specifics(&mut self, refresh_kind: DiskRefreshKind) -> bool {
self.efficient_refresh(refresh_kind, &disk_stats(&refresh_kind), false)
}
fn efficient_refresh(
&mut self,
refresh_kind: DiskRefreshKind,
procfs_disk_stats: &HashMap<String, DiskStat>,
first: bool,
) -> bool {
if refresh_kind.io_usage() {
if self.actual_device_name.is_none() {
self.actual_device_name = Some(get_actual_device_name(&self.device_name));
}
if let Some(stat) = self
.actual_device_name
.as_ref()
.and_then(|actual_device_name| procfs_disk_stats.get(actual_device_name))
{
self.old_read_bytes = self.read_bytes;
self.old_written_bytes = self.written_bytes;
self.read_bytes = stat.sectors_read * SECTOR_SIZE;
self.written_bytes = stat.sectors_written * SECTOR_SIZE;
} else {
sysinfo_debug!("Failed to update disk i/o stats");
}
}
if refresh_kind.kind() && self.type_ == DiskKind::Unknown(-1) {
self.type_ = find_type_for_device_name(&self.device_name);
}
if refresh_kind.storage()
&& let Some((total_space, available_space, is_read_only)) =
unsafe { load_statvfs_values(&self.mount_point) }
{
self.total_space = total_space;
self.available_space = available_space;
if first {
self.is_read_only = is_read_only;
}
}
true
}
pub(crate) fn usage(&self) -> DiskUsage {
DiskUsage {
read_bytes: self.read_bytes.saturating_sub(self.old_read_bytes),
total_read_bytes: self.read_bytes,
written_bytes: self.written_bytes.saturating_sub(self.old_written_bytes),
total_written_bytes: self.written_bytes,
}
}
}
impl crate::DisksInner {
pub(crate) fn new() -> Self {
Self {
disks: Vec::with_capacity(2),
}
}
pub(crate) fn refresh_specifics(
&mut self,
remove_not_listed_disks: bool,
refresh_kind: DiskRefreshKind,
) {
get_all_list(
&mut self.disks,
&get_all_utf8_data("/proc/mounts", 16_385).unwrap_or_default(),
refresh_kind,
);
if remove_not_listed_disks {
self.disks.retain_mut(|disk| {
if !disk.inner.updated {
return false;
}
disk.inner.updated = false;
true
});
} else {
for c in self.disks.iter_mut() {
c.inner.updated = false;
}
}
}
pub(crate) fn list(&self) -> &[Disk] {
&self.disks
}
pub(crate) fn list_mut(&mut self) -> &mut [Disk] {
&mut self.disks
}
}
fn get_actual_device_name(device: &OsStr) -> String {
let device_path = PathBuf::from(device);
std::fs::canonicalize(&device_path)
.ok()
.and_then(|path| path.strip_prefix("/dev").ok().map(Path::to_path_buf))
.unwrap_or(device_path)
.to_str()
.map(str::to_owned)
.unwrap_or_default()
}
unsafe fn load_statvfs_values(mount_point: &Path) -> Option<(u64, u64, bool)> {
let mount_point_cpath = to_cpath(mount_point);
let mut stat: MaybeUninit<statvfs> = MaybeUninit::uninit();
if unsafe {
retry_eintr!(statvfs(
mount_point_cpath.as_ptr() as *const _,
stat.as_mut_ptr()
))
} == 0
{
let stat = unsafe { stat.assume_init() };
let bsize = cast!(stat.f_bsize);
let blocks = cast!(stat.f_blocks);
let bavail = cast!(stat.f_bavail);
let total = bsize.saturating_mul(blocks);
if total == 0 {
return None;
}
let available = bsize.saturating_mul(bavail);
let is_read_only = (stat.f_flag & libc::ST_RDONLY) != 0;
Some((total, available, is_read_only))
} else {
None
}
}
fn new_disk(
device_name: &OsStr,
mount_point: &Path,
file_system: &OsStr,
removable_entries: &[PathBuf],
procfs_disk_stats: &HashMap<String, DiskStat>,
refresh_kind: DiskRefreshKind,
) -> Disk {
let is_removable = removable_entries
.iter()
.any(|e| e.as_os_str() == device_name);
let mut disk = Disk {
inner: DiskInner {
type_: DiskKind::Unknown(-1),
device_name: device_name.to_owned(),
actual_device_name: None,
file_system: file_system.to_owned(),
mount_point: mount_point.to_owned(),
total_space: 0,
available_space: 0,
is_removable,
is_read_only: false,
old_read_bytes: 0,
old_written_bytes: 0,
read_bytes: 0,
written_bytes: 0,
updated: true,
},
};
disk.inner
.efficient_refresh(refresh_kind, procfs_disk_stats, true);
disk
}
#[allow(clippy::manual_range_contains)]
fn find_type_for_device_name(device_name: &OsStr) -> DiskKind {
let device_name_path = device_name.to_str().unwrap_or_default();
let real_path = fs::canonicalize(device_name).unwrap_or_else(|_| PathBuf::from(device_name));
let mut real_path = real_path.to_str().unwrap_or_default();
if device_name_path.starts_with("/dev/mapper/") {
if real_path != device_name_path {
return find_type_for_device_name(OsStr::new(&real_path));
}
} else if device_name_path.starts_with("/dev/sd") || device_name_path.starts_with("/dev/vd") {
real_path = real_path.trim_start_matches("/dev/");
real_path = real_path.trim_end_matches(|c| c >= '0' && c <= '9');
} else if device_name_path.starts_with("/dev/nvme") {
real_path = match real_path.find('p') {
Some(idx) => &real_path["/dev/".len()..idx],
None => &real_path["/dev/".len()..],
};
} else if device_name_path.starts_with("/dev/root") {
if real_path != device_name_path {
return find_type_for_device_name(OsStr::new(&real_path));
}
} else if device_name_path.starts_with("/dev/mmcblk") {
real_path = match real_path.find('p') {
Some(idx) => &real_path["/dev/".len()..idx],
None => &real_path["/dev/".len()..],
};
} else {
real_path = real_path.trim_start_matches("/dev/");
}
let trimmed: &OsStr = OsStrExt::from_bytes(real_path.as_bytes());
let path = Path::new("/sys/block/")
.to_owned()
.join(trimmed)
.join("queue/rotational");
match get_all_utf8_data(path, 8)
.unwrap_or_default()
.trim()
.parse()
.ok()
{
Some(1) => DiskKind::HDD,
Some(0) => DiskKind::SSD,
Some(x) => DiskKind::Unknown(x),
None => DiskKind::Unknown(-1),
}
}
fn get_all_list(container: &mut Vec<Disk>, content: &str, refresh_kind: DiskRefreshKind) {
let removable_entries = match fs::read_dir("/dev/disk/by-id/") {
Ok(r) => r
.filter_map(|res| Some(res.ok()?.path()))
.filter_map(|e| {
if e.file_name()
.and_then(|x| Some(x.to_str()?.starts_with("usb-")))
.unwrap_or_default()
{
e.canonicalize().ok()
} else {
None
}
})
.collect::<Vec<PathBuf>>(),
_ => Vec::new(),
};
let procfs_disk_stats = disk_stats(&refresh_kind);
for (fs_spec, fs_file, fs_vfstype) in content
.lines()
.map(|line| {
let line = line.trim_start();
let mut fields = line.split_whitespace();
let fs_spec = fields.next().unwrap_or("");
let fs_file = fields
.next()
.unwrap_or("")
.replace("\\134", "\\")
.replace("\\040", " ")
.replace("\\011", "\t")
.replace("\\012", "\n");
let fs_vfstype = fields.next().unwrap_or("");
(fs_spec, fs_file, fs_vfstype)
})
.filter(|(fs_spec, fs_file, fs_vfstype)| {
let filtered = match *fs_vfstype {
"rootfs" | "sysfs" | "proc" | "devtmpfs" |
"cgroup" |
"cgroup2" |
"pstore" | "squashfs" | "rpc_pipefs" | "iso9660" | "devpts" | "hugetlbfs" | "mqueue" => true,
"tmpfs" => !cfg!(feature = "linux-tmpfs"),
"cifs" | "nfs" | "nfs4" | "autofs" => !cfg!(feature = "linux-netdevs"),
_ => false,
};
!(filtered ||
fs_file.starts_with("/sys") || fs_file.starts_with("/proc") ||
(fs_file.starts_with("/run") && !fs_file.starts_with("/run/media")) ||
fs_spec.starts_with("sunrpc"))
})
{
let mount_point = Path::new(&fs_file);
if let Some(disk) = container.iter_mut().find(|d| {
d.inner.mount_point == mount_point
&& d.inner.device_name == fs_spec
&& d.inner.file_system == fs_vfstype
}) {
disk.inner
.efficient_refresh(refresh_kind, &procfs_disk_stats, false);
disk.inner.updated = true;
continue;
}
container.push(new_disk(
fs_spec.as_ref(),
mount_point,
fs_vfstype.as_ref(),
&removable_entries,
&procfs_disk_stats,
refresh_kind,
));
}
}
#[derive(Debug, PartialEq)]
struct DiskStat {
sectors_read: u64,
sectors_written: u64,
}
impl DiskStat {
fn new_from_line(line: &str) -> Option<(String, Self)> {
let mut iter = line.split_whitespace();
let name = iter.nth(2).map(ToString::to_string)?;
let sectors_read = iter.nth(2).and_then(|v| u64::from_str(v).ok()).unwrap_or(0);
let sectors_written = iter.nth(3).and_then(|v| u64::from_str(v).ok()).unwrap_or(0);
Some((
name,
Self {
sectors_read,
sectors_written,
},
))
}
}
fn disk_stats(refresh_kind: &DiskRefreshKind) -> HashMap<String, DiskStat> {
if refresh_kind.io_usage() {
let path = "/proc/diskstats";
match fs::read_to_string(path) {
Ok(content) => disk_stats_inner(&content),
Err(_error) => {
sysinfo_debug!("failed to read {path:?}: {_error:?}");
HashMap::new()
}
}
} else {
Default::default()
}
}
fn disk_stats_inner(content: &str) -> HashMap<String, DiskStat> {
let mut data = HashMap::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if let Some((name, stats)) = DiskStat::new_from_line(line) {
data.insert(name, stats);
}
}
data
}
#[cfg(test)]
mod test {
use super::{DiskStat, disk_stats_inner};
use std::collections::HashMap;
#[test]
fn test_disk_stat_parsing() {
let file_content = "\
259 0 nvme0n1 571695 101559 38943220 165643 9824246 1076193 462375378 4140037 0 1038904 4740493 254020 0 1436922320 68519 306875 366293
259 1 nvme0n1p1 240 2360 15468 48 2 0 2 0 0 21 50 8 0 2373552 2 0 0
259 2 nvme0n1p2 243 10 11626 26 63 39 616 125 0 84 163 44 0 1075280 11 0 0
259 3 nvme0n1p3 571069 99189 38910302 165547 9824180 1076154 462374760 4139911 0 1084855 4373964 253968 0 1433473488 68505 0 0
253 0 dm-0 670206 0 38909056 259490 10900330 0 462374760 12906518 0 1177098 13195902 253968 0 1433473488 29894 0 0
252 0 zram0 2382 0 20984 11 260261 0 2082088 2063 0 1964 2074 0 0 0 0 0 0
1 2 bla 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
";
let data = disk_stats_inner(file_content);
let expected_data: HashMap<String, DiskStat> = HashMap::from([
(
"nvme0n1".to_string(),
DiskStat {
sectors_read: 38943220,
sectors_written: 462375378,
},
),
(
"nvme0n1p1".to_string(),
DiskStat {
sectors_read: 15468,
sectors_written: 2,
},
),
(
"nvme0n1p2".to_string(),
DiskStat {
sectors_read: 11626,
sectors_written: 616,
},
),
(
"nvme0n1p3".to_string(),
DiskStat {
sectors_read: 38910302,
sectors_written: 462374760,
},
),
(
"dm-0".to_string(),
DiskStat {
sectors_read: 38909056,
sectors_written: 462374760,
},
),
(
"zram0".to_string(),
DiskStat {
sectors_read: 20984,
sectors_written: 2082088,
},
),
(
"bla".to_string(),
DiskStat {
sectors_read: 6,
sectors_written: 10,
},
),
]);
assert_eq!(data, expected_data);
}
#[test]
fn disk_entry_with_less_information() {
let file_content = "\
systemd-1 /efi autofs rw,relatime,fd=181,pgrp=1,timeout=120,minproto=5,maxproto=5,direct,pipe_ino=8311 0 0
/dev/nvme0n1p1 /efi vfat rw,nosuid,nodev,noexec,relatime,nosymfollow,fmask=0077,dmask=0077 0 0
";
let data = disk_stats_inner(file_content);
let expected_data: HashMap<String, DiskStat> = HashMap::from([
(
"autofs".to_string(),
DiskStat {
sectors_read: 0,
sectors_written: 0,
},
),
(
"vfat".to_string(),
DiskStat {
sectors_read: 0,
sectors_written: 0,
},
),
]);
assert_eq!(data, expected_data);
}
}