diff --git a/src/engine/formatters/i8kmon.rs b/src/engine/formatters/i8kmon.rs new file mode 100644 index 0000000..d1b1129 --- /dev/null +++ b/src/engine/formatters/i8kmon.rs @@ -0,0 +1,41 @@ +pub struct I8kmonConfig { + pub t_ambient: f32, + pub t_max_fan: f32, +} + +pub struct I8kmonTranslator; + +impl I8kmonTranslator { + pub fn generate_conf(config: &I8kmonConfig) -> String { + let t_off = config.t_ambient + 5.0; + let t_low_on = config.t_ambient + 12.0; + let t_low_off = config.t_ambient + 10.0; + let t_high_on = config.t_max_fan; + let t_high_off = config.t_max_fan - 5.0; + let t_low_trigger = (config.t_max_fan - 15.0).max(t_low_on + 2.0); + + format!( + r#"# Generated by ember-tune Optimizer +# Grounded in physical thermal resistance + +set config(gen_shadow) 1 +set config(i8k_ignore_dmi) 1 + +# Fan states: {{state_low state_high temp_on temp_off}} +set config(0) {{0 0 {t_low_on:.0} {t_off:.0}}} +set config(1) {{1 1 {t_low_trigger:.0} {t_low_off:.0}}} +set config(2) {{2 2 {t_high_on:.0} {t_high_off:.0}}} + +# Speed thresholds (approximate for XPS 9380) +set config(speed_low) 2500 +set config(speed_high) 4500 +"#, + t_low_on = t_low_on, + t_off = t_off, + t_low_trigger = t_low_trigger, + t_low_off = t_low_off, + t_high_on = t_high_on, + t_high_off = t_high_off + ) + } +} diff --git a/src/engine/formatters/mod.rs b/src/engine/formatters/mod.rs index 5b87fcf..b379d04 100644 --- a/src/engine/formatters/mod.rs +++ b/src/engine/formatters/mod.rs @@ -1 +1,2 @@ pub mod throttled; +pub mod i8kmon; diff --git a/src/engine/formatters/throttled.rs b/src/engine/formatters/throttled.rs index a4ccdbd..3e4c771 100644 --- a/src/engine/formatters/throttled.rs +++ b/src/engine/formatters/throttled.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; pub struct ThrottledConfig { pub pl1_limit: f32, @@ -11,7 +12,7 @@ impl ThrottledTranslator { pub fn generate_conf(config: &ThrottledConfig) -> String { format!( r#"[GENERAL] -# Generated by FerroTherm Optimizer +# Generated by ember-tune Optimizer # Physical Sweet Spot found at {pl1:.1}W [BATTERY] @@ -35,4 +36,98 @@ Trip_Temp_C: {trip:.0} trip = config.trip_temp ) } + + /// Merges benchmarked values into an existing throttled.conf content. + /// Preserves all other sections (like [UnderVOLT]), comments, and formatting. + pub fn merge_conf(existing_content: &str, config: &ThrottledConfig) -> String { + let mut sections = Vec::new(); + let mut current_section_name = String::new(); + let mut current_section_lines = Vec::new(); + + // 1. Parse into sections to ensure we only update keys in [BATTERY] and [AC] + for line in existing_content.lines() { + let trimmed = line.trim(); + if trimmed.starts_with('[') && trimmed.ends_with(']') { + if !current_section_lines.is_empty() || !current_section_name.is_empty() { + sections.push((current_section_name.clone(), current_section_lines.clone())); + } + current_section_name = trimmed[1..trimmed.len() - 1].to_string(); + current_section_lines = vec![line.to_string()]; + } else { + current_section_lines.push(line.to_string()); + } + } + sections.push((current_section_name, current_section_lines)); + + let target_keys = [ + ("PL1_Tdp_W", format!("{:.0}", config.pl1_limit)), + ("PL2_Tdp_W", format!("{:.0}", config.pl2_limit)), + ("Trip_Temp_C", format!("{:.0}", config.trip_temp)), + ]; + + let mut result_lines = Vec::new(); + let mut handled_sections = HashSet::new(); + + // 2. Process sections + for (name, mut lines) in sections { + if name == "BATTERY" || name == "AC" { + handled_sections.insert(name.clone()); + let mut updated_keys = HashSet::new(); + + let mut new_lines = Vec::new(); + for line in lines { + let mut updated = false; + let trimmed = line.trim(); + + if !trimmed.starts_with('#') && !trimmed.is_empty() { + if let Some((key, _)) = trimmed.split_once(':') { + let key = key.trim(); + for (target_key, new_value) in &target_keys { + if key == *target_key { + if let Some(colon_idx) = line.find(':') { + let prefix = &line[..colon_idx + 1]; + let rest = &line[colon_idx + 1..]; + let comment = if let Some(hash_idx) = rest.find('#') { + &rest[hash_idx..] + } else { + "" + }; + new_lines.push(format!("{} {}{}", prefix, new_value, comment)); + updated_keys.insert(*target_key); + updated = true; + break; + } + } + } + } + } + + if !updated { + new_lines.push(line); + } + } + + for (target_key, new_value) in &target_keys { + if !updated_keys.contains(*target_key) { + new_lines.push(format!("{}: {}", target_key, new_value)); + } + } + lines = new_lines; + } + result_lines.extend(lines); + } + + // 3. Add missing sections if they didn't exist at all + for section_name in &["BATTERY", "AC"] { + if !handled_sections.contains(*section_name) { + result_lines.push(String::new()); + result_lines.push(format!("[{}]", section_name)); + for (target_key, new_value) in &target_keys { + result_lines.push(format!("{}: {}", target_key, new_value)); + } + } + } + + result_lines.join("\n") + } } diff --git a/src/orchestrator/mod.rs b/src/orchestrator/mod.rs index fc056fe..61bead3 100644 --- a/src/orchestrator/mod.rs +++ b/src/orchestrator/mod.rs @@ -182,7 +182,7 @@ impl BenchmarkOrchestrator { // Phase 5: Finalizing self.phase = BenchmarkPhase::Finalizing; - self.log("Benchmark sequence complete. Generating configuration...")?; + self.log("Benchmark sequence complete. Generating configurations...")?; let config = crate::engine::formatters::throttled::ThrottledConfig { pl1_limit: res.silicon_knee_watts, @@ -190,9 +190,25 @@ impl BenchmarkOrchestrator { trip_temp: res.max_temp_c.max(95.0), }; - let conf_content = crate::engine::formatters::throttled::ThrottledTranslator::generate_conf(&config); - std::fs::write("throttled.conf", conf_content)?; - self.log("✓ Saved 'throttled.conf'.")?; + // 1. Throttled (Merged if exists) + let throttled_path = "throttled.conf"; + let existing_throttled = std::fs::read_to_string(throttled_path).unwrap_or_default(); + let throttled_content = if existing_throttled.is_empty() { + crate::engine::formatters::throttled::ThrottledTranslator::generate_conf(&config) + } else { + crate::engine::formatters::throttled::ThrottledTranslator::merge_conf(&existing_throttled, &config) + }; + std::fs::write(throttled_path, throttled_content)?; + self.log("✓ Saved 'throttled.conf' (merged).")?; + + // 2. i8kmon + let i8k_config = crate::engine::formatters::i8kmon::I8kmonConfig { + t_ambient: self.profile.ambient_temp, + t_max_fan: res.max_temp_c - 5.0, // Aim to hit max fan before max temp + }; + let i8k_content = crate::engine::formatters::i8kmon::I8kmonTranslator::generate_conf(&i8k_config); + std::fs::write("i8kmon.conf", i8k_content)?; + self.log("✓ Saved 'i8kmon.conf'.")?; self.guard.restore()?; self.log("✓ Environment restored.")?; diff --git a/src/sal/dell_xps_9380.rs b/src/sal/dell_xps_9380.rs index 59c45a1..75d747e 100644 --- a/src/sal/dell_xps_9380.rs +++ b/src/sal/dell_xps_9380.rs @@ -96,7 +96,19 @@ impl PreflightAuditor for DellXps9380Sal { outcome: if unsafe { libc::getuid() } == 0 { Ok(()) } else { Err(AuditError::RootRequired) } }); - // 2. Kernel parameters check + // 2. Kernel modules check (simplified check via sysfs/proc) + let modules = ["dell_smm_hwmon", "msr", "intel_rapl_msr"]; + for mod_name in modules { + let path = format!("/sys/module/{}", mod_name); + steps.push(AuditStep { + description: format!("Kernel Module: {}", mod_name), + outcome: if PathBuf::from(path).exists() { Ok(()) } else { + Err(AuditError::ToolMissing(format!("Module '{}' not loaded. Run 'sudo modprobe {}'", mod_name, mod_name))) + } + }); + } + + // 3. 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(), @@ -104,6 +116,12 @@ impl PreflightAuditor for DellXps9380Sal { Err(AuditError::MissingKernelParam("dell_smm_hwmon.ignore_dmi=1".to_string())) } }); + steps.push(AuditStep { + description: "Kernel Param: dell_smm_hwmon.restricted=0".to_string(), + outcome: if cmdline.contains("dell_smm_hwmon.restricted=0") { Ok(()) } else { + Err(AuditError::MissingKernelParam("dell_smm_hwmon.restricted=0".to_string())) + } + }); steps.push(AuditStep { description: "Kernel Param: msr.allow_writes=on".to_string(), outcome: if cmdline.contains("msr.allow_writes=on") { Ok(()) } else { @@ -111,7 +129,16 @@ impl PreflightAuditor for DellXps9380Sal { } }); - // 3. Check AC power + // 4. Lockdown check + let lockdown = fs::read_to_string("/sys/kernel/security/lockdown").unwrap_or_default(); + steps.push(AuditStep { + description: "Kernel Lockdown Status".to_string(), + outcome: if lockdown.contains("[none]") || lockdown.is_empty() { Ok(()) } else { + Err(AuditError::KernelIncompatible("Kernel is in lockdown mode. Set to 'none' to allow MSR/SMM writes.".to_string())) + } + }); + + // 5. 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(),