xps 13 3980
This commit is contained in:
235
src/sal/dell_xps_9380.rs
Normal file
235
src/sal/dell_xps_9380.rs
Normal file
@@ -0,0 +1,235 @@
|
||||
use super::traits::{PreflightAuditor, EnvironmentGuard, SensorBus, ActuatorBus, HardwareWatchdog, AuditError, AuditStep};
|
||||
use anyhow::{Result, Context};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::sync::Mutex;
|
||||
use tracing::debug;
|
||||
|
||||
pub struct DellXps9380Sal {
|
||||
temp_path: PathBuf,
|
||||
pwr_path: PathBuf,
|
||||
fan_path: PathBuf,
|
||||
pl1_path: PathBuf,
|
||||
pl2_path: PathBuf,
|
||||
last_poll: Mutex<Instant>,
|
||||
last_temp: Mutex<f32>,
|
||||
last_fan: Mutex<u32>,
|
||||
}
|
||||
|
||||
impl DellXps9380Sal {
|
||||
pub fn init() -> Result<Self> {
|
||||
let mut temp_path = None;
|
||||
let mut pwr_path = None;
|
||||
let mut fan_path = None;
|
||||
let mut rapl_base_path = None;
|
||||
|
||||
// Dynamic hwmon discovery
|
||||
if let Ok(entries) = fs::read_dir("/sys/class/hwmon") {
|
||||
for entry in entries.flatten() {
|
||||
let p = entry.path();
|
||||
let name = fs::read_to_string(p.join("name")).unwrap_or_default().trim().to_string();
|
||||
|
||||
if name == "dell_smm" {
|
||||
temp_path = Some(p.join("temp1_input"));
|
||||
fan_path = Some(p.join("fan1_input"));
|
||||
}
|
||||
|
||||
if name == "intel_rapl" || name == "rapl" {
|
||||
pwr_path = Some(p.join("power1_average"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Discovery for RAPL via powercap
|
||||
if let Ok(entries) = fs::read_dir("/sys/class/powercap") {
|
||||
for entry in entries.flatten() {
|
||||
let p = entry.path();
|
||||
if let Ok(name) = fs::read_to_string(p.join("name")) {
|
||||
if name.trim() == "package-0" {
|
||||
rapl_base_path = Some(p.clone());
|
||||
if pwr_path.is_none() {
|
||||
pwr_path = Some(p.join("energy_uj"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let rapl_base = rapl_base_path.context("Could not find RAPL package-0 path in powercap")?;
|
||||
|
||||
Ok(Self {
|
||||
temp_path: temp_path.context("Could not find dell_smm temperature path")?,
|
||||
pwr_path: pwr_path.context("Could not find RAPL power path")?,
|
||||
fan_path: fan_path.context("Could not find dell_smm fan path")?,
|
||||
pl1_path: rapl_base.join("constraint_0_power_limit_uw"),
|
||||
pl2_path: rapl_base.join("constraint_1_power_limit_uw"),
|
||||
last_poll: Mutex::new(Instant::now() - Duration::from_secs(2)),
|
||||
last_temp: Mutex::new(0.0),
|
||||
last_fan: Mutex::new(0),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PreflightAuditor for DellXps9380Sal {
|
||||
fn audit(&self) -> Box<dyn Iterator<Item = AuditStep> + '_> {
|
||||
let mut steps = Vec::new();
|
||||
|
||||
// 1. Root check
|
||||
steps.push(AuditStep {
|
||||
description: "Root Privileges".to_string(),
|
||||
outcome: if unsafe { libc::getuid() } == 0 { Ok(()) } else { Err(AuditError::RootRequired) }
|
||||
});
|
||||
|
||||
// 2. Kernel parameters check
|
||||
let cmdline = fs::read_to_string("/proc/cmdline").unwrap_or_default();
|
||||
steps.push(AuditStep {
|
||||
description: "Kernel Param: dell_smm_hwmon.ignore_dmi=1".to_string(),
|
||||
outcome: if cmdline.contains("dell_smm_hwmon.ignore_dmi=1") { Ok(()) } else {
|
||||
Err(AuditError::MissingKernelParam("dell_smm_hwmon.ignore_dmi=1".to_string()))
|
||||
}
|
||||
});
|
||||
steps.push(AuditStep {
|
||||
description: "Kernel Param: msr.allow_writes=on".to_string(),
|
||||
outcome: if cmdline.contains("msr.allow_writes=on") { Ok(()) } else {
|
||||
Err(AuditError::MissingKernelParam("msr.allow_writes=on".to_string()))
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Check AC power
|
||||
let ac_status = fs::read_to_string("/sys/class/power_supply/AC/online").unwrap_or_else(|_| "0".to_string());
|
||||
steps.push(AuditStep {
|
||||
description: "AC Power Connection".to_string(),
|
||||
outcome: if ac_status.trim() == "1" { Ok(()) } else {
|
||||
Err(AuditError::AcPowerMissing("System must be on AC power for benchmarking".to_string()))
|
||||
}
|
||||
});
|
||||
|
||||
Box::new(steps.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DellXps9380Guard {
|
||||
stopped_services: Vec<String>,
|
||||
}
|
||||
|
||||
impl DellXps9380Guard {
|
||||
pub fn new() -> Self {
|
||||
Self { stopped_services: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl EnvironmentGuard for DellXps9380Guard {
|
||||
fn suppress(&mut self) -> Result<()> {
|
||||
let services = ["tlp", "thermald"];
|
||||
for s in services {
|
||||
if Command::new("systemctl").args(["is-active", "--quiet", s]).status()?.success() {
|
||||
Command::new("systemctl").args(["stop", s]).status()?;
|
||||
self.stopped_services.push(s.to_string());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore(&mut self) -> Result<()> {
|
||||
for s in &self.stopped_services {
|
||||
let _ = Command::new("systemctl").args(["start", s]).status();
|
||||
}
|
||||
self.stopped_services.clear();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DellXps9380Guard {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.restore();
|
||||
}
|
||||
}
|
||||
|
||||
impl SensorBus for DellXps9380Sal {
|
||||
fn get_temp(&self) -> Result<f32> {
|
||||
// Enforce 1000ms rate limit for Dell SMM as per GEMINI.md
|
||||
let mut last_poll = self.last_poll.lock().unwrap();
|
||||
let now = Instant::now();
|
||||
|
||||
if now.duration_since(*last_poll) < Duration::from_millis(1000) {
|
||||
return Ok(*self.last_temp.lock().unwrap());
|
||||
}
|
||||
|
||||
let s = fs::read_to_string(&self.temp_path)?;
|
||||
let val = s.trim().parse::<f32>()? / 1000.0;
|
||||
|
||||
*self.last_temp.lock().unwrap() = val;
|
||||
*last_poll = now;
|
||||
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
fn get_power_w(&self) -> Result<f32> {
|
||||
if self.pwr_path.to_string_lossy().contains("energy_uj") {
|
||||
let e1 = fs::read_to_string(&self.pwr_path)?.trim().parse::<u64>()?;
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
let e2 = fs::read_to_string(&self.pwr_path)?.trim().parse::<u64>()?;
|
||||
Ok((e2.saturating_sub(e1)) as f32 / 100000.0)
|
||||
} else {
|
||||
let s = fs::read_to_string(&self.pwr_path)?;
|
||||
Ok(s.trim().parse::<f32>()? / 1000000.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_fan_rpm(&self) -> Result<u32> {
|
||||
let mut last_poll = self.last_poll.lock().unwrap();
|
||||
let now = Instant::now();
|
||||
|
||||
if now.duration_since(*last_poll) < Duration::from_millis(1000) {
|
||||
return Ok(*self.last_fan.lock().unwrap());
|
||||
}
|
||||
|
||||
let s = fs::read_to_string(&self.fan_path)?;
|
||||
let val = s.trim().parse::<u32>()?;
|
||||
|
||||
*self.last_fan.lock().unwrap() = val;
|
||||
*last_poll = now;
|
||||
|
||||
Ok(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl ActuatorBus for DellXps9380Sal {
|
||||
fn set_fan_mode(&self, mode: &str) -> Result<()> {
|
||||
match mode {
|
||||
"max" | "Manual" => {
|
||||
Command::new("dell-bios-fan-control").arg("0").status()?;
|
||||
}
|
||||
"auto" | "Auto" => {
|
||||
Command::new("dell-bios-fan-control").arg("1").status()?;
|
||||
}
|
||||
_ => {
|
||||
debug!("Unknown fan mode requested: {}", mode);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_sustained_power_limit(&self, watts: f32) -> Result<()> {
|
||||
let uw = (watts * 1_000_000.0) as u64;
|
||||
fs::write(&self.pl1_path, uw.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_burst_power_limit(&self, watts: f32) -> Result<()> {
|
||||
let uw = (watts * 1_000_000.0) as u64;
|
||||
fs::write(&self.pl2_path, uw.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl HardwareWatchdog for DellXps9380Sal {
|
||||
fn check_emergency(&self) -> Result<bool> {
|
||||
// Check for thermal throttling or BD PROCHOT
|
||||
// Simplified for now
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
76
src/sal/mock.rs
Normal file
76
src/sal/mock.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use super::traits::{PreflightAuditor, EnvironmentGuard, SensorBus, ActuatorBus, HardwareWatchdog, AuditStep};
|
||||
use anyhow::Result;
|
||||
|
||||
pub struct MockAuditor;
|
||||
impl PreflightAuditor for MockAuditor {
|
||||
fn audit(&self) -> Box<dyn Iterator<Item = AuditStep> + '_> {
|
||||
let steps = vec![
|
||||
AuditStep {
|
||||
description: "Mock Root Privileges".to_string(),
|
||||
outcome: Ok(()),
|
||||
},
|
||||
AuditStep {
|
||||
description: "Mock AC Power Status".to_string(),
|
||||
outcome: Ok(()),
|
||||
},
|
||||
];
|
||||
Box::new(steps.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MockGuard {
|
||||
pub suppressed: bool,
|
||||
}
|
||||
impl MockGuard {
|
||||
pub fn new() -> Self {
|
||||
Self { suppressed: false }
|
||||
}
|
||||
}
|
||||
impl EnvironmentGuard for MockGuard {
|
||||
fn suppress(&mut self) -> Result<()> {
|
||||
self.suppressed = true;
|
||||
Ok(())
|
||||
}
|
||||
fn restore(&mut self) -> Result<()> {
|
||||
self.suppressed = false;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl Drop for MockGuard {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.restore();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MockSensorBus;
|
||||
impl SensorBus for MockSensorBus {
|
||||
fn get_temp(&self) -> Result<f32> {
|
||||
Ok(42.0)
|
||||
}
|
||||
fn get_power_w(&self) -> Result<f32> {
|
||||
Ok(15.0)
|
||||
}
|
||||
fn get_fan_rpm(&self) -> Result<u32> {
|
||||
Ok(2500)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MockActuatorBus;
|
||||
impl ActuatorBus for MockActuatorBus {
|
||||
fn set_fan_mode(&self, _mode: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn set_sustained_power_limit(&self, _watts: f32) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn set_burst_power_limit(&self, _watts: f32) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MockWatchdog;
|
||||
impl HardwareWatchdog for MockWatchdog {
|
||||
fn check_emergency(&self) -> Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
3
src/sal/mod.rs
Normal file
3
src/sal/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod traits;
|
||||
pub mod mock;
|
||||
pub mod dell_xps_9380;
|
||||
103
src/sal/traits.rs
Normal file
103
src/sal/traits.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use anyhow::Result;
|
||||
use thiserror::Error;
|
||||
use miette::Diagnostic;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Error, Diagnostic, Debug, Clone)]
|
||||
pub enum AuditError {
|
||||
#[error("Missing root privileges.")]
|
||||
#[diagnostic(code(ember_tune::root_required), severity(error))]
|
||||
#[help("ember-tune requires direct hardware access (MSRs, sysfs). Please run with 'sudo'.")]
|
||||
RootRequired,
|
||||
|
||||
#[error("Missing kernel parameter: {0}")]
|
||||
#[diagnostic(code(ember_tune::missing_kernel_param), severity(error))]
|
||||
#[help("Add '{0}' to your GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub, then run 'sudo update-grub' and reboot.")]
|
||||
MissingKernelParam(String),
|
||||
|
||||
#[error("System is running on battery: {0}")]
|
||||
#[diagnostic(code(ember_tune::ac_power_missing), severity(error))]
|
||||
#[help("Thermal benchmarking requires a stable AC power source to ensure consistent PL limits. Please plug in your charger.")]
|
||||
AcPowerMissing(String),
|
||||
|
||||
#[error("Incompatible kernel version: {0}")]
|
||||
#[diagnostic(code(ember_tune::kernel_incompatible), severity(error))]
|
||||
#[help("Your kernel version '{0}' may not support the required RAPL or SMM interfaces. Please upgrade to a recent LTS kernel (6.1+).")]
|
||||
KernelIncompatible(String),
|
||||
|
||||
#[error("Required tool missing: {0}")]
|
||||
#[diagnostic(code(ember_tune::tool_missing), severity(error))]
|
||||
#[help("The utility '{0}' is required for this SAL. Please install it using your package manager (e.g., 'sudo apt install {0}').")]
|
||||
ToolMissing(String),
|
||||
}
|
||||
|
||||
pub struct AuditStep {
|
||||
pub description: String,
|
||||
pub outcome: Result<(), AuditError>,
|
||||
}
|
||||
|
||||
/// Evaluates immutable system states (e.g., kernel bootline parameters, AC power status).
|
||||
pub trait PreflightAuditor: Send + Sync {
|
||||
fn audit(&self) -> Box<dyn Iterator<Item = AuditStep> + '_>;
|
||||
}
|
||||
|
||||
impl<T: PreflightAuditor + ?Sized> PreflightAuditor for Arc<T> {
|
||||
fn audit(&self) -> Box<dyn Iterator<Item = AuditStep> + '_> {
|
||||
(**self).audit()
|
||||
}
|
||||
}
|
||||
|
||||
/// Suppresses conflicting daemons (tlp, thermald).
|
||||
pub trait EnvironmentGuard {
|
||||
fn suppress(&mut self) -> Result<()>;
|
||||
fn restore(&mut self) -> Result<()>;
|
||||
}
|
||||
|
||||
/// Read-only interface for standardized metrics.
|
||||
pub trait SensorBus {
|
||||
fn get_temp(&self) -> Result<f32>;
|
||||
fn get_power_w(&self) -> Result<f32>;
|
||||
fn get_fan_rpm(&self) -> Result<u32>;
|
||||
}
|
||||
|
||||
impl<T: SensorBus + ?Sized> SensorBus for Arc<T> {
|
||||
fn get_temp(&self) -> Result<f32> {
|
||||
(**self).get_temp()
|
||||
}
|
||||
fn get_power_w(&self) -> Result<f32> {
|
||||
(**self).get_power_w()
|
||||
}
|
||||
fn get_fan_rpm(&self) -> Result<u32> {
|
||||
(**self).get_fan_rpm()
|
||||
}
|
||||
}
|
||||
|
||||
/// Write-only interface for hardware commands.
|
||||
pub trait ActuatorBus {
|
||||
fn set_fan_mode(&self, mode: &str) -> Result<()>;
|
||||
fn set_sustained_power_limit(&self, watts: f32) -> Result<()>;
|
||||
fn set_burst_power_limit(&self, watts: f32) -> Result<()>;
|
||||
}
|
||||
|
||||
impl<T: ActuatorBus + ?Sized> ActuatorBus for Arc<T> {
|
||||
fn set_fan_mode(&self, mode: &str) -> Result<()> {
|
||||
(**self).set_fan_mode(mode)
|
||||
}
|
||||
fn set_sustained_power_limit(&self, watts: f32) -> Result<()> {
|
||||
(**self).set_sustained_power_limit(watts)
|
||||
}
|
||||
fn set_burst_power_limit(&self, watts: f32) -> Result<()> {
|
||||
(**self).set_burst_power_limit(watts)
|
||||
}
|
||||
}
|
||||
|
||||
/// Concurrent monitor for catastrophic states.
|
||||
pub trait HardwareWatchdog {
|
||||
fn check_emergency(&self) -> Result<bool>;
|
||||
}
|
||||
|
||||
impl<T: HardwareWatchdog + ?Sized> HardwareWatchdog for Arc<T> {
|
||||
fn check_emergency(&self) -> Result<bool> {
|
||||
(**self).check_emergency()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user