diff --git a/Cargo.lock b/Cargo.lock index f9c8852ef..ab6cff46f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1491,7 +1491,6 @@ dependencies = [ "devolutions-log", "dlopen", "dlopen_derive", - "dunce", "embed-resource", "etherparse", "expect-test", @@ -1512,6 +1511,7 @@ dependencies = [ "job-queue", "job-queue-libsql", "kdc", + "libc", "multibase", "network-monitor", "network-scanner", @@ -1540,7 +1540,6 @@ dependencies = [ "sysevent-codes", "sysevent-syslog", "sysevent-winevent", - "sysinfo", "tap", "tempfile", "terminal-streamer", diff --git a/devolutions-gateway/Cargo.toml b/devolutions-gateway/Cargo.toml index 5fef872cf..40226fb8b 100644 --- a/devolutions-gateway/Cargo.toml +++ b/devolutions-gateway/Cargo.toml @@ -68,8 +68,6 @@ anyhow = "1.0" thiserror = "2" typed-builder = "0.21" backoff = "0.4" -sysinfo = { version = "0.35", default-features = false, features = ["disk"] } -dunce = "1.0" bitflags = "2.9" # Security, crypto… @@ -132,6 +130,7 @@ portpicker = "0.1" [target.'cfg(unix)'.dependencies] sysevent-syslog.path = "../crates/sysevent-syslog" +libc = "0.2" [target.'cfg(windows)'.dependencies] rustls-cng = { version = "0.5", default-features = false, features = ["logging", "tls12", "ring"] } diff --git a/devolutions-gateway/src/api/heartbeat.rs b/devolutions-gateway/src/api/heartbeat.rs index ffe5fcb09..b22a0890f 100644 --- a/devolutions-gateway/src/api/heartbeat.rs +++ b/devolutions-gateway/src/api/heartbeat.rs @@ -180,7 +180,11 @@ pub(crate) fn recording_storage_health(recording_path: &std::path::Path) -> Reco /// This supports UNC paths (`\\server\share\...`) and mapped drive letters without needing /// to enumerate mount points or canonicalize the path. /// -/// On other platforms, enumerates disks via `sysinfo` and finds the longest-prefix mount point. +/// On Unix, calls `statvfs(2)` directly against the configured recording path. +/// This supports network filesystems (NFS, CIFS/Samba) and any mount point without needing +/// to enumerate mount points. +/// +/// On other platforms, space values are not available and `(None, None)` is returned. fn query_storage_space(recording_path: &std::path::Path) -> (Option, Option) { static NO_DISK_STATE: NoDiskState = NoDiskState::new(); @@ -230,49 +234,65 @@ fn query_storage_space(recording_path: &std::path::Path) -> (Option, Option } } - #[cfg(not(windows))] + #[cfg(unix)] + #[allow( + clippy::useless_conversion, + reason = "statvfs field types differ across Unix platforms: fsblkcnt_t is u64 on Linux but u32 on macOS" + )] fn query_storage_space_impl(recording_path: &std::path::Path) -> (Option, Option) { - use sysinfo::Disks; + use std::ffi::CString; + use std::os::unix::ffi::OsStrExt as _; + + let c_path = match CString::new(recording_path.as_os_str().as_bytes()) { + Ok(p) => p, + Err(_) => { + NO_DISK_STATE.on_disk_missing(recording_path, "path contains null byte"); + return (None, None); + } + }; - if !sysinfo::IS_SUPPORTED_SYSTEM { - debug!("This system does not support listing storage disks"); - return (None, None); - } + // SAFETY: `stat` is zeroed stack memory whose layout matches what the OS writes into it. + let mut stat: libc::statvfs = unsafe { std::mem::zeroed() }; - trace!("System is supporting listing storage disks"); + // SAFETY: `c_path` is a valid null-terminated C string. + let ret = unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) }; - let recording_path = dunce::canonicalize(recording_path) - .inspect_err( - |error| debug!(%error, recording_path = %recording_path.display(), "Failed to canonicalize recording path"), - ) - .unwrap_or_else(|_| recording_path.to_owned()); + if ret == 0 { + NO_DISK_STATE.on_disk_present(); - let disks = Disks::new_with_refreshed_list(); + // f_frsize is the fundamental block size; fall back to f_bsize if zero. + let block_size = if stat.f_frsize != 0 { + u64::from(stat.f_frsize) + } else { + u64::from(stat.f_bsize) + }; - debug!(recording_path = %recording_path.display(), "Search mount point for path"); - debug!(?disks, "Found disks"); + let total = u64::from(stat.f_blocks).saturating_mul(block_size); - let mut recording_disk = None; - let mut longest_path = 0; + // f_bavail is the space available to unprivileged users (vs f_bfree which is root-only). + let available = u64::from(stat.f_bavail).saturating_mul(block_size); - for disk in disks.list() { - let mount_point = disk.mount_point(); - let path_len = mount_point.components().count(); - if recording_path.starts_with(mount_point) && longest_path < path_len { - recording_disk = Some(disk); - longest_path = path_len; - } - } + debug!( + recording_path = %recording_path.display(), + total_bytes = total, + free_bytes_available = available, + "Retrieved disk space via statvfs" + ); - if let Some(disk) = recording_disk { - NO_DISK_STATE.on_disk_present(); - debug!(?disk, "Disk used to store recordings"); - (Some(disk.total_space()), Some(disk.available_space())) + (Some(total), Some(available)) } else { - NO_DISK_STATE.on_disk_missing(&recording_path, "no matching disk mount point found"); + let error = std::io::Error::last_os_error(); + NO_DISK_STATE.on_disk_missing(recording_path, &error.to_string()); + (None, None) } } + + #[cfg(not(any(windows, unix)))] + fn query_storage_space_impl(recording_path: &std::path::Path) -> (Option, Option) { + NO_DISK_STATE.on_disk_missing(recording_path, "unsupported platform"); + (None, None) + } } #[cfg(test)] @@ -290,9 +310,8 @@ mod tests { assert!(result.recording_storage_is_writeable); - // Space values may be None only on platforms where sysinfo is unsupported; - // on Windows and common Unix platforms they should be Some. - #[cfg(any(windows, target_os = "linux", target_os = "macos"))] + // Space values are Some on Windows (GetDiskFreeSpaceExW) and all Unix platforms (statvfs). + #[cfg(any(windows, unix))] { assert!( result.recording_storage_total_space.is_some(), @@ -307,6 +326,13 @@ mod tests { "total space must be >= available space" ); } + + // Space values are None on other unsupported platforms. + #[cfg(not(any(windows, unix)))] + { + assert!(result.recording_storage_total_space.is_none()); + assert!(result.recording_storage_available_space.is_none()); + } } /// A non-existent path should be reported as not writeable. diff --git a/devolutions-gateway/src/main.rs b/devolutions-gateway/src/main.rs index 021cbd64b..8fda2c75e 100644 --- a/devolutions-gateway/src/main.rs +++ b/devolutions-gateway/src/main.rs @@ -12,7 +12,6 @@ use camino as _; use devolutions_agent_shared as _; use dlopen as _; use dlopen_derive as _; -use dunce as _; use etherparse as _; use hostname as _; use http_body_util as _; @@ -39,7 +38,6 @@ use rustls_cng as _; use serde as _; use serde_urlencoded as _; use smol_str as _; -use sysinfo as _; use thiserror as _; use time as _; use tokio_rustls as _;