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:
parent
da07011b02
commit
87085f913b
9 changed files with 904 additions and 1054 deletions
62
Cargo.lock
generated
62
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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) = ¤t_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("; ")
|
||||
);
|
||||
|
|
28
src/core.rs
28
src/core.rs
|
@ -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,
|
||||
|
|
825
src/cpu.rs
825
src/cpu.rs
|
@ -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)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
};
|
||||
|
||||
core_id.map_or_else(|| for_each_cpu_core(action), action)
|
||||
}
|
||||
|
||||
/// 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))
|
||||
}
|
||||
|
||||
/// 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)
|
||||
if !governors
|
||||
.iter()
|
||||
.any(|avail_governor| avail_governor == governor)
|
||||
{
|
||||
continue;
|
||||
bail!(
|
||||
"governor '{governor}' is not available for CPU {cpu}. valid governors: {governors}",
|
||||
governors = governors.join(", "),
|
||||
);
|
||||
}
|
||||
|
||||
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(),
|
||||
))
|
||||
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_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(());
|
||||
}
|
||||
/// 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();
|
||||
};
|
||||
|
||||
content
|
||||
.split_whitespace()
|
||||
.map(ToString::to_string)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, clap::ValueEnum)]
|
||||
pub enum Turbo {
|
||||
Always,
|
||||
Never,
|
||||
}
|
||||
|
||||
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(());
|
||||
}
|
||||
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(());
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(success)
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Also apply the governor immediately
|
||||
set_governor(&governor, None)?;
|
||||
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}'"
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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}'")
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(¤t_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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -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" }
|
||||
);
|
||||
|
|
903
src/main.rs
903
src/main.rs
|
@ -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()
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
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(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(())
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = command_result {
|
||||
error!("Error executing command: {e}");
|
||||
if let Some(source) = e.source() {
|
||||
error!("Caused by: {source}");
|
||||
}
|
||||
|
||||
// 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)."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
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)
|
||||
env_logger::Builder::new()
|
||||
.filter_level(cli.verbosity.log_level_filter())
|
||||
.format_timestamp(None)
|
||||
.format_module_path(false)
|
||||
.init();
|
||||
|
||||
debug!("Logger initialized with RUST_LOG={env_log}");
|
||||
});
|
||||
}
|
||||
let config = config::load_config().context("failed to load config")?;
|
||||
|
||||
/// 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 {
|
||||
match cli.command {
|
||||
Command::Info => todo!(),
|
||||
|
||||
Command::Start => {
|
||||
daemon::run_daemon(config)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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()?,
|
||||
};
|
||||
|
||||
for cpu in cpus {
|
||||
if let Some(governor) = governor.as_ref() {
|
||||
cpu::set_governor(governor, cpu)?;
|
||||
}
|
||||
|
||||
if let Some(epp) = energy_performance_preference.as_ref() {
|
||||
cpu::set_epp(epp, cpu)?;
|
||||
}
|
||||
|
||||
if let Some(epb) = energy_performance_bias.as_ref() {
|
||||
cpu::set_epb(epb, cpu)?;
|
||||
}
|
||||
|
||||
if let Some(mhz_minimum) = frequency_mhz_minimum {
|
||||
cpu::set_frequency_minimum(mhz_minimum, cpu)?;
|
||||
}
|
||||
|
||||
if let Some(mhz_maximum) = frequency_mhz_maximum {
|
||||
cpu::set_frequency_maximum(mhz_maximum, cpu)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(turbo) = turbo {
|
||||
cpu::set_turbo(turbo)?;
|
||||
}
|
||||
|
||||
if let Some(platform_profile) = platform_profile.as_ref() {
|
||||
cpu::set_platform_profile(platform_profile)?;
|
||||
}
|
||||
|
||||
// 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")
|
||||
})?;
|
||||
|
||||
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})"
|
||||
);
|
||||
}
|
||||
|
||||
battery::set_battery_charge_thresholds(
|
||||
charge_threshold_start,
|
||||
charge_threshold_end,
|
||||
)?;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
let mut err = io::stderr();
|
||||
|
||||
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(),
|
||||
);
|
||||
|
||||
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}")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
process::exit(1);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue