Files
watcheragent/WatcherAgent/src/hardware/cpu.rs
2025-10-01 12:11:34 +02:00

298 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.0100.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.0100.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())
}
}