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",
]
[[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]]
name = "clap_builder"
version = "4.5.38"
@ -131,6 +141,15 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "ctrlc"
version = "3.4.7"
@ -141,6 +160,28 @@ dependencies = [
"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]]
name = "dirs"
version = "6.0.0"
@ -453,7 +494,9 @@ version = "0.3.2"
dependencies = [
"anyhow",
"clap",
"clap-verbosity-flag",
"ctrlc",
"derive_more",
"dirs",
"env_logger",
"jiff",
@ -462,6 +505,7 @@ dependencies = [
"serde",
"thiserror",
"toml",
"yansi",
]
[[package]]
@ -542,6 +586,18 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "utf8parse"
version = "0.2.2"
@ -635,3 +691,9 @@ checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
dependencies = [
"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"
anyhow = "1.0"
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 log::{debug, warn};
use std::{
fs, io,
path::{Path, PathBuf},
@ -118,7 +117,7 @@ fn find_supported_batteries(power_supply_path: &Path) -> Result<Vec<SupportedBat
let entry = match entry {
Ok(e) => e,
Err(e) => {
warn!("Failed to read power-supply entry: {e}");
log::warn!("Failed to read power-supply entry: {e}");
continue;
}
};
@ -131,16 +130,17 @@ fn find_supported_batteries(power_supply_path: &Path) -> Result<Vec<SupportedBat
}
if supported_batteries.is_empty() {
warn!("No batteries with charge threshold support found");
log::warn!("No batteries with charge threshold support found");
} else {
debug!(
log::debug!(
"Found {} batteries with threshold support",
supported_batteries.len()
);
for battery in &supported_batteries {
debug!(
log::debug!(
"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 {
Ok(()) => {
debug!(
log::debug!(
"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;
}
@ -184,14 +187,16 @@ fn apply_thresholds_to_batteries(
if let Some(prev_stop) = &current_stop {
let restore_result = sysfs::write_sysfs_value(&stop_path, prev_stop);
if let Err(re) = restore_result {
warn!(
log::warn!(
"Failed to restore previous stop threshold for battery '{}': {}. Battery may be in an inconsistent state.",
battery.name, re
battery.name,
re
);
} else {
debug!(
log::debug!(
"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 !errors.is_empty() {
warn!(
log::warn!(
"Partial success setting battery thresholds: {}",
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 {
// Overall system details
pub cpu_model: String,

View file

@ -1,479 +1,321 @@
use crate::core::{GovernorOverrideMode, TurboSetting};
use crate::util::error::ControlError;
use core::str;
use log::debug;
use anyhow::{Context, bail};
use derive_more::Display;
use serde::{Deserialize, Serialize};
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
const VALID_EPB_STRINGS: &[&str] = &[
"performance",
"balance-performance",
"balance_performance", // alternative form
"balance-power",
"balance_power", // alternative form
"power",
];
fn exists(path: impl AsRef<Path>) -> bool {
let path = path.as_ref();
// 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",
];
path.exists()
}
// Write a value to a sysfs file
fn write_sysfs_value(path: impl AsRef<Path>, value: &str) -> Result<()> {
let p = path.as_ref();
// Not doing any anyhow stuff here as all the calls of this ignore errors.
fn read_u64(path: impl AsRef<Path>) -> anyhow::Result<u64> {
let path = path.as_ref();
fs::write(p, value).map_err(|e| {
let error_msg = format!("Path: {:?}, Value: '{}', Error: {}", p.display(), value, e);
match e.kind() {
io::ErrorKind::PermissionDenied => ControlError::PermissionDenied(error_msg),
io::ErrorKind::NotFound => {
ControlError::PathMissing(format!("Path '{}' does not exist", p.display()))
}
_ => ControlError::WriteError(error_msg),
}
let content = fs::read_to_string(path)?;
Ok(content.trim().parse::<u64>()?)
}
fn write(path: impl AsRef<Path>, value: &str) -> anyhow::Result<()> {
let path = path.as_ref();
fs::write(path, value).with_context(|| {
format!(
"failed to write '{value}' to '{path}'",
path = path.display(),
)
})
}
pub fn get_logical_core_count() -> Result<u32> {
// Using num_cpus::get() for a reliable count of logical cores accessible.
// The monitor module's get_logical_core_count might be more specific to cpufreq-capable cores,
// 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()
)));
}
/// Get real, tunable CPUs.
pub fn get_real_cpus() -> anyhow::Result<Vec<u32>> {
const PATH: &str = "/sys/devices/system/cpu";
let entries = fs::read_dir(path)
.map_err(|_| {
ControlError::PermissionDenied(format!("Cannot read contents of {}.", path.display()))
})?
.flatten();
let mut cpus = vec![];
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 Some(name) = entry_file_name.to_str() else {
continue;
};
// Skip non-CPU directories (e.g., cpuidle, cpufreq)
if !name.starts_with("cpu") || name.len() <= 3 || !name[3..].chars().all(char::is_numeric) {
let Some(cpu_prefix_removed) = name.strip_prefix("cpu") else {
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() {
continue;
}
if name[3..].parse::<u32>().is_ok() {
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;
cpus.push(cpu);
}
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<()>
where
F: FnMut(u32) -> Result<()>,
{
let num_cores: u32 = get_logical_core_count()?;
/// Set the governor for a CPU.
pub fn set_governor(governor: &str, cpu: u32) -> anyhow::Result<()> {
let governors = get_available_governors_for(cpu);
for core_id in 0u32..num_cores {
action(core_id)?;
if !governors
.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<()> {
// Validate the governor is available on this system
// This returns both the validation result and the list of available governors
let (is_valid, available_governors) = is_governor_valid(governor)?;
if !is_valid {
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(())
}
/// Get available CPU governors for a CPU.
fn get_available_governors_for(cpu: u32) -> Vec<String> {
let Ok(content) = fs::read_to_string(format!(
"/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_available_governors"
)) else {
return Vec::new();
};
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
/// Returns a tuple of (`is_valid`, `available_governors`) to avoid redundant file reads
fn is_governor_valid(governor: &str) -> Result<(bool, Vec<String>)> {
let governors = get_available_governors()?;
// 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))
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, clap::ValueEnum)]
pub enum Turbo {
Always,
Never,
}
/// Get available CPU governors from the system
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(());
}
};
pub fn set_turbo(setting: Turbo) -> anyhow::Result<()> {
let value_boost = match setting {
TurboSetting::Always => "1", // boost = 1 means turbo is enabled
TurboSetting::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(());
}
Turbo::Always => "1", // boost = 1 means turbo is enabled.
Turbo::Never => "0", // boost = 0 means turbo is disabled.
};
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
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";
// Path priority (from most to least specific)
let pstate_path = "/sys/devices/system/cpu/intel_pstate/no_turbo";
let boost_path = "/sys/devices/system/cpu/cpufreq/boost";
let intel_boost_path_negated = "/sys/devices/system/cpu/intel_pstate/no_turbo";
let generic_boost_path = "/sys/devices/system/cpu/cpufreq/boost";
// Try each boost control path in order of specificity
if Path::new(pstate_path).exists() {
write_sysfs_value(pstate_path, value_pstate)
} 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(intel_boost_path_negated, value_boost_negated).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> {
let mut success = false;
let num_cores = get_logical_core_count()?;
for core_id in 0..num_cores {
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;
}
if write(amd_boost_path, value_boost).is_ok() {
return Ok(());
}
if write(msr_boost_path, value_boost).is_ok() {
return Ok(());
}
if write(generic_boost_path, value_boost).is_ok() {
return Ok(());
}
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
let available_epp = get_available_epp_values()?;
if !available_epp.iter().any(|v| v.eq_ignore_ascii_case(epp)) {
return Err(ControlError::InvalidValueError(format!(
"Invalid EPP value: '{}'. Available values: {}",
epp,
available_epp.join(", ")
)));
let epps = get_available_epps(cpu);
if !epps.iter().any(|avail_epp| avail_epp == epp) {
bail!(
"epp value '{epp}' is not availabile for CPU {cpu}. valid epp values: {epps}",
epps = epps.join(", "),
);
}
let action = |id: u32| {
let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/energy_performance_preference");
if Path::new(&path).exists() {
write_sysfs_value(&path, epp)
} else {
Ok(())
}
};
core_id.map_or_else(|| for_each_cpu_core(action), action)
write(
format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/energy_performance_preference"),
epp,
)
.with_context(|| {
format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing EPP")
})
}
/// Get available EPP values from the system
fn get_available_epp_values() -> Result<Vec<String>> {
let path = "/sys/devices/system/cpu/cpu0/cpufreq/energy_performance_available_preferences";
/// Get available EPP values for a CPU.
fn get_available_epps(cpu: u32) -> Vec<String> {
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() {
// 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
content
.split_whitespace()
.map(ToString::to_string)
.collect())
.collect()
}
pub fn set_epb(epb: &str, core_id: Option<u32>) -> Result<()> {
// Validate EPB value - should be a number 0-15 or a recognized string value
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(epb)?;
let action = |id: u32| {
let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/energy_performance_bias");
if Path::new(&path).exists() {
write_sysfs_value(&path, epb)
} else {
Ok(())
}
};
core_id.map_or_else(|| for_each_cpu_core(action), action)
write(
format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/energy_performance_bias"),
epb,
)
.with_context(|| {
format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing EPB")
})
}
fn validate_epb_value(epb: &str) -> Result<()> {
// EPB can be a number from 0-15 or a recognized string
// Try parsing as a number first
fn validate_epb_value(epb: &str) -> anyhow::Result<()> {
// EPB can be a number from 0-15 or a recognized string.
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 value <= 15 {
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.
// This is using case-insensitive comparison
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() {
if VALID_EPB_STRINGS.contains(&epb) {
return Ok(());
}
let max_freq_khz = read_sysfs_value_as_u32(&max_freq_path)?;
let new_min_freq_khz = new_min_freq_mhz * 1000;
bail!(
"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 {
return Err(ControlError::InvalidValueError(format!(
"Minimum frequency ({} MHz) cannot be higher than maximum frequency ({} MHz) for core {}",
new_min_freq_mhz,
max_freq_khz / 1000,
core_id
)));
pub fn set_frequency_minimum(frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> {
validate_frequency_minimum(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_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(())
}
fn validate_max_frequency(core_id: u32, new_max_freq_mhz: u32) -> Result<()> {
let min_freq_path = format!("/sys/devices/system/cpu/cpu{core_id}/cpufreq/scaling_min_freq");
if !Path::new(&min_freq_path).exists() {
fn validate_max_frequency(new_frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> {
let Ok(maximum_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(());
}
};
let min_freq_khz = read_sysfs_value_as_u32(&min_freq_path)?;
let new_max_freq_khz = new_max_freq_mhz * 1000;
if new_max_freq_khz < min_freq_khz {
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
)));
if new_frequency_mhz * 1000 > maximum_frequency_khz {
bail!(
"new maximum frequency ({new_frequency_mhz} MHz) cannot be higher than the maximum frequency ({} MHz) for CPU {cpu}",
maximum_frequency_khz / 1000,
);
}
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.
///
/// [`The Kernel docs`]: <https://docs.kernel.org/userspace-api/sysfs-platform_profile.html>
///
/// # Examples
///
/// ```
/// set_platform_profile("balanced");
/// ```
///
pub fn set_platform_profile(profile: &str) -> Result<()> {
let path = "/sys/firmware/acpi/platform_profile";
if !Path::new(path).exists() {
return Err(ControlError::NotSupported(format!(
"Platform profile control not found at {path}.",
)));
pub fn set_platform_profile(profile: &str) -> anyhow::Result<()> {
let profiles = get_platform_profiles();
if !profiles
.iter()
.any(|avail_profile| avail_profile == profile)
{
bail!(
"profile '{profile}' is not available for system. valid profiles: {profiles}",
profiles = profiles.join(", "),
);
}
let available_profiles = get_platform_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)
write("/sys/firmware/acpi/platform_profile", profile)
.context("this probably means that your system does not support changing ACPI profiles")
}
/// Returns the list of available platform profiles.
///
/// # 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>> {
/// Get the list of available platform profiles.
pub fn get_platform_profiles() -> Vec<String> {
let path = "/sys/firmware/acpi/platform_profile_choices";
if !Path::new(path).exists() {
return Err(ControlError::NotSupported(format!(
"Platform profile choices not found at {path}."
)));
}
let Ok(content) = fs::read_to_string(path) else {
return Vec::new();
};
let content = fs::read_to_string(path)
.map_err(|_| ControlError::PermissionDenied(format!("Cannot read contents of {path}.")))?;
Ok(content
content
.split_whitespace()
.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";
/// 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/xdg/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)
}
#[derive(Display, Debug, Clone, Copy, clap::ValueEnum)]
pub enum GovernorOverride {
#[display("performance")]
Performance,
#[display("powersave")]
Powersave,
#[display("reset")]
Reset,
}
pub fn set_governor_override(mode: GovernorOverride) -> anyhow::Result<()> {
let parent = Path::new(GOVERNOR_OVERRIDE_PATH).parent().unwrap();
if !parent.exists() {
fs::create_dir_all(parent).with_context(|| {
format!(
"failed to create directory '{path}'",
path = parent.display(),
)
})?;
}
match mode {
GovernorOverrideMode::Reset => {
GovernorOverride::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)
let result = fs::remove_file(GOVERNOR_OVERRIDE_PATH);
if let Err(error) = result {
if error.kind() != io::ErrorKind::NotFound {
return Err(error).with_context(|| {
format!(
"failed to delete governor override file '{GOVERNOR_OVERRIDE_PATH}'"
)
});
}
})?;
}
// Also apply the governor immediately
set_governor(&governor, None)?;
println!(
"Governor override set to '{governor}'. This setting will persist across reboots."
log::info!(
"governor override has been deleted. normal profile-based settings will be used"
);
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
pub fn get_governor_override() -> Option<String> {
if Path::new(GOVERNOR_OVERRIDE_PATH).exists() {
fs::read_to_string(GOVERNOR_OVERRIDE_PATH).ok()
} else {
None
/// Get the current governor override if set.
pub fn get_governor_override() -> anyhow::Result<Option<String>> {
match fs::read_to_string(GOVERNOR_OVERRIDE_PATH) {
Ok(governor_override) => Ok(Some(governor_override)),
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::monitor;
use crate::util::error::{AppError, ControlError};
use log::{LevelFilter, debug, error, info, warn};
use std::collections::VecDeque;
use std::fs::File;
use std::io::Write;
@ -99,7 +98,7 @@ fn compute_new(
if idle_time_seconds > 0 {
let idle_factor = idle_multiplier(idle_time_seconds);
debug!(
log::debug!(
"System idle for {} seconds (approx. {} minutes), applying idle factor: {:.2}x",
idle_time_seconds,
(idle_time_seconds as f32 / 60.0).round(),
@ -226,7 +225,7 @@ impl SystemHistory {
> 15.0)
{
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 {
// 5°C rise in temperature
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
if new_state != SystemState::Idle && new_state != SystemState::LowLoad {
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
@ -313,7 +312,7 @@ impl SystemHistory {
// Check for significant load changes
if report.system_load.load_avg_1min > 1.0 {
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
pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> {
// Set effective log level based on config and verbose flag
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...");
pub fn run_daemon(config: AppConfig) -> Result<(), AppError> {
log::info!("Starting superfreq daemon...");
// Validate critical configuration values before proceeding
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
ctrlc::set_handler(move || {
info!("Received shutdown signal, exiting...");
log::info!("Received shutdown signal, exiting...");
r.store(false, Ordering::SeqCst);
})
.map_err(|e| AppError::Generic(format!("Error setting Ctrl-C handler: {e}")))?;
info!(
log::info!(
"Daemon initialized with poll interval: {}s",
config.daemon.poll_interval_sec
);
// Set up stats file if configured
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
// 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);
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();
@ -466,7 +449,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> {
match monitor::collect_system_report(&config) {
Ok(report) => {
debug!("Collected system report, applying settings...");
log::debug!("Collected system report, applying settings...");
// Store the current state before updating history
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
if let Some(stats_path) = &config.daemon.stats_file_path {
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) {
Ok(()) => {
debug!("Successfully applied system settings");
log::debug!("Successfully applied system settings");
// If system state changed, log the new state
if system_history.current_state != previous_state {
info!(
log::info!(
"System state changed to: {:?}",
system_history.current_state
);
}
}
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
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
match optimal_interval.cmp(&current_poll_interval) {
@ -528,7 +511,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> {
}
Err(e) => {
// 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);
break;
}
@ -540,7 +523,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> {
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 {
// If adaptive polling is disabled, still apply battery-saving adjustment
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)
.min(config.daemon.max_poll_interval_sec);
debug!(
log::debug!(
"On battery power, increased poll interval to {current_poll_interval}s"
);
} else {
// Use the configured poll interval
current_poll_interval = config.daemon.poll_interval_sec.max(1);
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) => {
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);
if elapsed < poll_duration {
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);
}
}
info!("Daemon stopped");
log::info!("Daemon stopped");
Ok(())
}

View file

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

View file

@ -8,469 +8,478 @@ mod engine;
mod monitor;
mod util;
use crate::config::AppConfig;
use crate::core::{GovernorOverrideMode, TurboSetting};
use crate::util::error::{AppError, ControlError};
use clap::{Parser, value_parser};
use env_logger::Builder;
use log::{debug, error, info};
use std::error::Error;
use std::sync::Once;
use anyhow::{Context, anyhow, bail};
use clap::Parser as _;
use std::fmt::Write as _;
use std::io::Write as _;
use std::{io, process};
use yansi::Paint as _;
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
#[derive(clap::Parser, Debug)]
#[clap(author, version, about)]
struct Cli {
#[command(flatten)]
verbosity: clap_verbosity_flag::Verbosity,
#[clap(subcommand)]
command: Option<Commands>,
command: Command,
}
#[derive(Parser, Debug)]
enum Commands {
/// Display current system information
#[derive(clap::Parser, Debug)]
enum Command {
/// Display information.
Info,
/// Run as a daemon in the background
Daemon {
#[clap(long)]
verbose: bool,
},
/// Set CPU governor
SetGovernor {
governor: String,
#[clap(long)]
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
SetTurbo {
#[clap(value_enum)]
setting: TurboSetting,
},
/// Display comprehensive debug information
Debug,
/// Set Energy Performance Preference (EPP)
SetEpp {
epp: String,
#[clap(long)]
core_id: Option<u32>,
},
/// Set Energy Performance Bias (EPB)
SetEpb {
epb: String, // Typically 0-15
#[clap(long)]
core_id: Option<u32>,
},
/// Set minimum CPU frequency
SetMinFreq {
freq_mhz: u32,
#[clap(long)]
core_id: Option<u32>,
},
/// Set maximum CPU frequency
SetMaxFreq {
freq_mhz: u32,
#[clap(long)]
core_id: Option<u32>,
},
/// Set ACPI platform profile
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,
/// Start the daemon.
Start,
/// Modify attributes.
Set {
/// The CPUs to apply the changes to. When unspecified, will be applied to all CPUs.
#[arg(short = 'c', long = "for")]
for_: Option<Vec<u32>>,
/// Set the CPU governor.
#[arg(long)]
governor: Option<String>, // TODO: Validate with clap for available governors.
/// Set the CPU governor persistently.
#[arg(long, conflicts_with = "governor")]
governor_persist: Option<String>, // TODO: Validate with clap for available governors.
/// Set CPU Energy Performance Preference (EPP). Short form: --epp.
#[arg(long, alias = "epp")]
energy_performance_preference: Option<String>,
/// Set CPU Energy Performance Bias (EPB). Short form: --epb.
#[arg(long, alias = "epb")]
energy_performance_bias: Option<String>,
/// Set minimum CPU frequency in MHz. Short form: --freq-min.
#[arg(short = 'f', long, alias = "freq-min", value_parser = clap::value_parser!(u64).range(1..=10_000))]
frequency_mhz_minimum: Option<u64>,
/// Set maximum CPU frequency in MHz. Short form: --freq-max.
#[arg(short = 'F', long, alias = "freq-max", value_parser = clap::value_parser!(u64).range(1..=10_000))]
frequency_mhz_maximum: Option<u64>,
/// Set turbo boost behaviour. Has to be for all CPUs.
#[arg(long, conflicts_with = "for_")]
turbo: Option<cpu::Turbo>,
/// Set ACPI platform profile. Has to be for all CPUs.
#[arg(long, alias = "profile", conflicts_with = "for_")]
platform_profile: Option<String>,
/// Set the percentage that the power supply has to drop under for charging to start. Short form: --charge-start.
#[arg(short = 'p', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100), conflicts_with = "for_")]
charge_threshold_start: Option<u8>,
/// 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_")]
charge_threshold_end: Option<u8>,
},
}
fn main() -> Result<(), AppError> {
// Initialize logger once for the entire application
init_logger();
fn real_main() -> anyhow::Result<()> {
let cli = Cli::parse();
// Load configuration first, as it might be needed by the monitor module
// E.g., for ignored power supplies
let config = match config::load_config() {
Ok(cfg) => cfg,
Err(e) => {
error!("Error loading configuration: {e}. Using default values.");
// Proceed with default config if loading fails
AppConfig::default()
env_logger::Builder::new()
.filter_level(cli.verbosity.log_level_filter())
.format_timestamp(None)
.format_module_path(false)
.init();
let config = config::load_config().context("failed to load config")?;
match cli.command {
Command::Info => todo!(),
Command::Start => {
daemon::run_daemon(config)?;
Ok(())
}
};
let command_result: Result<(), AppError> = match cli.command {
// TODO: This will be moved to a different module in the future.
Some(Commands::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);
Command::Set {
for_,
governor,
governor_persist,
energy_performance_preference,
energy_performance_bias,
frequency_mhz_minimum,
frequency_mhz_maximum,
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
println!("{title}");
if let Some(epp) = energy_performance_preference.as_ref() {
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");
println!("CPU Model: {}", report.system_info.cpu_model);
println!("Architecture: {}", report.system_info.architecture);
println!(
"Linux Distribution: {}",
report.system_info.linux_distribution
);
if let Some(mhz_minimum) = frequency_mhz_minimum {
cpu::set_frequency_minimum(mhz_minimum, cpu)?;
}
// Format timestamp in a readable way
println!("Current Time: {}", jiff::Timestamp::now());
if let Some(mhz_maximum) = frequency_mhz_maximum {
cpu::set_frequency_maximum(mhz_maximum, cpu)?;
}
}
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",
}
);
if let Some(turbo) = turbo {
cpu::set_turbo(turbo)?;
}
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")
)
);
if let Some(platform_profile) = platform_profile.as_ref() {
cpu::set_platform_profile(platform_profile)?;
}
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
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
if charge_threshold_start >= charge_threshold_end {
bail!(
"charge start threshold (given as {charge_threshold_start}) must be less than stop threshold (given as {charge_threshold_end})"
);
}
// 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(())
battery::set_battery_charge_thresholds(
charge_threshold_start,
charge_threshold_end,
)?;
}
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(())
}
}
// 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 {
error!("Error executing command: {e}");
if let Some(source) = e.source() {
error!("Caused by: {source}");
}
let mut err = io::stderr();
// Check for permission denied errors
if let AppError::Control(control_error) = &e {
if matches!(control_error, ControlError::PermissionDenied(_)) {
error!(
"Hint: This operation may require administrator privileges (e.g., run with sudo)."
);
let mut message = String::new();
let mut chain = error.chain().rev().peekable();
while let Some(error) = chain.next() {
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(())
}
/// 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(())
}
process::exit(1);
}

View file

@ -1,8 +1,7 @@
use crate::config::AppConfig;
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 log::debug;
use std::{
collections::HashMap,
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
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()))?;
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/");
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}");
0
});
@ -551,7 +550,7 @@ pub fn get_battery_info(config: &AppConfig) -> Result<Vec<BatteryInfo>> {
if ps_type == "Battery" {
// Skip peripheral batteries that aren't real laptop batteries
if is_peripheral_battery(&ps_path, &name) {
debug!("Skipping peripheral battery: {name}");
log::debug!("Skipping peripheral battery: {name}");
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 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)