use crate::device::common::command_executor::{CommandOptions, execute_command};
use crate::device::common::execute_command_default;
use std::collections::HashMap;
use std::time::Duration;
const SYSCTL_BIN: &str = "/usr/sbin/sysctl";
const SYSTEM_PROFILER_BIN: &str = "/usr/sbin/system_profiler";
const SYSTEM_PROFILER_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct IntelCpuHardware {
pub model: String,
pub socket_count: u32,
pub physical_cores: u32,
pub logical_cores: u32,
pub base_frequency_mhz: u32,
pub max_frequency_mhz: u32,
pub l3_cache_mb: u32,
}
impl IntelCpuHardware {
pub fn collect(logical_cpu_fallback: u32) -> Self {
let mut hardware = Self::from_sysctl();
if hardware.has_gaps()
&& let Some(fallback) = Self::from_system_profiler()
{
hardware.fill_gaps_from(&fallback);
}
if hardware.logical_cores == 0 {
hardware.logical_cores = logical_cpu_fallback;
}
hardware.apply_defaults();
hardware
}
fn from_sysctl() -> Self {
let stdout = execute_command_default(
SYSCTL_BIN,
&[
"machdep.cpu.brand_string",
"hw.packages",
"hw.physicalcpu",
"hw.logicalcpu",
"hw.cpufrequency",
"hw.cpufrequency_max",
"hw.l3cachesize",
],
)
.map(|output| output.stdout)
.unwrap_or_default();
let values = parse_sysctl_pairs(&stdout);
let read_u64 = |key: &str| values.get(key).and_then(|v| v.parse::<u64>().ok());
let model = values
.get("machdep.cpu.brand_string")
.map(|v| (*v).to_string())
.unwrap_or_default();
let base_frequency_mhz = read_u64("hw.cpufrequency")
.map(hz_to_mhz)
.filter(|mhz| *mhz > 0)
.or_else(|| parse_frequency_from_brand_string(&model))
.unwrap_or(0);
Self {
model,
socket_count: read_u64("hw.packages").unwrap_or(0) as u32,
physical_cores: read_u64("hw.physicalcpu").unwrap_or(0) as u32,
logical_cores: read_u64("hw.logicalcpu").unwrap_or(0) as u32,
base_frequency_mhz,
max_frequency_mhz: read_u64("hw.cpufrequency_max").map(hz_to_mhz).unwrap_or(0),
l3_cache_mb: read_u64("hw.l3cachesize")
.map(|bytes| (bytes / 1024 / 1024) as u32)
.unwrap_or(0),
}
}
fn from_system_profiler() -> Option<Self> {
let output = execute_command(
SYSTEM_PROFILER_BIN,
&["SPHardwareDataType"],
&CommandOptions {
timeout: Some(SYSTEM_PROFILER_TIMEOUT),
check_status: false,
},
)
.ok()?;
Some(parse_system_profiler_hardware(&output.stdout))
}
fn has_gaps(&self) -> bool {
self.model.is_empty()
|| self.physical_cores == 0
|| self.base_frequency_mhz == 0
|| self.l3_cache_mb == 0
}
fn fill_gaps_from(&mut self, other: &Self) {
if self.model.is_empty() {
self.model = other.model.clone();
}
if self.socket_count == 0 {
self.socket_count = other.socket_count;
}
if self.physical_cores == 0 {
self.physical_cores = other.physical_cores;
}
if self.logical_cores == 0 {
self.logical_cores = other.logical_cores;
}
if self.base_frequency_mhz == 0 {
self.base_frequency_mhz = other.base_frequency_mhz;
}
if self.max_frequency_mhz == 0 {
self.max_frequency_mhz = other.max_frequency_mhz;
}
if self.l3_cache_mb == 0 {
self.l3_cache_mb = other.l3_cache_mb;
}
}
fn apply_defaults(&mut self) {
if self.model.is_empty() {
self.model = "Unknown Intel CPU".to_string();
}
if self.socket_count == 0 {
self.socket_count = 1;
}
if self.physical_cores == 0 {
self.physical_cores = self.logical_cores.max(1);
}
if self.logical_cores == 0 {
self.logical_cores = self.physical_cores;
}
if self.max_frequency_mhz == 0 {
self.max_frequency_mhz = self.base_frequency_mhz;
}
}
}
fn parse_sysctl_pairs(output: &str) -> HashMap<&str, &str> {
output
.lines()
.filter_map(|line| line.split_once(": "))
.map(|(key, value)| (key.trim(), value.trim()))
.collect()
}
fn hz_to_mhz(hz: u64) -> u32 {
((hz + 500_000) / 1_000_000) as u32
}
fn parse_frequency_from_brand_string(brand: &str) -> Option<u32> {
let tail = brand.rsplit_once('@')?.1.trim();
let (number, unit) = match tail.strip_suffix("GHz") {
Some(rest) => (rest, 1000.0),
None => (tail.strip_suffix("MHz")?, 1.0),
};
let value = number.trim().parse::<f64>().ok()?;
if !value.is_finite() || value <= 0.0 {
return None;
}
Some((value * unit).round() as u32)
}
fn parse_system_profiler_hardware(hardware_info: &str) -> IntelCpuHardware {
let mut hardware = IntelCpuHardware::default();
for line in hardware_info.lines() {
let line = line.trim();
if line.starts_with("Processor Name:") {
hardware.model = line.split(':').nth(1).unwrap_or("").trim().to_string();
} else if line.starts_with("Processor Speed:") {
if let Some(ghz) = crate::parse_colon_value!(line, f64) {
hardware.base_frequency_mhz = (ghz * 1000.0) as u32;
}
} else if line.starts_with("Number of Processors:") {
if let Some(procs) = crate::parse_colon_value!(line, u32) {
hardware.socket_count = procs;
}
} else if line.starts_with("Total Number of Cores:") {
if let Some(cores) = crate::parse_colon_value!(line, u32) {
hardware.physical_cores = cores;
}
} else if line.starts_with("L3 Cache:")
&& let Some(size) = crate::parse_colon_value!(line, u32)
{
hardware.l3_cache_mb = size;
}
}
hardware
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_sysctl_key_value_block() {
let output = "machdep.cpu.brand_string: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz\n\
hw.packages: 1\n\
hw.physicalcpu: 8\n\
hw.logicalcpu: 16\n";
let values = parse_sysctl_pairs(output);
assert_eq!(
values.get("machdep.cpu.brand_string").copied(),
Some("Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz")
);
assert_eq!(values.get("hw.physicalcpu").copied(), Some("8"));
assert_eq!(values.get("hw.logicalcpu").copied(), Some("16"));
assert!(!values.contains_key("hw.cpufrequency"));
}
#[test]
fn parses_frequency_from_intel_brand_strings() {
assert_eq!(
parse_frequency_from_brand_string("Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz"),
Some(2300)
);
assert_eq!(
parse_frequency_from_brand_string("Intel(R) Core(TM) i5-8210Y CPU @ 1.60GHz"),
Some(1600)
);
assert_eq!(
parse_frequency_from_brand_string("Some CPU @ 800MHz"),
Some(800)
);
assert_eq!(parse_frequency_from_brand_string("Apple M2 Pro"), None);
assert_eq!(parse_frequency_from_brand_string(""), None);
assert_eq!(
parse_frequency_from_brand_string("Weird CPU @ fastGHz"),
None
);
}
#[test]
fn converts_hz_to_mhz_by_rounding() {
assert_eq!(hz_to_mhz(2_300_000_000), 2300);
assert_eq!(hz_to_mhz(2_299_800_000), 2300);
assert_eq!(hz_to_mhz(0), 0);
}
#[test]
fn system_profiler_fallback_does_not_assume_hyperthreading() {
let output = "Hardware Overview:\n\
\n\
Model Name: MacBook Pro\n\
Processor Name: 8-Core Intel Core i9\n\
Processor Speed: 2.3 GHz\n\
Number of Processors: 1\n\
Total Number of Cores: 8\n\
L2 Cache (per Core): 256 KB\n\
L3 Cache: 16 MB\n";
let hardware = parse_system_profiler_hardware(output);
assert_eq!(hardware.model, "8-Core Intel Core i9");
assert_eq!(hardware.socket_count, 1);
assert_eq!(hardware.physical_cores, 8);
assert_eq!(hardware.base_frequency_mhz, 2300);
assert_eq!(hardware.l3_cache_mb, 16);
assert_eq!(
hardware.logical_cores, 0,
"system_profiler cannot report logical CPUs, so it must leave the field unset"
);
}
#[test]
fn sysctl_values_win_over_the_system_profiler_fallback() {
let mut sysctl = IntelCpuHardware {
model: "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz".to_string(),
socket_count: 1,
physical_cores: 8,
logical_cores: 16,
base_frequency_mhz: 2300,
max_frequency_mhz: 4800,
l3_cache_mb: 0,
};
let fallback = IntelCpuHardware {
model: "8-Core Intel Core i9".to_string(),
socket_count: 1,
physical_cores: 8,
logical_cores: 0,
base_frequency_mhz: 2300,
max_frequency_mhz: 0,
l3_cache_mb: 16,
};
sysctl.fill_gaps_from(&fallback);
assert_eq!(sysctl.model, "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz");
assert_eq!(sysctl.logical_cores, 16);
assert_eq!(sysctl.max_frequency_mhz, 4800);
assert_eq!(sysctl.l3_cache_mb, 16, "the only gap should be filled");
}
#[test]
fn defaults_never_assume_hyperthreading_or_zero_sockets() {
let mut hardware = IntelCpuHardware {
physical_cores: 6,
..Default::default()
};
hardware.apply_defaults();
assert_eq!(hardware.logical_cores, 6);
assert_eq!(hardware.socket_count, 1);
assert_eq!(hardware.model, "Unknown Intel CPU");
}
#[test]
fn missing_max_frequency_falls_back_to_base() {
let mut hardware = IntelCpuHardware {
physical_cores: 4,
logical_cores: 8,
base_frequency_mhz: 2600,
max_frequency_mhz: 0,
..Default::default()
};
hardware.apply_defaults();
assert_eq!(hardware.max_frequency_mhz, 2600);
}
#[test]
fn gap_detection_ignores_optional_fields() {
let complete = IntelCpuHardware {
model: "Intel(R) Core(TM) i7".to_string(),
socket_count: 1,
physical_cores: 4,
logical_cores: 8,
base_frequency_mhz: 2600,
max_frequency_mhz: 0,
l3_cache_mb: 8,
};
assert!(
!complete.has_gaps(),
"a missing turbo clock alone must not trigger the slow system_profiler path"
);
let missing_cache = IntelCpuHardware {
l3_cache_mb: 0,
..complete
};
assert!(missing_cache.has_gaps());
}
#[test]
fn apply_defaults_produces_safe_values_from_a_fully_empty_record() {
let mut hardware = IntelCpuHardware::default();
hardware.apply_defaults();
assert!(!hardware.model.is_empty());
assert!(hardware.socket_count >= 1);
assert!(hardware.physical_cores >= 1);
assert!(hardware.logical_cores >= 1);
assert!(hardware.max_frequency_mhz >= hardware.base_frequency_mhz);
}
}