use crate::{Disk, DiskKind, DiskRefreshKind};
use crate::{DiskUsage, sys::ffi};
use objc2_core_foundation::{
CFArray, CFBoolean, CFDictionary, CFNumber, CFRetained, CFString, CFURL, kCFAllocatorDefault,
kCFTypeArrayCallBacks, kCFURLVolumeAvailableCapacityForImportantUsageKey,
kCFURLVolumeAvailableCapacityKey, kCFURLVolumeIsBrowsableKey, kCFURLVolumeIsEjectableKey,
kCFURLVolumeIsInternalKey, kCFURLVolumeIsLocalKey, kCFURLVolumeIsRemovableKey,
kCFURLVolumeNameKey, kCFURLVolumeTotalCapacityKey, kCFURLVolumeUUIDStringKey,
};
use libc::c_void;
use std::ffi::{CStr, OsStr, OsString};
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::{Path, PathBuf};
use std::ptr;
pub(crate) struct DiskInner {
pub(crate) type_: DiskKind,
pub(crate) name: OsString,
#[cfg(target_os = "macos")]
bsd_name: Option<Vec<u8>>,
pub(crate) file_system: OsString,
pub(crate) mount_point: PathBuf,
volume_url: CFRetained<CFURL>,
pub(crate) total_space: u64,
pub(crate) available_space: u64,
pub(crate) is_removable: bool,
pub(crate) is_read_only: bool,
pub(crate) old_written_bytes: u64,
pub(crate) old_read_bytes: u64,
pub(crate) written_bytes: u64,
pub(crate) read_bytes: u64,
updated: bool,
uuid: OsString,
}
#[cfg(test)]
impl Default for DiskInner {
fn default() -> DiskInner {
Self {
type_: DiskKind::HDD,
name: OsString::new(),
#[cfg(target_os = "macos")]
bsd_name: None,
file_system: OsString::new(),
mount_point: PathBuf::new(),
volume_url: unsafe { CFRetained::from_raw(std::ptr::NonNull::dangling()) },
total_space: 0,
available_space: 0,
is_removable: false,
is_read_only: false,
old_written_bytes: 0,
old_read_bytes: 0,
written_bytes: 0,
read_bytes: 0,
updated: false,
uuid: OsString::new(),
}
}
}
unsafe impl Send for DiskInner {}
impl DiskInner {
pub(crate) fn kind(&self) -> DiskKind {
self.type_
}
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 {
self.refresh_kind(refresh_kind);
self.refresh_io(refresh_kind);
if refresh_kind.storage() {
unsafe {
if let Some(requested_properties) = build_requested_properties(&[
kCFURLVolumeTotalCapacityKey,
kCFURLVolumeAvailableCapacityKey,
kCFURLVolumeAvailableCapacityForImportantUsageKey,
]) {
match get_disk_properties(&self.volume_url, &requested_properties) {
Some(disk_props) => {
match get_int_value(&disk_props, kCFURLVolumeTotalCapacityKey) {
Some(total_space) => self.total_space = total_space,
None => {
sysinfo_debug!("Failed to get disk total space");
}
}
match get_available_volume_space(&disk_props) {
Some(available_space) => self.available_space = available_space,
None => {
sysinfo_debug!("Failed to get disk available space");
}
}
}
None => {
sysinfo_debug!("Failed to get disk properties");
}
}
} else {
sysinfo_debug!("failed to create volume key list, skipping refresh");
}
}
}
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,
}
}
fn refresh_kind(&mut self, refresh_kind: DiskRefreshKind) {
if refresh_kind.kind() && self.type_ == DiskKind::Unknown(-1) {
#[cfg(target_os = "macos")]
{
match self
.bsd_name
.as_ref()
.and_then(|name| crate::sys::inner::disk::get_disk_type(name))
{
Some(type_) => self.type_ = type_,
None => {
sysinfo_debug!("Failed to retrieve `DiskKind`");
}
}
}
#[cfg(not(target_os = "macos"))]
{
self.type_ = DiskKind::SSD;
}
}
}
#[cfg(target_os = "macos")]
fn refresh_io(&mut self, refresh_kind: DiskRefreshKind) {
if refresh_kind.io_usage() {
match self
.bsd_name
.as_ref()
.and_then(|name| crate::sys::inner::disk::get_disk_io(name))
{
Some((read_bytes, written_bytes)) => {
self.old_read_bytes = self.read_bytes;
self.old_written_bytes = self.written_bytes;
self.read_bytes = read_bytes;
self.written_bytes = written_bytes;
}
None => {
sysinfo_debug!("Failed to update disk i/o stats");
}
}
}
}
#[cfg(not(target_os = "macos"))]
fn refresh_io(&mut self, _refresh_kind: DiskRefreshKind) {}
}
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 {
with_autorelease(|| {
get_list(&mut self.disks, 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
}
}
unsafe fn get_list(container: &mut Vec<Disk>, refresh_kind: DiskRefreshKind) {
unsafe {
let raw_disks = {
let count = libc::getfsstat(ptr::null_mut(), 0, libc::MNT_NOWAIT);
if count < 1 {
return;
}
let bufsize = count * std::mem::size_of::<libc::statfs>() as libc::c_int;
let mut disks = Vec::with_capacity(count as _);
let count = libc::getfsstat(disks.as_mut_ptr(), bufsize, libc::MNT_NOWAIT);
if count < 1 {
return;
}
disks.set_len(count as usize);
disks
};
let mut properties = Vec::with_capacity(10);
properties.push(kCFURLVolumeNameKey);
properties.push(kCFURLVolumeIsBrowsableKey);
properties.push(kCFURLVolumeIsLocalKey);
properties.push(kCFURLVolumeUUIDStringKey);
properties.push(kCFURLVolumeIsEjectableKey);
properties.push(kCFURLVolumeIsRemovableKey);
properties.push(kCFURLVolumeIsInternalKey);
if refresh_kind.storage() {
properties.push(kCFURLVolumeTotalCapacityKey);
properties.push(kCFURLVolumeAvailableCapacityForImportantUsageKey);
properties.push(kCFURLVolumeAvailableCapacityKey);
}
let requested_properties = match build_requested_properties(&properties) {
Some(properties) => properties,
None => {
sysinfo_debug!("failed to create volume key list");
return;
}
};
for c_disk in raw_disks {
let volume_url = match CFURL::from_file_system_representation(
kCFAllocatorDefault,
c_disk.f_mntonname.as_ptr() as *const _,
c_disk.f_mntonname.len() as _,
false as _,
) {
Some(url) => url,
None => {
sysinfo_debug!("getfsstat returned incompatible paths");
continue;
}
};
let prop_dict = match get_disk_properties(&volume_url, &requested_properties) {
Some(props) => props,
None => continue,
};
let browsable =
get_bool_value(&prop_dict, kCFURLVolumeIsBrowsableKey).unwrap_or_default();
if !browsable {
continue;
}
let local_only = get_bool_value(&prop_dict, kCFURLVolumeIsLocalKey).unwrap_or(true);
if !local_only {
continue;
}
let mount_point = PathBuf::from(OsStr::from_bytes(
CStr::from_ptr(c_disk.f_mntonname.as_ptr()).to_bytes(),
));
let uuid = OsStr::from_bytes(CStr::from_ptr(c_disk.f_mntonname.as_ptr()).to_bytes())
.to_os_string();
let disk = container.iter_mut().find(|d| d.inner.uuid == uuid);
if let Some(disk) = new_disk(
disk,
mount_point,
volume_url,
c_disk,
&prop_dict,
refresh_kind,
uuid,
) {
container.push(disk);
}
}
}
}
unsafe fn build_requested_properties(
properties: &[Option<&CFString>],
) -> Option<CFRetained<CFArray>> {
unsafe {
CFArray::new(
None,
properties.as_ptr() as *mut *const c_void,
properties.len() as _,
&kCFTypeArrayCallBacks,
)
}
}
fn get_disk_properties(
volume_url: &CFURL,
requested_properties: &CFArray,
) -> Option<CFRetained<CFDictionary>> {
unsafe { volume_url.resource_properties_for_keys(Some(requested_properties), ptr::null_mut()) }
}
fn get_available_volume_space(disk_props: &CFDictionary) -> Option<u64> {
unsafe {
get_int_value(
disk_props,
kCFURLVolumeAvailableCapacityForImportantUsageKey,
)
.filter(|bytes| *bytes != 0)
.or_else(|| get_int_value(disk_props, kCFURLVolumeAvailableCapacityKey))
}
}
unsafe fn get_dict_value<T, F: FnOnce(*const c_void) -> Option<T>>(
dict: &CFDictionary,
key: Option<&CFString>,
callback: F,
) -> Option<T> {
let mut value = std::ptr::null();
let key: *const CFString = key.map(|key| key as *const CFString).unwrap_or(ptr::null());
if unsafe { dict.value_if_present(key.cast(), &mut value) } {
callback(value)
} else {
None
}
}
pub(super) unsafe fn get_str_value(dict: &CFDictionary, key: Option<&CFString>) -> Option<String> {
unsafe {
get_dict_value(dict, key, |v| {
let v = &*v.cast::<CFString>();
Some(v.to_string())
})
}
}
unsafe fn get_bool_value(dict: &CFDictionary, key: Option<&CFString>) -> Option<bool> {
unsafe {
get_dict_value(dict, key, |v| {
let v = &*v.cast::<CFBoolean>();
Some(v.as_bool())
})
}
}
pub(super) unsafe fn get_int_value(dict: &CFDictionary, key: Option<&CFString>) -> Option<u64> {
unsafe {
get_dict_value(dict, key, |v| {
let v = &*v.cast::<CFNumber>();
Some(v.as_i64()? as u64)
})
}
}
unsafe fn new_disk(
disk: Option<&mut Disk>,
mount_point: PathBuf,
volume_url: CFRetained<CFURL>,
c_disk: libc::statfs,
disk_props: &CFDictionary,
refresh_kind: DiskRefreshKind,
uuid: OsString,
) -> Option<Disk> {
let (total_space, available_space) = if refresh_kind.storage() {
(
unsafe { get_int_value(disk_props, kCFURLVolumeTotalCapacityKey) },
get_available_volume_space(disk_props),
)
} else {
(None, None)
};
if let Some(disk) = disk {
let disk = &mut disk.inner;
if let Some(total_space) = total_space {
disk.total_space = total_space;
}
if let Some(available_space) = available_space {
disk.available_space = available_space;
}
disk.refresh_io(refresh_kind);
disk.refresh_kind(refresh_kind);
disk.updated = true;
return None;
}
let name = unsafe { get_str_value(disk_props, kCFURLVolumeNameKey) }.map(OsString::from)?;
let file_system = {
let len = c_disk
.f_fstypename
.iter()
.position(|&b| b == 0)
.unwrap_or(c_disk.f_fstypename.len());
OsString::from_vec(
c_disk.f_fstypename[..len]
.iter()
.map(|c| *c as u8)
.collect(),
)
};
#[cfg(target_os = "macos")]
let bsd_name = get_bsd_name(&c_disk);
let ejectable =
unsafe { get_bool_value(disk_props, kCFURLVolumeIsEjectableKey) }.unwrap_or(false);
let removable =
unsafe { get_bool_value(disk_props, kCFURLVolumeIsRemovableKey) }.unwrap_or(false);
let is_removable = if ejectable || removable {
true
} else {
let internal =
unsafe { get_bool_value(disk_props, kCFURLVolumeIsInternalKey) }.unwrap_or_default();
!internal
};
let is_read_only = (c_disk.f_flags & libc::MNT_RDONLY as u32) != 0;
let mut disk = DiskInner {
type_: DiskKind::Unknown(-1),
name,
#[cfg(target_os = "macos")]
bsd_name,
file_system,
mount_point,
volume_url,
total_space: total_space.unwrap_or(0),
available_space: available_space.unwrap_or(0),
is_removable,
is_read_only,
read_bytes: 0,
written_bytes: 0,
old_read_bytes: 0,
old_written_bytes: 0,
updated: true,
uuid,
};
disk.refresh_kind(refresh_kind);
disk.refresh_io(refresh_kind);
Some(Disk { inner: disk })
}
unsafe fn with_autorelease<T, F: FnOnce() -> T>(call: F) -> T {
struct DrainPool {
ctx: *mut c_void,
}
impl Drop for DrainPool {
fn drop(&mut self) {
unsafe { ffi::objc_autoreleasePoolPop(self.ctx) }
}
}
let _pool_ctx = DrainPool {
ctx: unsafe { ffi::objc_autoreleasePoolPush() },
};
call()
}
#[cfg(target_os = "macos")]
fn get_bsd_name(disk: &libc::statfs) -> Option<Vec<u8>> {
unsafe {
CStr::from_ptr(disk.f_mntfromname.as_ptr())
.to_bytes_with_nul()
.strip_prefix(b"/dev/")
.map(|slice| slice.to_vec())
.or_else(|| {
sysinfo_debug!("unknown disk mount path format");
None
})
}
}