1
Fork 0
mirror of https://github.com/RGBCube/superfreq synced 2025-07-27 17:07:44 +00:00

daemon: add conflict detection and governor override

Also adds governor override functionality allowing users to persistently force
a specific CPU governor mode.
This commit is contained in:
NotAShelf 2025-05-13 23:55:27 +03:00
parent 4bf4ab5673
commit 72ebbf3761
No known key found for this signature in database
GPG key ID: 29D95B64378DB4BF
8 changed files with 377 additions and 128 deletions

View file

@ -18,7 +18,7 @@ pub struct ProfileConfig {
impl Default for ProfileConfig { impl Default for ProfileConfig {
fn default() -> Self { fn default() -> Self {
ProfileConfig { Self {
governor: Some("schedutil".to_string()), // common sensible default (?) governor: Some("schedutil".to_string()), // common sensible default (?)
turbo: Some(TurboSetting::Auto), turbo: Some(TurboSetting::Auto),
epp: None, // defaults depend on governor and system epp: None, // defaults depend on governor and system
@ -45,7 +45,7 @@ pub struct AppConfig {
pub daemon: DaemonConfig, pub daemon: DaemonConfig,
} }
fn default_poll_interval_sec() -> u64 { const fn default_poll_interval_sec() -> u64 {
5 5
} }
@ -59,24 +59,24 @@ pub enum ConfigError {
} }
impl From<std::io::Error> for ConfigError { impl From<std::io::Error> for ConfigError {
fn from(err: std::io::Error) -> ConfigError { fn from(err: std::io::Error) -> Self {
ConfigError::Io(err) Self::Io(err)
} }
} }
impl From<toml::de::Error> for ConfigError { impl From<toml::de::Error> for ConfigError {
fn from(err: toml::de::Error) -> ConfigError { fn from(err: toml::de::Error) -> Self {
ConfigError::Toml(err) Self::Toml(err)
} }
} }
impl std::fmt::Display for ConfigError { impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
ConfigError::Io(e) => write!(f, "I/O error: {}", e), Self::Io(e) => write!(f, "I/O error: {e}"),
ConfigError::Toml(e) => write!(f, "TOML parsing error: {}", e), Self::Toml(e) => write!(f, "TOML parsing error: {e}"),
ConfigError::NoValidConfigFound => write!(f, "No valid configuration file found."), Self::NoValidConfigFound => write!(f, "No valid configuration file found."),
ConfigError::HomeDirNotFound => write!(f, "Could not determine user home directory."), Self::HomeDirNotFound => write!(f, "Could not determine user home directory."),
} }
} }
} }
@ -128,8 +128,8 @@ pub fn load_config() -> Result<AppConfig, ConfigError> {
.daemon .daemon
.max_poll_interval_sec, .max_poll_interval_sec,
throttle_on_battery: toml_app_config.daemon.throttle_on_battery, throttle_on_battery: toml_app_config.daemon.throttle_on_battery,
log_level: toml_app_config.daemon.log_level.clone(), log_level: toml_app_config.daemon.log_level,
stats_file_path: toml_app_config.daemon.stats_file_path.clone(), stats_file_path: toml_app_config.daemon.stats_file_path,
}, },
}; };
return Ok(app_config); return Ok(app_config);
@ -187,7 +187,7 @@ pub struct AppConfigToml {
impl Default for ProfileConfigToml { impl Default for ProfileConfigToml {
fn default() -> Self { fn default() -> Self {
ProfileConfigToml { Self {
governor: Some("schedutil".to_string()), governor: Some("schedutil".to_string()),
turbo: Some("auto".to_string()), turbo: Some("auto".to_string()),
epp: None, epp: None,
@ -214,19 +214,19 @@ pub const DEFAULT_LOAD_THRESHOLD_HIGH: f32 = 70.0; // enable turbo if load is ab
pub const DEFAULT_LOAD_THRESHOLD_LOW: f32 = 30.0; // disable turbo if load is below this pub const DEFAULT_LOAD_THRESHOLD_LOW: f32 = 30.0; // disable turbo if load is below this
pub const DEFAULT_TEMP_THRESHOLD_HIGH: f32 = 75.0; // disable turbo if temperature is above this pub const DEFAULT_TEMP_THRESHOLD_HIGH: f32 = 75.0; // disable turbo if temperature is above this
fn default_load_threshold_high() -> f32 { const fn default_load_threshold_high() -> f32 {
DEFAULT_LOAD_THRESHOLD_HIGH DEFAULT_LOAD_THRESHOLD_HIGH
} }
fn default_load_threshold_low() -> f32 { const fn default_load_threshold_low() -> f32 {
DEFAULT_LOAD_THRESHOLD_LOW DEFAULT_LOAD_THRESHOLD_LOW
} }
fn default_temp_threshold_high() -> f32 { const fn default_temp_threshold_high() -> f32 {
DEFAULT_TEMP_THRESHOLD_HIGH DEFAULT_TEMP_THRESHOLD_HIGH
} }
impl Default for TurboAutoSettings { impl Default for TurboAutoSettings {
fn default() -> Self { fn default() -> Self {
TurboAutoSettings { Self {
load_threshold_high: DEFAULT_LOAD_THRESHOLD_HIGH, load_threshold_high: DEFAULT_LOAD_THRESHOLD_HIGH,
load_threshold_low: DEFAULT_LOAD_THRESHOLD_LOW, load_threshold_low: DEFAULT_LOAD_THRESHOLD_LOW,
temp_threshold_high: DEFAULT_TEMP_THRESHOLD_HIGH, temp_threshold_high: DEFAULT_TEMP_THRESHOLD_HIGH,
@ -236,7 +236,7 @@ impl Default for TurboAutoSettings {
impl From<ProfileConfigToml> for ProfileConfig { impl From<ProfileConfigToml> for ProfileConfig {
fn from(toml_config: ProfileConfigToml) -> Self { fn from(toml_config: ProfileConfigToml) -> Self {
ProfileConfig { Self {
governor: toml_config.governor, governor: toml_config.governor,
turbo: toml_config turbo: toml_config
.turbo .turbo
@ -274,7 +274,7 @@ pub struct DaemonConfig {
pub stats_file_path: Option<String>, pub stats_file_path: Option<String>,
} }
#[derive(Deserialize, Debug, Clone, PartialEq)] #[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel { pub enum LogLevel {
Error, Error,
Warning, Warning,
@ -296,27 +296,27 @@ impl Default for DaemonConfig {
} }
} }
fn default_adaptive_interval() -> bool { const fn default_adaptive_interval() -> bool {
false false
} }
fn default_min_poll_interval_sec() -> u64 { const fn default_min_poll_interval_sec() -> u64 {
1 1
} }
fn default_max_poll_interval_sec() -> u64 { const fn default_max_poll_interval_sec() -> u64 {
30 30
} }
fn default_throttle_on_battery() -> bool { const fn default_throttle_on_battery() -> bool {
true true
} }
fn default_log_level() -> LogLevel { const fn default_log_level() -> LogLevel {
LogLevel::Info LogLevel::Info
} }
fn default_stats_file_path() -> Option<String> { const fn default_stats_file_path() -> Option<String> {
None None
} }

128
src/conflict.rs Normal file
View file

@ -0,0 +1,128 @@
use std::path::Path;
use std::process::Command;
/// Represents detected conflicts with other power management services
#[derive(Debug)]
pub struct ConflictDetection {
/// Whether TLP service was detected
pub tlp: bool,
/// Whether GNOME Power Profiles daemon was detected
pub gnome_power: bool,
/// Whether tuned service was detected
pub tuned: bool,
/// Other power managers that were detected
pub other: Vec<String>,
}
impl ConflictDetection {
/// Returns true if any conflicts were detected
pub fn has_conflicts(&self) -> bool {
self.tlp || self.gnome_power || self.tuned || !self.other.is_empty()
}
/// Get formatted conflict information
pub fn get_conflict_message(&self) -> String {
if !self.has_conflicts() {
return "No conflicts detected with other power management services.".to_string();
}
let mut message =
"Potential conflicts detected with other power management services:\n".to_string();
if self.tlp {
message.push_str("- TLP service is active. This may interfere with CPU settings.\n");
}
if self.gnome_power {
message.push_str(
"- GNOME Power Profiles daemon is active. This may override CPU/power settings.\n",
);
}
if self.tuned {
message.push_str(
"- Tuned service is active. This may conflict with CPU frequency settings.\n",
);
}
for other in &self.other {
message.push_str(&format!(
"- {other} is active. This may conflict with superfreq.\n"
));
}
message.push_str("\nConsider disabling conflicting services for optimal operation.");
message
}
}
/// Detect if systemctl is available
fn systemctl_exists() -> bool {
Command::new("sh")
.arg("-c")
.arg("command -v systemctl")
.status()
.is_ok_and(|status| status.success())
}
/// Check if a specific systemd service is active.
// TODO: maybe we can use some kind of a binding here
// or figure out a better detection method?
fn is_service_active(service: &str) -> bool {
if !systemctl_exists() {
return false;
}
Command::new("systemctl")
.arg("--quiet")
.arg("is-active")
.arg(service)
.status()
.is_ok_and(|status| status.success())
}
/// Check for conflicts with other power management services
pub fn detect_conflicts() -> ConflictDetection {
let mut conflicts = ConflictDetection {
tlp: false,
gnome_power: false,
tuned: false,
other: Vec::new(),
};
// Check for TLP
conflicts.tlp = is_service_active("tlp.service");
// Check for GNOME Power Profiles daemon
conflicts.gnome_power = is_service_active("power-profiles-daemon.service");
// Check for tuned
conflicts.tuned = is_service_active("tuned.service");
// Check for other common power managers
let other_services = ["thermald.service", "powertop.service"];
for service in other_services {
if is_service_active(service) {
conflicts.other.push(service.to_string());
}
}
// Also check if TLP is installed but not running as a service
// FIXME: This will obviously not work on non-FHS distros like NixOS
// which I kinda want to prioritize. Though, since we can't actually
// predict store paths I also don't know how else we can perform this
// check...
if !conflicts.tlp
&& Path::new("/usr/share/tlp").exists()
&& Command::new("sh")
.arg("-c")
.arg("tlp-stat -s 2>/dev/null | grep -q 'TLP power save = enabled'")
.status()
.is_ok_and(|status| status.success())
{
conflicts.tlp = true;
}
conflicts
}

View file

@ -1,3 +1,31 @@
use clap::ValueEnum;
use serde::Deserialize;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, ValueEnum)]
pub enum TurboSetting {
Always, // turbo is forced on (if possible)
Auto, // system or driver controls turbo
Never, // turbo is forced off
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum GovernorOverrideMode {
Performance,
Powersave,
Reset,
}
impl fmt::Display for GovernorOverrideMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Performance => write!(f, "performance"),
Self::Powersave => write!(f, "powersave"),
Self::Reset => write!(f, "reset"),
}
}
}
pub struct SystemInfo { pub struct SystemInfo {
// Overall system details // Overall system details
pub cpu_model: String, pub cpu_model: String,
@ -59,13 +87,3 @@ pub enum OperationalMode {
Powersave, Powersave,
Performance, Performance,
} }
use clap::ValueEnum;
use serde::Deserialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, ValueEnum)]
pub enum TurboSetting {
Always, // turbo is forced on (if possible)
Auto, // system or driver controls turbo
Never, // turbo is forced off
}

View file

@ -1,4 +1,4 @@
use crate::core::TurboSetting; use crate::core::{GovernorOverrideMode, TurboSetting};
use core::str; use core::str;
use std::{fs, io, path::Path, string::ToString}; use std::{fs, io, path::Path, string::ToString};
@ -276,3 +276,78 @@ pub fn get_platform_profiles() -> Result<Vec<String>> {
.map(ToString::to_string) .map(ToString::to_string)
.collect()) .collect())
} }
/// Path for storing the governor override state
const GOVERNOR_OVERRIDE_PATH: &str = "/etc/superfreq/governor_override";
/// Force a specific CPU governor or reset to automatic mode
pub fn force_governor(mode: GovernorOverrideMode) -> Result<()> {
// Create directory if it doesn't exist
let dir_path = Path::new("/etc/superfreq");
if !dir_path.exists() {
fs::create_dir_all(dir_path).map_err(|e| {
if e.kind() == io::ErrorKind::PermissionDenied {
ControlError::PermissionDenied(format!(
"Permission denied creating directory: {}. Try running with sudo.",
dir_path.display()
))
} else {
ControlError::Io(e)
}
})?;
}
match mode {
GovernorOverrideMode::Reset => {
// Remove the override file if it exists
if Path::new(GOVERNOR_OVERRIDE_PATH).exists() {
fs::remove_file(GOVERNOR_OVERRIDE_PATH).map_err(|e| {
if e.kind() == io::ErrorKind::PermissionDenied {
ControlError::PermissionDenied(format!(
"Permission denied removing override file: {GOVERNOR_OVERRIDE_PATH}. Try running with sudo."
))
} else {
ControlError::Io(e)
}
})?;
println!(
"Governor override has been reset. Normal profile-based settings will be used."
);
} else {
println!("No governor override was set.");
}
Ok(())
}
GovernorOverrideMode::Performance | GovernorOverrideMode::Powersave => {
// Create the override file with the selected governor
let governor = mode.to_string().to_lowercase();
fs::write(GOVERNOR_OVERRIDE_PATH, &governor).map_err(|e| {
if e.kind() == io::ErrorKind::PermissionDenied {
ControlError::PermissionDenied(format!(
"Permission denied writing to override file: {GOVERNOR_OVERRIDE_PATH}. Try running with sudo."
))
} else {
ControlError::Io(e)
}
})?;
// Also apply the governor immediately
set_governor(&governor, None)?;
println!(
"Governor override set to '{governor}'. This setting will persist across reboots."
);
println!("To reset, use: superfreq force-governor reset");
Ok(())
}
}
}
/// Get the current governor override if set
pub fn get_governor_override() -> Option<String> {
if Path::new(GOVERNOR_OVERRIDE_PATH).exists() {
fs::read_to_string(GOVERNOR_OVERRIDE_PATH).ok()
} else {
None
}
}

View file

@ -1,4 +1,5 @@
use crate::config::{AppConfig, LogLevel}; use crate::config::{AppConfig, LogLevel};
use crate::conflict;
use crate::core::SystemReport; use crate::core::SystemReport;
use crate::engine; use crate::engine;
use crate::monitor; use crate::monitor;
@ -14,7 +15,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), Box<dyn std::e
let effective_log_level = if verbose { let effective_log_level = if verbose {
LogLevel::Debug LogLevel::Debug
} else { } else {
config.daemon.log_level.clone() config.daemon.log_level
}; };
log_message( log_message(
@ -23,6 +24,16 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), Box<dyn std::e
"Starting superfreq daemon...", "Starting superfreq daemon...",
); );
// Check for conflicts with other power management services
let conflicts = conflict::detect_conflicts();
if conflicts.has_conflicts() {
log_message(
&effective_log_level,
LogLevel::Warning,
&conflicts.get_conflict_message(),
);
}
// Create a flag that will be set to true when a signal is received // Create a flag that will be set to true when a signal is received
let running = Arc::new(AtomicBool::new(true)); let running = Arc::new(AtomicBool::new(true));
let r = running.clone(); let r = running.clone();
@ -48,7 +59,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), Box<dyn std::e
log_message( log_message(
&effective_log_level, &effective_log_level,
LogLevel::Info, LogLevel::Info,
&format!("Stats will be written to: {}", stats_path), &format!("Stats will be written to: {stats_path}"),
); );
} }
@ -78,7 +89,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), Box<dyn std::e
log_message( log_message(
&effective_log_level, &effective_log_level,
LogLevel::Error, LogLevel::Error,
&format!("Failed to write stats file: {}", e), &format!("Failed to write stats file: {e}"),
); );
} }
} }
@ -99,7 +110,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), Box<dyn std::e
log_message( log_message(
&effective_log_level, &effective_log_level,
LogLevel::Info, LogLevel::Info,
&format!("System state changed to: {:?}", current_state), &format!("System state changed to: {current_state:?}"),
); );
} }
} }
@ -107,7 +118,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), Box<dyn std::e
log_message( log_message(
&effective_log_level, &effective_log_level,
LogLevel::Error, LogLevel::Error,
&format!("Error applying system settings: {}", e), &format!("Error applying system settings: {e}"),
); );
} }
} }
@ -125,8 +136,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), Box<dyn std::e
&effective_log_level, &effective_log_level,
LogLevel::Debug, LogLevel::Debug,
&format!( &format!(
"Adaptive polling: increasing interval to {}s", "Adaptive polling: increasing interval to {current_poll_interval}s"
current_poll_interval
), ),
); );
} else if time_since_change < 10 { } else if time_since_change < 10 {
@ -138,8 +148,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), Box<dyn std::e
&effective_log_level, &effective_log_level,
LogLevel::Debug, LogLevel::Debug,
&format!( &format!(
"Adaptive polling: decreasing interval to {}s", "Adaptive polling: decreasing interval to {current_poll_interval}s"
current_poll_interval
), ),
); );
} }
@ -151,7 +160,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), Box<dyn std::e
// If on battery and throttling is enabled, lengthen the poll interval to save power // If on battery and throttling is enabled, lengthen the poll interval to save power
if config.daemon.throttle_on_battery if config.daemon.throttle_on_battery
&& !report.batteries.is_empty() && !report.batteries.is_empty()
&& report.batteries.first().map_or(false, |b| !b.ac_connected) && report.batteries.first().is_some_and(|b| !b.ac_connected)
{ {
let battery_multiplier = 2; // Poll half as often on battery let battery_multiplier = 2; // Poll half as often on battery
current_poll_interval = (current_poll_interval * battery_multiplier) current_poll_interval = (current_poll_interval * battery_multiplier)
@ -168,7 +177,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), Box<dyn std::e
log_message( log_message(
&effective_log_level, &effective_log_level,
LogLevel::Error, LogLevel::Error,
&format!("Error collecting system report: {}", e), &format!("Error collecting system report: {e}"),
); );
} }
} }
@ -206,10 +215,10 @@ fn log_message(effective_level: &LogLevel, msg_level: LogLevel, message: &str) {
if should_log { if should_log {
match msg_level { match msg_level {
LogLevel::Error => eprintln!("ERROR: {}", message), LogLevel::Error => eprintln!("ERROR: {message}"),
LogLevel::Warning => eprintln!("WARNING: {}", message), LogLevel::Warning => eprintln!("WARNING: {message}"),
LogLevel::Info => println!("INFO: {}", message), LogLevel::Info => println!("INFO: {message}"),
LogLevel::Debug => println!("DEBUG: {}", message), LogLevel::Debug => println!("DEBUG: {message}"),
} }
} }
} }
@ -225,7 +234,7 @@ fn write_stats_file(path: &str, report: &SystemReport) -> Result<(), std::io::Er
writeln!(file, "turbo={:?}", report.cpu_global.turbo_status)?; writeln!(file, "turbo={:?}", report.cpu_global.turbo_status)?;
if let Some(temp) = report.cpu_global.average_temperature_celsius { if let Some(temp) = report.cpu_global.average_temperature_celsius {
writeln!(file, "cpu_temp={:.1}", temp)?; writeln!(file, "cpu_temp={temp:.1}")?;
} }
// Battery info // Battery info
@ -233,7 +242,7 @@ fn write_stats_file(path: &str, report: &SystemReport) -> Result<(), std::io::Er
let battery = &report.batteries[0]; let battery = &report.batteries[0];
writeln!(file, "ac_power={}", battery.ac_connected)?; writeln!(file, "ac_power={}", battery.ac_connected)?;
if let Some(cap) = battery.capacity_percent { if let Some(cap) = battery.capacity_percent {
writeln!(file, "battery_percent={}", cap)?; writeln!(file, "battery_percent={cap}")?;
} }
} }
@ -263,12 +272,13 @@ fn determine_system_state(report: &SystemReport) -> SystemState {
if let Some(battery) = report.batteries.first() { if let Some(battery) = report.batteries.first() {
if battery.ac_connected { if battery.ac_connected {
return SystemState::OnAC; return SystemState::OnAC;
} else { }
return SystemState::OnBattery; return SystemState::OnBattery;
} }
} }
} else {
// No batteries means desktop, so always AC // No batteries means desktop, so always AC
if report.batteries.is_empty() {
return SystemState::OnAC; return SystemState::OnAC;
} }
@ -283,7 +293,8 @@ fn determine_system_state(report: &SystemReport) -> SystemState {
let avg_load = report.system_load.load_avg_1min; let avg_load = report.system_load.load_avg_1min;
if avg_load > 3.0 { if avg_load > 3.0 {
return SystemState::HighLoad; return SystemState::HighLoad;
} else if avg_load < 0.5 { }
if avg_load < 0.5 {
return SystemState::LowLoad; return SystemState::LowLoad;
} }

View file

@ -10,15 +10,15 @@ pub enum EngineError {
impl From<ControlError> for EngineError { impl From<ControlError> for EngineError {
fn from(err: ControlError) -> Self { fn from(err: ControlError) -> Self {
EngineError::ControlError(err) Self::ControlError(err)
} }
} }
impl std::fmt::Display for EngineError { impl std::fmt::Display for EngineError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
EngineError::ControlError(e) => write!(f, "CPU control error: {}", e), Self::ControlError(e) => write!(f, "CPU control error: {e}"),
EngineError::ConfigurationError(s) => write!(f, "Configuration error: {}", s), Self::ConfigurationError(s) => write!(f, "Configuration error: {s}"),
} }
} }
} }
@ -26,8 +26,8 @@ impl std::fmt::Display for EngineError {
impl std::error::Error for EngineError { impl std::error::Error for EngineError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self { match self {
EngineError::ControlError(e) => Some(e), Self::ControlError(e) => Some(e),
EngineError::ConfigurationError(_) => None, Self::ConfigurationError(_) => None,
} }
} }
} }
@ -39,6 +39,15 @@ pub fn determine_and_apply_settings(
config: &AppConfig, config: &AppConfig,
force_mode: Option<OperationalMode>, force_mode: Option<OperationalMode>,
) -> Result<(), EngineError> { ) -> Result<(), EngineError> {
// First, check if there's a governor override set
if let Some(override_governor) = cpu::get_governor_override() {
println!(
"Engine: Governor override is active: '{}'. Setting governor.",
override_governor.trim()
);
cpu::set_governor(override_governor.trim(), None)?;
}
let selected_profile_config: &ProfileConfig; let selected_profile_config: &ProfileConfig;
if let Some(mode) = force_mode { if let Some(mode) = force_mode {
@ -58,7 +67,7 @@ pub fn determine_and_apply_settings(
// Otherwise, check the ac_connected status from the (first) battery. // Otherwise, check the ac_connected status from the (first) battery.
// XXX: This relies on the setting ac_connected in BatteryInfo being set correctly. // XXX: This relies on the setting ac_connected in BatteryInfo being set correctly.
let on_ac_power = report.batteries.is_empty() let on_ac_power = report.batteries.is_empty()
|| report.batteries.first().map_or(false, |b| b.ac_connected); || report.batteries.first().is_some_and(|b| b.ac_connected);
if on_ac_power { if on_ac_power {
println!("Engine: On AC power, selecting Charger profile."); println!("Engine: On AC power, selecting Charger profile.");
@ -74,12 +83,12 @@ pub fn determine_and_apply_settings(
// and we'd like to replace them with proper logging in the future. // and we'd like to replace them with proper logging in the future.
if let Some(governor) = &selected_profile_config.governor { if let Some(governor) = &selected_profile_config.governor {
println!("Engine: Setting governor to '{}'", governor); println!("Engine: Setting governor to '{governor}'");
cpu::set_governor(governor, None)?; cpu::set_governor(governor, None)?;
} }
if let Some(turbo_setting) = selected_profile_config.turbo { if let Some(turbo_setting) = selected_profile_config.turbo {
println!("Engine: Setting turbo to '{:?}'", turbo_setting); println!("Engine: Setting turbo to '{turbo_setting:?}'");
match turbo_setting { match turbo_setting {
TurboSetting::Auto => { TurboSetting::Auto => {
println!("Engine: Managing turbo in auto mode based on system conditions"); println!("Engine: Managing turbo in auto mode based on system conditions");
@ -90,27 +99,27 @@ pub fn determine_and_apply_settings(
} }
if let Some(epp) = &selected_profile_config.epp { if let Some(epp) = &selected_profile_config.epp {
println!("Engine: Setting EPP to '{}'", epp); println!("Engine: Setting EPP to '{epp}'");
cpu::set_epp(epp, None)?; cpu::set_epp(epp, None)?;
} }
if let Some(epb) = &selected_profile_config.epb { if let Some(epb) = &selected_profile_config.epb {
println!("Engine: Setting EPB to '{}'", epb); println!("Engine: Setting EPB to '{epb}'");
cpu::set_epb(epb, None)?; cpu::set_epb(epb, None)?;
} }
if let Some(min_freq) = selected_profile_config.min_freq_mhz { if let Some(min_freq) = selected_profile_config.min_freq_mhz {
println!("Engine: Setting min frequency to '{} MHz'", min_freq); println!("Engine: Setting min frequency to '{min_freq} MHz'");
cpu::set_min_frequency(min_freq, None)?; cpu::set_min_frequency(min_freq, None)?;
} }
if let Some(max_freq) = selected_profile_config.max_freq_mhz { if let Some(max_freq) = selected_profile_config.max_freq_mhz {
println!("Engine: Setting max frequency to '{} MHz'", max_freq); println!("Engine: Setting max frequency to '{max_freq} MHz'");
cpu::set_max_frequency(max_freq, None)?; cpu::set_max_frequency(max_freq, None)?;
} }
if let Some(profile) = &selected_profile_config.platform_profile { if let Some(profile) = &selected_profile_config.platform_profile {
println!("Engine: Setting platform profile to '{}'", profile); println!("Engine: Setting platform profile to '{profile}'");
cpu::set_platform_profile(profile)?; cpu::set_platform_profile(profile)?;
} }
@ -127,7 +136,9 @@ fn manage_auto_turbo(report: &SystemReport, config: &ProfileConfig) -> Result<()
let cpu_temp = report.cpu_global.average_temperature_celsius; let cpu_temp = report.cpu_global.average_temperature_celsius;
// Check if we have CPU usage data available // Check if we have CPU usage data available
let avg_cpu_usage = if !report.cpu_cores.is_empty() { let avg_cpu_usage = if report.cpu_cores.is_empty() {
None
} else {
let sum: f32 = report let sum: f32 = report
.cpu_cores .cpu_cores
.iter() .iter()
@ -144,8 +155,6 @@ fn manage_auto_turbo(report: &SystemReport, config: &ProfileConfig) -> Result<()
} else { } else {
None None
} }
} else {
None
}; };
// Decision logic for enabling/disabling turbo // Decision logic for enabling/disabling turbo
@ -190,7 +199,7 @@ fn manage_auto_turbo(report: &SystemReport, config: &ProfileConfig) -> Result<()
}; };
match cpu::set_turbo(turbo_setting) { match cpu::set_turbo(turbo_setting) {
Ok(_) => { Ok(()) => {
println!( println!(
"Engine: Auto Turbo: Successfully set turbo to {}", "Engine: Auto Turbo: Successfully set turbo to {}",
if enable_turbo { "enabled" } else { "disabled" } if enable_turbo { "enabled" } else { "disabled" }

View file

@ -1,4 +1,5 @@
mod config; mod config;
mod conflict;
mod core; mod core;
mod cpu; mod cpu;
mod daemon; mod daemon;
@ -6,7 +7,7 @@ mod engine;
mod monitor; mod monitor;
use crate::config::AppConfig; use crate::config::AppConfig;
use crate::core::TurboSetting; use crate::core::{GovernorOverrideMode, TurboSetting};
use clap::Parser; use clap::Parser;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@ -31,6 +32,12 @@ enum Commands {
#[clap(long)] #[clap(long)]
core_id: Option<u32>, core_id: Option<u32>,
}, },
/// Force a specific governor mode persistently
ForceGovernor {
/// Mode to force: performance, powersave, or reset
#[clap(value_enum)]
mode: GovernorOverrideMode,
},
/// Set turbo boost behavior /// Set turbo boost behavior
SetTurbo { SetTurbo {
#[clap(value_enum)] #[clap(value_enum)]
@ -72,7 +79,7 @@ fn main() {
let config = match config::load_config() { let config = match config::load_config() {
Ok(cfg) => cfg, Ok(cfg) => cfg,
Err(e) => { Err(e) => {
eprintln!("Error loading configuration: {}. Using default values.", e); eprintln!("Error loading configuration: {e}. Using default values.");
// Proceed with default config if loading fails, as per previous steps // Proceed with default config if loading fails, as per previous steps
AppConfig::default() AppConfig::default()
} }
@ -104,7 +111,7 @@ fn main() {
"Average CPU Temperature: {}", "Average CPU Temperature: {}",
report.cpu_global.average_temperature_celsius.map_or_else( report.cpu_global.average_temperature_celsius.map_or_else(
|| "N/A (CPU temperature sensor not detected)".to_string(), || "N/A (CPU temperature sensor not detected)".to_string(),
|t| format!("{:.1}°C", t) |t| format!("{t:.1}°C")
) )
); );
@ -124,10 +131,10 @@ fn main() {
.map_or_else(|| "N/A".to_string(), |f| f.to_string()), .map_or_else(|| "N/A".to_string(), |f| f.to_string()),
core_info core_info
.usage_percent .usage_percent
.map_or_else(|| "N/A".to_string(), |f| format!("{:.1}", f)), .map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}")),
core_info core_info
.temperature_celsius .temperature_celsius
.map_or_else(|| "N/A".to_string(), |f| format!("{:.1}", f)) .map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}"))
); );
} }
@ -146,7 +153,7 @@ fn main() {
.map_or_else(|| "N/A".to_string(), |c| c.to_string()), .map_or_else(|| "N/A".to_string(), |c| c.to_string()),
battery_info battery_info
.power_rate_watts .power_rate_watts
.map_or_else(|| "N/A".to_string(), |p| format!("{:.2}", p)), .map_or_else(|| "N/A".to_string(), |p| format!("{p:.2}")),
battery_info battery_info
.charge_start_threshold .charge_start_threshold
.map_or_else(|| "N/A".to_string(), |t| t.to_string()), .map_or_else(|| "N/A".to_string(), |t| t.to_string()),
@ -170,6 +177,9 @@ fn main() {
}, },
Some(Commands::SetGovernor { governor, core_id }) => cpu::set_governor(&governor, core_id) Some(Commands::SetGovernor { governor, core_id }) => cpu::set_governor(&governor, core_id)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>), .map_err(|e| Box::new(e) as Box<dyn std::error::Error>),
Some(Commands::ForceGovernor { mode }) => {
cpu::force_governor(mode).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
}
Some(Commands::SetTurbo { setting }) => { Some(Commands::SetTurbo { setting }) => {
cpu::set_turbo(setting).map_err(|e| Box::new(e) as Box<dyn std::error::Error>) cpu::set_turbo(setting).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
} }
@ -192,15 +202,15 @@ fn main() {
Some(Commands::Daemon { verbose }) => daemon::run_daemon(config, verbose), Some(Commands::Daemon { verbose }) => daemon::run_daemon(config, verbose),
None => { None => {
println!("Welcome to superfreq! Use --help for commands."); println!("Welcome to superfreq! Use --help for commands.");
println!("Current effective configuration: {:?}", config); println!("Current effective configuration: {config:?}");
Ok(()) Ok(())
} }
}; };
if let Err(e) = command_result { if let Err(e) = command_result {
eprintln!("Error executing command: {}", e); eprintln!("Error executing command: {e}");
if let Some(source) = e.source() { if let Some(source) = e.source() {
eprintln!("Caused by: {}", source); eprintln!("Caused by: {source}");
} }
// TODO: Consider specific error handling for PermissionDenied from cpu here // TODO: Consider specific error handling for PermissionDenied from cpu here
// For example, check if e.downcast_ref::<cpu::ControlError>() matches PermissionDenied // For example, check if e.downcast_ref::<cpu::ControlError>() matches PermissionDenied

View file

@ -20,21 +20,21 @@ pub enum SysMonitorError {
} }
impl From<io::Error> for SysMonitorError { impl From<io::Error> for SysMonitorError {
fn from(err: io::Error) -> SysMonitorError { fn from(err: io::Error) -> Self {
SysMonitorError::Io(err) Self::Io(err)
} }
} }
impl std::fmt::Display for SysMonitorError { impl std::fmt::Display for SysMonitorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
SysMonitorError::Io(e) => write!(f, "I/O error: {}", e), Self::Io(e) => write!(f, "I/O error: {e}"),
SysMonitorError::ReadError(s) => write!(f, "Failed to read sysfs path: {}", s), Self::ReadError(s) => write!(f, "Failed to read sysfs path: {s}"),
SysMonitorError::ParseError(s) => write!(f, "Failed to parse value: {}", s), Self::ParseError(s) => write!(f, "Failed to parse value: {s}"),
SysMonitorError::ProcStatParseError(s) => { Self::ProcStatParseError(s) => {
write!(f, "Failed to parse /proc/stat: {}", s) write!(f, "Failed to parse /proc/stat: {s}")
} }
SysMonitorError::NotAvailable(s) => write!(f, "Information not available: {}", s), Self::NotAvailable(s) => write!(f, "Information not available: {s}"),
} }
} }
} }
@ -123,7 +123,7 @@ fn get_logical_core_count() -> Result<u32> {
// Check if it's a directory representing a core that can have cpufreq // Check if it's a directory representing a core that can have cpufreq
if entry.path().join("cpufreq").exists() { if entry.path().join("cpufreq").exists() {
count += 1; count += 1;
} else if Path::new(&format!("/sys/devices/system/cpu/{}/online", name_str)) } else if Path::new(&format!("/sys/devices/system/cpu/{name_str}/online"))
.exists() .exists()
{ {
// Fallback for cores that might not have cpufreq but are online (e.g. E-cores on some setups before driver loads) // Fallback for cores that might not have cpufreq but are online (e.g. E-cores on some setups before driver loads)
@ -159,7 +159,7 @@ struct CpuTimes {
} }
impl CpuTimes { impl CpuTimes {
fn total_time(&self) -> u64 { const fn total_time(&self) -> u64 {
self.user self.user
+ self.nice + self.nice
+ self.system + self.system
@ -170,7 +170,7 @@ impl CpuTimes {
+ self.steal + self.steal
} }
fn idle_time(&self) -> u64 { const fn idle_time(&self) -> u64 {
self.idle + self.iowait self.idle + self.iowait
} }
} }
@ -180,20 +180,18 @@ fn read_all_cpu_times() -> Result<HashMap<u32, CpuTimes>> {
let mut cpu_times_map = HashMap::new(); let mut cpu_times_map = HashMap::new();
for line in content.lines() { for line in content.lines() {
if line.starts_with("cpu") && line.chars().nth(3).map_or(false, |c| c.is_digit(10)) { if line.starts_with("cpu") && line.chars().nth(3).is_some_and(|c| c.is_ascii_digit()) {
let parts: Vec<&str> = line.split_whitespace().collect(); let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 11 { if parts.len() < 11 {
return Err(SysMonitorError::ProcStatParseError(format!( return Err(SysMonitorError::ProcStatParseError(format!(
"Line too short: {}", "Line too short: {line}"
line
))); )));
} }
let core_id_str = &parts[0][3..]; let core_id_str = &parts[0][3..];
let core_id = core_id_str.parse::<u32>().map_err(|_| { let core_id = core_id_str.parse::<u32>().map_err(|_| {
SysMonitorError::ProcStatParseError(format!( SysMonitorError::ProcStatParseError(format!(
"Failed to parse core_id: {}", "Failed to parse core_id: {core_id_str}"
core_id_str
)) ))
})?; })?;
@ -270,7 +268,7 @@ pub fn get_cpu_core_info(
prev_times: &CpuTimes, prev_times: &CpuTimes,
current_times: &CpuTimes, current_times: &CpuTimes,
) -> Result<CpuCoreInfo> { ) -> Result<CpuCoreInfo> {
let cpufreq_path = PathBuf::from(format!("/sys/devices/system/cpu/cpu{}/cpufreq/", core_id)); let cpufreq_path = PathBuf::from(format!("/sys/devices/system/cpu/cpu{core_id}/cpufreq/"));
let current_frequency_mhz = read_sysfs_value::<u32>(cpufreq_path.join("scaling_cur_freq")) let current_frequency_mhz = read_sysfs_value::<u32>(cpufreq_path.join("scaling_cur_freq"))
.map(|khz| khz / 1000) .map(|khz| khz / 1000)
@ -405,21 +403,21 @@ pub fn get_cpu_core_info(
fn get_temperature_for_core(hw_path: &Path, core_id: u32, label_prefix: &str) -> Option<f32> { fn get_temperature_for_core(hw_path: &Path, core_id: u32, label_prefix: &str) -> Option<f32> {
for i in 1..=32 { for i in 1..=32 {
// Increased range to handle systems with many sensors // Increased range to handle systems with many sensors
let label_path = hw_path.join(format!("temp{}_label", i)); let label_path = hw_path.join(format!("temp{i}_label"));
let input_path = hw_path.join(format!("temp{}_input", i)); let input_path = hw_path.join(format!("temp{i}_input"));
if label_path.exists() && input_path.exists() { if label_path.exists() && input_path.exists() {
if let Ok(label) = read_sysfs_file_trimmed(&label_path) { if let Ok(label) = read_sysfs_file_trimmed(&label_path) {
// Match various common label formats: // Match various common label formats:
// "Core X", "core X", "Core-X", "CPU Core X", etc. // "Core X", "core X", "Core-X", "CPU Core X", etc.
let core_pattern = format!("{} {}", label_prefix, core_id); let core_pattern = format!("{label_prefix} {core_id}");
let alt_pattern = format!("{}-{}", label_prefix, core_id); let alt_pattern = format!("{label_prefix}-{core_id}");
if label.eq_ignore_ascii_case(&core_pattern) if label.eq_ignore_ascii_case(&core_pattern)
|| label.eq_ignore_ascii_case(&alt_pattern) || label.eq_ignore_ascii_case(&alt_pattern)
|| label || label
.to_lowercase() .to_lowercase()
.contains(&format!("core {}", core_id).to_lowercase()) .contains(&format!("core {core_id}").to_lowercase())
{ {
if let Ok(temp_mc) = read_sysfs_value::<i32>(&input_path) { if let Ok(temp_mc) = read_sysfs_value::<i32>(&input_path) {
return Some(temp_mc as f32 / 1000.0); return Some(temp_mc as f32 / 1000.0);
@ -434,8 +432,8 @@ fn get_temperature_for_core(hw_path: &Path, core_id: u32, label_prefix: &str) ->
// Finds generic sensor temperatures by label // Finds generic sensor temperatures by label
fn get_generic_sensor_temperature(hw_path: &Path, label_name: &str) -> Option<f32> { fn get_generic_sensor_temperature(hw_path: &Path, label_name: &str) -> Option<f32> {
for i in 1..=32 { for i in 1..=32 {
let label_path = hw_path.join(format!("temp{}_label", i)); let label_path = hw_path.join(format!("temp{i}_label"));
let input_path = hw_path.join(format!("temp{}_input", i)); let input_path = hw_path.join(format!("temp{i}_input"));
if label_path.exists() && input_path.exists() { if label_path.exists() && input_path.exists() {
if let Ok(label) = read_sysfs_file_trimmed(&label_path) { if let Ok(label) = read_sysfs_file_trimmed(&label_path) {
@ -460,7 +458,7 @@ fn get_generic_sensor_temperature(hw_path: &Path, label_name: &str) -> Option<f3
// Fallback to any temperature reading from a sensor // Fallback to any temperature reading from a sensor
fn get_fallback_temperature(hw_path: &Path) -> Option<f32> { fn get_fallback_temperature(hw_path: &Path) -> Option<f32> {
for i in 1..=32 { for i in 1..=32 {
let input_path = hw_path.join(format!("temp{}_input", i)); let input_path = hw_path.join(format!("temp{i}_input"));
if input_path.exists() { if input_path.exists() {
if let Ok(temp_mc) = read_sysfs_value::<i32>(&input_path) { if let Ok(temp_mc) = read_sysfs_value::<i32>(&input_path) {
@ -488,12 +486,12 @@ pub fn get_all_cpu_core_info() -> Result<Vec<CpuCoreInfo>> {
Ok(info) => core_infos.push(info), Ok(info) => core_infos.push(info),
Err(e) => { Err(e) => {
// Log or handle error for a single core, maybe push a partial info or skip // Log or handle error for a single core, maybe push a partial info or skip
eprintln!("Error getting info for core {}: {}", core_id, e); eprintln!("Error getting info for core {core_id}: {e}");
} }
} }
} else { } else {
// Log or handle missing times for a core // Log or handle missing times for a core
eprintln!("Missing CPU time data for core {}", core_id); eprintln!("Missing CPU time data for core {core_id}");
} }
} }
Ok(core_infos) Ok(core_infos)
@ -511,9 +509,7 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> Result<CpuGlobalInfo> {
}; };
let available_governors = if cpufreq_base.join("scaling_available_governors").exists() { let available_governors = if cpufreq_base.join("scaling_available_governors").exists() {
read_sysfs_file_trimmed(cpufreq_base.join("scaling_available_governors")) read_sysfs_file_trimmed(cpufreq_base.join("scaling_available_governors")).map_or_else(|_| vec![], |s| s.split_whitespace().map(String::from).collect())
.map(|s| s.split_whitespace().map(String::from).collect())
.unwrap_or_else(|_| vec![])
} else { } else {
vec![] vec![]
}; };
@ -532,43 +528,46 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> Result<CpuGlobalInfo> {
None None
}; };
let epp = read_sysfs_file_trimmed(cpufreq_base.join("energy_performance_preference")).ok(); // EPP (Energy Performance Preference)
let energy_perf_pref =
read_sysfs_file_trimmed(cpufreq_base.join("energy_performance_preference")).ok();
// EPB is often an integer 0-15. Reading as string for now. // EPB (Energy Performance Bias)
let epb = read_sysfs_file_trimmed(cpufreq_base.join("energy_performance_bias")).ok(); let energy_perf_bias =
read_sysfs_file_trimmed(cpufreq_base.join("energy_performance_bias")).ok();
let platform_profile = read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile").ok(); let platform_profile = read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile").ok();
let _platform_profile_choices = let _platform_profile_choices =
read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile_choices").ok(); read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile_choices").ok();
// Calculate average CPU temperature from the core temperatures // Calculate average CPU temperature from the core temperatures
let average_temperature_celsius = if !cpu_cores.is_empty() { let average_temperature_celsius = if cpu_cores.is_empty() {
None
} else {
// Filter cores with temperature readings, then calculate average // Filter cores with temperature readings, then calculate average
let cores_with_temp: Vec<&CpuCoreInfo> = cpu_cores let cores_with_temp: Vec<&CpuCoreInfo> = cpu_cores
.iter() .iter()
.filter(|core| core.temperature_celsius.is_some()) .filter(|core| core.temperature_celsius.is_some())
.collect(); .collect();
if !cores_with_temp.is_empty() { if cores_with_temp.is_empty() {
None
} else {
// Sum up all temperatures and divide by count // Sum up all temperatures and divide by count
let sum: f32 = cores_with_temp let sum: f32 = cores_with_temp
.iter() .iter()
.map(|core| core.temperature_celsius.unwrap()) .map(|core| core.temperature_celsius.unwrap())
.sum(); .sum();
Some(sum / cores_with_temp.len() as f32) Some(sum / cores_with_temp.len() as f32)
} else {
None
} }
} else {
None
}; };
Ok(CpuGlobalInfo { Ok(CpuGlobalInfo {
current_governor, current_governor,
available_governors, available_governors,
turbo_status, turbo_status,
epp, epp: energy_perf_pref,
epb, epb: energy_perf_bias,
platform_profile, platform_profile,
average_temperature_celsius, average_temperature_celsius,
}) })
@ -584,8 +583,7 @@ pub fn get_battery_info(config: &AppConfig) -> Result<Vec<BatteryInfo>> {
let ignored_supplies = config let ignored_supplies = config
.ignored_power_supplies .ignored_power_supplies
.as_ref() .clone()
.cloned()
.unwrap_or_default(); .unwrap_or_default();
// Determine overall AC connection status // Determine overall AC connection status
@ -649,7 +647,7 @@ pub fn get_battery_info(config: &AppConfig) -> Result<Vec<BatteryInfo>> {
if let (Some(c), Some(v)) = (current_ua, voltage_uv) { if let (Some(c), Some(v)) = (current_ua, voltage_uv) {
// Power (W) = (Voltage (V) * Current (A)) // Power (W) = (Voltage (V) * Current (A))
// (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W // (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W
Some((c as f64 * v as f64 / 1_000_000_000_000.0) as f32) Some((f64::from(c) * f64::from(v) / 1_000_000_000_000.0) as f32)
} else { } else {
None None
} }