From 4a0efa602003ae24d2bb8b39cb74a42ab76aa971 Mon Sep 17 00:00:00 2001 From: Shrey Amin Date: Fri, 1 Nov 2024 12:47:21 -0400 Subject: [PATCH 1/8] Add disk i/o stats on macOS and Linux --- Cargo.toml | 3 + src/common/disk.rs | 20 ++++- src/debug.rs | 3 +- src/unix/apple/disk.rs | 95 ++++++++++++++++++++---- src/unix/apple/macos/disk.rs | 137 ++++++++++++++++++++++------------- src/unix/apple/macos/ffi.rs | 11 +++ src/unix/linux/disk.rs | 111 +++++++++++++++++++++++++++- 7 files changed, 313 insertions(+), 67 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 249e90dab..94f18f68d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,6 +111,9 @@ core-foundation-sys = "0.8" [target.'cfg(all(target_os = "linux", not(target_os = "android")))'.dev-dependencies] tempfile = "3.9" +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] +procfs = "0.17.0" + [dev-dependencies] serde_json = "1.0" # Used in documentation tests. bstr = "1.9.0" diff --git a/src/common/disk.rs b/src/common/disk.rs index 9057a5c41..1fe004f11 100644 --- a/src/common/disk.rs +++ b/src/common/disk.rs @@ -4,6 +4,8 @@ use std::ffi::OsStr; use std::fmt; use std::path::Path; +use crate::DiskUsage; + /// Struct containing a disk information. /// /// ```no_run @@ -144,6 +146,20 @@ impl Disk { pub fn refresh(&mut self) -> bool { self.inner.refresh() } + + /// Returns number of bytes read and written by the disk + /// + /// ```no_run + /// use sysinfo::Disks; + /// + /// let disks = Disks::new_with_refreshed_list(); + /// for disk in disks.list() { + /// println!("[{:?}] disk usage: {:?}", disk.name(), disk.usage()); + /// } + /// ``` + pub fn usage(&self) -> DiskUsage { + self.inner.usage() + } } /// Disks interface. @@ -289,9 +305,7 @@ impl Disks { /// disks.refresh(); /// ``` pub fn refresh(&mut self) { - for disk in self.list_mut() { - disk.refresh(); - } + self.inner.refresh(); } /// The disk list will be emptied then completely recomputed. diff --git a/src/debug.rs b/src/debug.rs index 2cb15e5ac..4f4dfcebc 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -57,11 +57,12 @@ impl std::fmt::Debug for crate::Disk { fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( fmt, - "Disk({:?})[FS: {:?}][Type: {:?}][removable: {}] mounted on {:?}: {}/{} B", + "Disk({:?})[FS: {:?}][Type: {:?}][removable: {}][I/O: {:?}] mounted on {:?}: {}/{} B", self.name(), self.file_system(), self.kind(), if self.is_removable() { "yes" } else { "no" }, + self.usage(), self.mount_point(), self.available_space(), self.total_space(), diff --git a/src/unix/apple/disk.rs b/src/unix/apple/disk.rs index 7680b6c9d..39fa98ecc 100644 --- a/src/unix/apple/disk.rs +++ b/src/unix/apple/disk.rs @@ -1,8 +1,11 @@ // Take a look at the license at the top of the repository in the LICENSE file. -use crate::sys::{ - ffi, - utils::{self, CFReleaser}, +use crate::{ + sys::{ + ffi, + utils::{self, CFReleaser}, + }, + DiskUsage, }; use crate::{Disk, DiskKind}; @@ -22,6 +25,7 @@ use std::ptr; pub(crate) struct DiskInner { pub(crate) type_: DiskKind, pub(crate) name: OsString, + bsd_name: Option>, pub(crate) file_system: OsString, pub(crate) mount_point: PathBuf, volume_url: RetainedCFURL, @@ -29,6 +33,10 @@ pub(crate) struct DiskInner { 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, } impl DiskInner { @@ -65,6 +73,17 @@ impl DiskInner { } pub(crate) fn refresh(&mut self) -> bool { + self.old_read_bytes = self.read_bytes; + self.old_written_bytes = self.written_bytes; + + let (read_bytes, written_bytes) = self + .bsd_name + .as_ref() + .and_then(|name| crate::sys::inner::disk::get_disk_io(name)) + .unwrap_or_default(); + self.read_bytes = read_bytes; + self.written_bytes = written_bytes; + unsafe { if let Some(requested_properties) = build_requested_properties(&[ ffi::kCFURLVolumeAvailableCapacityKey, @@ -86,6 +105,15 @@ impl DiskInner { } } } + + 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 { @@ -97,7 +125,7 @@ impl crate::DisksInner { pub(crate) fn refresh_list(&mut self) { unsafe { - // SAFETY: We don't keep any Objective-C objects around because we + // SAFETY: We don't keep any Objective-C objects around because we // don't make any direct Objective-C calls in this code. with_autorelease(|| { get_list(&mut self.disks); @@ -105,6 +133,12 @@ impl crate::DisksInner { } } + pub(crate) fn refresh(&mut self) { + for disk in self.list_mut() { + disk.refresh(); + } + } + pub(crate) fn list(&self) -> &[Disk] { &self.disks } @@ -219,8 +253,9 @@ unsafe fn get_list(container: &mut Vec) { } type RetainedCFArray = CFReleaser; -type RetainedCFDictionary = CFReleaser; +pub(crate) type RetainedCFDictionary = CFReleaser; type RetainedCFURL = CFReleaser; +pub(crate) type RetainedCFString = CFReleaser; unsafe fn build_requested_properties(properties: &[CFStringRef]) -> Option { CFReleaser::new(CFArrayCreate( @@ -337,7 +372,7 @@ unsafe fn get_bool_value(dict: CFDictionaryRef, key: DictKey) -> Option { get_dict_value(dict, key, |v| Some(v as CFBooleanRef == kCFBooleanTrue)) } -unsafe fn get_int_value(dict: CFDictionaryRef, key: DictKey) -> Option { +pub(super) unsafe fn get_int_value(dict: CFDictionaryRef, key: DictKey) -> Option { get_dict_value(dict, key, |v| { let mut val: i64 = 0; if CFNumberGetValue( @@ -358,15 +393,29 @@ unsafe fn new_disk( c_disk: libc::statfs, disk_props: &RetainedCFDictionary, ) -> Option { + let bsd_name = get_bsd_name(&c_disk); + // IOKit is not available on any but the most recent (16+) iOS and iPadOS versions. - // Due to this, we can't query the medium type. All iOS devices use flash-based storage - // so we just assume the disk type is an SSD until Rust has a way to conditionally link to + // Due to this, we can't query the medium type and disk i/o stats. All iOS devices use flash-based storage + // so we just assume the disk type is an SSD and set disk i/o stats to 0 until Rust has a way to conditionally link to // IOKit in more recent deployment versions. + #[cfg(target_os = "macos")] - let type_ = crate::sys::inner::disk::get_disk_type(&c_disk).unwrap_or(DiskKind::Unknown(-1)); + let type_ = bsd_name + .as_ref() + .and_then(|name| crate::sys::inner::disk::get_disk_type(name)) + .unwrap_or(DiskKind::Unknown(-1)); #[cfg(not(target_os = "macos"))] let type_ = DiskKind::SSD; + #[cfg(target_os = "macos")] + let (read_bytes, written_bytes) = bsd_name + .as_ref() + .and_then(|name| crate::sys::inner::disk::get_disk_io(name)) + .unwrap_or_default(); + #[cfg(not(target_os = "macos"))] + let (read_bytes, written_bytes) = (0, 0); + // Note: Since we requested these properties from the system, we don't expect // these property retrievals to fail. @@ -433,6 +482,7 @@ unsafe fn new_disk( inner: DiskInner { type_, name, + bsd_name, file_system, mount_point, volume_url, @@ -440,21 +490,24 @@ unsafe fn new_disk( available_space, is_removable, is_read_only, + read_bytes, + written_bytes, + old_read_bytes: 0, + old_written_bytes: 0, }, }) } - /// Calls the provided closure in the context of a new autorelease pool that is drained /// before returning. -/// +/// /// ## SAFETY: /// You must not return an Objective-C object that is autoreleased from this function since it /// will be freed before usable. unsafe fn with_autorelease T>(call: F) -> T { // NB: This struct exists to help prevent memory leaking if `call` were to panic. // Otherwise, the call to `objc_autoreleasePoolPop` would never be made as the stack unwinds. - // `Drop` destructors for existing types on the stack are run during unwinding, so we can + // `Drop` destructors for existing types on the stack are run during unwinding, so we can // ensure the autorelease pool is drained by using a RAII pattern here. struct DrainPool { ctx: *mut c_void, @@ -471,7 +524,23 @@ unsafe fn with_autorelease T>(call: F) -> T { // SAFETY: Creating a new pool is safe in any context. They can be arbitrarily nested // as long as pool objects are not used in deeper layers, but we only have one and don't // allow it to leave this scope. - let _pool_ctx = DrainPool { ctx: unsafe { ffi::objc_autoreleasePoolPush() } }; + let _pool_ctx = DrainPool { + ctx: unsafe { ffi::objc_autoreleasePoolPush() }, + }; call() // Pool is drained here before returning } + +fn get_bsd_name(disk: &libc::statfs) -> Option> { + // Removes `/dev/` from the value. + unsafe { + CStr::from_ptr(disk.f_mntfromname.as_ptr()) + .to_bytes() + .strip_prefix(b"/dev/") + .map(|slice| slice.to_vec()) + .or_else(|| { + sysinfo_debug!("unknown disk mount path format"); + None + }) + } +} diff --git a/src/unix/apple/macos/disk.rs b/src/unix/apple/macos/disk.rs index 91631a263..86f20dfbf 100644 --- a/src/unix/apple/macos/disk.rs +++ b/src/unix/apple/macos/disk.rs @@ -2,40 +2,20 @@ use crate::sys::ffi; use crate::sys::{ - disk::{get_str_value, DictKey}, + disk::{get_int_value, get_str_value, DictKey}, macos::utils::IOReleaser, utils::CFReleaser, }; +use crate::unix::apple::disk::{RetainedCFDictionary, RetainedCFString}; use crate::DiskKind; use core_foundation_sys::base::{kCFAllocatorDefault, kCFAllocatorNull}; -use core_foundation_sys::string as cfs; - -use std::ffi::CStr; - -pub(crate) fn get_disk_type(disk: &libc::statfs) -> Option { - let characteristics_string = unsafe { - CFReleaser::new(cfs::CFStringCreateWithBytesNoCopy( - kCFAllocatorDefault, - ffi::kIOPropertyDeviceCharacteristicsKey.as_ptr(), - ffi::kIOPropertyDeviceCharacteristicsKey.len() as _, - cfs::kCFStringEncodingUTF8, - false as _, - kCFAllocatorNull, - ))? - }; - - // Removes `/dev/` from the value. - let bsd_name = unsafe { - CStr::from_ptr(disk.f_mntfromname.as_ptr()) - .to_bytes() - .strip_prefix(b"/dev/") - .or_else(|| { - sysinfo_debug!("unknown disk mount path format"); - None - })? - }; +use core_foundation_sys::string::{self as cfs}; +fn iterate_service_tree(bsd_name: &[u8], key: RetainedCFString, eval: F) -> Option +where + F: Fn(ffi::io_registry_entry_t, &RetainedCFDictionary) -> Option, +{ // We don't need to wrap this in an auto-releaser because the following call to `IOServiceGetMatchingServices` // will take ownership of one retain reference. let matching = @@ -91,36 +71,95 @@ pub(crate) fn get_disk_type(disk: &libc::statfs) -> Option { let properties_result = unsafe { CFReleaser::new(ffi::IORegistryEntryCreateCFProperty( current_service_entry.inner(), - characteristics_string.inner(), + key.inner(), kCFAllocatorDefault, 0, )) }; - if let Some(device_properties) = properties_result { - let disk_type = unsafe { - super::disk::get_str_value( - device_properties.inner(), - DictKey::Defined(ffi::kIOPropertyMediumTypeKey), - ) - }; - - if let Some(disk_type) = disk_type.and_then(|medium| match medium.as_str() { - _ if medium == ffi::kIOPropertyMediumTypeSolidStateKey => Some(DiskKind::SSD), - _ if medium == ffi::kIOPropertyMediumTypeRotationalKey => Some(DiskKind::HDD), - _ => None, - }) { - return Some(disk_type); - } else { - // Many external drive vendors do not advertise their device's storage medium. - // - // In these cases, assuming that there were _any_ properties about them registered, we fallback - // to `HDD` when no storage medium is provided by the device instead of `Unknown`. - return Some(DiskKind::HDD); - } + if let Some(result) = + properties_result.and_then(|properties| eval(parent_entry, &properties)) + { + return Some(result); } } } None } + +pub(crate) fn get_disk_type(bsd_name: &[u8]) -> Option { + let characteristics_string = unsafe { + CFReleaser::new(cfs::CFStringCreateWithBytesNoCopy( + kCFAllocatorDefault, + ffi::kIOPropertyDeviceCharacteristicsKey.as_ptr(), + ffi::kIOPropertyDeviceCharacteristicsKey.len() as _, + cfs::kCFStringEncodingUTF8, + false as _, + kCFAllocatorNull, + ))? + }; + + iterate_service_tree(bsd_name, characteristics_string, |_, properties| { + let disk_type = unsafe { + super::disk::get_str_value( + properties.inner(), + DictKey::Defined(ffi::kIOPropertyMediumTypeKey), + ) + }; + + if let Some(disk_type) = disk_type.and_then(|medium| match medium.as_str() { + _ if medium == ffi::kIOPropertyMediumTypeSolidStateKey => Some(DiskKind::SSD), + _ if medium == ffi::kIOPropertyMediumTypeRotationalKey => Some(DiskKind::HDD), + _ => None, + }) { + Some(disk_type) + } else { + // Many external drive vendors do not advertise their device's storage medium. + // + // In these cases, assuming that there were _any_ properties about them registered, we fallback + // to `HDD` when no storage medium is provided by the device instead of `Unknown`. + Some(DiskKind::HDD) + } + }) +} + +/// Returns a tuple consisting of the total number of bytes read and written by the specified disk +pub(crate) fn get_disk_io(bsd_name: &[u8]) -> Option<(u64, u64)> { + let stat_string = unsafe { + CFReleaser::new(cfs::CFStringCreateWithBytesNoCopy( + kCFAllocatorDefault, + ffi::kIOBlockStorageDriverStatisticsKey.as_ptr(), + ffi::kIOBlockStorageDriverStatisticsKey.len() as _, + cfs::kCFStringEncodingUTF8, + false as _, + kCFAllocatorNull, + ))? + }; + + iterate_service_tree(bsd_name, stat_string, |parent_entry, properties| { + if unsafe { + ffi::IOObjectConformsTo(parent_entry, b"IOBlockStorageDriver\0".as_ptr() as *const _) + } == 0 + { + return None; + } + + unsafe { + super::disk::get_int_value( + properties.inner(), + DictKey::Defined(ffi::kIOBlockStorageDriverStatisticsBytesReadKey), + ) + .zip(super::disk::get_int_value( + properties.inner(), + DictKey::Defined(ffi::kIOBlockStorageDriverStatisticsBytesWrittenKey), + )) + } + .and_then(|(read_bytes, written_bytes)| { + read_bytes + .try_into() + .ok() + .zip(written_bytes.try_into().ok()) + }) + }) +} diff --git a/src/unix/apple/macos/ffi.rs b/src/unix/apple/macos/ffi.rs index 0c6dbab51..3e0dfc6e0 100644 --- a/src/unix/apple/macos/ffi.rs +++ b/src/unix/apple/macos/ffi.rs @@ -68,6 +68,12 @@ cfg_if! { pub const kIOPropertyMediumTypeSolidStateKey: &str = "Solid State"; #[allow(non_upper_case_globals)] pub const kIOPropertyMediumTypeRotationalKey: &str = "Rotational"; + #[allow(non_upper_case_globals)] + pub const kIOBlockStorageDriverStatisticsKey: &str = "Statistics"; + #[allow(non_upper_case_globals)] + pub const kIOBlockStorageDriverStatisticsBytesReadKey: &str = "Bytes (Read)"; + #[allow(non_upper_case_globals)] + pub const kIOBlockStorageDriverStatisticsBytesWrittenKey: &str = "Bytes (Write)"; } } @@ -125,6 +131,11 @@ extern "C" { ) -> CFMutableDictionaryRef; #[cfg(feature = "system")] pub fn IORegistryEntryGetName(entry: io_registry_entry_t, name: io_name_t) -> kern_return_t; + #[cfg(feature = "disk")] + pub fn IOObjectConformsTo( + object: io_object_t, + className: *const c_char, + ) -> libc::boolean_t; } #[cfg(any( diff --git a/src/unix/linux/disk.rs b/src/unix/linux/disk.rs index db96d7ac8..e00ee1cac 100644 --- a/src/unix/linux/disk.rs +++ b/src/unix/linux/disk.rs @@ -1,7 +1,7 @@ // Take a look at the license at the top of the repository in the LICENSE file. use crate::sys::utils::{get_all_utf8_data, to_cpath}; -use crate::{Disk, DiskKind}; +use crate::{Disk, DiskKind, DiskUsage}; use libc::statvfs; use std::ffi::{OsStr, OsString}; @@ -10,6 +10,22 @@ use std::mem; use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; +/// Copied from [`psutil`]: +/// +/// "man iostat" states that sectors are equivalent with blocks and have +/// a size of 512 bytes. Despite this value can be queried at runtime +/// via /sys/block/{DISK}/queue/hw_sector_size and results may vary +/// between 1k, 2k, or 4k... 512 appears to be a magic constant used +/// throughout Linux source code: +/// * +/// * +/// * +/// * +/// * +/// +/// [`psutil]: +const SECTOR_SIZE: u64 = 512; + macro_rules! cast { ($x:expr) => { u64::from($x) @@ -19,12 +35,17 @@ macro_rules! cast { pub(crate) struct DiskInner { type_: DiskKind, device_name: OsString, + actual_device_name: 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, } impl DiskInner { @@ -61,6 +82,33 @@ impl DiskInner { } pub(crate) fn refresh(&mut self) -> bool { + self.efficient_refresh(None) + } + + fn efficient_refresh(&mut self, procfs_disk_stats: Option<&[procfs::DiskStat]>) -> bool { + let Some((read_bytes, written_bytes)) = procfs_disk_stats + .or(procfs::diskstats().ok().as_deref()) + .unwrap_or_default() + .iter() + .find_map(|stat| { + if stat.name != self.actual_device_name { + return None; + } + + Some(( + stat.sectors_read * SECTOR_SIZE, + stat.sectors_written * SECTOR_SIZE, + )) + }) + else { + return false; + }; + + self.old_read_bytes = self.read_bytes; + self.old_written_bytes = self.written_bytes; + self.read_bytes = read_bytes; + self.written_bytes = written_bytes; + unsafe { let mut stat: statvfs = mem::zeroed(); let mount_point_cpath = to_cpath(&self.mount_point); @@ -73,6 +121,15 @@ impl DiskInner { } } } + + 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 { @@ -89,6 +146,13 @@ impl crate::DisksInner { ) } + pub(crate) fn refresh(&mut self) { + let procfs_disk_stats = procfs::diskstats().ok(); + for disk in self.list_mut() { + disk.inner.efficient_refresh(procfs_disk_stats.as_deref()); + } + } + pub(crate) fn list(&self) -> &[Disk] { &self.disks } @@ -98,11 +162,31 @@ impl crate::DisksInner { } } +/// Resolves the actual device name for a specified `device` from `/proc/mounts` +/// +/// This function is inspired by the [`bottom`] crate implementation and essentially does the following: +/// 1. Canonicalizes the specified device path to its absolute form +/// 2. Strips the "/dev" prefix from the canonicalized path +/// +/// [`bottom`]: +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() +} + fn new_disk( device_name: &OsStr, mount_point: &Path, file_system: &OsStr, removable_entries: &[PathBuf], + procfs_disk_stats: &[procfs::DiskStat], ) -> Option { let mount_point_cpath = to_cpath(mount_point); let type_ = find_type_for_device_name(device_name); @@ -126,16 +210,38 @@ fn new_disk( let is_removable = removable_entries .iter() .any(|e| e.as_os_str() == device_name); + + let actual_device_name = get_actual_device_name(device_name); + + let (read_bytes, written_bytes) = procfs_disk_stats + .iter() + .find_map(|stat| { + if stat.name != actual_device_name { + return None; + } + + Some(( + stat.sectors_read * SECTOR_SIZE, + stat.sectors_written * SECTOR_SIZE, + )) + }) + .unwrap_or_default(); + Some(Disk { inner: DiskInner { type_, device_name: device_name.to_owned(), + actual_device_name, file_system: file_system.to_owned(), mount_point, total_space: cast!(total), available_space: cast!(available), is_removable, is_read_only, + old_read_bytes: 0, + old_written_bytes: 0, + read_bytes, + written_bytes, }, }) } @@ -233,6 +339,8 @@ fn get_all_list(container: &mut Vec, content: &str) { _ => Vec::new(), }; + let procfs_disk_stats = procfs::diskstats().unwrap_or_default(); + for disk in content .lines() .map(|line| { @@ -284,6 +392,7 @@ fn get_all_list(container: &mut Vec, content: &str) { Path::new(&fs_file), fs_vfstype.as_ref(), &removable_entries, + &procfs_disk_stats, ) }) { From 620c7172e8c8a4f35ff0d56ead4a0ec57eec3a52 Mon Sep 17 00:00:00 2001 From: Shrey Amin Date: Sat, 2 Nov 2024 17:02:46 -0400 Subject: [PATCH 2/8] Add disk i/o stats on Windows --- src/windows/disk.rs | 87 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 3 deletions(-) diff --git a/src/windows/disk.rs b/src/windows/disk.rs index 5c880670e..50670ab4b 100644 --- a/src/windows/disk.rs +++ b/src/windows/disk.rs @@ -1,7 +1,7 @@ // Take a look at the license at the top of the repository in the LICENSE file. use crate::sys::utils::HandleWrapper; -use crate::{Disk, DiskKind}; +use crate::{Disk, DiskKind, DiskUsage}; use std::ffi::{c_void, OsStr, OsString}; use std::mem::size_of; @@ -14,11 +14,11 @@ use windows::Win32::Storage::FileSystem::{ FindFirstVolumeW, FindNextVolumeW, FindVolumeClose, GetDiskFreeSpaceExW, GetDriveTypeW, GetVolumeInformationW, GetVolumePathNamesForVolumeNameW, }; -use windows::Win32::System::SystemServices::FILE_READ_ONLY_VOLUME; use windows::Win32::System::Ioctl::{ PropertyStandardQuery, StorageDeviceSeekPenaltyProperty, DEVICE_SEEK_PENALTY_DESCRIPTOR, - IOCTL_STORAGE_QUERY_PROPERTY, STORAGE_PROPERTY_QUERY, + DISK_PERFORMANCE, IOCTL_DISK_PERFORMANCE, IOCTL_STORAGE_QUERY_PROPERTY, STORAGE_PROPERTY_QUERY, }; +use windows::Win32::System::SystemServices::FILE_READ_ONLY_VOLUME; use windows::Win32::System::WindowsProgramming::{DRIVE_FIXED, DRIVE_REMOVABLE}; use windows::Win32::System::IO::DeviceIoControl; @@ -127,6 +127,11 @@ pub(crate) struct DiskInner { available_space: u64, is_removable: bool, is_read_only: bool, + device_path: Vec, + old_written_bytes: u64, + old_read_bytes: u64, + written_bytes: u64, + read_bytes: u64, } impl DiskInner { @@ -163,6 +168,15 @@ impl DiskInner { } pub(crate) fn refresh(&mut self) -> bool { + let Some((read_bytes, written_bytes)) = get_disk_io(&self.device_path, None) else { + return false; + }; + + self.old_read_bytes = self.read_bytes; + self.old_written_bytes = self.written_bytes; + self.read_bytes = read_bytes; + self.written_bytes = written_bytes; + if self.total_space != 0 { unsafe { let mut tmp = 0; @@ -175,6 +189,15 @@ impl DiskInner { } false } + + 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, + } + } } pub(crate) struct DisksInner { @@ -202,6 +225,12 @@ impl DisksInner { } } + pub(crate) fn refresh(&mut self) { + for disk in self.list_mut() { + disk.refresh(); + } + } + pub(crate) fn list(&self) -> &[Disk] { &self.disks } @@ -318,6 +347,9 @@ pub(crate) unsafe fn get_list() -> Vec { } }; + let (read_bytes, written_bytes) = + get_disk_io(&device_path, Some(handle)).unwrap_or_default(); + let name = os_string_from_zero_terminated(&name); let file_system = os_string_from_zero_terminated(&file_system); mount_paths @@ -333,6 +365,11 @@ pub(crate) unsafe fn get_list() -> Vec { available_space, is_removable, is_read_only, + device_path: device_path.clone(), + old_read_bytes: 0, + old_written_bytes: 0, + read_bytes, + written_bytes, }, }) .collect::>() @@ -344,3 +381,47 @@ fn os_string_from_zero_terminated(name: &[u16]) -> OsString { let len = name.iter().position(|&x| x == 0).unwrap_or(name.len()); OsString::from_wide(&name[..len]) } + +/// Returns a tuple consisting of the total number of bytes read and written by the volume with the specified device path +fn get_disk_io(device_path: &[u16], handle: Option) -> Option<(u64, u64)> { + let handle = + handle.or(unsafe { HandleWrapper::new_from_file(device_path, Default::default()) })?; + + if handle.is_invalid() { + sysinfo_debug!( + "Expected handle to '{:?}' to be valid", + String::from_utf16_lossy(device_path) + ); + return None; + } + + let mut disk_perf = DISK_PERFORMANCE::default(); + let mut bytes_returned = 0; + + // SAFETY: the handle is checked for validity above + unsafe { + // See for reference + DeviceIoControl( + handle.0, + IOCTL_DISK_PERFORMANCE, + None, // Must be None as per docs + 0, + Some(&mut disk_perf as *mut _ as _), + size_of::() as u32, + Some(&mut bytes_returned), + None, + ) + } + .map_err(|err| { + sysinfo_debug!("Error: DeviceIoControl(IOCTL_DISK_PERFORMANCE) = {:?}", err); + err + }) + .ok() + .and_then(|_| { + disk_perf + .BytesRead + .try_into() + .ok() + .zip(disk_perf.BytesWritten.try_into().ok()) + }) +} From 40e7f830b64f3bb02a42d8f08b15b9b69b2507d3 Mon Sep 17 00:00:00 2001 From: Shrey Amin Date: Sat, 2 Nov 2024 17:10:24 -0400 Subject: [PATCH 3/8] Add debug logs --- src/unix/apple/disk.rs | 13 ++++++++----- src/unix/linux/disk.rs | 1 + src/windows/disk.rs | 1 + 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/unix/apple/disk.rs b/src/unix/apple/disk.rs index 39fa98ecc..b80cbd789 100644 --- a/src/unix/apple/disk.rs +++ b/src/unix/apple/disk.rs @@ -73,14 +73,17 @@ impl DiskInner { } pub(crate) fn refresh(&mut self) -> bool { - self.old_read_bytes = self.read_bytes; - self.old_written_bytes = self.written_bytes; - - let (read_bytes, written_bytes) = self + let Some((read_bytes, written_bytes)) = self .bsd_name .as_ref() .and_then(|name| crate::sys::inner::disk::get_disk_io(name)) - .unwrap_or_default(); + else { + sysinfo_debug!("Failed to update disk i/o stats"); + return false; + }; + + self.old_read_bytes = self.read_bytes; + self.old_written_bytes = self.written_bytes; self.read_bytes = read_bytes; self.written_bytes = written_bytes; diff --git a/src/unix/linux/disk.rs b/src/unix/linux/disk.rs index e00ee1cac..cfa39cfb7 100644 --- a/src/unix/linux/disk.rs +++ b/src/unix/linux/disk.rs @@ -101,6 +101,7 @@ impl DiskInner { )) }) else { + sysinfo_debug!("Failed to update disk i/o stats"); return false; }; diff --git a/src/windows/disk.rs b/src/windows/disk.rs index 50670ab4b..bb37766d4 100644 --- a/src/windows/disk.rs +++ b/src/windows/disk.rs @@ -169,6 +169,7 @@ impl DiskInner { pub(crate) fn refresh(&mut self) -> bool { let Some((read_bytes, written_bytes)) = get_disk_io(&self.device_path, None) else { + sysinfo_debug!("Failed to update disk i/o stats"); return false; }; From ab5a95ffa3e07f7c657ff5fdeb833cb21f3a9d7f Mon Sep 17 00:00:00 2001 From: Shrey Amin Date: Sat, 2 Nov 2024 17:42:43 -0400 Subject: [PATCH 4/8] Update for unsupported/unknown platforms --- src/unix/freebsd/disk.rs | 11 +++++++++++ src/unknown/disk.rs | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/src/unix/freebsd/disk.rs b/src/unix/freebsd/disk.rs index ab3908854..c19ce8f8e 100644 --- a/src/unix/freebsd/disk.rs +++ b/src/unix/freebsd/disk.rs @@ -58,6 +58,11 @@ impl DiskInner { refresh_disk(self, &mut vfs) } } + + pub(crate) fn usage(&self) -> DiskUsage { + // TODO: Until disk i/o stats are added, return the default + DiskUsage::default() + } } impl crate::DisksInner { @@ -78,6 +83,12 @@ impl crate::DisksInner { pub(crate) fn list_mut(&mut self) -> &mut [Disk] { &mut self.disks } + + pub(crate) fn refresh(&mut self) { + for disk in self.list_mut() { + disk.refresh(); + } + } } // FIXME: if you want to get disk I/O usage: diff --git a/src/unknown/disk.rs b/src/unknown/disk.rs index af2183f1f..901c3114b 100644 --- a/src/unknown/disk.rs +++ b/src/unknown/disk.rs @@ -42,6 +42,10 @@ impl DiskInner { pub(crate) fn refresh(&mut self) -> bool { true } + + pub(crate) fn usage(&self) -> DiskUsage { + unreachable!() + } } pub(crate) struct DisksInner { @@ -65,6 +69,10 @@ impl DisksInner { // Does nothing. } + pub(crate) fn refresh(&mut self) { + // Does nothing. + } + pub(crate) fn list(&self) -> &[Disk] { &self.disks } From cc7c94574b97257a6b9b769e81c5285f93bbb1c9 Mon Sep 17 00:00:00 2001 From: Shrey Amin Date: Sat, 2 Nov 2024 19:22:50 -0400 Subject: [PATCH 5/8] Fix doc string error --- src/unix/linux/disk.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unix/linux/disk.rs b/src/unix/linux/disk.rs index cfa39cfb7..3919465d5 100644 --- a/src/unix/linux/disk.rs +++ b/src/unix/linux/disk.rs @@ -23,7 +23,7 @@ use std::path::{Path, PathBuf}; /// * /// * /// -/// [`psutil]: +/// [`psutil`]: const SECTOR_SIZE: u64 = 512; macro_rules! cast { From a4e7bd8472780a0908f159e53549d4e078e5f3be Mon Sep 17 00:00:00 2001 From: Shrey Amin Date: Sat, 2 Nov 2024 19:47:26 -0400 Subject: [PATCH 6/8] Fix CI build errors --- src/common/mod.rs | 40 ++++++++++++++++++++++++++++++++++++++++ src/common/system.rs | 39 +-------------------------------------- src/lib.rs | 9 ++++++--- src/unix/apple/disk.rs | 3 +++ src/unix/freebsd/disk.rs | 2 +- src/unknown/disk.rs | 2 +- 6 files changed, 52 insertions(+), 43 deletions(-) diff --git a/src/common/mod.rs b/src/common/mod.rs index acc96013e..d83dd9fe3 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -11,6 +11,46 @@ pub(crate) mod system; #[cfg(feature = "user")] pub(crate) mod user; +/// Type containing read and written bytes. +/// +/// It is returned by [`Process::disk_usage`][crate::Process::disk_usage] and [`Disk::usage`][crate::Disk::usage]. +/// +#[cfg_attr(not(all(feature = "system", feature = "disk")), doc = "```ignore")] +/// ```no_run +/// use sysinfo::{Disks, System}; +/// +/// let s = System::new_all(); +/// for (pid, process) in s.processes() { +/// let disk_usage = process.disk_usage(); +/// println!("[{}] read bytes : new/total => {}/{} B", +/// pid, +/// disk_usage.read_bytes, +/// disk_usage.total_read_bytes, +/// ); +/// println!("[{}] written bytes: new/total => {}/{} B", +/// pid, +/// disk_usage.written_bytes, +/// disk_usage.total_written_bytes, +/// ); +/// } +/// +/// let disks = Disks::new_with_refreshed_list(); +/// for disk in disks.list() { +/// println!("[{:?}] disk usage: {:?}", disk.name(), disk.usage()); +/// } +/// ``` +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd)] +pub struct DiskUsage { + /// Total number of written bytes. + pub total_written_bytes: u64, + /// Number of written bytes since the last refresh. + pub written_bytes: u64, + /// Total number of read bytes. + pub total_read_bytes: u64, + /// Number of read bytes since the last refresh. + pub read_bytes: u64, +} + macro_rules! xid { ($(#[$outer:meta])+ $name:ident, $type:ty $(, $trait:ty)?) => { #[cfg(any(feature = "system", feature = "user"))] diff --git a/src/common/system.rs b/src/common/system.rs index 1409becd1..71c71e5b1 100644 --- a/src/common/system.rs +++ b/src/common/system.rs @@ -6,6 +6,7 @@ use std::fmt; use std::path::Path; use std::str::FromStr; +use crate::common::DiskUsage; use crate::{CpuInner, Gid, ProcessInner, SystemInner, Uid}; /// Structs containing system's information such as processes, memory and CPU. @@ -938,44 +939,6 @@ pub struct CGroupLimits { pub rss: u64, } -/// Type containing read and written bytes. -/// -/// It is returned by [`Process::disk_usage`][crate::Process::disk_usage]. -/// -/// ⚠️ Files might be cached in memory by your OS, meaning that reading/writing them might not -/// increase the `read_bytes`/`written_bytes` values. You can find more information about it -/// in the `proc_pid_io` manual (`man proc_pid_io` on unix platforms). -/// -/// ```no_run -/// use sysinfo::System; -/// -/// let s = System::new_all(); -/// for (pid, process) in s.processes() { -/// let disk_usage = process.disk_usage(); -/// println!("[{}] read bytes : new/total => {}/{} B", -/// pid, -/// disk_usage.read_bytes, -/// disk_usage.total_read_bytes, -/// ); -/// println!("[{}] written bytes: new/total => {}/{} B", -/// pid, -/// disk_usage.written_bytes, -/// disk_usage.total_written_bytes, -/// ); -/// } -/// ``` -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd)] -pub struct DiskUsage { - /// Total number of written bytes. - pub total_written_bytes: u64, - /// Number of written bytes since the last refresh. - pub written_bytes: u64, - /// Total number of read bytes. - pub total_read_bytes: u64, - /// Number of read bytes since the last refresh. - pub read_bytes: u64, -} - /// Enum describing the different status of a process. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ProcessStatus { diff --git a/src/lib.rs b/src/lib.rs index 4f208e258..9c12c2e6e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,9 +76,9 @@ pub use crate::common::disk::{Disk, DiskKind, Disks}; pub use crate::common::network::{IpNetwork, MacAddr, NetworkData, Networks}; #[cfg(feature = "system")] pub use crate::common::system::{ - get_current_pid, CGroupLimits, Cpu, CpuRefreshKind, DiskUsage, LoadAvg, MemoryRefreshKind, Pid, - Process, ProcessRefreshKind, ProcessStatus, ProcessesToUpdate, RefreshKind, Signal, System, - ThreadKind, UpdateKind, + get_current_pid, CGroupLimits, Cpu, CpuRefreshKind, LoadAvg, MemoryRefreshKind, Pid, Process, + ProcessRefreshKind, ProcessStatus, ProcessesToUpdate, RefreshKind, Signal, System, ThreadKind, + UpdateKind, }; #[cfg(feature = "user")] pub use crate::common::user::{Group, Groups, User, Users}; @@ -87,6 +87,9 @@ pub use crate::common::{Gid, Uid}; #[cfg(feature = "system")] pub use crate::sys::{MINIMUM_CPU_UPDATE_INTERVAL, SUPPORTED_SIGNALS}; +#[cfg(any(feature = "system", feature = "disk"))] +pub use crate::common::DiskUsage; + #[cfg(feature = "user")] pub(crate) use crate::common::user::GroupInner; #[cfg(feature = "user")] diff --git a/src/unix/apple/disk.rs b/src/unix/apple/disk.rs index b80cbd789..2ac36ac17 100644 --- a/src/unix/apple/disk.rs +++ b/src/unix/apple/disk.rs @@ -73,6 +73,7 @@ impl DiskInner { } pub(crate) fn refresh(&mut self) -> bool { + #[cfg(target_os = "macos")] let Some((read_bytes, written_bytes)) = self .bsd_name .as_ref() @@ -81,6 +82,8 @@ impl DiskInner { sysinfo_debug!("Failed to update disk i/o stats"); return false; }; + #[cfg(not(target_os = "macos"))] + let (read_bytes, written_bytes) = (0, 0); self.old_read_bytes = self.read_bytes; self.old_written_bytes = self.written_bytes; diff --git a/src/unix/freebsd/disk.rs b/src/unix/freebsd/disk.rs index c19ce8f8e..b4194e33f 100644 --- a/src/unix/freebsd/disk.rs +++ b/src/unix/freebsd/disk.rs @@ -1,6 +1,6 @@ // Take a look at the license at the top of the repository in the LICENSE file. -use crate::{Disk, DiskKind}; +use crate::{Disk, DiskKind, DiskUsage}; use std::ffi::{OsStr, OsString}; use std::os::unix::ffi::OsStringExt; diff --git a/src/unknown/disk.rs b/src/unknown/disk.rs index 901c3114b..7190547b0 100644 --- a/src/unknown/disk.rs +++ b/src/unknown/disk.rs @@ -1,6 +1,6 @@ // Take a look at the license at the top of the repository in the LICENSE file. -use crate::{Disk, DiskKind}; +use crate::{Disk, DiskKind, DiskUsage}; use std::{ffi::OsStr, path::Path}; From fd52c86b7c8efd4af4f5e1a8e52bc54a963e1766 Mon Sep 17 00:00:00 2001 From: Shrey Amin Date: Sun, 3 Nov 2024 11:16:52 -0500 Subject: [PATCH 7/8] Address PR feedback --- src/common/disk.rs | 2 ++ src/common/mod.rs | 1 + src/unix/apple/macos/disk.rs | 32 ++++++++++---------------------- src/unknown/disk.rs | 6 +++--- src/windows/disk.rs | 14 ++++++-------- 5 files changed, 22 insertions(+), 33 deletions(-) diff --git a/src/common/disk.rs b/src/common/disk.rs index 1fe004f11..0c2712bf2 100644 --- a/src/common/disk.rs +++ b/src/common/disk.rs @@ -149,6 +149,8 @@ impl Disk { /// Returns number of bytes read and written by the disk /// + /// ⚠️ Note that FreeBSD is not yet supported + /// /// ```no_run /// use sysinfo::Disks; /// diff --git a/src/common/mod.rs b/src/common/mod.rs index d83dd9fe3..cbbf52131 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -39,6 +39,7 @@ pub(crate) mod user; /// println!("[{:?}] disk usage: {:?}", disk.name(), disk.usage()); /// } /// ``` +#[cfg(any(feature = "disk", feature = "system"))] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd)] pub struct DiskUsage { /// Total number of written bytes. diff --git a/src/unix/apple/macos/disk.rs b/src/unix/apple/macos/disk.rs index 86f20dfbf..34dba93dc 100644 --- a/src/unix/apple/macos/disk.rs +++ b/src/unix/apple/macos/disk.rs @@ -101,25 +101,17 @@ pub(crate) fn get_disk_type(bsd_name: &[u8]) -> Option { }; iterate_service_tree(bsd_name, characteristics_string, |_, properties| { - let disk_type = unsafe { + let medium = unsafe { super::disk::get_str_value( properties.inner(), DictKey::Defined(ffi::kIOPropertyMediumTypeKey), ) - }; + }?; - if let Some(disk_type) = disk_type.and_then(|medium| match medium.as_str() { + match medium.as_str() { _ if medium == ffi::kIOPropertyMediumTypeSolidStateKey => Some(DiskKind::SSD), _ if medium == ffi::kIOPropertyMediumTypeRotationalKey => Some(DiskKind::HDD), - _ => None, - }) { - Some(disk_type) - } else { - // Many external drive vendors do not advertise their device's storage medium. - // - // In these cases, assuming that there were _any_ properties about them registered, we fallback - // to `HDD` when no storage medium is provided by the device instead of `Unknown`. - Some(DiskKind::HDD) + _ => Some(DiskKind::Unknown(-1)), } }) } @@ -146,20 +138,16 @@ pub(crate) fn get_disk_io(bsd_name: &[u8]) -> Option<(u64, u64)> { } unsafe { - super::disk::get_int_value( + let read_bytes = super::disk::get_int_value( properties.inner(), DictKey::Defined(ffi::kIOBlockStorageDriverStatisticsBytesReadKey), - ) - .zip(super::disk::get_int_value( + )?; + let written_bytes = super::disk::get_int_value( properties.inner(), DictKey::Defined(ffi::kIOBlockStorageDriverStatisticsBytesWrittenKey), - )) + )?; + + Some((read_bytes.try_into().ok()?, written_bytes.try_into().ok()?)) } - .and_then(|(read_bytes, written_bytes)| { - read_bytes - .try_into() - .ok() - .zip(written_bytes.try_into().ok()) - }) }) } diff --git a/src/unknown/disk.rs b/src/unknown/disk.rs index 7190547b0..7ab253b47 100644 --- a/src/unknown/disk.rs +++ b/src/unknown/disk.rs @@ -8,11 +8,11 @@ pub(crate) struct DiskInner; impl DiskInner { pub(crate) fn kind(&self) -> DiskKind { - unreachable!() + DiskKind::Unknown(-1) } pub(crate) fn name(&self) -> &OsStr { - unreachable!() + OsStr::new("") } pub(crate) fn file_system(&self) -> &OsStr { @@ -44,7 +44,7 @@ impl DiskInner { } pub(crate) fn usage(&self) -> DiskUsage { - unreachable!() + DiskUsage::default() } } diff --git a/src/windows/disk.rs b/src/windows/disk.rs index bb37766d4..5d635c396 100644 --- a/src/windows/disk.rs +++ b/src/windows/disk.rs @@ -417,12 +417,10 @@ fn get_disk_io(device_path: &[u16], handle: Option) -> Option<(u6 sysinfo_debug!("Error: DeviceIoControl(IOCTL_DISK_PERFORMANCE) = {:?}", err); err }) - .ok() - .and_then(|_| { - disk_perf - .BytesRead - .try_into() - .ok() - .zip(disk_perf.BytesWritten.try_into().ok()) - }) + .ok()?; + + Some(( + disk_perf.BytesRead.try_into().ok()?, + disk_perf.BytesWritten.try_into().ok()?, + )) } From 7df7803a43312173b385e4d01362efd2ec00a42e Mon Sep 17 00:00:00 2001 From: Shrey Amin Date: Sun, 3 Nov 2024 20:51:38 -0500 Subject: [PATCH 8/8] Add test for disk I/O usage --- Cargo.toml | 1 + tests/disk.rs | 69 ++++++++++++++++++++++++++++++++++++++++++++++ tests/disk_list.rs | 16 ----------- 3 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 tests/disk.rs delete mode 100644 tests/disk_list.rs diff --git a/Cargo.toml b/Cargo.toml index 94f18f68d..806bfe520 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -117,6 +117,7 @@ procfs = "0.17.0" [dev-dependencies] serde_json = "1.0" # Used in documentation tests. bstr = "1.9.0" +tempfile = "3.9" [[example]] name = "simple" diff --git a/tests/disk.rs b/tests/disk.rs new file mode 100644 index 000000000..5097c887e --- /dev/null +++ b/tests/disk.rs @@ -0,0 +1,69 @@ +// Take a look at the license at the top of the repository in the LICENSE file. + +#[test] +#[cfg(all(feature = "system", feature = "disk"))] +fn test_disks() { + if sysinfo::IS_SUPPORTED_SYSTEM { + let s = sysinfo::System::new_all(); + // If we don't have any physical core present, it's very likely that we're inside a VM... + if s.physical_core_count().unwrap_or_default() > 0 { + let mut disks = sysinfo::Disks::new(); + assert!(disks.list().is_empty()); + disks.refresh_list(); + assert!(!disks.list().is_empty()); + } + } +} + +#[test] +#[cfg(feature = "disk")] +fn test_disks_usage() { + use std::io::Write; + use tempfile::NamedTempFile; + + let s = sysinfo::System::new_all(); + + // Skip the tests on unsupported platforms and on systems with no physical cores (likely a VM) + if !sysinfo::IS_SUPPORTED_SYSTEM || s.physical_core_count().unwrap_or_default() == 0 { + return; + } + + // The test always fails in CI on Linux. For some unknown reason, /proc/diskstats just doesn't update, regardless + // of how long we wait. Until the root cause is discovered, skip the test in CI + if cfg!(target_os = "linux") && std::env::var("CI").is_ok() { + return; + } + + let mut disks = sysinfo::Disks::new_with_refreshed_list(); + + let mut file = NamedTempFile::new().unwrap(); + + // Write 10mb worth of data to the temp file. + let data = vec![1u8; 10 * 1024 * 1024]; + file.write_all(&data).unwrap(); + // The sync_all call is important to ensure all the data is persisted to disk. Without + // the call, this test is flaky. + file.as_file().sync_all().unwrap(); + + // Wait a bit just in case + std::thread::sleep(std::time::Duration::from_millis(100)); + disks.refresh(); + + // Depending on the OS and how disks are configured, the disk usage may be the exact same + // across multiple disks. To account for this, collect the disk usages and dedup + let mut disk_usages = disks.list().iter().map(|d| d.usage()).collect::>(); + disk_usages.dedup(); + + let mut written_bytes = 0; + for disk_usage in disk_usages { + written_bytes += disk_usage.written_bytes; + } + + // written_bytes should have increased by about 10mb, but this is not fully reliable in CI Linux. For now, + // just verify the number is non-zero. + #[cfg(not(target_os = "freebsd"))] + assert!(written_bytes > 0); + // Disk usage is not yet supported on freebsd + #[cfg(target_os = "freebsd")] + assert_eq!(written_bytes, 0); +} diff --git a/tests/disk_list.rs b/tests/disk_list.rs deleted file mode 100644 index 9ddaefb50..000000000 --- a/tests/disk_list.rs +++ /dev/null @@ -1,16 +0,0 @@ -// Take a look at the license at the top of the repository in the LICENSE file. - -#[test] -#[cfg(all(feature = "system", feature = "disk"))] -fn test_disks() { - if sysinfo::IS_SUPPORTED_SYSTEM { - let s = sysinfo::System::new_all(); - // If we don't have any physical core present, it's very likely that we're inside a VM... - if s.physical_core_count().unwrap_or_default() > 0 { - let mut disks = sysinfo::Disks::new(); - assert!(disks.list().is_empty()); - disks.refresh_list(); - assert!(!disks.list().is_empty()); - } - } -}