use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use std::marker::PhantomData;
use std::os::unix::ffi::OsStringExt;
use std::path::{Path, PathBuf};
use std::ptr::{NonNull, null_mut};
use std::sync::OnceLock;
use libc::{c_void, devstat, devstat_getversion};
use super::ffi::{
DEVSTAT_READ, DEVSTAT_WRITE, geom_stats_open, geom_stats_snapshot_free,
geom_stats_snapshot_get, geom_stats_snapshot_next, geom_stats_snapshot_reset,
};
use super::utils::{c_buf_to_utf8_str, get_sys_value_str_by_name};
use crate::{Disk, DiskKind, DiskRefreshKind, DiskUsage};
#[derive(Debug)]
pub(crate) struct DiskInner {
name: OsString,
c_mount_point: Vec<libc::c_char>,
dev_id: Option<String>,
mount_point: PathBuf,
total_space: u64,
available_space: u64,
pub(crate) file_system: OsString,
is_removable: bool,
is_read_only: bool,
read_bytes: u64,
old_read_bytes: u64,
written_bytes: u64,
old_written_bytes: u64,
updated: bool,
}
#[cfg(test)]
impl Default for DiskInner {
fn default() -> Self {
Self {
name: OsString::new(),
c_mount_point: Vec::new(),
dev_id: None,
mount_point: PathBuf::new(),
total_space: 0,
available_space: 0,
file_system: OsString::new(),
is_removable: false,
is_read_only: false,
read_bytes: 0,
old_read_bytes: 0,
written_bytes: 0,
old_written_bytes: 0,
updated: false,
}
}
}
impl DiskInner {
pub(crate) fn kind(&self) -> DiskKind {
DiskKind::Unknown(-1)
}
pub(crate) fn name(&self) -> &OsStr {
&self.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 {
refresh_disk(self, refresh_kind)
}
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,
) {
unsafe { get_all_list(&mut self.disks, remove_not_listed_disks, refresh_kind) }
}
pub(crate) fn list(&self) -> &[Disk] {
&self.disks
}
pub(crate) fn list_mut(&mut self) -> &mut [Disk] {
&mut self.disks
}
}
trait GetValues {
fn update_old(&mut self);
fn get_read(&mut self) -> &mut u64;
fn get_written(&mut self) -> &mut u64;
fn dev_id(&self) -> Option<&String>;
}
impl GetValues for crate::Disk {
fn update_old(&mut self) {
self.inner.update_old()
}
fn get_read(&mut self) -> &mut u64 {
self.inner.get_read()
}
fn get_written(&mut self) -> &mut u64 {
self.inner.get_written()
}
fn dev_id(&self) -> Option<&String> {
self.inner.dev_id()
}
}
impl GetValues for &mut DiskInner {
fn update_old(&mut self) {
self.old_read_bytes = self.read_bytes;
self.old_written_bytes = self.written_bytes;
}
fn get_read(&mut self) -> &mut u64 {
&mut self.read_bytes
}
fn get_written(&mut self) -> &mut u64 {
&mut self.written_bytes
}
fn dev_id(&self) -> Option<&String> {
self.dev_id.as_ref()
}
}
impl GetValues for DiskInner {
fn update_old(&mut self) {
self.old_read_bytes = self.read_bytes;
self.old_written_bytes = self.written_bytes;
}
fn get_read(&mut self) -> &mut u64 {
&mut self.read_bytes
}
fn get_written(&mut self) -> &mut u64 {
&mut self.written_bytes
}
fn dev_id(&self) -> Option<&String> {
self.dev_id.as_ref()
}
}
unsafe fn get_statvfs(
c_mount_point: &[libc::c_char],
vfs: &mut libc::statvfs,
) -> Option<(u64, u64, bool)> {
if unsafe { libc::statvfs(c_mount_point.as_ptr() as *const _, vfs as *mut _) < 0 } {
sysinfo_debug!("statvfs failed");
None
} else {
let block_size: u64 = vfs.f_frsize as _;
Some((
vfs.f_blocks.saturating_mul(block_size),
vfs.f_favail.saturating_mul(block_size),
(vfs.f_flag & libc::ST_RDONLY) != 0,
))
}
}
fn refresh_disk(disk: &mut DiskInner, refresh_kind: DiskRefreshKind) -> bool {
if refresh_kind.storage() {
unsafe {
let mut vfs: libc::statvfs = std::mem::zeroed();
if let Some((total_space, available_space, is_read_only)) =
get_statvfs(&disk.c_mount_point, &mut vfs)
{
disk.total_space = total_space;
disk.available_space = available_space;
disk.is_read_only = is_read_only;
}
}
}
if refresh_kind.io_usage() {
unsafe {
refresh_disk_io(&mut [disk]);
}
}
true
}
unsafe fn initialize_geom() -> Result<(), ()> {
let version = unsafe { devstat_getversion(null_mut()) };
if version != 6 {
sysinfo_debug!("version {version} of devstat is not supported");
return Err(());
}
let r = unsafe { geom_stats_open() };
if r != 0 {
sysinfo_debug!("`geom_stats_open` failed: {r}");
Err(())
} else {
Ok(())
}
}
unsafe fn refresh_disk_io<T: GetValues>(disks: &mut [T]) {
static GEOM_STATS: OnceLock<Result<(), ()>> = OnceLock::new();
if GEOM_STATS
.get_or_init(|| unsafe { initialize_geom() })
.is_err()
{
return;
}
let snap = unsafe { GeomSnapshot::new() };
let Some(mut snap) = snap else {
return;
};
for device in snap.iter() {
let device = unsafe { device.devstat.as_ref() };
let Some(device_name) = c_buf_to_utf8_str(&device.device_name) else {
continue;
};
let dev_stat_name = format!("{device_name}{}", device.unit_number);
for disk in disks
.iter_mut()
.filter(|d| d.dev_id().is_some_and(|id| *id == dev_stat_name))
{
disk.update_old();
*disk.get_read() = device.bytes[DEVSTAT_READ];
*disk.get_written() = device.bytes[DEVSTAT_WRITE];
}
}
}
fn get_disks_mapping() -> HashMap<String, String> {
let mut disk_mapping = HashMap::new();
let Some(mapping) = get_sys_value_str_by_name(b"kern.geom.conftxt\0") else {
return disk_mapping;
};
let mut last_id = String::new();
for line in mapping.lines() {
let mut parts = line.split_whitespace();
let Some(kind) = parts.next() else { continue };
#[allow(clippy::collapsible_if)]
if kind == "0" {
if let Some("DISK") = parts.next()
&& let Some(id) = parts.next()
{
last_id.clear();
last_id.push_str(id);
}
} else if kind == "2" && !last_id.is_empty() {
if let Some("LABEL") = parts.next()
&& let Some(path) = parts.next()
{
disk_mapping.insert(format!("/dev/{path}"), last_id.clone());
}
}
}
disk_mapping
}
pub unsafe fn get_all_list(
container: &mut Vec<Disk>,
remove_not_listed_disks: bool,
refresh_kind: DiskRefreshKind,
) {
let mut fs_infos: *mut libc::statfs = null_mut();
let count = unsafe { libc::getmntinfo(&mut fs_infos, libc::MNT_WAIT) };
if count < 1 {
return;
}
let disk_mapping = get_disks_mapping();
let fs_infos: &[libc::statfs] =
unsafe { std::slice::from_raw_parts(fs_infos as _, count as _) };
for fs_info in fs_infos {
if fs_info.f_mntfromname[0] == 0 || fs_info.f_mntonname[0] == 0 {
continue;
}
let fs_type: Vec<u8> = {
let len = fs_info
.f_fstypename
.iter()
.position(|x| *x == 0)
.unwrap_or(fs_info.f_fstypename.len());
fs_info.f_fstypename[..len]
.iter()
.map(|c| *c as u8)
.collect()
};
match &fs_type[..] {
b"autofs" | b"devfs" | b"linprocfs" | b"procfs" | b"fdesckfs" | b"tmpfs"
| b"linsysfs" => {
sysinfo_debug!(
"Memory filesystem `{:?}`, ignoring it.",
c_buf_to_utf8_str(&fs_info.f_fstypename),
);
continue;
}
_ => {}
}
let mount_point = match c_buf_to_utf8_str(&fs_info.f_mntonname) {
Some(m) => m,
None => {
sysinfo_debug!("Cannot get disk mount point, ignoring it.");
continue;
}
};
if mount_point == "/boot/efi" {
continue;
}
let name = if mount_point == "/" {
OsString::from("root")
} else {
OsString::from(mount_point)
};
if let Some(disk) = container.iter_mut().find(|d| {
d.inner.name == name
&& d.inner
.file_system
.as_encoded_bytes()
.iter()
.zip(fs_type.iter())
.all(|(a, b)| a == b)
}) {
refresh_disk(&mut disk.inner, refresh_kind.without_io_usage());
disk.inner.updated = true;
} else {
let dev_mount_point = c_buf_to_utf8_str(&fs_info.f_mntfromname).unwrap_or("");
let is_removable = if refresh_kind.storage() {
[b"USB", b"usb"].iter().any(|b| *b == &fs_type[..])
|| fs_type.starts_with(b"/dev/cd")
} else {
false
};
let mut disk = DiskInner {
name,
c_mount_point: fs_info.f_mntonname.to_vec(),
mount_point: PathBuf::from(mount_point),
dev_id: disk_mapping.get(dev_mount_point).map(ToString::to_string),
total_space: 0,
available_space: 0,
file_system: OsString::from_vec(fs_type),
is_removable,
is_read_only: false,
read_bytes: 0,
old_read_bytes: 0,
written_bytes: 0,
old_written_bytes: 0,
updated: true,
};
refresh_disk(&mut disk, refresh_kind.without_io_usage());
container.push(Disk { inner: disk });
}
}
if remove_not_listed_disks {
container.retain_mut(|disk| {
if !disk.inner.updated {
return false;
}
disk.inner.updated = false;
true
});
} else {
for c in container.iter_mut() {
c.inner.updated = false;
}
}
if refresh_kind.io_usage() {
unsafe {
refresh_disk_io(container.as_mut_slice());
}
}
}
struct GeomSnapshot(NonNull<c_void>);
impl GeomSnapshot {
unsafe fn new() -> Option<Self> {
match NonNull::new(unsafe { geom_stats_snapshot_get() }) {
Some(n) => Some(Self(n)),
None => {
sysinfo_debug!("geom_stats_snapshot_get failed");
None
}
}
}
fn iter(&mut self) -> GeomSnapshotIter<'_> {
GeomSnapshotIter(self)
}
fn reset(&mut self) {
unsafe { geom_stats_snapshot_reset(self.0.as_mut()) }
}
}
impl Drop for GeomSnapshot {
fn drop(&mut self) {
unsafe { geom_stats_snapshot_free(self.0.as_mut()) };
}
}
#[repr(transparent)]
struct Devstat<'a> {
devstat: NonNull<devstat>,
phantom: PhantomData<&'a devstat>,
}
struct GeomSnapshotIter<'a>(&'a mut GeomSnapshot);
impl<'a> Iterator for GeomSnapshotIter<'a> {
type Item = Devstat<'a>;
fn next(&mut self) -> Option<Self::Item> {
let raw = unsafe { geom_stats_snapshot_next(self.0.0.as_mut()) };
NonNull::new(raw).map(|devstat| Devstat {
devstat,
phantom: PhantomData,
})
}
}
impl Drop for GeomSnapshotIter<'_> {
fn drop(&mut self) {
self.0.reset();
}
}