298 lines
10 KiB
Rust
298 lines
10 KiB
Rust
use anyhow::Result;
|
||
use std::error::Error;
|
||
use sysinfo::System;
|
||
|
||
/// # CPU Hardware Module
|
||
///
|
||
/// This module provides CPU information collection for WatcherAgent, including load, temperature, and system uptime.
|
||
///
|
||
/// ## Responsibilities
|
||
/// - **CPU Detection:** Identifies CPU model and core count.
|
||
/// - **Metric Collection:** Queries CPU load, temperature, and uptime.
|
||
/// - **Error Handling:** Graceful fallback if metrics are unavailable.
|
||
///
|
||
/// ## Units
|
||
/// - `current_load`: CPU usage as a percentage (**0.0–100.0**)
|
||
/// - `current_temp`: CPU temperature in **degrees Celsius (°C)**
|
||
/// - `uptime`: System uptime in **seconds (s)**
|
||
///
|
||
/// CPU statistics for the host system.
|
||
///
|
||
/// # Fields
|
||
/// - `name`: CPU model name (string)
|
||
/// - `cores`: Number of physical CPU cores (integer)
|
||
/// - `current_load`: CPU usage as a percentage (**0.0–100.0**)
|
||
/// - `current_temp`: CPU temperature in **degrees Celsius (°C)**
|
||
/// - `uptime`: System uptime in **seconds (s)**
|
||
/// - `host_name`: Hostname of the system (string)
|
||
#[derive(Debug)]
|
||
pub struct CpuInfo {
|
||
pub name: Option<String>,
|
||
pub cores: Option<i32>,
|
||
pub current_load: Option<f64>,
|
||
pub current_temp: Option<f64>,
|
||
pub uptime: Option<f64>,
|
||
pub host_name: Option<String>,
|
||
}
|
||
|
||
/// Collects CPU information (model, cores, load, temperature, uptime).
|
||
///
|
||
/// # Returns
|
||
/// * `Result<CpuInfo, Box<dyn Error + Send + Sync>>` - CPU statistics or error if unavailable.
|
||
pub async fn get_cpu_info() -> Result<CpuInfo, Box<dyn Error + Send + Sync>> {
|
||
let mut sys = System::new_all();
|
||
|
||
let cpus = sys.cpus();
|
||
let uptime = System::uptime() as f64;
|
||
|
||
let host_name = System::host_name().unwrap_or_else(|| "unknown".to_string());
|
||
Ok(CpuInfo {
|
||
name: Some(
|
||
cpus.first()
|
||
.map(|c| c.brand().to_string())
|
||
.unwrap_or_default(),
|
||
),
|
||
cores: Some(cpus.len() as i32),
|
||
current_load: get_cpu_load(&mut sys).await.ok(),
|
||
current_temp: get_cpu_temp().await.ok(),
|
||
uptime: Some(uptime),
|
||
host_name: Some(host_name),
|
||
})
|
||
}
|
||
|
||
/// Queries system for current CPU load (percentage).
|
||
///
|
||
/// # Arguments
|
||
/// * `sys` - Mutable reference to sysinfo::System
|
||
///
|
||
/// # Returns
|
||
/// * `Result<f64, Box<dyn Error + Send + Sync>>` - CPU load as percentage.
|
||
pub async fn get_cpu_load(sys: &mut System) -> Result<f64, Box<dyn Error + Send + Sync>> {
|
||
sys.refresh_cpu_all();
|
||
tokio::task::yield_now().await; // Allow other tasks to run
|
||
Ok(sys.global_cpu_usage() as f64)
|
||
}
|
||
|
||
/// Attempts to read CPU temperature from system sensors (Linux only).
|
||
///
|
||
/// # Returns
|
||
/// * `Result<f64, Box<dyn Error + Send + Sync>>` - CPU temperature in degrees Celsius (°C).
|
||
pub async fn get_cpu_temp() -> Result<f64, Box<dyn Error + Send + Sync>> {
|
||
println!("Attempting to get CPU temperature...");
|
||
|
||
#[cfg(target_os = "linux")]
|
||
{
|
||
use std::fs;
|
||
use std::process::Command;
|
||
println!("");
|
||
if let Ok(output) = Command::new("sensors").output() {
|
||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||
for line in stdout.lines() {
|
||
if line.contains("Package id") || line.contains("Tdie") || line.contains("CPU Temp")
|
||
{
|
||
if let Some(temp_str) = line
|
||
.split('+')
|
||
.nth(1)
|
||
.and_then(|s| s.split_whitespace().next())
|
||
{
|
||
if let Ok(temp) = temp_str.replace("°C", "").parse::<f32>() {
|
||
return Ok(temp.into());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. Sysfs (Intel/AMD)
|
||
if let Ok(content) = fs::read_to_string("/sys/class/thermal/thermal_zone0/temp") {
|
||
if let Ok(temp) = content.trim().parse::<f32>() {
|
||
return Ok((temp / 1000.0).into());
|
||
}
|
||
}
|
||
|
||
// 3. Alternative Sysfs-Pfade
|
||
let paths = [
|
||
"/sys/class/hwmon/hwmontemp1_input",
|
||
"/sys/class/hwmon/hwmondevice/temp1_input",
|
||
];
|
||
|
||
for path_pattern in &paths {
|
||
if let Ok(paths) = glob::glob(path_pattern) {
|
||
for path in paths.flatten() {
|
||
if let Ok(content) = fs::read_to_string(&path) {
|
||
if let Ok(temp) = content.trim().parse::<f32>() {
|
||
return Ok((temp / 1000.0).into());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Err(anyhow::anyhow!("Could not find CPU temperature using sensors or sysfs").into())
|
||
}
|
||
|
||
#[cfg(target_os = "windows")]
|
||
fn failed(hr: winapi::shared::winerror::HRESULT) -> bool {
|
||
hr < 0
|
||
}
|
||
|
||
#[cfg(target_os = "windows")]
|
||
{
|
||
use com::runtime::init_runtime;
|
||
use com::sys::CLSCTX_INPROC_SERVER;
|
||
use widestring::U16CString;
|
||
use winapi::shared::rpcdce::*;
|
||
use winapi::shared::wtypes::VT_I4;
|
||
use winapi::um::oaidl::VARIANT;
|
||
use winapi::um::objidlbase::EOAC_NONE;
|
||
use winapi::um::{combaseapi, wbemcli};
|
||
|
||
init_runtime().ok();
|
||
|
||
unsafe {
|
||
use anyhow::Ok;
|
||
|
||
let mut locator: *mut wbemcli::IWbemLocator = std::ptr::null_mut();
|
||
let hr = combaseapi::CoCreateInstance(
|
||
&wbemcli::CLSID_WbemLocator,
|
||
std::ptr::null_mut(),
|
||
CLSCTX_INPROC_SERVER,
|
||
&wbemcli::IID_IWbemLocator,
|
||
&mut locator as *mut _ as *mut _,
|
||
);
|
||
|
||
if hr != 0 {
|
||
eprintln!("Failed to create WbemLocator (HRESULT: {})", hr);
|
||
return Err(("Failed to create WbemLocator").into());
|
||
}
|
||
|
||
let mut services: *mut wbemcli::IWbemServices = std::ptr::null_mut();
|
||
let namespace = U16CString::from_str("root\\cimv2").unwrap(); // Changed to more common namespace
|
||
let hr = (*locator).ConnectServer(
|
||
namespace.as_ptr().cast_mut(),
|
||
std::ptr::null_mut(),
|
||
std::ptr::null_mut(),
|
||
std::ptr::null_mut(),
|
||
0,
|
||
std::ptr::null_mut(),
|
||
std::ptr::null_mut(),
|
||
&mut services,
|
||
);
|
||
|
||
if hr != 0 {
|
||
eprintln!("Failed to connect to WMI (HRESULT: {})", hr);
|
||
(*locator).Release();
|
||
return Err(("Failed to connect to WMI").into());
|
||
}
|
||
|
||
// Set security levels
|
||
let hr = combaseapi::CoSetProxyBlanket(
|
||
services as *mut _,
|
||
RPC_C_AUTHN_WINNT,
|
||
RPC_C_AUTHZ_NONE,
|
||
std::ptr::null_mut(),
|
||
RPC_C_AUTHN_LEVEL_CALL,
|
||
RPC_C_IMP_LEVEL_IMPERSONATE,
|
||
std::ptr::null_mut(),
|
||
EOAC_NONE,
|
||
);
|
||
|
||
if hr != 0 {
|
||
eprintln!("Failed to set proxy blanket (HRESULT: {})", hr);
|
||
(*services).Release();
|
||
(*locator).Release();
|
||
return Err(("Failed to set proxy blanket").into());
|
||
}
|
||
|
||
// Try different temperature queries - some systems might have different WMI classes
|
||
let queries = [
|
||
"SELECT * FROM Win32_PerfFormattedData_Counters_ThermalZoneInformation",
|
||
"SELECT * FROM MSAcpi_ThermalZoneTemperature",
|
||
"SELECT * FROM Win32_TemperatureProbe",
|
||
];
|
||
|
||
let mut result = None;
|
||
|
||
for query_str in queries.iter() {
|
||
let query = U16CString::from_str(query_str).unwrap();
|
||
let mut enumerator: *mut wbemcli::IEnumWbemClassObject = std::ptr::null_mut();
|
||
let hr = (*services).ExecQuery(
|
||
U16CString::from_str("WQL").unwrap().as_ptr().cast_mut(),
|
||
query.as_ptr().cast_mut(),
|
||
wbemcli::WBEM_FLAG_FORWARD_ONLY as i32,
|
||
std::ptr::null_mut(),
|
||
&mut enumerator,
|
||
);
|
||
|
||
if hr != 0 {
|
||
continue; // Try next query if this one fails
|
||
}
|
||
|
||
let mut obj: *mut wbemcli::IWbemClassObject = std::ptr::null_mut();
|
||
let mut returned = 0;
|
||
let hr = (*enumerator).Next(
|
||
wbemcli::WBEM_INFINITE as i32, // Fixed: cast directly to i32
|
||
1,
|
||
&mut obj,
|
||
&mut returned,
|
||
);
|
||
|
||
if failed(hr) {
|
||
eprintln!("Failed to enumerate WMI objects (HRESULT: {})", hr);
|
||
(*enumerator).Release();
|
||
continue;
|
||
}
|
||
|
||
if returned == 0 {
|
||
// No more items
|
||
(*enumerator).Release();
|
||
continue;
|
||
}
|
||
|
||
if hr == 0 && returned > 0 {
|
||
let mut variant = std::mem::zeroed::<VARIANT>();
|
||
// Try different possible property names
|
||
let property_names = ["CurrentTemperature", "Temperature", "CurrentReading"];
|
||
|
||
for prop in property_names.iter() {
|
||
let hr = (*obj).Get(
|
||
U16CString::from_str(prop).unwrap().as_ptr(),
|
||
0,
|
||
&mut variant,
|
||
std::ptr::null_mut(),
|
||
std::ptr::null_mut(),
|
||
);
|
||
|
||
if hr == 0 && variant.n1.n2().vt as u32 == VT_I4 {
|
||
let temp_kelvin = *variant.n1.n2().n3.intVal() as f32 / 10.0;
|
||
result = Some(temp_kelvin - 273.15); // Convert to Celsius
|
||
break;
|
||
}
|
||
}
|
||
|
||
(*obj).Release();
|
||
(*enumerator).Release();
|
||
if result.is_some() {
|
||
break;
|
||
}
|
||
}
|
||
|
||
if !enumerator.is_null() {
|
||
(*enumerator).Release();
|
||
}
|
||
}
|
||
|
||
(*services).Release();
|
||
(*locator).Release();
|
||
|
||
Ok(result.unwrap() as f64).map_err(|e| e.into())
|
||
}
|
||
}
|
||
|
||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||
{
|
||
println!("CPU temperature retrieval not supported on this OS.");
|
||
Err(anyhow::anyhow!("CPU temperature retrieval not supported on this OS").into())
|
||
}
|
||
}
|