#![cfg(target_os = "linux")]
use std::fs;
use std::path::{Path, PathBuf};
use super::PlpStatus;
use crate::hardware::cpu::CpuInfo;
use crate::hardware::drive::{DriveInfo, DriveKind};
use crate::hardware::io_primitives::IoPrimitives;
use crate::hardware::memory::MemoryInfo;
pub(crate) fn probe_drive() -> DriveInfo {
let cwd = match std::env::current_dir() {
Ok(p) => p,
Err(_) => return DriveInfo::default(),
};
let block_dir = match resolve_block_device(&cwd) {
Some(p) => p,
None => return DriveInfo::default(),
};
let mut info = DriveInfo::default();
let rotational = read_sys_u32(block_dir.join("queue/rotational")).unwrap_or(1);
let dev_name = block_dir.file_name().and_then(|n| n.to_str()).unwrap_or("");
info.kind = classify_drive_kind(dev_name, rotational);
if let Some(v) = read_sys_u32(block_dir.join("queue/logical_block_size")) {
if v > 0 {
info.logical_sector = v;
}
}
if let Some(v) = read_sys_u32(block_dir.join("queue/physical_block_size")) {
if v > 0 {
info.physical_sector = v;
}
}
if let Some(v) = read_sys_u32(block_dir.join("queue/optimal_io_size")) {
if v > 0 {
info.optimal_block = v;
}
}
if let Some(v) = read_sys_u32(block_dir.join("queue/nr_requests")) {
if v > 0 {
info.queue_depth = v;
}
}
if let Some(sectors) = read_sys_u64(block_dir.join("size")) {
info.total_bytes = sectors.saturating_mul(512);
}
if let Some(avail) = available_bytes(&cwd) {
info.available_bytes = avail;
}
info.plp = probe_plp_linux(&block_dir);
if matches!(info.kind, DriveKind::Nvme) {
if let Some((nawun, nawupf)) = probe_nawun_nawupf_linux(&block_dir) {
info.nawun_lba = nawun;
info.nawupf_lba = nawupf;
}
}
info
}
fn probe_nawun_nawupf_linux(block_dir: &std::path::Path) -> Option<(Option<u32>, Option<u32>)> {
use crate::platform::linux_iouring;
use std::os::fd::AsRawFd;
let nsid = std::fs::read_to_string(block_dir.join("nsid"))
.ok()
.and_then(|s| s.trim().parse::<u32>().ok())
.unwrap_or(1);
let dev_name = block_dir.file_name().and_then(|n| n.to_str())?;
let char_name = dev_name
.split('n')
.next()
.map(|prefix| format!("/dev/{prefix}"))?;
let nvme = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&char_name)
.ok()?;
let id_buf = linux_iouring::nvme_identify_namespace(nvme.as_raw_fd(), nsid).ok()?;
Some(linux_iouring::parse_nawun_nawupf(&id_buf))
}
fn probe_plp_linux(block_dir: &std::path::Path) -> PlpStatus {
let device_dir = block_dir.join("device");
let vendor = std::fs::read_to_string(device_dir.join("vendor"))
.map(|s| s.trim().to_string())
.unwrap_or_default();
let model = std::fs::read_to_string(device_dir.join("model"))
.map(|s| s.trim().to_string())
.unwrap_or_default();
crate::hardware::plp::lookup_table(&vendor, &model)
}
fn classify_drive_kind(dev_name: &str, rotational: u32) -> DriveKind {
if dev_name.starts_with("nvme") {
DriveKind::Nvme
} else if rotational == 0 {
DriveKind::SataSsd
} else if rotational == 1 {
DriveKind::Hdd
} else {
DriveKind::Unknown
}
}
fn resolve_block_device(path: &Path) -> Option<PathBuf> {
let mountinfo = fs::read_to_string("/proc/self/mountinfo").ok()?;
let canon = fs::canonicalize(path).ok()?;
let mut best: Option<(usize, &str)> = None;
for line in mountinfo.lines() {
let mut fields = line.split_ascii_whitespace();
let _id = fields.next()?;
let _parent = fields.next()?;
let dev = fields.next()?;
let _root = fields.next()?;
let mount_point = fields.next()?;
if canon.starts_with(mount_point) {
let len = mount_point.len();
if best.map(|(b, _)| len > b).unwrap_or(true) {
best = Some((len, dev));
}
}
}
let dev = best?.1;
let sys_dev = PathBuf::from("/sys/dev/block").join(dev);
let canon_sys = fs::canonicalize(&sys_dev).ok()?;
let mut cur = canon_sys.clone();
while cur.join("partition").exists() {
cur = match cur.parent() {
Some(p) => p.to_path_buf(),
None => break,
};
}
Some(cur)
}
fn read_sys_u32(p: PathBuf) -> Option<u32> {
fs::read_to_string(&p).ok()?.trim().parse::<u32>().ok()
}
fn read_sys_u64(p: PathBuf) -> Option<u64> {
fs::read_to_string(&p).ok()?.trim().parse::<u64>().ok()
}
fn available_bytes(path: &Path) -> Option<u64> {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let cstr = CString::new(path.as_os_str().as_bytes()).ok()?;
let mut sv: libc::statvfs = unsafe { std::mem::zeroed() };
let ret = unsafe { libc::statvfs(cstr.as_ptr(), &mut sv) };
if ret != 0 {
return None;
}
Some((sv.f_bavail as u64).saturating_mul(sv.f_frsize as u64))
}
pub(crate) fn probe_memory() -> MemoryInfo {
let content = match fs::read_to_string("/proc/meminfo") {
Ok(s) => s,
Err(_) => return MemoryInfo::default(),
};
let mut total = 0u64;
let mut avail = 0u64;
for line in content.lines() {
if let Some(rest) = line.strip_prefix("MemTotal:") {
total = parse_kib(rest);
} else if let Some(rest) = line.strip_prefix("MemAvailable:") {
avail = parse_kib(rest);
}
}
MemoryInfo {
total_bytes: total,
available_bytes: avail,
}
}
fn parse_kib(s: &str) -> u64 {
let trimmed = s.trim().trim_end_matches("kB").trim();
trimmed
.parse::<u64>()
.map(|kib| kib.saturating_mul(1024))
.unwrap_or(0)
}
pub(crate) fn probe_cpu() -> CpuInfo {
let cores_logical = std::thread::available_parallelism()
.map(|n| u32::try_from(n.get()).unwrap_or(u32::MAX))
.unwrap_or(1);
let cores_physical = probe_physical_cores().unwrap_or(cores_logical);
let cache_l1 = read_cache_size(1).unwrap_or(0);
let cache_l2 = read_cache_size(2).unwrap_or(0);
let cache_l3 = read_cache_size(3).unwrap_or(0);
CpuInfo {
cores_logical,
cores_physical,
features: super::super::cpu::runtime_features(),
cache_l1,
cache_l2,
cache_l3,
}
}
fn probe_physical_cores() -> Option<u32> {
let content = fs::read_to_string("/proc/cpuinfo").ok()?;
let mut seen = std::collections::BTreeSet::new();
let mut current_phys: Option<&str> = None;
let mut current_core: Option<&str> = None;
for line in content.lines() {
if let Some(rest) = line.strip_prefix("physical id") {
current_phys = rest.split(':').nth(1).map(str::trim);
} else if let Some(rest) = line.strip_prefix("core id") {
current_core = rest.split(':').nth(1).map(str::trim);
} else if line.is_empty() {
if let (Some(p), Some(c)) = (current_phys.take(), current_core.take()) {
let _ = seen.insert(format!("{p}:{c}"));
}
}
}
if let (Some(p), Some(c)) = (current_phys, current_core) {
let _ = seen.insert(format!("{p}:{c}"));
}
let n = seen.len();
if n == 0 {
None
} else {
Some(u32::try_from(n).unwrap_or(u32::MAX))
}
}
fn read_cache_size(level: u8) -> Option<usize> {
for idx in 0..=10 {
let dir = PathBuf::from(format!("/sys/devices/system/cpu/cpu0/cache/index{idx}"));
let lvl = match fs::read_to_string(dir.join("level"))
.ok()
.and_then(|s| s.trim().parse::<u8>().ok())
{
Some(v) => v,
None => continue,
};
if lvl != level {
continue;
}
let size_str = match fs::read_to_string(dir.join("size")).ok() {
Some(s) => s,
None => continue,
};
return parse_cache_size(size_str.trim());
}
None
}
fn parse_cache_size(s: &str) -> Option<usize> {
let (num, suffix): (&str, &str) =
if let Some(stripped) = s.strip_suffix('K').or_else(|| s.strip_suffix('k')) {
(stripped, "K")
} else if let Some(stripped) = s.strip_suffix('M').or_else(|| s.strip_suffix('m')) {
(stripped, "M")
} else {
(s, "")
};
let n: usize = num.parse().ok()?;
Some(match suffix {
"K" => n * 1024,
"M" => n * 1024 * 1024,
_ => n,
})
}
pub(crate) fn probe_io_primitives() -> IoPrimitives {
IoPrimitives {
io_uring: probe_io_uring_available(),
iocp: false,
kqueue: false,
nvme_passthrough: false, direct_io: true,
mmap: true,
}
}
fn probe_io_uring_available() -> bool {
io_uring::IoUring::new(1).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_probe_drive_returns_non_default_capacity() {
let info = probe_drive();
assert!(
info.logical_sector >= 512,
"logical sector size must be at least 512"
);
}
#[test]
fn test_probe_memory_reports_non_zero_total() {
let m = probe_memory();
assert!(
m.total_bytes > 0,
"Linux /proc/meminfo must report >0 total"
);
}
#[test]
fn test_probe_cpu_reports_at_least_one_logical_core() {
let c = probe_cpu();
assert!(c.cores_logical >= 1);
assert!(c.cores_physical >= 1);
}
#[test]
fn test_classify_drive_kind() {
assert_eq!(classify_drive_kind("nvme0n1", 0), DriveKind::Nvme);
assert_eq!(classify_drive_kind("sda", 0), DriveKind::SataSsd);
assert_eq!(classify_drive_kind("sda", 1), DriveKind::Hdd);
assert_eq!(classify_drive_kind("xvdb", 2), DriveKind::Unknown);
}
#[test]
fn test_parse_kib_strips_unit_and_multiplies() {
assert_eq!(parse_kib(" 1024 kB"), 1024 * 1024);
assert_eq!(parse_kib("0 kB"), 0);
assert_eq!(parse_kib("garbage"), 0);
}
#[test]
fn test_parse_cache_size_handles_units() {
assert_eq!(parse_cache_size("32K"), Some(32 * 1024));
assert_eq!(parse_cache_size("8M"), Some(8 * 1024 * 1024));
assert_eq!(parse_cache_size("4096"), Some(4096));
}
#[test]
fn test_probe_io_primitives_io_uring_runtime_check() {
let p = probe_io_primitives();
let _ = p.io_uring;
assert!(p.mmap);
assert!(p.direct_io);
}
}