xps 13 3980

This commit is contained in:
2026-02-26 13:37:04 +01:00
commit d6ac8e5931
21 changed files with 4562 additions and 0 deletions

235
src/sal/dell_xps_9380.rs Normal file
View 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
View 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
View File

@@ -0,0 +1,3 @@
pub mod traits;
pub mod mock;
pub mod dell_xps_9380;

103
src/sal/traits.rs Normal file
View 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()
}
}