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

cpu: clean up, clean main too

This commit is contained in:
RGBCube 2025-05-18 17:26:48 +03:00
parent da07011b02
commit 87085f913b
Signed by: RGBCube
SSH key fingerprint: SHA256:CzqbPcfwt+GxFYNnFVCqoN5Itn4YFrshg1TrnACpA5M
9 changed files with 904 additions and 1054 deletions

62
Cargo.lock generated
View file

@ -95,6 +95,16 @@ dependencies = [
"clap_derive", "clap_derive",
] ]
[[package]]
name = "clap-verbosity-flag"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2678fade3b77aa3a8ff3aae87e9c008d3fb00473a41c71fbf74e91c8c7b37e84"
dependencies = [
"clap",
"log",
]
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.38" version = "4.5.38"
@ -131,6 +141,15 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "convert_case"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "ctrlc" name = "ctrlc"
version = "3.4.7" version = "3.4.7"
@ -141,6 +160,28 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "derive_more"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"syn",
"unicode-xid",
]
[[package]] [[package]]
name = "dirs" name = "dirs"
version = "6.0.0" version = "6.0.0"
@ -453,7 +494,9 @@ version = "0.3.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
"clap-verbosity-flag",
"ctrlc", "ctrlc",
"derive_more",
"dirs", "dirs",
"env_logger", "env_logger",
"jiff", "jiff",
@ -462,6 +505,7 @@ dependencies = [
"serde", "serde",
"thiserror", "thiserror",
"toml", "toml",
"yansi",
] ]
[[package]] [[package]]
@ -542,6 +586,18 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]] [[package]]
name = "utf8parse" name = "utf8parse"
version = "0.2.2" version = "0.2.2"
@ -635,3 +691,9 @@ checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"

View file

@ -18,3 +18,6 @@ env_logger = "0.11"
thiserror = "2.0" thiserror = "2.0"
anyhow = "1.0" anyhow = "1.0"
jiff = "0.2.13" jiff = "0.2.13"
clap-verbosity-flag = "3.0.2"
yansi = "1.0.1"
derive_more = { version = "2.0.1", features = ["full"] }

View file

@ -1,5 +1,4 @@
use crate::{config::types::BatteryChargeThresholds, util::error::ControlError, util::sysfs}; use crate::{config::types::BatteryChargeThresholds, util::error::ControlError, util::sysfs};
use log::{debug, warn};
use std::{ use std::{
fs, io, fs, io,
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -118,7 +117,7 @@ fn find_supported_batteries(power_supply_path: &Path) -> Result<Vec<SupportedBat
let entry = match entry { let entry = match entry {
Ok(e) => e, Ok(e) => e,
Err(e) => { Err(e) => {
warn!("Failed to read power-supply entry: {e}"); log::warn!("Failed to read power-supply entry: {e}");
continue; continue;
} }
}; };
@ -131,16 +130,17 @@ fn find_supported_batteries(power_supply_path: &Path) -> Result<Vec<SupportedBat
} }
if supported_batteries.is_empty() { if supported_batteries.is_empty() {
warn!("No batteries with charge threshold support found"); log::warn!("No batteries with charge threshold support found");
} else { } else {
debug!( log::debug!(
"Found {} batteries with threshold support", "Found {} batteries with threshold support",
supported_batteries.len() supported_batteries.len()
); );
for battery in &supported_batteries { for battery in &supported_batteries {
debug!( log::debug!(
"Battery '{}' supports {} threshold control", "Battery '{}' supports {} threshold control",
battery.name, battery.pattern.description battery.name,
battery.pattern.description
); );
} }
} }
@ -173,9 +173,12 @@ fn apply_thresholds_to_batteries(
match start_result { match start_result {
Ok(()) => { Ok(()) => {
debug!( log::debug!(
"Set {}-{}% charge thresholds for {} battery '{}'", "Set {}-{}% charge thresholds for {} battery '{}'",
start_threshold, stop_threshold, battery.pattern.description, battery.name start_threshold,
stop_threshold,
battery.pattern.description,
battery.name
); );
success_count += 1; success_count += 1;
} }
@ -184,14 +187,16 @@ fn apply_thresholds_to_batteries(
if let Some(prev_stop) = &current_stop { if let Some(prev_stop) = &current_stop {
let restore_result = sysfs::write_sysfs_value(&stop_path, prev_stop); let restore_result = sysfs::write_sysfs_value(&stop_path, prev_stop);
if let Err(re) = restore_result { if let Err(re) = restore_result {
warn!( log::warn!(
"Failed to restore previous stop threshold for battery '{}': {}. Battery may be in an inconsistent state.", "Failed to restore previous stop threshold for battery '{}': {}. Battery may be in an inconsistent state.",
battery.name, re battery.name,
re
); );
} else { } else {
debug!( log::debug!(
"Restored previous stop threshold ({}) for battery '{}'", "Restored previous stop threshold ({}) for battery '{}'",
prev_stop, battery.name prev_stop,
battery.name
); );
} }
} }
@ -212,7 +217,7 @@ fn apply_thresholds_to_batteries(
if success_count > 0 { if success_count > 0 {
if !errors.is_empty() { if !errors.is_empty() {
warn!( log::warn!(
"Partial success setting battery thresholds: {}", "Partial success setting battery thresholds: {}",
errors.join("; ") errors.join("; ")
); );

View file

@ -1,31 +1,3 @@
use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, 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,

View file

@ -1,479 +1,321 @@
use crate::core::{GovernorOverrideMode, TurboSetting}; use anyhow::{Context, bail};
use crate::util::error::ControlError; use derive_more::Display;
use core::str; use serde::{Deserialize, Serialize};
use log::debug;
use std::{fs, io, path::Path, string::ToString}; use std::{fs, io, path::Path, string::ToString};
pub type Result<T, E = ControlError> = std::result::Result<T, E>; // // Valid EPP (Energy Performance Preference) string values.
// const EPP_FALLBACK_VALUES: &[&str] = &[
// "default",
// "performance",
// "balance-performance",
// "balance_performance", // Alternative form with underscore.
// "balance-power",
// "balance_power", // Alternative form with underscore.
// "power",
// ];
// Valid EPB string values fn exists(path: impl AsRef<Path>) -> bool {
const VALID_EPB_STRINGS: &[&str] = &[ let path = path.as_ref();
"performance",
"balance-performance",
"balance_performance", // alternative form
"balance-power",
"balance_power", // alternative form
"power",
];
// EPP (Energy Performance Preference) string values path.exists()
const EPP_FALLBACK_VALUES: &[&str] = &[ }
"default",
"performance",
"balance-performance",
"balance_performance", // alternative form with underscore
"balance-power",
"balance_power", // alternative form with underscore
"power",
];
// Write a value to a sysfs file // Not doing any anyhow stuff here as all the calls of this ignore errors.
fn write_sysfs_value(path: impl AsRef<Path>, value: &str) -> Result<()> { fn read_u64(path: impl AsRef<Path>) -> anyhow::Result<u64> {
let p = path.as_ref(); let path = path.as_ref();
fs::write(p, value).map_err(|e| { let content = fs::read_to_string(path)?;
let error_msg = format!("Path: {:?}, Value: '{}', Error: {}", p.display(), value, e);
match e.kind() { Ok(content.trim().parse::<u64>()?)
io::ErrorKind::PermissionDenied => ControlError::PermissionDenied(error_msg), }
io::ErrorKind::NotFound => {
ControlError::PathMissing(format!("Path '{}' does not exist", p.display())) fn write(path: impl AsRef<Path>, value: &str) -> anyhow::Result<()> {
} let path = path.as_ref();
_ => ControlError::WriteError(error_msg),
} fs::write(path, value).with_context(|| {
format!(
"failed to write '{value}' to '{path}'",
path = path.display(),
)
}) })
} }
pub fn get_logical_core_count() -> Result<u32> { /// Get real, tunable CPUs.
// Using num_cpus::get() for a reliable count of logical cores accessible. pub fn get_real_cpus() -> anyhow::Result<Vec<u32>> {
// The monitor module's get_logical_core_count might be more specific to cpufreq-capable cores, const PATH: &str = "/sys/devices/system/cpu";
// but for applying settings, we might want to iterate over all reported by OS.
// However, settings usually apply to cores with cpufreq.
// Let's use a similar discovery to monitor's get_logical_core_count
let mut num_cores: u32 = 0;
let path = Path::new("/sys/devices/system/cpu");
if !path.exists() {
return Err(ControlError::NotSupported(format!(
"No logical cores found at {}.",
path.display()
)));
}
let entries = fs::read_dir(path) let mut cpus = vec![];
.map_err(|_| {
ControlError::PermissionDenied(format!("Cannot read contents of {}.", path.display()))
})?
.flatten();
for entry in entries { for entry in fs::read_dir(PATH)
.with_context(|| format!("failed to read contents of '{PATH}'"))?
.flatten()
{
let entry_file_name = entry.file_name(); let entry_file_name = entry.file_name();
let Some(name) = entry_file_name.to_str() else { let Some(name) = entry_file_name.to_str() else {
continue; continue;
}; };
// Skip non-CPU directories (e.g., cpuidle, cpufreq) let Some(cpu_prefix_removed) = name.strip_prefix("cpu") else {
if !name.starts_with("cpu") || name.len() <= 3 || !name[3..].chars().all(char::is_numeric) {
continue; continue;
} };
// Has to match "cpu{N}".
let Ok(cpu) = cpu_prefix_removed.parse::<u32>() else {
continue;
};
// Has to match "cpu{N}/cpufreq".
if !entry.path().join("cpufreq").exists() { if !entry.path().join("cpufreq").exists() {
continue; continue;
} }
if name[3..].parse::<u32>().is_ok() { cpus.push(cpu);
num_cores += 1;
}
}
if num_cores == 0 {
// Fallback if sysfs iteration above fails to find any cpufreq cores
num_cores = num_cpus::get() as u32;
} }
Ok(num_cores) // Fall back if sysfs iteration above fails to find any cpufreq CPUs.
if cpus.is_empty() {
cpus = (0..num_cpus::get() as u32).collect();
}
Ok(cpus)
} }
fn for_each_cpu_core<F>(mut action: F) -> Result<()> /// Set the governor for a CPU.
where pub fn set_governor(governor: &str, cpu: u32) -> anyhow::Result<()> {
F: FnMut(u32) -> Result<()>, let governors = get_available_governors_for(cpu);
{
let num_cores: u32 = get_logical_core_count()?;
for core_id in 0u32..num_cores { if !governors
action(core_id)?; .iter()
.any(|avail_governor| avail_governor == governor)
{
bail!(
"governor '{governor}' is not available for CPU {cpu}. valid governors: {governors}",
governors = governors.join(", "),
);
} }
Ok(())
write(
format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_governor"),
governor,
)
.with_context(|| {
format!(
"this probably means that CPU {cpu} doesn't exist or doesn't support changing governors"
)
})
} }
pub fn set_governor(governor: &str, core_id: Option<u32>) -> Result<()> { /// Get available CPU governors for a CPU.
// Validate the governor is available on this system fn get_available_governors_for(cpu: u32) -> Vec<String> {
// This returns both the validation result and the list of available governors let Ok(content) = fs::read_to_string(format!(
let (is_valid, available_governors) = is_governor_valid(governor)?; "/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_available_governors"
)) else {
if !is_valid { return Vec::new();
return Err(ControlError::InvalidGovernor(format!(
"Governor '{}' is not available on this system. Valid governors: {}",
governor,
available_governors.join(", ")
)));
}
let action = |id: u32| {
let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/scaling_governor");
if Path::new(&path).exists() {
write_sysfs_value(&path, governor)
} else {
// Silently ignore if the path doesn't exist for a specific core,
// as not all cores might have cpufreq (e.g. offline cores)
Ok(())
}
}; };
core_id.map_or_else(|| for_each_cpu_core(action), action) content
.split_whitespace()
.map(ToString::to_string)
.collect()
} }
/// Check if the provided governor is available in the system #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, clap::ValueEnum)]
/// Returns a tuple of (`is_valid`, `available_governors`) to avoid redundant file reads pub enum Turbo {
fn is_governor_valid(governor: &str) -> Result<(bool, Vec<String>)> { Always,
let governors = get_available_governors()?; Never,
// Convert input governor to lowercase for case-insensitive comparison
let governor_lower = governor.to_lowercase();
// Convert all available governors to lowercase for comparison
let governors_lower: Vec<String> = governors.iter().map(|g| g.to_lowercase()).collect();
// Check if the lowercase governor is in the lowercase list
Ok((governors_lower.contains(&governor_lower), governors))
} }
/// Get available CPU governors from the system pub fn set_turbo(setting: Turbo) -> anyhow::Result<()> {
fn get_available_governors() -> Result<Vec<String>> {
let cpu_base_path = Path::new("/sys/devices/system/cpu");
// First try the traditional path with cpu0. This is the most common case
// and will usually catch early, but we should try to keep the code to handle
// "edge" cases lightweight, for the (albeit smaller) number of users that
// run Superfreq on unusual systems.
let cpu0_path = "/sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors";
if Path::new(cpu0_path).exists() {
let content = fs::read_to_string(cpu0_path).map_err(|e| {
ControlError::ReadError(format!("Failed to read available governors from cpu0: {e}"))
})?;
let governors: Vec<String> = content
.split_whitespace()
.map(ToString::to_string)
.collect();
if !governors.is_empty() {
return Ok(governors);
}
}
// If cpu0 doesn't have the file or it's empty, scan all CPUs
// This handles heterogeneous systems where cpu0 might not have cpufreq
if let Ok(entries) = fs::read_dir(cpu_base_path) {
for entry in entries.flatten() {
let path = entry.path();
let file_name = entry.file_name();
let name = match file_name.to_str() {
Some(name) => name,
None => continue,
};
// Skip non-CPU directories
if !name.starts_with("cpu")
|| name.len() <= 3
|| !name[3..].chars().all(char::is_numeric)
{
continue;
}
let governor_path = path.join("cpufreq/scaling_available_governors");
if governor_path.exists() {
match fs::read_to_string(&governor_path) {
Ok(content) => {
let governors: Vec<String> = content
.split_whitespace()
.map(ToString::to_string)
.collect();
if !governors.is_empty() {
return Ok(governors);
}
}
Err(_) => continue, // try next CPU if this one fails
}
}
}
}
// If we get here, we couldn't find any valid governors list
Err(ControlError::NotSupported(
"Could not determine available governors on any CPU".to_string(),
))
}
pub fn set_turbo(setting: TurboSetting) -> Result<()> {
let value_pstate = match setting {
TurboSetting::Always => "0", // no_turbo = 0 means turbo is enabled
TurboSetting::Never => "1", // no_turbo = 1 means turbo is disabled
// Auto mode is handled at the engine level, not directly at the sysfs level
TurboSetting::Auto => {
debug!("Turbo Auto mode is managed by engine logic based on system conditions");
return Ok(());
}
};
let value_boost = match setting { let value_boost = match setting {
TurboSetting::Always => "1", // boost = 1 means turbo is enabled Turbo::Always => "1", // boost = 1 means turbo is enabled.
TurboSetting::Never => "0", // boost = 0 means turbo is disabled Turbo::Never => "0", // boost = 0 means turbo is disabled.
TurboSetting::Auto => { };
debug!("Turbo Auto mode is managed by engine logic based on system conditions");
return Ok(()); let value_boost_negated = match setting {
} Turbo::Always => "0", // no_turbo = 0 means turbo is enabled.
Turbo::Never => "1", // no_turbo = 1 means turbo is disabled.
}; };
// AMD specific paths // AMD specific paths
let amd_pstate_path = "/sys/devices/system/cpu/amd_pstate/cpufreq/boost"; let amd_boost_path = "/sys/devices/system/cpu/amd_pstate/cpufreq/boost";
let msr_boost_path = "/sys/devices/system/cpu/cpufreq/amd_pstate_enable_boost"; let msr_boost_path = "/sys/devices/system/cpu/cpufreq/amd_pstate_enable_boost";
// Path priority (from most to least specific) // Path priority (from most to least specific)
let pstate_path = "/sys/devices/system/cpu/intel_pstate/no_turbo"; let intel_boost_path_negated = "/sys/devices/system/cpu/intel_pstate/no_turbo";
let boost_path = "/sys/devices/system/cpu/cpufreq/boost"; let generic_boost_path = "/sys/devices/system/cpu/cpufreq/boost";
// Try each boost control path in order of specificity // Try each boost control path in order of specificity
if Path::new(pstate_path).exists() { if write(intel_boost_path_negated, value_boost_negated).is_ok() {
write_sysfs_value(pstate_path, value_pstate) return Ok(());
} else if Path::new(amd_pstate_path).exists() {
write_sysfs_value(amd_pstate_path, value_boost)
} else if Path::new(msr_boost_path).exists() {
write_sysfs_value(msr_boost_path, value_boost)
} else if Path::new(boost_path).exists() {
write_sysfs_value(boost_path, value_boost)
} else {
// Also try per-core cpufreq boost for some AMD systems
let result = try_set_per_core_boost(value_boost)?;
if result {
Ok(())
} else {
Err(ControlError::NotSupported(
"No supported CPU boost control mechanism found.".to_string(),
))
}
} }
} if write(amd_boost_path, value_boost).is_ok() {
return Ok(());
/// Try to set boost on a per-core basis for systems that support it }
fn try_set_per_core_boost(value: &str) -> Result<bool> { if write(msr_boost_path, value_boost).is_ok() {
let mut success = false; return Ok(());
let num_cores = get_logical_core_count()?; }
if write(generic_boost_path, value_boost).is_ok() {
for core_id in 0..num_cores { return Ok(());
let boost_path = format!("/sys/devices/system/cpu/cpu{core_id}/cpufreq/boost");
if Path::new(&boost_path).exists() {
write_sysfs_value(&boost_path, value)?;
success = true;
}
} }
Ok(success) // Also try per-core cpufreq boost for some AMD systems.
if get_real_cpus()?.iter().any(|cpu| {
write(
&format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/boost"),
value_boost,
)
.is_ok()
}) {
return Ok(());
}
bail!("no supported CPU boost control mechanism found");
} }
pub fn set_epp(epp: &str, core_id: Option<u32>) -> Result<()> { pub fn set_epp(epp: &str, cpu: u32) -> anyhow::Result<()> {
// Validate the EPP value against available options // Validate the EPP value against available options
let available_epp = get_available_epp_values()?; let epps = get_available_epps(cpu);
if !available_epp.iter().any(|v| v.eq_ignore_ascii_case(epp)) {
return Err(ControlError::InvalidValueError(format!( if !epps.iter().any(|avail_epp| avail_epp == epp) {
"Invalid EPP value: '{}'. Available values: {}", bail!(
epp, "epp value '{epp}' is not availabile for CPU {cpu}. valid epp values: {epps}",
available_epp.join(", ") epps = epps.join(", "),
))); );
} }
let action = |id: u32| { write(
let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/energy_performance_preference"); format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/energy_performance_preference"),
if Path::new(&path).exists() { epp,
write_sysfs_value(&path, epp) )
} else { .with_context(|| {
Ok(()) format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing EPP")
} })
};
core_id.map_or_else(|| for_each_cpu_core(action), action)
} }
/// Get available EPP values from the system /// Get available EPP values for a CPU.
fn get_available_epp_values() -> Result<Vec<String>> { fn get_available_epps(cpu: u32) -> Vec<String> {
let path = "/sys/devices/system/cpu/cpu0/cpufreq/energy_performance_available_preferences"; let Ok(content) = fs::read_to_string(format!(
"/sys/devices/system/cpu/cpu{cpu}/cpufreq/energy_performance_available_preferences"
)) else {
return Vec::new();
};
if !Path::new(path).exists() { content
// If the file doesn't exist, fall back to a default set of common values
// This is safer than failing outright, as some systems may allow these values │
// even without explicitly listing them
return Ok(EPP_FALLBACK_VALUES.iter().map(|&s| s.to_string()).collect());
}
let content = fs::read_to_string(path).map_err(|e| {
ControlError::ReadError(format!("Failed to read available EPP values: {e}"))
})?;
Ok(content
.split_whitespace() .split_whitespace()
.map(ToString::to_string) .map(ToString::to_string)
.collect()) .collect()
} }
pub fn set_epb(epb: &str, core_id: Option<u32>) -> Result<()> { pub fn set_epb(epb: &str, cpu: u32) -> anyhow::Result<()> {
// Validate EPB value - should be a number 0-15 or a recognized string value // Validate EPB value - should be a number 0-15 or a recognized string value.
validate_epb_value(epb)?; validate_epb_value(epb)?;
let action = |id: u32| { write(
let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/energy_performance_bias"); format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/energy_performance_bias"),
if Path::new(&path).exists() { epb,
write_sysfs_value(&path, epb) )
} else { .with_context(|| {
Ok(()) format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing EPB")
} })
};
core_id.map_or_else(|| for_each_cpu_core(action), action)
} }
fn validate_epb_value(epb: &str) -> Result<()> { fn validate_epb_value(epb: &str) -> anyhow::Result<()> {
// EPB can be a number from 0-15 or a recognized string // EPB can be a number from 0-15 or a recognized string.
// Try parsing as a number first
const VALID_EPB_STRINGS: &[&str] = &[
"performance",
"balance-performance",
"balance_performance", // Alternative form with underscore.
"balance-power",
"balance_power", // Alternative form with underscore.
"power",
];
// Try parsing as a number first.
if let Ok(value) = epb.parse::<u8>() { if let Ok(value) = epb.parse::<u8>() {
if value <= 15 { if value <= 15 {
return Ok(()); return Ok(());
} }
return Err(ControlError::InvalidValueError(format!(
"EPB numeric value must be between 0 and 15, got {value}" bail!("EPB numeric value must be between 0 and 15, got {value}");
)));
} }
// If not a number, check if it's a recognized string value. // If not a number, check if it's a recognized string value.
// This is using case-insensitive comparison if VALID_EPB_STRINGS.contains(&epb) {
if VALID_EPB_STRINGS
.iter()
.any(|valid| valid.eq_ignore_ascii_case(epb))
{
Ok(())
} else {
Err(ControlError::InvalidValueError(format!(
"Invalid EPB value: '{}'. Must be a number 0-15 or one of: {}",
epb,
VALID_EPB_STRINGS.join(", ")
)))
}
}
pub fn set_min_frequency(freq_mhz: u32, core_id: Option<u32>) -> Result<()> {
// Check if the new minimum frequency would be greater than current maximum
if let Some(id) = core_id {
validate_min_frequency(id, freq_mhz)?;
} else {
// Check for all cores
let num_cores = get_logical_core_count()?;
for id in 0..num_cores {
validate_min_frequency(id, freq_mhz)?;
}
}
// XXX: We use u64 for the intermediate calculation to prevent overflow
let freq_khz = u64::from(freq_mhz) * 1000;
let freq_khz_str = freq_khz.to_string();
let action = |id: u32| {
let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/scaling_min_freq");
if Path::new(&path).exists() {
write_sysfs_value(&path, &freq_khz_str)
} else {
Ok(())
}
};
core_id.map_or_else(|| for_each_cpu_core(action), action)
}
pub fn set_max_frequency(freq_mhz: u32, core_id: Option<u32>) -> Result<()> {
// Check if the new maximum frequency would be less than current minimum
if let Some(id) = core_id {
validate_max_frequency(id, freq_mhz)?;
} else {
// Check for all cores
let num_cores = get_logical_core_count()?;
for id in 0..num_cores {
validate_max_frequency(id, freq_mhz)?;
}
}
// XXX: Use a u64 here as well.
let freq_khz = u64::from(freq_mhz) * 1000;
let freq_khz_str = freq_khz.to_string();
let action = |id: u32| {
let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/scaling_max_freq");
if Path::new(&path).exists() {
write_sysfs_value(&path, &freq_khz_str)
} else {
Ok(())
}
};
core_id.map_or_else(|| for_each_cpu_core(action), action)
}
fn read_sysfs_value_as_u32(path: &str) -> Result<u32> {
if !Path::new(path).exists() {
return Err(ControlError::NotSupported(format!(
"File does not exist: {path}"
)));
}
let content = fs::read_to_string(path)
.map_err(|e| ControlError::ReadError(format!("Failed to read {path}: {e}")))?;
content
.trim()
.parse::<u32>()
.map_err(|e| ControlError::ParseError(format!("Failed to parse value from {path}: {e}")))
}
fn validate_min_frequency(core_id: u32, new_min_freq_mhz: u32) -> Result<()> {
let max_freq_path = format!("/sys/devices/system/cpu/cpu{core_id}/cpufreq/scaling_max_freq");
if !Path::new(&max_freq_path).exists() {
return Ok(()); return Ok(());
} }
let max_freq_khz = read_sysfs_value_as_u32(&max_freq_path)?; bail!(
let new_min_freq_khz = new_min_freq_mhz * 1000; "invalid EPB value: '{epb}'. must be a number between 0-15 inclusive or one of: {valid}",
valid = VALID_EPB_STRINGS.join(", "),
);
}
if new_min_freq_khz > max_freq_khz { pub fn set_frequency_minimum(frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> {
return Err(ControlError::InvalidValueError(format!( validate_frequency_minimum(frequency_mhz, cpu)?;
"Minimum frequency ({} MHz) cannot be higher than maximum frequency ({} MHz) for core {}",
new_min_freq_mhz, // We use u64 for the intermediate calculation to prevent overflow
max_freq_khz / 1000, let frequency_khz = u64::from(frequency_mhz) * 1000;
core_id let frequency_khz = frequency_khz.to_string();
)));
write(
format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_min_freq"),
&frequency_khz,
)
.with_context(|| {
format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing minimum frequency")
})
}
pub fn set_frequency_maximum(frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> {
validate_max_frequency(frequency_mhz, cpu)?;
// We use u64 for the intermediate calculation to prevent overflow
let frequency_khz = u64::from(frequency_mhz) * 1000;
let frequency_khz = frequency_khz.to_string();
write(
format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_max_freq"),
&frequency_khz,
)
.with_context(|| {
format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing maximum frequency")
})
}
fn validate_frequency_minimum(new_frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> {
let Ok(minimum_frequency_khz) = read_u64(format!(
"/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_min_freq"
)) else {
// Just let it pass if we can't find anything.
return Ok(());
};
if new_frequency_mhz as u64 * 1000 < minimum_frequency_khz {
bail!(
"new minimum frequency ({new_frequency_mhz} MHz) cannot be lower than the minimum frequency ({} MHz) for CPU {cpu}",
minimum_frequency_khz / 1000,
);
} }
Ok(()) Ok(())
} }
fn validate_max_frequency(core_id: u32, new_max_freq_mhz: u32) -> Result<()> { fn validate_max_frequency(new_frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> {
let min_freq_path = format!("/sys/devices/system/cpu/cpu{core_id}/cpufreq/scaling_min_freq"); let Ok(maximum_frequency_khz) = read_u64(format!(
"/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_min_freq"
if !Path::new(&min_freq_path).exists() { )) else {
// Just let it pass if we can't find anything.
return Ok(()); return Ok(());
} };
let min_freq_khz = read_sysfs_value_as_u32(&min_freq_path)?; if new_frequency_mhz * 1000 > maximum_frequency_khz {
let new_max_freq_khz = new_max_freq_mhz * 1000; bail!(
"new maximum frequency ({new_frequency_mhz} MHz) cannot be higher than the maximum frequency ({} MHz) for CPU {cpu}",
if new_max_freq_khz < min_freq_khz { maximum_frequency_khz / 1000,
return Err(ControlError::InvalidValueError(format!( );
"Maximum frequency ({} MHz) cannot be lower than minimum frequency ({} MHz) for core {}",
new_max_freq_mhz,
min_freq_khz / 1000,
core_id
)));
} }
Ok(()) Ok(())
@ -485,137 +327,108 @@ fn validate_max_frequency(core_id: u32, new_max_freq_mhz: u32) -> Result<()> {
/// Also see [`The Kernel docs`] for this. /// Also see [`The Kernel docs`] for this.
/// ///
/// [`The Kernel docs`]: <https://docs.kernel.org/userspace-api/sysfs-platform_profile.html> /// [`The Kernel docs`]: <https://docs.kernel.org/userspace-api/sysfs-platform_profile.html>
/// pub fn set_platform_profile(profile: &str) -> anyhow::Result<()> {
/// # Examples let profiles = get_platform_profiles();
///
/// ``` if !profiles
/// set_platform_profile("balanced"); .iter()
/// ``` .any(|avail_profile| avail_profile == profile)
/// {
pub fn set_platform_profile(profile: &str) -> Result<()> { bail!(
let path = "/sys/firmware/acpi/platform_profile"; "profile '{profile}' is not available for system. valid profiles: {profiles}",
if !Path::new(path).exists() { profiles = profiles.join(", "),
return Err(ControlError::NotSupported(format!( );
"Platform profile control not found at {path}.",
)));
} }
let available_profiles = get_platform_profiles()?; write("/sys/firmware/acpi/platform_profile", profile)
.context("this probably means that your system does not support changing ACPI profiles")
if !available_profiles.contains(&profile.to_string()) {
return Err(ControlError::InvalidProfile(format!(
"Invalid platform control profile provided.\n\
Provided profile: {} \n\
Available profiles:\n\
{}",
profile,
available_profiles.join(", ")
)));
}
write_sysfs_value(path, profile)
} }
/// Returns the list of available platform profiles. /// Get the list of available platform profiles.
/// pub fn get_platform_profiles() -> Vec<String> {
/// # Errors
///
/// # Returns
///
/// - [`ControlError::NotSupported`] if:
/// - The file `/sys/firmware/acpi/platform_profile_choices` does not exist.
/// - The file `/sys/firmware/acpi/platform_profile_choices` is empty.
///
/// - [`ControlError::PermissionDenied`] if the file `/sys/firmware/acpi/platform_profile_choices` cannot be read.
///
pub fn get_platform_profiles() -> Result<Vec<String>> {
let path = "/sys/firmware/acpi/platform_profile_choices"; let path = "/sys/firmware/acpi/platform_profile_choices";
if !Path::new(path).exists() { let Ok(content) = fs::read_to_string(path) else {
return Err(ControlError::NotSupported(format!( return Vec::new();
"Platform profile choices not found at {path}." };
)));
}
let content = fs::read_to_string(path) content
.map_err(|_| ControlError::PermissionDenied(format!("Cannot read contents of {path}.")))?;
Ok(content
.split_whitespace() .split_whitespace()
.map(ToString::to_string) .map(ToString::to_string)
.collect()) .collect()
} }
/// Path for storing the governor override state /// Path for storing the governor override state.
const GOVERNOR_OVERRIDE_PATH: &str = "/etc/xdg/superfreq/governor_override"; const GOVERNOR_OVERRIDE_PATH: &str = "/etc/xdg/superfreq/governor_override";
/// Force a specific CPU governor or reset to automatic mode #[derive(Display, Debug, Clone, Copy, clap::ValueEnum)]
pub fn force_governor(mode: GovernorOverrideMode) -> Result<()> { pub enum GovernorOverride {
// Create directory if it doesn't exist #[display("performance")]
let dir_path = Path::new("/etc/xdg/superfreq"); Performance,
if !dir_path.exists() { #[display("powersave")]
fs::create_dir_all(dir_path).map_err(|e| { Powersave,
if e.kind() == io::ErrorKind::PermissionDenied { #[display("reset")]
ControlError::PermissionDenied(format!( Reset,
"Permission denied creating directory: {}. Try running with sudo.", }
dir_path.display()
)) pub fn set_governor_override(mode: GovernorOverride) -> anyhow::Result<()> {
} else { let parent = Path::new(GOVERNOR_OVERRIDE_PATH).parent().unwrap();
ControlError::Io(e) if !parent.exists() {
} fs::create_dir_all(parent).with_context(|| {
format!(
"failed to create directory '{path}'",
path = parent.display(),
)
})?; })?;
} }
match mode { match mode {
GovernorOverrideMode::Reset => { GovernorOverride::Reset => {
// Remove the override file if it exists // Remove the override file if it exists
if Path::new(GOVERNOR_OVERRIDE_PATH).exists() { let result = fs::remove_file(GOVERNOR_OVERRIDE_PATH);
fs::remove_file(GOVERNOR_OVERRIDE_PATH).map_err(|e| {
if e.kind() == io::ErrorKind::PermissionDenied { if let Err(error) = result {
ControlError::PermissionDenied(format!( if error.kind() != io::ErrorKind::NotFound {
"Permission denied removing override file: {GOVERNOR_OVERRIDE_PATH}. Try running with sudo." return Err(error).with_context(|| {
)) format!(
} else { "failed to delete governor override file '{GOVERNOR_OVERRIDE_PATH}'"
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 log::info!(
set_governor(&governor, None)?; "governor override has been deleted. normal profile-based settings will be used"
println!(
"Governor override set to '{governor}'. This setting will persist across reboots."
); );
println!("To reset, use: superfreq force-governor reset"); }
Ok(())
GovernorOverride::Performance | GovernorOverride::Powersave => {
let governor = mode.to_string();
write(GOVERNOR_OVERRIDE_PATH, &governor)
.context("failed to write governor override")?;
// TODO: Apply the setting too.
log::info!(
"governor override set to '{governor}'. this setting will persist across reboots"
);
log::info!("to reset, run: superfreq set --governor-persist reset");
} }
} }
Ok(())
} }
/// Get the current governor override if set /// Get the current governor override if set.
pub fn get_governor_override() -> Option<String> { pub fn get_governor_override() -> anyhow::Result<Option<String>> {
if Path::new(GOVERNOR_OVERRIDE_PATH).exists() { match fs::read_to_string(GOVERNOR_OVERRIDE_PATH) {
fs::read_to_string(GOVERNOR_OVERRIDE_PATH).ok() Ok(governor_override) => Ok(Some(governor_override)),
} else {
None Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error).with_context(|| {
format!("failed to read governor override at '{GOVERNOR_OVERRIDE_PATH}'")
}),
} }
} }

View file

@ -3,7 +3,6 @@ use crate::core::SystemReport;
use crate::engine; use crate::engine;
use crate::monitor; use crate::monitor;
use crate::util::error::{AppError, ControlError}; use crate::util::error::{AppError, ControlError};
use log::{LevelFilter, debug, error, info, warn};
use std::collections::VecDeque; use std::collections::VecDeque;
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
@ -99,7 +98,7 @@ fn compute_new(
if idle_time_seconds > 0 { if idle_time_seconds > 0 {
let idle_factor = idle_multiplier(idle_time_seconds); let idle_factor = idle_multiplier(idle_time_seconds);
debug!( log::debug!(
"System idle for {} seconds (approx. {} minutes), applying idle factor: {:.2}x", "System idle for {} seconds (approx. {} minutes), applying idle factor: {:.2}x",
idle_time_seconds, idle_time_seconds,
(idle_time_seconds as f32 / 60.0).round(), (idle_time_seconds as f32 / 60.0).round(),
@ -226,7 +225,7 @@ impl SystemHistory {
> 15.0) > 15.0)
{ {
self.last_user_activity = Instant::now(); self.last_user_activity = Instant::now();
debug!("User activity detected based on CPU usage"); log::debug!("User activity detected based on CPU usage");
} }
} }
} }
@ -245,7 +244,7 @@ impl SystemHistory {
if temp_change > 5.0 { if temp_change > 5.0 {
// 5°C rise in temperature // 5°C rise in temperature
self.last_user_activity = Instant::now(); self.last_user_activity = Instant::now();
debug!("User activity detected based on temperature change"); log::debug!("User activity detected based on temperature change");
} }
} }
} }
@ -302,7 +301,7 @@ impl SystemHistory {
// State changes (except to Idle) likely indicate user activity // State changes (except to Idle) likely indicate user activity
if new_state != SystemState::Idle && new_state != SystemState::LowLoad { if new_state != SystemState::Idle && new_state != SystemState::LowLoad {
self.last_user_activity = Instant::now(); self.last_user_activity = Instant::now();
debug!("User activity detected based on system state change to {new_state:?}"); log::debug!("User activity detected based on system state change to {new_state:?}");
} }
// Update state // Update state
@ -313,7 +312,7 @@ impl SystemHistory {
// Check for significant load changes // Check for significant load changes
if report.system_load.load_avg_1min > 1.0 { if report.system_load.load_avg_1min > 1.0 {
self.last_user_activity = Instant::now(); self.last_user_activity = Instant::now();
debug!("User activity detected based on system load"); log::debug!("User activity detected based on system load");
} }
} }
@ -402,26 +401,8 @@ fn validate_poll_intervals(min_interval: u64, max_interval: u64) -> Result<(), C
} }
/// Run the daemon /// Run the daemon
pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> { pub fn run_daemon(config: AppConfig) -> Result<(), AppError> {
// Set effective log level based on config and verbose flag log::info!("Starting superfreq daemon...");
let effective_log_level = if verbose {
LogLevel::Debug
} else {
config.daemon.log_level
};
// Get the appropriate level filter
let level_filter = match effective_log_level {
LogLevel::Error => LevelFilter::Error,
LogLevel::Warning => LevelFilter::Warn,
LogLevel::Info => LevelFilter::Info,
LogLevel::Debug => LevelFilter::Debug,
};
// Update the log level filter if needed, without re-initializing the logger
log::set_max_level(level_filter);
info!("Starting superfreq daemon...");
// Validate critical configuration values before proceeding // Validate critical configuration values before proceeding
if let Err(err) = validate_poll_intervals( if let Err(err) = validate_poll_intervals(
@ -437,26 +418,28 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> {
// Set up signal handlers // Set up signal handlers
ctrlc::set_handler(move || { ctrlc::set_handler(move || {
info!("Received shutdown signal, exiting..."); log::info!("Received shutdown signal, exiting...");
r.store(false, Ordering::SeqCst); r.store(false, Ordering::SeqCst);
}) })
.map_err(|e| AppError::Generic(format!("Error setting Ctrl-C handler: {e}")))?; .map_err(|e| AppError::Generic(format!("Error setting Ctrl-C handler: {e}")))?;
info!( log::info!(
"Daemon initialized with poll interval: {}s", "Daemon initialized with poll interval: {}s",
config.daemon.poll_interval_sec config.daemon.poll_interval_sec
); );
// Set up stats file if configured // Set up stats file if configured
if let Some(stats_path) = &config.daemon.stats_file_path { if let Some(stats_path) = &config.daemon.stats_file_path {
info!("Stats will be written to: {stats_path}"); log::info!("Stats will be written to: {stats_path}");
} }
// Variables for adaptive polling // Variables for adaptive polling
// Make sure that the poll interval is *never* zero to prevent a busy loop // Make sure that the poll interval is *never* zero to prevent a busy loop
let mut current_poll_interval = config.daemon.poll_interval_sec.max(1); let mut current_poll_interval = config.daemon.poll_interval_sec.max(1);
if config.daemon.poll_interval_sec == 0 { if config.daemon.poll_interval_sec == 0 {
warn!("Poll interval is set to zero in config, using 1s minimum to prevent a busy loop"); log::warn!(
"Poll interval is set to zero in config, using 1s minimum to prevent a busy loop"
);
} }
let mut system_history = SystemHistory::default(); let mut system_history = SystemHistory::default();
@ -466,7 +449,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> {
match monitor::collect_system_report(&config) { match monitor::collect_system_report(&config) {
Ok(report) => { Ok(report) => {
debug!("Collected system report, applying settings..."); log::debug!("Collected system report, applying settings...");
// Store the current state before updating history // Store the current state before updating history
let previous_state = system_history.current_state.clone(); let previous_state = system_history.current_state.clone();
@ -477,24 +460,24 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> {
// Update the stats file if configured // Update the stats file if configured
if let Some(stats_path) = &config.daemon.stats_file_path { if let Some(stats_path) = &config.daemon.stats_file_path {
if let Err(e) = write_stats_file(stats_path, &report) { if let Err(e) = write_stats_file(stats_path, &report) {
error!("Failed to write stats file: {e}"); log::error!("Failed to write stats file: {e}");
} }
} }
match engine::determine_and_apply_settings(&report, &config, None) { match engine::determine_and_apply_settings(&report, &config, None) {
Ok(()) => { Ok(()) => {
debug!("Successfully applied system settings"); log::debug!("Successfully applied system settings");
// If system state changed, log the new state // If system state changed, log the new state
if system_history.current_state != previous_state { if system_history.current_state != previous_state {
info!( log::info!(
"System state changed to: {:?}", "System state changed to: {:?}",
system_history.current_state system_history.current_state
); );
} }
} }
Err(e) => { Err(e) => {
error!("Error applying system settings: {e}"); log::error!("Error applying system settings: {e}");
} }
} }
@ -509,7 +492,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> {
// Store the new interval // Store the new interval
system_history.last_computed_interval = Some(optimal_interval); system_history.last_computed_interval = Some(optimal_interval);
debug!("Recalculated optimal interval: {optimal_interval}s"); log::debug!("Recalculated optimal interval: {optimal_interval}s");
// Don't change the interval too dramatically at once // Don't change the interval too dramatically at once
match optimal_interval.cmp(&current_poll_interval) { match optimal_interval.cmp(&current_poll_interval) {
@ -528,7 +511,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> {
} }
Err(e) => { Err(e) => {
// Log the error and stop the daemon when an invalid configuration is detected // Log the error and stop the daemon when an invalid configuration is detected
error!("Critical configuration error: {e}"); log::error!("Critical configuration error: {e}");
running.store(false, Ordering::SeqCst); running.store(false, Ordering::SeqCst);
break; break;
} }
@ -540,7 +523,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> {
config.daemon.max_poll_interval_sec, config.daemon.max_poll_interval_sec,
); );
debug!("Adaptive polling: set interval to {current_poll_interval}s"); log::debug!("Adaptive polling: set interval to {current_poll_interval}s");
} else { } else {
// If adaptive polling is disabled, still apply battery-saving adjustment // If adaptive polling is disabled, still apply battery-saving adjustment
if config.daemon.throttle_on_battery && on_battery { if config.daemon.throttle_on_battery && on_battery {
@ -552,20 +535,22 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> {
current_poll_interval = (safe_interval * battery_multiplier) current_poll_interval = (safe_interval * battery_multiplier)
.min(config.daemon.max_poll_interval_sec); .min(config.daemon.max_poll_interval_sec);
debug!( log::debug!(
"On battery power, increased poll interval to {current_poll_interval}s" "On battery power, increased poll interval to {current_poll_interval}s"
); );
} else { } else {
// Use the configured poll interval // Use the configured poll interval
current_poll_interval = config.daemon.poll_interval_sec.max(1); current_poll_interval = config.daemon.poll_interval_sec.max(1);
if config.daemon.poll_interval_sec == 0 { if config.daemon.poll_interval_sec == 0 {
debug!("Using minimum poll interval of 1s instead of configured 0s"); log::debug!(
"Using minimum poll interval of 1s instead of configured 0s"
);
} }
} }
} }
} }
Err(e) => { Err(e) => {
error!("Error collecting system report: {e}"); log::error!("Error collecting system report: {e}");
} }
} }
@ -574,12 +559,12 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> {
let poll_duration = Duration::from_secs(current_poll_interval); let poll_duration = Duration::from_secs(current_poll_interval);
if elapsed < poll_duration { if elapsed < poll_duration {
let sleep_time = poll_duration - elapsed; let sleep_time = poll_duration - elapsed;
debug!("Sleeping for {}s until next cycle", sleep_time.as_secs()); log::debug!("Sleeping for {}s until next cycle", sleep_time.as_secs());
std::thread::sleep(sleep_time); std::thread::sleep(sleep_time);
} }
} }
info!("Daemon stopped"); log::info!("Daemon stopped");
Ok(()) Ok(())
} }

View file

@ -3,7 +3,6 @@ use crate::config::{AppConfig, ProfileConfig, TurboAutoSettings};
use crate::core::{OperationalMode, SystemReport, TurboSetting}; use crate::core::{OperationalMode, SystemReport, TurboSetting};
use crate::cpu::{self}; use crate::cpu::{self};
use crate::util::error::{ControlError, EngineError}; use crate::util::error::{ControlError, EngineError};
use log::{debug, info, warn};
use std::sync::OnceLock; use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
@ -128,13 +127,13 @@ fn try_apply_feature<F, T>(
where where
F: FnOnce() -> Result<T, ControlError>, F: FnOnce() -> Result<T, ControlError>,
{ {
info!("Setting {feature_name} to '{value_description}'"); log::info!("Setting {feature_name} to '{value_description}'");
match apply_fn() { match apply_fn() {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(e) => { Err(e) => {
if matches!(e, ControlError::NotSupported(_)) { if matches!(e, ControlError::NotSupported(_)) {
warn!( log::warn!(
"{feature_name} setting is not supported on this system. Skipping {feature_name} configuration." "{feature_name} setting is not supported on this system. Skipping {feature_name} configuration."
); );
Ok(()) Ok(())
@ -155,7 +154,7 @@ pub fn determine_and_apply_settings(
) -> Result<(), EngineError> { ) -> Result<(), EngineError> {
// First, check if there's a governor override set // First, check if there's a governor override set
if let Some(override_governor) = cpu::get_governor_override() { if let Some(override_governor) = cpu::get_governor_override() {
info!( log::info!(
"Governor override is active: '{}'. Setting governor.", "Governor override is active: '{}'. Setting governor.",
override_governor.trim() override_governor.trim()
); );
@ -182,35 +181,35 @@ pub fn determine_and_apply_settings(
if let Some(mode) = force_mode { if let Some(mode) = force_mode {
match mode { match mode {
OperationalMode::Powersave => { OperationalMode::Powersave => {
info!("Forced Powersave mode selected. Applying 'battery' profile."); log::info!("Forced Powersave mode selected. Applying 'battery' profile.");
selected_profile_config = &config.battery; selected_profile_config = &config.battery;
} }
OperationalMode::Performance => { OperationalMode::Performance => {
info!("Forced Performance mode selected. Applying 'charger' profile."); log::info!("Forced Performance mode selected. Applying 'charger' profile.");
selected_profile_config = &config.charger; selected_profile_config = &config.charger;
} }
} }
} else { } else {
// Use the previously computed on_ac_power value // Use the previously computed on_ac_power value
if on_ac_power { if on_ac_power {
info!("On AC power, selecting Charger profile."); log::info!("On AC power, selecting Charger profile.");
selected_profile_config = &config.charger; selected_profile_config = &config.charger;
} else { } else {
info!("On Battery power, selecting Battery profile."); log::info!("On Battery power, selecting Battery profile.");
selected_profile_config = &config.battery; selected_profile_config = &config.battery;
} }
} }
// Apply settings from selected_profile_config // Apply settings from selected_profile_config
if let Some(governor) = &selected_profile_config.governor { if let Some(governor) = &selected_profile_config.governor {
info!("Setting governor to '{governor}'"); log::info!("Setting governor to '{governor}'");
// Let set_governor handle the validation // Let set_governor handle the validation
if let Err(e) = cpu::set_governor(governor, None) { if let Err(e) = cpu::set_governor(governor, None) {
// If the governor is not available, log a warning // If the governor is not available, log a warning
if matches!(e, ControlError::InvalidGovernor(_)) if matches!(e, ControlError::InvalidGovernor(_))
|| matches!(e, ControlError::NotSupported(_)) || matches!(e, ControlError::NotSupported(_))
{ {
warn!( log::warn!(
"Configured governor '{governor}' is not available on this system. Skipping." "Configured governor '{governor}' is not available on this system. Skipping."
); );
} else { } else {
@ -220,14 +219,14 @@ pub fn determine_and_apply_settings(
} }
if let Some(turbo_setting) = selected_profile_config.turbo { if let Some(turbo_setting) = selected_profile_config.turbo {
info!("Setting turbo to '{turbo_setting:?}'"); log::info!("Setting turbo to '{turbo_setting:?}'");
match turbo_setting { match turbo_setting {
TurboSetting::Auto => { TurboSetting::Auto => {
if selected_profile_config.enable_auto_turbo { if selected_profile_config.enable_auto_turbo {
debug!("Managing turbo in auto mode based on system conditions"); log::debug!("Managing turbo in auto mode based on system conditions");
manage_auto_turbo(report, selected_profile_config, on_ac_power)?; manage_auto_turbo(report, selected_profile_config, on_ac_power)?;
} else { } else {
debug!( log::debug!(
"Superfreq's dynamic turbo management is disabled by configuration. Ensuring system uses its default behavior for automatic turbo control." "Superfreq's dynamic turbo management is disabled by configuration. Ensuring system uses its default behavior for automatic turbo control."
); );
// Make sure the system is set to its default automatic turbo mode. // Make sure the system is set to its default automatic turbo mode.
@ -255,13 +254,13 @@ pub fn determine_and_apply_settings(
if let Some(min_freq) = selected_profile_config.min_freq_mhz { if let Some(min_freq) = selected_profile_config.min_freq_mhz {
try_apply_feature("min frequency", &format!("{min_freq} MHz"), || { try_apply_feature("min frequency", &format!("{min_freq} MHz"), || {
cpu::set_min_frequency(min_freq, None) cpu::set_frequency_minimum(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 {
try_apply_feature("max frequency", &format!("{max_freq} MHz"), || { try_apply_feature("max frequency", &format!("{max_freq} MHz"), || {
cpu::set_max_frequency(max_freq, None) cpu::set_frequency_maximum(max_freq, None)
})?; })?;
} }
@ -277,19 +276,19 @@ pub fn determine_and_apply_settings(
let stop_threshold = thresholds.stop; let stop_threshold = thresholds.stop;
if start_threshold < stop_threshold && stop_threshold <= 100 { if start_threshold < stop_threshold && stop_threshold <= 100 {
info!("Setting battery charge thresholds: {start_threshold}-{stop_threshold}%"); log::info!("Setting battery charge thresholds: {start_threshold}-{stop_threshold}%");
match battery::set_battery_charge_thresholds(start_threshold, stop_threshold) { match battery::set_battery_charge_thresholds(start_threshold, stop_threshold) {
Ok(()) => debug!("Battery charge thresholds set successfully"), Ok(()) => log::debug!("Battery charge thresholds set successfully"),
Err(e) => warn!("Failed to set battery charge thresholds: {e}"), Err(e) => log::warn!("Failed to set battery charge thresholds: {e}"),
} }
} else { } else {
warn!( log::warn!(
"Invalid battery threshold values: start={start_threshold}, stop={stop_threshold}" "Invalid battery threshold values: start={start_threshold}, stop={stop_threshold}"
); );
} }
} }
debug!("Profile settings applied successfully."); log::debug!("Profile settings applied successfully.");
Ok(()) Ok(())
} }
@ -346,27 +345,30 @@ fn manage_auto_turbo(
let enable_turbo = match (cpu_temp, avg_cpu_usage, previous_turbo_enabled) { let enable_turbo = match (cpu_temp, avg_cpu_usage, previous_turbo_enabled) {
// If temperature is too high, disable turbo regardless of load // If temperature is too high, disable turbo regardless of load
(Some(temp), _, _) if temp >= turbo_settings.temp_threshold_high => { (Some(temp), _, _) if temp >= turbo_settings.temp_threshold_high => {
info!( log::info!(
"Auto Turbo: Disabled due to high temperature ({:.1}°C >= {:.1}°C)", "Auto Turbo: Disabled due to high temperature ({:.1}°C >= {:.1}°C)",
temp, turbo_settings.temp_threshold_high temp,
turbo_settings.temp_threshold_high
); );
false false
} }
// If load is high enough, enable turbo (unless temp already caused it to disable) // If load is high enough, enable turbo (unless temp already caused it to disable)
(_, Some(usage), _) if usage >= turbo_settings.load_threshold_high => { (_, Some(usage), _) if usage >= turbo_settings.load_threshold_high => {
info!( log::info!(
"Auto Turbo: Enabled due to high CPU load ({:.1}% >= {:.1}%)", "Auto Turbo: Enabled due to high CPU load ({:.1}% >= {:.1}%)",
usage, turbo_settings.load_threshold_high usage,
turbo_settings.load_threshold_high
); );
true true
} }
// If load is low, disable turbo // If load is low, disable turbo
(_, Some(usage), _) if usage <= turbo_settings.load_threshold_low => { (_, Some(usage), _) if usage <= turbo_settings.load_threshold_low => {
info!( log::info!(
"Auto Turbo: Disabled due to low CPU load ({:.1}% <= {:.1}%)", "Auto Turbo: Disabled due to low CPU load ({:.1}% <= {:.1}%)",
usage, turbo_settings.load_threshold_low usage,
turbo_settings.load_threshold_low
); );
false false
} }
@ -376,7 +378,7 @@ fn manage_auto_turbo(
if usage > turbo_settings.load_threshold_low if usage > turbo_settings.load_threshold_low
&& usage < turbo_settings.load_threshold_high => && usage < turbo_settings.load_threshold_high =>
{ {
info!( log::info!(
"Auto Turbo: Maintaining previous state ({}) due to intermediate load ({:.1}%)", "Auto Turbo: Maintaining previous state ({}) due to intermediate load ({:.1}%)",
if prev_state { "enabled" } else { "disabled" }, if prev_state { "enabled" } else { "disabled" },
usage usage
@ -386,7 +388,7 @@ fn manage_auto_turbo(
// When CPU load data is present but temperature is missing, use the same hysteresis logic // When CPU load data is present but temperature is missing, use the same hysteresis logic
(None, Some(usage), prev_state) => { (None, Some(usage), prev_state) => {
info!( log::info!(
"Auto Turbo: Maintaining previous state ({}) due to missing temperature data (load: {:.1}%)", "Auto Turbo: Maintaining previous state ({}) due to missing temperature data (load: {:.1}%)",
if prev_state { "enabled" } else { "disabled" }, if prev_state { "enabled" } else { "disabled" },
usage usage
@ -396,7 +398,7 @@ fn manage_auto_turbo(
// When all metrics are missing, maintain the previous state // When all metrics are missing, maintain the previous state
(None, None, prev_state) => { (None, None, prev_state) => {
info!( log::info!(
"Auto Turbo: Maintaining previous state ({}) due to missing all CPU metrics", "Auto Turbo: Maintaining previous state ({}) due to missing all CPU metrics",
if prev_state { "enabled" } else { "disabled" } if prev_state { "enabled" } else { "disabled" }
); );
@ -405,7 +407,7 @@ fn manage_auto_turbo(
// Any other cases with partial metrics, maintain previous state for stability // Any other cases with partial metrics, maintain previous state for stability
(_, _, prev_state) => { (_, _, prev_state) => {
info!( log::info!(
"Auto Turbo: Maintaining previous state ({}) due to incomplete CPU metrics", "Auto Turbo: Maintaining previous state ({}) due to incomplete CPU metrics",
if prev_state { "enabled" } else { "disabled" } if prev_state { "enabled" } else { "disabled" }
); );
@ -429,7 +431,7 @@ fn manage_auto_turbo(
TurboSetting::Never TurboSetting::Never
}; };
info!( log::info!(
"Auto Turbo: Applying turbo change from {} to {}", "Auto Turbo: Applying turbo change from {} to {}",
if previous_turbo_enabled { if previous_turbo_enabled {
"enabled" "enabled"
@ -441,7 +443,7 @@ fn manage_auto_turbo(
match cpu::set_turbo(turbo_setting) { match cpu::set_turbo(turbo_setting) {
Ok(()) => { Ok(()) => {
debug!( log::debug!(
"Auto Turbo: Successfully set turbo to {}", "Auto Turbo: Successfully set turbo to {}",
if enable_turbo { "enabled" } else { "disabled" } if enable_turbo { "enabled" } else { "disabled" }
); );
@ -450,7 +452,7 @@ fn manage_auto_turbo(
Err(e) => Err(EngineError::ControlError(e)), Err(e) => Err(EngineError::ControlError(e)),
} }
} else { } else {
debug!( log::debug!(
"Auto Turbo: Maintaining turbo state ({}) - no change needed", "Auto Turbo: Maintaining turbo state ({}) - no change needed",
if enable_turbo { "enabled" } else { "disabled" } if enable_turbo { "enabled" } else { "disabled" }
); );

View file

@ -8,469 +8,478 @@ mod engine;
mod monitor; mod monitor;
mod util; mod util;
use crate::config::AppConfig; use anyhow::{Context, anyhow, bail};
use crate::core::{GovernorOverrideMode, TurboSetting}; use clap::Parser as _;
use crate::util::error::{AppError, ControlError}; use std::fmt::Write as _;
use clap::{Parser, value_parser}; use std::io::Write as _;
use env_logger::Builder; use std::{io, process};
use log::{debug, error, info}; use yansi::Paint as _;
use std::error::Error;
use std::sync::Once;
#[derive(Parser, Debug)] #[derive(clap::Parser, Debug)]
#[clap(author, version, about, long_about = None)] #[clap(author, version, about)]
struct Cli { struct Cli {
#[command(flatten)]
verbosity: clap_verbosity_flag::Verbosity,
#[clap(subcommand)] #[clap(subcommand)]
command: Option<Commands>, command: Command,
} }
#[derive(Parser, Debug)] #[derive(clap::Parser, Debug)]
enum Commands { enum Command {
/// Display current system information /// Display information.
Info, Info,
/// Run as a daemon in the background
Daemon { /// Start the daemon.
#[clap(long)] Start,
verbose: bool,
}, /// Modify attributes.
/// Set CPU governor Set {
SetGovernor { /// The CPUs to apply the changes to. When unspecified, will be applied to all CPUs.
governor: String, #[arg(short = 'c', long = "for")]
#[clap(long)] for_: Option<Vec<u32>>,
core_id: Option<u32>,
}, /// Set the CPU governor.
/// Force a specific governor mode persistently #[arg(long)]
ForceGovernor { governor: Option<String>, // TODO: Validate with clap for available governors.
/// Mode to force: performance, powersave, or reset
#[clap(value_enum)] /// Set the CPU governor persistently.
mode: GovernorOverrideMode, #[arg(long, conflicts_with = "governor")]
}, governor_persist: Option<String>, // TODO: Validate with clap for available governors.
/// Set turbo boost behavior
SetTurbo { /// Set CPU Energy Performance Preference (EPP). Short form: --epp.
#[clap(value_enum)] #[arg(long, alias = "epp")]
setting: TurboSetting, energy_performance_preference: Option<String>,
},
/// Display comprehensive debug information /// Set CPU Energy Performance Bias (EPB). Short form: --epb.
Debug, #[arg(long, alias = "epb")]
/// Set Energy Performance Preference (EPP) energy_performance_bias: Option<String>,
SetEpp {
epp: String, /// Set minimum CPU frequency in MHz. Short form: --freq-min.
#[clap(long)] #[arg(short = 'f', long, alias = "freq-min", value_parser = clap::value_parser!(u64).range(1..=10_000))]
core_id: Option<u32>, frequency_mhz_minimum: Option<u64>,
},
/// Set Energy Performance Bias (EPB) /// Set maximum CPU frequency in MHz. Short form: --freq-max.
SetEpb { #[arg(short = 'F', long, alias = "freq-max", value_parser = clap::value_parser!(u64).range(1..=10_000))]
epb: String, // Typically 0-15 frequency_mhz_maximum: Option<u64>,
#[clap(long)]
core_id: Option<u32>, /// Set turbo boost behaviour. Has to be for all CPUs.
}, #[arg(long, conflicts_with = "for_")]
/// Set minimum CPU frequency turbo: Option<cpu::Turbo>,
SetMinFreq {
freq_mhz: u32, /// Set ACPI platform profile. Has to be for all CPUs.
#[clap(long)] #[arg(long, alias = "profile", conflicts_with = "for_")]
core_id: Option<u32>, platform_profile: Option<String>,
},
/// Set maximum CPU frequency /// Set the percentage that the power supply has to drop under for charging to start. Short form: --charge-start.
SetMaxFreq { #[arg(short = 'p', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100), conflicts_with = "for_")]
freq_mhz: u32, charge_threshold_start: Option<u8>,
#[clap(long)]
core_id: Option<u32>, /// Set the percentage where charging will stop. Short form: --charge-end.
}, #[arg(short = 'P', long, alias = "charge-end", value_parser = clap::value_parser!(u8).range(0..=100), conflicts_with = "for_")]
/// Set ACPI platform profile charge_threshold_end: Option<u8>,
SetPlatformProfile { profile: String },
/// Set battery charge thresholds to extend battery lifespan
SetBatteryThresholds {
/// Percentage at which charging starts (when below this value)
#[clap(value_parser = value_parser!(u8).range(0..=99))]
start_threshold: u8,
/// Percentage at which charging stops (when it reaches this value)
#[clap(value_parser = value_parser!(u8).range(1..=100))]
stop_threshold: u8,
}, },
} }
fn main() -> Result<(), AppError> { fn real_main() -> anyhow::Result<()> {
// Initialize logger once for the entire application
init_logger();
let cli = Cli::parse(); let cli = Cli::parse();
// Load configuration first, as it might be needed by the monitor module env_logger::Builder::new()
// E.g., for ignored power supplies .filter_level(cli.verbosity.log_level_filter())
let config = match config::load_config() { .format_timestamp(None)
Ok(cfg) => cfg, .format_module_path(false)
Err(e) => { .init();
error!("Error loading configuration: {e}. Using default values.");
// Proceed with default config if loading fails let config = config::load_config().context("failed to load config")?;
AppConfig::default()
match cli.command {
Command::Info => todo!(),
Command::Start => {
daemon::run_daemon(config)?;
Ok(())
} }
};
let command_result: Result<(), AppError> = match cli.command { Command::Set {
// TODO: This will be moved to a different module in the future. for_,
Some(Commands::Info) => match monitor::collect_system_report(&config) { governor,
Ok(report) => { governor_persist,
// Format section headers with proper centering energy_performance_preference,
let format_section = |title: &str| { energy_performance_bias,
let title_len = title.len(); frequency_mhz_minimum,
let total_width = title_len + 8; // 8 is for padding (4 on each side) frequency_mhz_maximum,
let separator = "".repeat(total_width); turbo,
platform_profile,
charge_threshold_start,
charge_threshold_end,
} => {
let cpus = match for_ {
Some(cpus) => cpus,
None => cpu::get_real_cpus()?,
};
println!("\n{separator}"); for cpu in cpus {
if let Some(governor) = governor.as_ref() {
cpu::set_governor(governor, cpu)?;
}
// Calculate centering if let Some(epp) = energy_performance_preference.as_ref() {
println!("{title}"); cpu::set_epp(epp, cpu)?;
}
println!("{separator}"); if let Some(epb) = energy_performance_bias.as_ref() {
}; cpu::set_epb(epb, cpu)?;
}
format_section("System Information"); if let Some(mhz_minimum) = frequency_mhz_minimum {
println!("CPU Model: {}", report.system_info.cpu_model); cpu::set_frequency_minimum(mhz_minimum, cpu)?;
println!("Architecture: {}", report.system_info.architecture); }
println!(
"Linux Distribution: {}",
report.system_info.linux_distribution
);
// Format timestamp in a readable way if let Some(mhz_maximum) = frequency_mhz_maximum {
println!("Current Time: {}", jiff::Timestamp::now()); cpu::set_frequency_maximum(mhz_maximum, cpu)?;
}
}
format_section("CPU Global Info"); if let Some(turbo) = turbo {
println!( cpu::set_turbo(turbo)?;
"Current Governor: {}", }
report
.cpu_global
.current_governor
.as_deref()
.unwrap_or("N/A")
);
println!(
"Available Governors: {}", // 21 length baseline
report.cpu_global.available_governors.join(", ")
);
println!(
"Turbo Status: {}",
match report.cpu_global.turbo_status {
Some(true) => "Enabled",
Some(false) => "Disabled",
None => "Unknown",
}
);
println!( if let Some(platform_profile) = platform_profile.as_ref() {
"EPP: {}", cpu::set_platform_profile(platform_profile)?;
report.cpu_global.epp.as_deref().unwrap_or("N/A") }
);
println!(
"EPB: {}",
report.cpu_global.epb.as_deref().unwrap_or("N/A")
);
println!(
"Platform Profile: {}",
report
.cpu_global
.platform_profile
.as_deref()
.unwrap_or("N/A")
);
println!(
"CPU Temperature: {}",
report.cpu_global.average_temperature_celsius.map_or_else(
|| "N/A (No sensor detected)".to_string(),
|t| format!("{t:.1}°C")
)
);
format_section("CPU Core Info"); // TODO: This is like this because [`cpu`] doesn't expose
// a way of setting them individually. Will clean this up
// after that is cleaned.
if charge_threshold_start.is_some() || charge_threshold_end.is_some() {
let charge_threshold_start = charge_threshold_start.ok_or_else(|| {
anyhow!("both charge thresholds should be given at the same time")
})?;
let charge_threshold_end = charge_threshold_end.ok_or_else(|| {
anyhow!("both charge thresholds should be given at the same time")
})?;
// Get max core ID length for padding if charge_threshold_start >= charge_threshold_end {
let max_core_id_len = report bail!(
.cpu_cores "charge start threshold (given as {charge_threshold_start}) must be less than stop threshold (given as {charge_threshold_end})"
.last()
.map_or(1, |core| core.core_id.to_string().len());
// Table headers
println!(
" {:>width$} │ {:^10} │ {:^10} │ {:^10} │ {:^7} │ {:^9}",
"Core",
"Current",
"Min",
"Max",
"Usage",
"Temp",
width = max_core_id_len + 4
);
println!(
" {:─>width$}──┼─{:─^10}─┼─{:─^10}─┼─{:─^10}─┼─{:─^7}─┼─{:─^9}",
"",
"",
"",
"",
"",
"",
width = max_core_id_len + 4
);
for core_info in &report.cpu_cores {
// Format frequencies: if current > max, show in a special way
let current_freq = match core_info.current_frequency_mhz {
Some(freq) => {
let max_freq = core_info.max_frequency_mhz.unwrap_or(0);
if freq > max_freq && max_freq > 0 {
// Special format for boosted frequencies
format!("{freq}*")
} else {
format!("{freq}")
}
}
None => "N/A".to_string(),
};
// CPU core display
println!(
" Core {:<width$} │ {:>10} │ {:>10} │ {:>10} │ {:>7} │ {:>9}",
core_info.core_id,
format!("{} MHz", current_freq),
format!(
"{} MHz",
core_info
.min_frequency_mhz
.map_or_else(|| "N/A".to_string(), |f| f.to_string())
),
format!(
"{} MHz",
core_info
.max_frequency_mhz
.map_or_else(|| "N/A".to_string(), |f| f.to_string())
),
format!(
"{}%",
core_info
.usage_percent
.map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}"))
),
format!(
"{}°C",
core_info
.temperature_celsius
.map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}"))
),
width = max_core_id_len
); );
} }
// Only display battery info for systems that have real batteries battery::set_battery_charge_thresholds(
// Skip this section entirely on desktop systems charge_threshold_start,
if !report.batteries.is_empty() { charge_threshold_end,
let has_real_batteries = report.batteries.iter().any(|b| { )?;
// Check if any battery has actual battery data
// (as opposed to peripherals like wireless mice)
b.capacity_percent.is_some() || b.power_rate_watts.is_some()
});
if has_real_batteries {
format_section("Battery Info");
for battery_info in &report.batteries {
// Check if this appears to be a real system battery
if battery_info.capacity_percent.is_some()
|| battery_info.power_rate_watts.is_some()
{
let power_status = if battery_info.ac_connected {
"Connected to AC"
} else {
"Running on Battery"
};
println!("Battery {}:", battery_info.name);
println!(" Power Status: {power_status}");
println!(
" State: {}",
battery_info.charging_state.as_deref().unwrap_or("Unknown")
);
if let Some(capacity) = battery_info.capacity_percent {
println!(" Capacity: {capacity}%");
}
if let Some(power) = battery_info.power_rate_watts {
let direction = if power >= 0.0 {
"charging"
} else {
"discharging"
};
println!(
" Power Rate: {:.2} W ({})",
power.abs(),
direction
);
}
// Display charge thresholds if available
if battery_info.charge_start_threshold.is_some()
|| battery_info.charge_stop_threshold.is_some()
{
println!(
" Charge Thresholds: {}-{}",
battery_info
.charge_start_threshold
.map_or_else(|| "N/A".to_string(), |t| t.to_string()),
battery_info
.charge_stop_threshold
.map_or_else(|| "N/A".to_string(), |t| t.to_string())
);
}
}
}
}
}
format_section("System Load");
println!(
"Load Average (1m): {:.2}",
report.system_load.load_avg_1min
);
println!(
"Load Average (5m): {:.2}",
report.system_load.load_avg_5min
);
println!(
"Load Average (15m): {:.2}",
report.system_load.load_avg_15min
);
Ok(())
} }
Err(e) => Err(AppError::Monitor(e)),
},
Some(Commands::SetGovernor { governor, core_id }) => {
cpu::set_governor(&governor, core_id).map_err(AppError::Control)
}
Some(Commands::ForceGovernor { mode }) => {
cpu::force_governor(mode).map_err(AppError::Control)
}
Some(Commands::SetTurbo { setting }) => cpu::set_turbo(setting).map_err(AppError::Control),
Some(Commands::SetEpp { epp, core_id }) => {
cpu::set_epp(&epp, core_id).map_err(AppError::Control)
}
Some(Commands::SetEpb { epb, core_id }) => {
cpu::set_epb(&epb, core_id).map_err(AppError::Control)
}
Some(Commands::SetMinFreq { freq_mhz, core_id }) => {
// Basic validation for reasonable CPU frequency values
validate_freq(freq_mhz, "Minimum")?;
cpu::set_min_frequency(freq_mhz, core_id).map_err(AppError::Control)
}
Some(Commands::SetMaxFreq { freq_mhz, core_id }) => {
// Basic validation for reasonable CPU frequency values
validate_freq(freq_mhz, "Maximum")?;
cpu::set_max_frequency(freq_mhz, core_id).map_err(AppError::Control)
}
Some(Commands::SetPlatformProfile { profile }) => {
// Get available platform profiles and validate early if possible
match cpu::get_platform_profiles() {
Ok(available_profiles) => {
if available_profiles.contains(&profile) {
info!("Setting platform profile to '{profile}'");
cpu::set_platform_profile(&profile).map_err(AppError::Control)
} else {
error!(
"Invalid platform profile: '{}'. Available profiles: {}",
profile,
available_profiles.join(", ")
);
Err(AppError::Generic(format!(
"Invalid platform profile: '{}'. Available profiles: {}",
profile,
available_profiles.join(", ")
)))
}
}
Err(_e) => {
// If we can't get profiles (e.g., feature not supported), pass through to the function
cpu::set_platform_profile(&profile).map_err(AppError::Control)
}
}
}
Some(Commands::SetBatteryThresholds {
start_threshold,
stop_threshold,
}) => {
// We only need to check if start < stop since the range validation is handled by Clap
if start_threshold >= stop_threshold {
error!(
"Start threshold ({start_threshold}) must be less than stop threshold ({stop_threshold})"
);
Err(AppError::Generic(format!(
"Start threshold ({start_threshold}) must be less than stop threshold ({stop_threshold})"
)))
} else {
info!(
"Setting battery thresholds: start at {start_threshold}%, stop at {stop_threshold}%"
);
battery::set_battery_charge_thresholds(start_threshold, stop_threshold)
.map_err(AppError::Control)
}
}
Some(Commands::Daemon { verbose }) => daemon::run_daemon(config, verbose),
Some(Commands::Debug) => cli::debug::run_debug(&config),
None => {
info!("Welcome to superfreq! Use --help for commands.");
debug!("Current effective configuration: {config:?}");
Ok(()) Ok(())
} }
}
// TODO: This will be moved to a different module in the future.
// Some(Command::Info) => match monitor::collect_system_report(&config) {
// Ok(report) => {
// // Format section headers with proper centering
// let format_section = |title: &str| {
// let title_len = title.len();
// let total_width = title_len + 8; // 8 is for padding (4 on each side)
// let separator = "═".repeat(total_width);
// println!("\n╔{separator}╗");
// // Calculate centering
// println!("║ {title} ║");
// println!("╚{separator}╝");
// };
// format_section("System Information");
// println!("CPU Model: {}", report.system_info.cpu_model);
// println!("Architecture: {}", report.system_info.architecture);
// println!(
// "Linux Distribution: {}",
// report.system_info.linux_distribution
// );
// // Format timestamp in a readable way
// println!("Current Time: {}", jiff::Timestamp::now());
// format_section("CPU Global Info");
// println!(
// "Current Governor: {}",
// report
// .cpu_global
// .current_governor
// .as_deref()
// .unwrap_or("N/A")
// );
// println!(
// "Available Governors: {}", // 21 length baseline
// report.cpu_global.available_governors.join(", ")
// );
// println!(
// "Turbo Status: {}",
// match report.cpu_global.turbo_status {
// Some(true) => "Enabled",
// Some(false) => "Disabled",
// None => "Unknown",
// }
// );
// println!(
// "EPP: {}",
// report.cpu_global.epp.as_deref().unwrap_or("N/A")
// );
// println!(
// "EPB: {}",
// report.cpu_global.epb.as_deref().unwrap_or("N/A")
// );
// println!(
// "Platform Profile: {}",
// report
// .cpu_global
// .platform_profile
// .as_deref()
// .unwrap_or("N/A")
// );
// println!(
// "CPU Temperature: {}",
// report.cpu_global.average_temperature_celsius.map_or_else(
// || "N/A (No sensor detected)".to_string(),
// |t| format!("{t:.1}°C")
// )
// );
// format_section("CPU Core Info");
// // Get max core ID length for padding
// let max_core_id_len = report
// .cpu_cores
// .last()
// .map_or(1, |core| core.core_id.to_string().len());
// // Table headers
// println!(
// " {:>width$} │ {:^10} │ {:^10} │ {:^10} │ {:^7} │ {:^9}",
// "Core",
// "Current",
// "Min",
// "Max",
// "Usage",
// "Temp",
// width = max_core_id_len + 4
// );
// println!(
// " {:─>width$}──┼─{:─^10}─┼─{:─^10}─┼─{:─^10}─┼─{:─^7}─┼─{:─^9}",
// "",
// "",
// "",
// "",
// "",
// "",
// width = max_core_id_len + 4
// );
// for core_info in &report.cpu_cores {
// // Format frequencies: if current > max, show in a special way
// let current_freq = match core_info.current_frequency_mhz {
// Some(freq) => {
// let max_freq = core_info.max_frequency_mhz.unwrap_or(0);
// if freq > max_freq && max_freq > 0 {
// // Special format for boosted frequencies
// format!("{freq}*")
// } else {
// format!("{freq}")
// }
// }
// None => "N/A".to_string(),
// };
// // CPU core display
// println!(
// " Core {:<width$} │ {:>10} │ {:>10} │ {:>10} │ {:>7} │ {:>9}",
// core_info.core_id,
// format!("{} MHz", current_freq),
// format!(
// "{} MHz",
// core_info
// .min_frequency_mhz
// .map_or_else(|| "N/A".to_string(), |f| f.to_string())
// ),
// format!(
// "{} MHz",
// core_info
// .max_frequency_mhz
// .map_or_else(|| "N/A".to_string(), |f| f.to_string())
// ),
// format!(
// "{}%",
// core_info
// .usage_percent
// .map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}"))
// ),
// format!(
// "{}°C",
// core_info
// .temperature_celsius
// .map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}"))
// ),
// width = max_core_id_len
// );
// }
// // Only display battery info for systems that have real batteries
// // Skip this section entirely on desktop systems
// if !report.batteries.is_empty() {
// let has_real_batteries = report.batteries.iter().any(|b| {
// // Check if any battery has actual battery data
// // (as opposed to peripherals like wireless mice)
// b.capacity_percent.is_some() || b.power_rate_watts.is_some()
// });
// if has_real_batteries {
// format_section("Battery Info");
// for battery_info in &report.batteries {
// // Check if this appears to be a real system battery
// if battery_info.capacity_percent.is_some()
// || battery_info.power_rate_watts.is_some()
// {
// let power_status = if battery_info.ac_connected {
// "Connected to AC"
// } else {
// "Running on Battery"
// };
// println!("Battery {}:", battery_info.name);
// println!(" Power Status: {power_status}");
// println!(
// " State: {}",
// battery_info.charging_state.as_deref().unwrap_or("Unknown")
// );
// if let Some(capacity) = battery_info.capacity_percent {
// println!(" Capacity: {capacity}%");
// }
// if let Some(power) = battery_info.power_rate_watts {
// let direction = if power >= 0.0 {
// "charging"
// } else {
// "discharging"
// };
// println!(
// " Power Rate: {:.2} W ({})",
// power.abs(),
// direction
// );
// }
// // Display charge thresholds if available
// if battery_info.charge_start_threshold.is_some()
// || battery_info.charge_stop_threshold.is_some()
// {
// println!(
// " Charge Thresholds: {}-{}",
// battery_info
// .charge_start_threshold
// .map_or_else(|| "N/A".to_string(), |t| t.to_string()),
// battery_info
// .charge_stop_threshold
// .map_or_else(|| "N/A".to_string(), |t| t.to_string())
// );
// }
// }
// }
// }
// }
// format_section("System Load");
// println!(
// "Load Average (1m): {:.2}",
// report.system_load.load_avg_1min
// );
// println!(
// "Load Average (5m): {:.2}",
// report.system_load.load_avg_5min
// );
// println!(
// "Load Average (15m): {:.2}",
// report.system_load.load_avg_15min
// );
// Ok(())
// }
// Err(e) => Err(AppError::Monitor(e)),
// },
// Some(CliCommand::SetPlatformProfile { profile }) => {
// // Get available platform profiles and validate early if possible
// match cpu::get_platform_profiles() {
// Ok(available_profiles) => {
// if available_profiles.contains(&profile) {
// log::info!("Setting platform profile to '{profile}'");
// cpu::set_platform_profile(&profile).map_err(AppError::Control)
// } else {
// log::error!(
// "Invalid platform profile: '{}'. Available profiles: {}",
// profile,
// available_profiles.join(", ")
// );
// Err(AppError::Generic(format!(
// "Invalid platform profile: '{}'. Available profiles: {}",
// profile,
// available_profiles.join(", ")
// )))
// }
// }
// Err(_e) => {
// // If we can't get profiles (e.g., feature not supported), pass through to the function
// cpu::set_platform_profile(&profile).map_err(AppError::Control)
// }
// }
// }
}
fn main() {
let Err(error) = real_main() else {
return;
}; };
if let Err(e) = command_result { let mut err = io::stderr();
error!("Error executing command: {e}");
if let Some(source) = e.source() {
error!("Caused by: {source}");
}
// Check for permission denied errors let mut message = String::new();
if let AppError::Control(control_error) = &e { let mut chain = error.chain().rev().peekable();
if matches!(control_error, ControlError::PermissionDenied(_)) {
error!( while let Some(error) = chain.next() {
"Hint: This operation may require administrator privileges (e.g., run with sudo)." let _ = write!(
); err,
"{header} ",
header = if chain.peek().is_none() {
"error:"
} else {
"cause:"
} }
} .red()
.bold(),
);
std::process::exit(1); String::clear(&mut message);
let _ = write!(message, "{error}");
let mut chars = message.char_indices();
let _ = match (chars.next(), chars.next()) {
(Some((_, first)), Some((second_start, second))) if second.is_lowercase() => {
writeln!(
err,
"{first_lowercase}{rest}",
first_lowercase = first.to_lowercase(),
rest = &message[second_start..],
)
}
_ => {
writeln!(err, "{message}")
}
};
} }
Ok(()) process::exit(1);
}
/// Initialize the logger for the entire application
static LOGGER_INIT: Once = Once::new();
fn init_logger() {
LOGGER_INIT.call_once(|| {
// Set default log level based on environment or default to Info
let env_log = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string());
Builder::new()
.parse_filters(&env_log)
.format_timestamp(None)
.format_module_path(false)
.init();
debug!("Logger initialized with RUST_LOG={env_log}");
});
}
/// Validate CPU frequency input values
fn validate_freq(freq_mhz: u32, label: &str) -> Result<(), AppError> {
if freq_mhz == 0 {
error!("{label} frequency cannot be zero");
Err(AppError::Generic(format!(
"{label} frequency cannot be zero"
)))
} else if freq_mhz > 10000 {
// Extremely high value unlikely to be valid
error!("{label} frequency ({freq_mhz} MHz) is unreasonably high");
Err(AppError::Generic(format!(
"{label} frequency ({freq_mhz} MHz) is unreasonably high"
)))
} else {
Ok(())
}
} }

View file

@ -1,8 +1,7 @@
use crate::config::AppConfig; use crate::config::AppConfig;
use crate::core::{BatteryInfo, CpuCoreInfo, CpuGlobalInfo, SystemInfo, SystemLoad, SystemReport}; use crate::core::{BatteryInfo, CpuCoreInfo, CpuGlobalInfo, SystemInfo, SystemLoad, SystemReport};
use crate::cpu::get_logical_core_count; use crate::cpu::get_real_cpus;
use crate::util::error::SysMonitorError; use crate::util::error::SysMonitorError;
use log::debug;
use std::{ use std::{
collections::HashMap, collections::HashMap,
fs, fs,
@ -364,7 +363,7 @@ pub fn get_all_cpu_core_info() -> Result<Vec<CpuCoreInfo>> {
thread::sleep(Duration::from_millis(250)); // interval for CPU usage calculation thread::sleep(Duration::from_millis(250)); // interval for CPU usage calculation
let final_cpu_times = read_all_cpu_times()?; let final_cpu_times = read_all_cpu_times()?;
let num_cores = get_logical_core_count() let num_cores = get_real_cpus()
.map_err(|_| SysMonitorError::ReadError("Could not get the number of cores".to_string()))?; .map_err(|_| SysMonitorError::ReadError("Could not get the number of cores".to_string()))?;
let mut core_infos = Vec::with_capacity(num_cores as usize); let mut core_infos = Vec::with_capacity(num_cores as usize);
@ -395,7 +394,7 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo {
let mut cpufreq_base_path_buf = PathBuf::from("/sys/devices/system/cpu/cpu0/cpufreq/"); let mut cpufreq_base_path_buf = PathBuf::from("/sys/devices/system/cpu/cpu0/cpufreq/");
if !cpufreq_base_path_buf.exists() { if !cpufreq_base_path_buf.exists() {
let core_count = get_logical_core_count().unwrap_or_else(|e| { let core_count = get_real_cpus().unwrap_or_else(|e| {
eprintln!("Warning: {e}"); eprintln!("Warning: {e}");
0 0
}); });
@ -551,7 +550,7 @@ pub fn get_battery_info(config: &AppConfig) -> Result<Vec<BatteryInfo>> {
if ps_type == "Battery" { if ps_type == "Battery" {
// Skip peripheral batteries that aren't real laptop batteries // Skip peripheral batteries that aren't real laptop batteries
if is_peripheral_battery(&ps_path, &name) { if is_peripheral_battery(&ps_path, &name) {
debug!("Skipping peripheral battery: {name}"); log::debug!("Skipping peripheral battery: {name}");
continue; continue;
} }
@ -598,7 +597,7 @@ pub fn get_battery_info(config: &AppConfig) -> Result<Vec<BatteryInfo>> {
// If we found no batteries but have power supplies, we're likely on a desktop // If we found no batteries but have power supplies, we're likely on a desktop
if batteries.is_empty() && overall_ac_connected { if batteries.is_empty() && overall_ac_connected {
debug!("No laptop batteries found, likely a desktop system"); log::debug!("No laptop batteries found, likely a desktop system");
} }
Ok(batteries) Ok(batteries)