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",
|
"clap_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap-verbosity-flag"
|
||||||
|
version = "3.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2678fade3b77aa3a8ff3aae87e9c008d3fb00473a41c71fbf74e91c8c7b37e84"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.38"
|
version = "4.5.38"
|
||||||
|
@ -131,6 +141,15 @@ version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "convert_case"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctrlc"
|
name = "ctrlc"
|
||||||
version = "3.4.7"
|
version = "3.4.7"
|
||||||
|
@ -141,6 +160,28 @@ dependencies = [
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_more"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
|
||||||
|
dependencies = [
|
||||||
|
"derive_more-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_more-impl"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
|
||||||
|
dependencies = [
|
||||||
|
"convert_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"unicode-xid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs"
|
name = "dirs"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
|
@ -453,7 +494,9 @@ version = "0.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
|
"clap-verbosity-flag",
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
|
"derive_more",
|
||||||
"dirs",
|
"dirs",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"jiff",
|
"jiff",
|
||||||
|
@ -462,6 +505,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"toml",
|
"toml",
|
||||||
|
"yansi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -542,6 +586,18 @@ version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
@ -635,3 +691,9 @@ checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yansi"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||||
|
|
|
@ -18,3 +18,6 @@ env_logger = "0.11"
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
jiff = "0.2.13"
|
jiff = "0.2.13"
|
||||||
|
clap-verbosity-flag = "3.0.2"
|
||||||
|
yansi = "1.0.1"
|
||||||
|
derive_more = { version = "2.0.1", features = ["full"] }
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::{config::types::BatteryChargeThresholds, util::error::ControlError, util::sysfs};
|
use crate::{config::types::BatteryChargeThresholds, util::error::ControlError, util::sysfs};
|
||||||
use log::{debug, warn};
|
|
||||||
use std::{
|
use std::{
|
||||||
fs, io,
|
fs, io,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
@ -118,7 +117,7 @@ fn find_supported_batteries(power_supply_path: &Path) -> Result<Vec<SupportedBat
|
||||||
let entry = match entry {
|
let entry = match entry {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to read power-supply entry: {e}");
|
log::warn!("Failed to read power-supply entry: {e}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -131,16 +130,17 @@ fn find_supported_batteries(power_supply_path: &Path) -> Result<Vec<SupportedBat
|
||||||
}
|
}
|
||||||
|
|
||||||
if supported_batteries.is_empty() {
|
if supported_batteries.is_empty() {
|
||||||
warn!("No batteries with charge threshold support found");
|
log::warn!("No batteries with charge threshold support found");
|
||||||
} else {
|
} else {
|
||||||
debug!(
|
log::debug!(
|
||||||
"Found {} batteries with threshold support",
|
"Found {} batteries with threshold support",
|
||||||
supported_batteries.len()
|
supported_batteries.len()
|
||||||
);
|
);
|
||||||
for battery in &supported_batteries {
|
for battery in &supported_batteries {
|
||||||
debug!(
|
log::debug!(
|
||||||
"Battery '{}' supports {} threshold control",
|
"Battery '{}' supports {} threshold control",
|
||||||
battery.name, battery.pattern.description
|
battery.name,
|
||||||
|
battery.pattern.description
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -173,9 +173,12 @@ fn apply_thresholds_to_batteries(
|
||||||
|
|
||||||
match start_result {
|
match start_result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
debug!(
|
log::debug!(
|
||||||
"Set {}-{}% charge thresholds for {} battery '{}'",
|
"Set {}-{}% charge thresholds for {} battery '{}'",
|
||||||
start_threshold, stop_threshold, battery.pattern.description, battery.name
|
start_threshold,
|
||||||
|
stop_threshold,
|
||||||
|
battery.pattern.description,
|
||||||
|
battery.name
|
||||||
);
|
);
|
||||||
success_count += 1;
|
success_count += 1;
|
||||||
}
|
}
|
||||||
|
@ -184,14 +187,16 @@ fn apply_thresholds_to_batteries(
|
||||||
if let Some(prev_stop) = ¤t_stop {
|
if let Some(prev_stop) = ¤t_stop {
|
||||||
let restore_result = sysfs::write_sysfs_value(&stop_path, prev_stop);
|
let restore_result = sysfs::write_sysfs_value(&stop_path, prev_stop);
|
||||||
if let Err(re) = restore_result {
|
if let Err(re) = restore_result {
|
||||||
warn!(
|
log::warn!(
|
||||||
"Failed to restore previous stop threshold for battery '{}': {}. Battery may be in an inconsistent state.",
|
"Failed to restore previous stop threshold for battery '{}': {}. Battery may be in an inconsistent state.",
|
||||||
battery.name, re
|
battery.name,
|
||||||
|
re
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
debug!(
|
log::debug!(
|
||||||
"Restored previous stop threshold ({}) for battery '{}'",
|
"Restored previous stop threshold ({}) for battery '{}'",
|
||||||
prev_stop, battery.name
|
prev_stop,
|
||||||
|
battery.name
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -212,7 +217,7 @@ fn apply_thresholds_to_batteries(
|
||||||
|
|
||||||
if success_count > 0 {
|
if success_count > 0 {
|
||||||
if !errors.is_empty() {
|
if !errors.is_empty() {
|
||||||
warn!(
|
log::warn!(
|
||||||
"Partial success setting battery thresholds: {}",
|
"Partial success setting battery thresholds: {}",
|
||||||
errors.join("; ")
|
errors.join("; ")
|
||||||
);
|
);
|
||||||
|
|
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 {
|
pub struct SystemInfo {
|
||||||
// Overall system details
|
// Overall system details
|
||||||
pub cpu_model: String,
|
pub cpu_model: String,
|
||||||
|
|
815
src/cpu.rs
815
src/cpu.rs
|
@ -1,479 +1,321 @@
|
||||||
use crate::core::{GovernorOverrideMode, TurboSetting};
|
use anyhow::{Context, bail};
|
||||||
use crate::util::error::ControlError;
|
use derive_more::Display;
|
||||||
use core::str;
|
use serde::{Deserialize, Serialize};
|
||||||
use log::debug;
|
|
||||||
use std::{fs, io, path::Path, string::ToString};
|
use std::{fs, io, path::Path, string::ToString};
|
||||||
|
|
||||||
pub type Result<T, E = ControlError> = std::result::Result<T, E>;
|
// // Valid EPP (Energy Performance Preference) string values.
|
||||||
|
// const EPP_FALLBACK_VALUES: &[&str] = &[
|
||||||
|
// "default",
|
||||||
|
// "performance",
|
||||||
|
// "balance-performance",
|
||||||
|
// "balance_performance", // Alternative form with underscore.
|
||||||
|
// "balance-power",
|
||||||
|
// "balance_power", // Alternative form with underscore.
|
||||||
|
// "power",
|
||||||
|
// ];
|
||||||
|
|
||||||
// Valid EPB string values
|
fn exists(path: impl AsRef<Path>) -> bool {
|
||||||
const VALID_EPB_STRINGS: &[&str] = &[
|
let path = path.as_ref();
|
||||||
"performance",
|
|
||||||
"balance-performance",
|
|
||||||
"balance_performance", // alternative form
|
|
||||||
"balance-power",
|
|
||||||
"balance_power", // alternative form
|
|
||||||
"power",
|
|
||||||
];
|
|
||||||
|
|
||||||
// EPP (Energy Performance Preference) string values
|
path.exists()
|
||||||
const EPP_FALLBACK_VALUES: &[&str] = &[
|
}
|
||||||
"default",
|
|
||||||
"performance",
|
|
||||||
"balance-performance",
|
|
||||||
"balance_performance", // alternative form with underscore
|
|
||||||
"balance-power",
|
|
||||||
"balance_power", // alternative form with underscore
|
|
||||||
"power",
|
|
||||||
];
|
|
||||||
|
|
||||||
// Write a value to a sysfs file
|
// Not doing any anyhow stuff here as all the calls of this ignore errors.
|
||||||
fn write_sysfs_value(path: impl AsRef<Path>, value: &str) -> Result<()> {
|
fn read_u64(path: impl AsRef<Path>) -> anyhow::Result<u64> {
|
||||||
let p = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
|
||||||
fs::write(p, value).map_err(|e| {
|
let content = fs::read_to_string(path)?;
|
||||||
let error_msg = format!("Path: {:?}, Value: '{}', Error: {}", p.display(), value, e);
|
|
||||||
match e.kind() {
|
Ok(content.trim().parse::<u64>()?)
|
||||||
io::ErrorKind::PermissionDenied => ControlError::PermissionDenied(error_msg),
|
}
|
||||||
io::ErrorKind::NotFound => {
|
|
||||||
ControlError::PathMissing(format!("Path '{}' does not exist", p.display()))
|
fn write(path: impl AsRef<Path>, value: &str) -> anyhow::Result<()> {
|
||||||
}
|
let path = path.as_ref();
|
||||||
_ => ControlError::WriteError(error_msg),
|
|
||||||
}
|
fs::write(path, value).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to write '{value}' to '{path}'",
|
||||||
|
path = path.display(),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_logical_core_count() -> Result<u32> {
|
/// Get real, tunable CPUs.
|
||||||
// Using num_cpus::get() for a reliable count of logical cores accessible.
|
pub fn get_real_cpus() -> anyhow::Result<Vec<u32>> {
|
||||||
// The monitor module's get_logical_core_count might be more specific to cpufreq-capable cores,
|
const PATH: &str = "/sys/devices/system/cpu";
|
||||||
// but for applying settings, we might want to iterate over all reported by OS.
|
|
||||||
// However, settings usually apply to cores with cpufreq.
|
|
||||||
// Let's use a similar discovery to monitor's get_logical_core_count
|
|
||||||
let mut num_cores: u32 = 0;
|
|
||||||
let path = Path::new("/sys/devices/system/cpu");
|
|
||||||
if !path.exists() {
|
|
||||||
return Err(ControlError::NotSupported(format!(
|
|
||||||
"No logical cores found at {}.",
|
|
||||||
path.display()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let entries = fs::read_dir(path)
|
let mut cpus = vec![];
|
||||||
.map_err(|_| {
|
|
||||||
ControlError::PermissionDenied(format!("Cannot read contents of {}.", path.display()))
|
|
||||||
})?
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
for entry in entries {
|
for entry in fs::read_dir(PATH)
|
||||||
|
.with_context(|| format!("failed to read contents of '{PATH}'"))?
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
let entry_file_name = entry.file_name();
|
let entry_file_name = entry.file_name();
|
||||||
|
|
||||||
let Some(name) = entry_file_name.to_str() else {
|
let Some(name) = entry_file_name.to_str() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Skip non-CPU directories (e.g., cpuidle, cpufreq)
|
let Some(cpu_prefix_removed) = name.strip_prefix("cpu") else {
|
||||||
if !name.starts_with("cpu") || name.len() <= 3 || !name[3..].chars().all(char::is_numeric) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
// Has to match "cpu{N}".
|
||||||
|
let Ok(cpu) = cpu_prefix_removed.parse::<u32>() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Has to match "cpu{N}/cpufreq".
|
||||||
if !entry.path().join("cpufreq").exists() {
|
if !entry.path().join("cpufreq").exists() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if name[3..].parse::<u32>().is_ok() {
|
cpus.push(cpu);
|
||||||
num_cores += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if num_cores == 0 {
|
|
||||||
// Fallback if sysfs iteration above fails to find any cpufreq cores
|
|
||||||
num_cores = num_cpus::get() as u32;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(num_cores)
|
// Fall back if sysfs iteration above fails to find any cpufreq CPUs.
|
||||||
|
if cpus.is_empty() {
|
||||||
|
cpus = (0..num_cpus::get() as u32).collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(cpus)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn for_each_cpu_core<F>(mut action: F) -> Result<()>
|
/// Set the governor for a CPU.
|
||||||
where
|
pub fn set_governor(governor: &str, cpu: u32) -> anyhow::Result<()> {
|
||||||
F: FnMut(u32) -> Result<()>,
|
let governors = get_available_governors_for(cpu);
|
||||||
{
|
|
||||||
let num_cores: u32 = get_logical_core_count()?;
|
|
||||||
|
|
||||||
for core_id in 0u32..num_cores {
|
if !governors
|
||||||
action(core_id)?;
|
.iter()
|
||||||
|
.any(|avail_governor| avail_governor == governor)
|
||||||
|
{
|
||||||
|
bail!(
|
||||||
|
"governor '{governor}' is not available for CPU {cpu}. valid governors: {governors}",
|
||||||
|
governors = governors.join(", "),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
|
write(
|
||||||
|
format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_governor"),
|
||||||
|
governor,
|
||||||
|
)
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"this probably means that CPU {cpu} doesn't exist or doesn't support changing governors"
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_governor(governor: &str, core_id: Option<u32>) -> Result<()> {
|
/// Get available CPU governors for a CPU.
|
||||||
// Validate the governor is available on this system
|
fn get_available_governors_for(cpu: u32) -> Vec<String> {
|
||||||
// This returns both the validation result and the list of available governors
|
let Ok(content) = fs::read_to_string(format!(
|
||||||
let (is_valid, available_governors) = is_governor_valid(governor)?;
|
"/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_available_governors"
|
||||||
|
)) else {
|
||||||
if !is_valid {
|
return Vec::new();
|
||||||
return Err(ControlError::InvalidGovernor(format!(
|
|
||||||
"Governor '{}' is not available on this system. Valid governors: {}",
|
|
||||||
governor,
|
|
||||||
available_governors.join(", ")
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let action = |id: u32| {
|
|
||||||
let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/scaling_governor");
|
|
||||||
if Path::new(&path).exists() {
|
|
||||||
write_sysfs_value(&path, governor)
|
|
||||||
} else {
|
|
||||||
// Silently ignore if the path doesn't exist for a specific core,
|
|
||||||
// as not all cores might have cpufreq (e.g. offline cores)
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
core_id.map_or_else(|| for_each_cpu_core(action), action)
|
content
|
||||||
|
.split_whitespace()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the provided governor is available in the system
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, clap::ValueEnum)]
|
||||||
/// Returns a tuple of (`is_valid`, `available_governors`) to avoid redundant file reads
|
pub enum Turbo {
|
||||||
fn is_governor_valid(governor: &str) -> Result<(bool, Vec<String>)> {
|
Always,
|
||||||
let governors = get_available_governors()?;
|
Never,
|
||||||
|
|
||||||
// Convert input governor to lowercase for case-insensitive comparison
|
|
||||||
let governor_lower = governor.to_lowercase();
|
|
||||||
|
|
||||||
// Convert all available governors to lowercase for comparison
|
|
||||||
let governors_lower: Vec<String> = governors.iter().map(|g| g.to_lowercase()).collect();
|
|
||||||
|
|
||||||
// Check if the lowercase governor is in the lowercase list
|
|
||||||
Ok((governors_lower.contains(&governor_lower), governors))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get available CPU governors from the system
|
pub fn set_turbo(setting: Turbo) -> anyhow::Result<()> {
|
||||||
fn get_available_governors() -> Result<Vec<String>> {
|
|
||||||
let cpu_base_path = Path::new("/sys/devices/system/cpu");
|
|
||||||
|
|
||||||
// First try the traditional path with cpu0. This is the most common case
|
|
||||||
// and will usually catch early, but we should try to keep the code to handle
|
|
||||||
// "edge" cases lightweight, for the (albeit smaller) number of users that
|
|
||||||
// run Superfreq on unusual systems.
|
|
||||||
let cpu0_path = "/sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors";
|
|
||||||
if Path::new(cpu0_path).exists() {
|
|
||||||
let content = fs::read_to_string(cpu0_path).map_err(|e| {
|
|
||||||
ControlError::ReadError(format!("Failed to read available governors from cpu0: {e}"))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let governors: Vec<String> = content
|
|
||||||
.split_whitespace()
|
|
||||||
.map(ToString::to_string)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if !governors.is_empty() {
|
|
||||||
return Ok(governors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If cpu0 doesn't have the file or it's empty, scan all CPUs
|
|
||||||
// This handles heterogeneous systems where cpu0 might not have cpufreq
|
|
||||||
if let Ok(entries) = fs::read_dir(cpu_base_path) {
|
|
||||||
for entry in entries.flatten() {
|
|
||||||
let path = entry.path();
|
|
||||||
let file_name = entry.file_name();
|
|
||||||
let name = match file_name.to_str() {
|
|
||||||
Some(name) => name,
|
|
||||||
None => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Skip non-CPU directories
|
|
||||||
if !name.starts_with("cpu")
|
|
||||||
|| name.len() <= 3
|
|
||||||
|| !name[3..].chars().all(char::is_numeric)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let governor_path = path.join("cpufreq/scaling_available_governors");
|
|
||||||
if governor_path.exists() {
|
|
||||||
match fs::read_to_string(&governor_path) {
|
|
||||||
Ok(content) => {
|
|
||||||
let governors: Vec<String> = content
|
|
||||||
.split_whitespace()
|
|
||||||
.map(ToString::to_string)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if !governors.is_empty() {
|
|
||||||
return Ok(governors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => continue, // try next CPU if this one fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we get here, we couldn't find any valid governors list
|
|
||||||
Err(ControlError::NotSupported(
|
|
||||||
"Could not determine available governors on any CPU".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_turbo(setting: TurboSetting) -> Result<()> {
|
|
||||||
let value_pstate = match setting {
|
|
||||||
TurboSetting::Always => "0", // no_turbo = 0 means turbo is enabled
|
|
||||||
TurboSetting::Never => "1", // no_turbo = 1 means turbo is disabled
|
|
||||||
// Auto mode is handled at the engine level, not directly at the sysfs level
|
|
||||||
TurboSetting::Auto => {
|
|
||||||
debug!("Turbo Auto mode is managed by engine logic based on system conditions");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let value_boost = match setting {
|
let value_boost = match setting {
|
||||||
TurboSetting::Always => "1", // boost = 1 means turbo is enabled
|
Turbo::Always => "1", // boost = 1 means turbo is enabled.
|
||||||
TurboSetting::Never => "0", // boost = 0 means turbo is disabled
|
Turbo::Never => "0", // boost = 0 means turbo is disabled.
|
||||||
TurboSetting::Auto => {
|
};
|
||||||
debug!("Turbo Auto mode is managed by engine logic based on system conditions");
|
|
||||||
return Ok(());
|
let value_boost_negated = match setting {
|
||||||
}
|
Turbo::Always => "0", // no_turbo = 0 means turbo is enabled.
|
||||||
|
Turbo::Never => "1", // no_turbo = 1 means turbo is disabled.
|
||||||
};
|
};
|
||||||
|
|
||||||
// AMD specific paths
|
// AMD specific paths
|
||||||
let amd_pstate_path = "/sys/devices/system/cpu/amd_pstate/cpufreq/boost";
|
let amd_boost_path = "/sys/devices/system/cpu/amd_pstate/cpufreq/boost";
|
||||||
let msr_boost_path = "/sys/devices/system/cpu/cpufreq/amd_pstate_enable_boost";
|
let msr_boost_path = "/sys/devices/system/cpu/cpufreq/amd_pstate_enable_boost";
|
||||||
|
|
||||||
// Path priority (from most to least specific)
|
// Path priority (from most to least specific)
|
||||||
let pstate_path = "/sys/devices/system/cpu/intel_pstate/no_turbo";
|
let intel_boost_path_negated = "/sys/devices/system/cpu/intel_pstate/no_turbo";
|
||||||
let boost_path = "/sys/devices/system/cpu/cpufreq/boost";
|
let generic_boost_path = "/sys/devices/system/cpu/cpufreq/boost";
|
||||||
|
|
||||||
// Try each boost control path in order of specificity
|
// Try each boost control path in order of specificity
|
||||||
if Path::new(pstate_path).exists() {
|
if write(intel_boost_path_negated, value_boost_negated).is_ok() {
|
||||||
write_sysfs_value(pstate_path, value_pstate)
|
return Ok(());
|
||||||
} else if Path::new(amd_pstate_path).exists() {
|
|
||||||
write_sysfs_value(amd_pstate_path, value_boost)
|
|
||||||
} else if Path::new(msr_boost_path).exists() {
|
|
||||||
write_sysfs_value(msr_boost_path, value_boost)
|
|
||||||
} else if Path::new(boost_path).exists() {
|
|
||||||
write_sysfs_value(boost_path, value_boost)
|
|
||||||
} else {
|
|
||||||
// Also try per-core cpufreq boost for some AMD systems
|
|
||||||
let result = try_set_per_core_boost(value_boost)?;
|
|
||||||
if result {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(ControlError::NotSupported(
|
|
||||||
"No supported CPU boost control mechanism found.".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
if write(amd_boost_path, value_boost).is_ok() {
|
||||||
|
return Ok(());
|
||||||
/// Try to set boost on a per-core basis for systems that support it
|
}
|
||||||
fn try_set_per_core_boost(value: &str) -> Result<bool> {
|
if write(msr_boost_path, value_boost).is_ok() {
|
||||||
let mut success = false;
|
return Ok(());
|
||||||
let num_cores = get_logical_core_count()?;
|
}
|
||||||
|
if write(generic_boost_path, value_boost).is_ok() {
|
||||||
for core_id in 0..num_cores {
|
return Ok(());
|
||||||
let boost_path = format!("/sys/devices/system/cpu/cpu{core_id}/cpufreq/boost");
|
|
||||||
|
|
||||||
if Path::new(&boost_path).exists() {
|
|
||||||
write_sysfs_value(&boost_path, value)?;
|
|
||||||
success = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(success)
|
// Also try per-core cpufreq boost for some AMD systems.
|
||||||
|
if get_real_cpus()?.iter().any(|cpu| {
|
||||||
|
write(
|
||||||
|
&format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/boost"),
|
||||||
|
value_boost,
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
}) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
bail!("no supported CPU boost control mechanism found");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_epp(epp: &str, core_id: Option<u32>) -> Result<()> {
|
pub fn set_epp(epp: &str, cpu: u32) -> anyhow::Result<()> {
|
||||||
// Validate the EPP value against available options
|
// Validate the EPP value against available options
|
||||||
let available_epp = get_available_epp_values()?;
|
let epps = get_available_epps(cpu);
|
||||||
if !available_epp.iter().any(|v| v.eq_ignore_ascii_case(epp)) {
|
|
||||||
return Err(ControlError::InvalidValueError(format!(
|
if !epps.iter().any(|avail_epp| avail_epp == epp) {
|
||||||
"Invalid EPP value: '{}'. Available values: {}",
|
bail!(
|
||||||
epp,
|
"epp value '{epp}' is not availabile for CPU {cpu}. valid epp values: {epps}",
|
||||||
available_epp.join(", ")
|
epps = epps.join(", "),
|
||||||
)));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let action = |id: u32| {
|
write(
|
||||||
let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/energy_performance_preference");
|
format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/energy_performance_preference"),
|
||||||
if Path::new(&path).exists() {
|
epp,
|
||||||
write_sysfs_value(&path, epp)
|
)
|
||||||
} else {
|
.with_context(|| {
|
||||||
Ok(())
|
format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing EPP")
|
||||||
}
|
})
|
||||||
};
|
|
||||||
core_id.map_or_else(|| for_each_cpu_core(action), action)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get available EPP values from the system
|
/// Get available EPP values for a CPU.
|
||||||
fn get_available_epp_values() -> Result<Vec<String>> {
|
fn get_available_epps(cpu: u32) -> Vec<String> {
|
||||||
let path = "/sys/devices/system/cpu/cpu0/cpufreq/energy_performance_available_preferences";
|
let Ok(content) = fs::read_to_string(format!(
|
||||||
|
"/sys/devices/system/cpu/cpu{cpu}/cpufreq/energy_performance_available_preferences"
|
||||||
|
)) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
if !Path::new(path).exists() {
|
content
|
||||||
// If the file doesn't exist, fall back to a default set of common values
|
|
||||||
// This is safer than failing outright, as some systems may allow these values │
|
|
||||||
// even without explicitly listing them
|
|
||||||
return Ok(EPP_FALLBACK_VALUES.iter().map(|&s| s.to_string()).collect());
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = fs::read_to_string(path).map_err(|e| {
|
|
||||||
ControlError::ReadError(format!("Failed to read available EPP values: {e}"))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(content
|
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
.map(ToString::to_string)
|
.map(ToString::to_string)
|
||||||
.collect())
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_epb(epb: &str, core_id: Option<u32>) -> Result<()> {
|
pub fn set_epb(epb: &str, cpu: u32) -> anyhow::Result<()> {
|
||||||
// Validate EPB value - should be a number 0-15 or a recognized string value
|
// Validate EPB value - should be a number 0-15 or a recognized string value.
|
||||||
validate_epb_value(epb)?;
|
validate_epb_value(epb)?;
|
||||||
|
|
||||||
let action = |id: u32| {
|
write(
|
||||||
let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/energy_performance_bias");
|
format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/energy_performance_bias"),
|
||||||
if Path::new(&path).exists() {
|
epb,
|
||||||
write_sysfs_value(&path, epb)
|
)
|
||||||
} else {
|
.with_context(|| {
|
||||||
Ok(())
|
format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing EPB")
|
||||||
}
|
})
|
||||||
};
|
|
||||||
core_id.map_or_else(|| for_each_cpu_core(action), action)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_epb_value(epb: &str) -> Result<()> {
|
fn validate_epb_value(epb: &str) -> anyhow::Result<()> {
|
||||||
// EPB can be a number from 0-15 or a recognized string
|
// EPB can be a number from 0-15 or a recognized string.
|
||||||
// Try parsing as a number first
|
|
||||||
|
const VALID_EPB_STRINGS: &[&str] = &[
|
||||||
|
"performance",
|
||||||
|
"balance-performance",
|
||||||
|
"balance_performance", // Alternative form with underscore.
|
||||||
|
"balance-power",
|
||||||
|
"balance_power", // Alternative form with underscore.
|
||||||
|
"power",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Try parsing as a number first.
|
||||||
if let Ok(value) = epb.parse::<u8>() {
|
if let Ok(value) = epb.parse::<u8>() {
|
||||||
if value <= 15 {
|
if value <= 15 {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
return Err(ControlError::InvalidValueError(format!(
|
|
||||||
"EPB numeric value must be between 0 and 15, got {value}"
|
bail!("EPB numeric value must be between 0 and 15, got {value}");
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not a number, check if it's a recognized string value.
|
// If not a number, check if it's a recognized string value.
|
||||||
// This is using case-insensitive comparison
|
if VALID_EPB_STRINGS.contains(&epb) {
|
||||||
if VALID_EPB_STRINGS
|
|
||||||
.iter()
|
|
||||||
.any(|valid| valid.eq_ignore_ascii_case(epb))
|
|
||||||
{
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(ControlError::InvalidValueError(format!(
|
|
||||||
"Invalid EPB value: '{}'. Must be a number 0-15 or one of: {}",
|
|
||||||
epb,
|
|
||||||
VALID_EPB_STRINGS.join(", ")
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_min_frequency(freq_mhz: u32, core_id: Option<u32>) -> Result<()> {
|
|
||||||
// Check if the new minimum frequency would be greater than current maximum
|
|
||||||
if let Some(id) = core_id {
|
|
||||||
validate_min_frequency(id, freq_mhz)?;
|
|
||||||
} else {
|
|
||||||
// Check for all cores
|
|
||||||
let num_cores = get_logical_core_count()?;
|
|
||||||
for id in 0..num_cores {
|
|
||||||
validate_min_frequency(id, freq_mhz)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// XXX: We use u64 for the intermediate calculation to prevent overflow
|
|
||||||
let freq_khz = u64::from(freq_mhz) * 1000;
|
|
||||||
let freq_khz_str = freq_khz.to_string();
|
|
||||||
|
|
||||||
let action = |id: u32| {
|
|
||||||
let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/scaling_min_freq");
|
|
||||||
if Path::new(&path).exists() {
|
|
||||||
write_sysfs_value(&path, &freq_khz_str)
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
core_id.map_or_else(|| for_each_cpu_core(action), action)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_max_frequency(freq_mhz: u32, core_id: Option<u32>) -> Result<()> {
|
|
||||||
// Check if the new maximum frequency would be less than current minimum
|
|
||||||
if let Some(id) = core_id {
|
|
||||||
validate_max_frequency(id, freq_mhz)?;
|
|
||||||
} else {
|
|
||||||
// Check for all cores
|
|
||||||
let num_cores = get_logical_core_count()?;
|
|
||||||
for id in 0..num_cores {
|
|
||||||
validate_max_frequency(id, freq_mhz)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// XXX: Use a u64 here as well.
|
|
||||||
let freq_khz = u64::from(freq_mhz) * 1000;
|
|
||||||
let freq_khz_str = freq_khz.to_string();
|
|
||||||
|
|
||||||
let action = |id: u32| {
|
|
||||||
let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/scaling_max_freq");
|
|
||||||
if Path::new(&path).exists() {
|
|
||||||
write_sysfs_value(&path, &freq_khz_str)
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
core_id.map_or_else(|| for_each_cpu_core(action), action)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_sysfs_value_as_u32(path: &str) -> Result<u32> {
|
|
||||||
if !Path::new(path).exists() {
|
|
||||||
return Err(ControlError::NotSupported(format!(
|
|
||||||
"File does not exist: {path}"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = fs::read_to_string(path)
|
|
||||||
.map_err(|e| ControlError::ReadError(format!("Failed to read {path}: {e}")))?;
|
|
||||||
|
|
||||||
content
|
|
||||||
.trim()
|
|
||||||
.parse::<u32>()
|
|
||||||
.map_err(|e| ControlError::ParseError(format!("Failed to parse value from {path}: {e}")))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_min_frequency(core_id: u32, new_min_freq_mhz: u32) -> Result<()> {
|
|
||||||
let max_freq_path = format!("/sys/devices/system/cpu/cpu{core_id}/cpufreq/scaling_max_freq");
|
|
||||||
|
|
||||||
if !Path::new(&max_freq_path).exists() {
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let max_freq_khz = read_sysfs_value_as_u32(&max_freq_path)?;
|
bail!(
|
||||||
let new_min_freq_khz = new_min_freq_mhz * 1000;
|
"invalid EPB value: '{epb}'. must be a number between 0-15 inclusive or one of: {valid}",
|
||||||
|
valid = VALID_EPB_STRINGS.join(", "),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if new_min_freq_khz > max_freq_khz {
|
pub fn set_frequency_minimum(frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> {
|
||||||
return Err(ControlError::InvalidValueError(format!(
|
validate_frequency_minimum(frequency_mhz, cpu)?;
|
||||||
"Minimum frequency ({} MHz) cannot be higher than maximum frequency ({} MHz) for core {}",
|
|
||||||
new_min_freq_mhz,
|
// We use u64 for the intermediate calculation to prevent overflow
|
||||||
max_freq_khz / 1000,
|
let frequency_khz = u64::from(frequency_mhz) * 1000;
|
||||||
core_id
|
let frequency_khz = frequency_khz.to_string();
|
||||||
)));
|
|
||||||
|
write(
|
||||||
|
format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_min_freq"),
|
||||||
|
&frequency_khz,
|
||||||
|
)
|
||||||
|
.with_context(|| {
|
||||||
|
format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing minimum frequency")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_frequency_maximum(frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> {
|
||||||
|
validate_max_frequency(frequency_mhz, cpu)?;
|
||||||
|
|
||||||
|
// We use u64 for the intermediate calculation to prevent overflow
|
||||||
|
let frequency_khz = u64::from(frequency_mhz) * 1000;
|
||||||
|
let frequency_khz = frequency_khz.to_string();
|
||||||
|
|
||||||
|
write(
|
||||||
|
format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_max_freq"),
|
||||||
|
&frequency_khz,
|
||||||
|
)
|
||||||
|
.with_context(|| {
|
||||||
|
format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing maximum frequency")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_frequency_minimum(new_frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> {
|
||||||
|
let Ok(minimum_frequency_khz) = read_u64(format!(
|
||||||
|
"/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_min_freq"
|
||||||
|
)) else {
|
||||||
|
// Just let it pass if we can't find anything.
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
if new_frequency_mhz as u64 * 1000 < minimum_frequency_khz {
|
||||||
|
bail!(
|
||||||
|
"new minimum frequency ({new_frequency_mhz} MHz) cannot be lower than the minimum frequency ({} MHz) for CPU {cpu}",
|
||||||
|
minimum_frequency_khz / 1000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_max_frequency(core_id: u32, new_max_freq_mhz: u32) -> Result<()> {
|
fn validate_max_frequency(new_frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> {
|
||||||
let min_freq_path = format!("/sys/devices/system/cpu/cpu{core_id}/cpufreq/scaling_min_freq");
|
let Ok(maximum_frequency_khz) = read_u64(format!(
|
||||||
|
"/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_min_freq"
|
||||||
if !Path::new(&min_freq_path).exists() {
|
)) else {
|
||||||
|
// Just let it pass if we can't find anything.
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
};
|
||||||
|
|
||||||
let min_freq_khz = read_sysfs_value_as_u32(&min_freq_path)?;
|
if new_frequency_mhz * 1000 > maximum_frequency_khz {
|
||||||
let new_max_freq_khz = new_max_freq_mhz * 1000;
|
bail!(
|
||||||
|
"new maximum frequency ({new_frequency_mhz} MHz) cannot be higher than the maximum frequency ({} MHz) for CPU {cpu}",
|
||||||
if new_max_freq_khz < min_freq_khz {
|
maximum_frequency_khz / 1000,
|
||||||
return Err(ControlError::InvalidValueError(format!(
|
);
|
||||||
"Maximum frequency ({} MHz) cannot be lower than minimum frequency ({} MHz) for core {}",
|
|
||||||
new_max_freq_mhz,
|
|
||||||
min_freq_khz / 1000,
|
|
||||||
core_id
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -485,137 +327,108 @@ fn validate_max_frequency(core_id: u32, new_max_freq_mhz: u32) -> Result<()> {
|
||||||
/// Also see [`The Kernel docs`] for this.
|
/// Also see [`The Kernel docs`] for this.
|
||||||
///
|
///
|
||||||
/// [`The Kernel docs`]: <https://docs.kernel.org/userspace-api/sysfs-platform_profile.html>
|
/// [`The Kernel docs`]: <https://docs.kernel.org/userspace-api/sysfs-platform_profile.html>
|
||||||
///
|
pub fn set_platform_profile(profile: &str) -> anyhow::Result<()> {
|
||||||
/// # Examples
|
let profiles = get_platform_profiles();
|
||||||
///
|
|
||||||
/// ```
|
if !profiles
|
||||||
/// set_platform_profile("balanced");
|
.iter()
|
||||||
/// ```
|
.any(|avail_profile| avail_profile == profile)
|
||||||
///
|
{
|
||||||
pub fn set_platform_profile(profile: &str) -> Result<()> {
|
bail!(
|
||||||
let path = "/sys/firmware/acpi/platform_profile";
|
"profile '{profile}' is not available for system. valid profiles: {profiles}",
|
||||||
if !Path::new(path).exists() {
|
profiles = profiles.join(", "),
|
||||||
return Err(ControlError::NotSupported(format!(
|
);
|
||||||
"Platform profile control not found at {path}.",
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let available_profiles = get_platform_profiles()?;
|
write("/sys/firmware/acpi/platform_profile", profile)
|
||||||
|
.context("this probably means that your system does not support changing ACPI profiles")
|
||||||
if !available_profiles.contains(&profile.to_string()) {
|
|
||||||
return Err(ControlError::InvalidProfile(format!(
|
|
||||||
"Invalid platform control profile provided.\n\
|
|
||||||
Provided profile: {} \n\
|
|
||||||
Available profiles:\n\
|
|
||||||
{}",
|
|
||||||
profile,
|
|
||||||
available_profiles.join(", ")
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
write_sysfs_value(path, profile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the list of available platform profiles.
|
/// Get the list of available platform profiles.
|
||||||
///
|
pub fn get_platform_profiles() -> Vec<String> {
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// - [`ControlError::NotSupported`] if:
|
|
||||||
/// - The file `/sys/firmware/acpi/platform_profile_choices` does not exist.
|
|
||||||
/// - The file `/sys/firmware/acpi/platform_profile_choices` is empty.
|
|
||||||
///
|
|
||||||
/// - [`ControlError::PermissionDenied`] if the file `/sys/firmware/acpi/platform_profile_choices` cannot be read.
|
|
||||||
///
|
|
||||||
pub fn get_platform_profiles() -> Result<Vec<String>> {
|
|
||||||
let path = "/sys/firmware/acpi/platform_profile_choices";
|
let path = "/sys/firmware/acpi/platform_profile_choices";
|
||||||
|
|
||||||
if !Path::new(path).exists() {
|
let Ok(content) = fs::read_to_string(path) else {
|
||||||
return Err(ControlError::NotSupported(format!(
|
return Vec::new();
|
||||||
"Platform profile choices not found at {path}."
|
};
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = fs::read_to_string(path)
|
content
|
||||||
.map_err(|_| ControlError::PermissionDenied(format!("Cannot read contents of {path}.")))?;
|
|
||||||
|
|
||||||
Ok(content
|
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
.map(ToString::to_string)
|
.map(ToString::to_string)
|
||||||
.collect())
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Path for storing the governor override state
|
/// Path for storing the governor override state.
|
||||||
const GOVERNOR_OVERRIDE_PATH: &str = "/etc/xdg/superfreq/governor_override";
|
const GOVERNOR_OVERRIDE_PATH: &str = "/etc/xdg/superfreq/governor_override";
|
||||||
|
|
||||||
/// Force a specific CPU governor or reset to automatic mode
|
#[derive(Display, Debug, Clone, Copy, clap::ValueEnum)]
|
||||||
pub fn force_governor(mode: GovernorOverrideMode) -> Result<()> {
|
pub enum GovernorOverride {
|
||||||
// Create directory if it doesn't exist
|
#[display("performance")]
|
||||||
let dir_path = Path::new("/etc/xdg/superfreq");
|
Performance,
|
||||||
if !dir_path.exists() {
|
#[display("powersave")]
|
||||||
fs::create_dir_all(dir_path).map_err(|e| {
|
Powersave,
|
||||||
if e.kind() == io::ErrorKind::PermissionDenied {
|
#[display("reset")]
|
||||||
ControlError::PermissionDenied(format!(
|
Reset,
|
||||||
"Permission denied creating directory: {}. Try running with sudo.",
|
}
|
||||||
dir_path.display()
|
|
||||||
))
|
pub fn set_governor_override(mode: GovernorOverride) -> anyhow::Result<()> {
|
||||||
} else {
|
let parent = Path::new(GOVERNOR_OVERRIDE_PATH).parent().unwrap();
|
||||||
ControlError::Io(e)
|
if !parent.exists() {
|
||||||
}
|
fs::create_dir_all(parent).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to create directory '{path}'",
|
||||||
|
path = parent.display(),
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
match mode {
|
match mode {
|
||||||
GovernorOverrideMode::Reset => {
|
GovernorOverride::Reset => {
|
||||||
// Remove the override file if it exists
|
// Remove the override file if it exists
|
||||||
if Path::new(GOVERNOR_OVERRIDE_PATH).exists() {
|
let result = fs::remove_file(GOVERNOR_OVERRIDE_PATH);
|
||||||
fs::remove_file(GOVERNOR_OVERRIDE_PATH).map_err(|e| {
|
|
||||||
if e.kind() == io::ErrorKind::PermissionDenied {
|
if let Err(error) = result {
|
||||||
ControlError::PermissionDenied(format!(
|
if error.kind() != io::ErrorKind::NotFound {
|
||||||
"Permission denied removing override file: {GOVERNOR_OVERRIDE_PATH}. Try running with sudo."
|
return Err(error).with_context(|| {
|
||||||
))
|
format!(
|
||||||
} else {
|
"failed to delete governor override file '{GOVERNOR_OVERRIDE_PATH}'"
|
||||||
ControlError::Io(e)
|
)
|
||||||
}
|
});
|
||||||
})?;
|
|
||||||
println!(
|
|
||||||
"Governor override has been reset. Normal profile-based settings will be used."
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
println!("No governor override was set.");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
GovernorOverrideMode::Performance | GovernorOverrideMode::Powersave => {
|
|
||||||
// Create the override file with the selected governor
|
|
||||||
let governor = mode.to_string().to_lowercase();
|
|
||||||
fs::write(GOVERNOR_OVERRIDE_PATH, &governor).map_err(|e| {
|
|
||||||
if e.kind() == io::ErrorKind::PermissionDenied {
|
|
||||||
ControlError::PermissionDenied(format!(
|
|
||||||
"Permission denied writing to override file: {GOVERNOR_OVERRIDE_PATH}. Try running with sudo."
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
ControlError::Io(e)
|
|
||||||
}
|
}
|
||||||
})?;
|
}
|
||||||
|
|
||||||
// Also apply the governor immediately
|
log::info!(
|
||||||
set_governor(&governor, None)?;
|
"governor override has been deleted. normal profile-based settings will be used"
|
||||||
|
|
||||||
println!(
|
|
||||||
"Governor override set to '{governor}'. This setting will persist across reboots."
|
|
||||||
);
|
);
|
||||||
println!("To reset, use: superfreq force-governor reset");
|
}
|
||||||
Ok(())
|
|
||||||
|
GovernorOverride::Performance | GovernorOverride::Powersave => {
|
||||||
|
let governor = mode.to_string();
|
||||||
|
|
||||||
|
write(GOVERNOR_OVERRIDE_PATH, &governor)
|
||||||
|
.context("failed to write governor override")?;
|
||||||
|
|
||||||
|
// TODO: Apply the setting too.
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"governor override set to '{governor}'. this setting will persist across reboots"
|
||||||
|
);
|
||||||
|
log::info!("to reset, run: superfreq set --governor-persist reset");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current governor override if set
|
/// Get the current governor override if set.
|
||||||
pub fn get_governor_override() -> Option<String> {
|
pub fn get_governor_override() -> anyhow::Result<Option<String>> {
|
||||||
if Path::new(GOVERNOR_OVERRIDE_PATH).exists() {
|
match fs::read_to_string(GOVERNOR_OVERRIDE_PATH) {
|
||||||
fs::read_to_string(GOVERNOR_OVERRIDE_PATH).ok()
|
Ok(governor_override) => Ok(Some(governor_override)),
|
||||||
} else {
|
|
||||||
None
|
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
|
||||||
|
|
||||||
|
Err(error) => Err(error).with_context(|| {
|
||||||
|
format!("failed to read governor override at '{GOVERNOR_OVERRIDE_PATH}'")
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ use crate::core::SystemReport;
|
||||||
use crate::engine;
|
use crate::engine;
|
||||||
use crate::monitor;
|
use crate::monitor;
|
||||||
use crate::util::error::{AppError, ControlError};
|
use crate::util::error::{AppError, ControlError};
|
||||||
use log::{LevelFilter, debug, error, info, warn};
|
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
@ -99,7 +98,7 @@ fn compute_new(
|
||||||
if idle_time_seconds > 0 {
|
if idle_time_seconds > 0 {
|
||||||
let idle_factor = idle_multiplier(idle_time_seconds);
|
let idle_factor = idle_multiplier(idle_time_seconds);
|
||||||
|
|
||||||
debug!(
|
log::debug!(
|
||||||
"System idle for {} seconds (approx. {} minutes), applying idle factor: {:.2}x",
|
"System idle for {} seconds (approx. {} minutes), applying idle factor: {:.2}x",
|
||||||
idle_time_seconds,
|
idle_time_seconds,
|
||||||
(idle_time_seconds as f32 / 60.0).round(),
|
(idle_time_seconds as f32 / 60.0).round(),
|
||||||
|
@ -226,7 +225,7 @@ impl SystemHistory {
|
||||||
> 15.0)
|
> 15.0)
|
||||||
{
|
{
|
||||||
self.last_user_activity = Instant::now();
|
self.last_user_activity = Instant::now();
|
||||||
debug!("User activity detected based on CPU usage");
|
log::debug!("User activity detected based on CPU usage");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -245,7 +244,7 @@ impl SystemHistory {
|
||||||
if temp_change > 5.0 {
|
if temp_change > 5.0 {
|
||||||
// 5°C rise in temperature
|
// 5°C rise in temperature
|
||||||
self.last_user_activity = Instant::now();
|
self.last_user_activity = Instant::now();
|
||||||
debug!("User activity detected based on temperature change");
|
log::debug!("User activity detected based on temperature change");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -302,7 +301,7 @@ impl SystemHistory {
|
||||||
// State changes (except to Idle) likely indicate user activity
|
// State changes (except to Idle) likely indicate user activity
|
||||||
if new_state != SystemState::Idle && new_state != SystemState::LowLoad {
|
if new_state != SystemState::Idle && new_state != SystemState::LowLoad {
|
||||||
self.last_user_activity = Instant::now();
|
self.last_user_activity = Instant::now();
|
||||||
debug!("User activity detected based on system state change to {new_state:?}");
|
log::debug!("User activity detected based on system state change to {new_state:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
|
@ -313,7 +312,7 @@ impl SystemHistory {
|
||||||
// Check for significant load changes
|
// Check for significant load changes
|
||||||
if report.system_load.load_avg_1min > 1.0 {
|
if report.system_load.load_avg_1min > 1.0 {
|
||||||
self.last_user_activity = Instant::now();
|
self.last_user_activity = Instant::now();
|
||||||
debug!("User activity detected based on system load");
|
log::debug!("User activity detected based on system load");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -402,26 +401,8 @@ fn validate_poll_intervals(min_interval: u64, max_interval: u64) -> Result<(), C
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the daemon
|
/// Run the daemon
|
||||||
pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> {
|
pub fn run_daemon(config: AppConfig) -> Result<(), AppError> {
|
||||||
// Set effective log level based on config and verbose flag
|
log::info!("Starting superfreq daemon...");
|
||||||
let effective_log_level = if verbose {
|
|
||||||
LogLevel::Debug
|
|
||||||
} else {
|
|
||||||
config.daemon.log_level
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the appropriate level filter
|
|
||||||
let level_filter = match effective_log_level {
|
|
||||||
LogLevel::Error => LevelFilter::Error,
|
|
||||||
LogLevel::Warning => LevelFilter::Warn,
|
|
||||||
LogLevel::Info => LevelFilter::Info,
|
|
||||||
LogLevel::Debug => LevelFilter::Debug,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update the log level filter if needed, without re-initializing the logger
|
|
||||||
log::set_max_level(level_filter);
|
|
||||||
|
|
||||||
info!("Starting superfreq daemon...");
|
|
||||||
|
|
||||||
// Validate critical configuration values before proceeding
|
// Validate critical configuration values before proceeding
|
||||||
if let Err(err) = validate_poll_intervals(
|
if let Err(err) = validate_poll_intervals(
|
||||||
|
@ -437,26 +418,28 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> {
|
||||||
|
|
||||||
// Set up signal handlers
|
// Set up signal handlers
|
||||||
ctrlc::set_handler(move || {
|
ctrlc::set_handler(move || {
|
||||||
info!("Received shutdown signal, exiting...");
|
log::info!("Received shutdown signal, exiting...");
|
||||||
r.store(false, Ordering::SeqCst);
|
r.store(false, Ordering::SeqCst);
|
||||||
})
|
})
|
||||||
.map_err(|e| AppError::Generic(format!("Error setting Ctrl-C handler: {e}")))?;
|
.map_err(|e| AppError::Generic(format!("Error setting Ctrl-C handler: {e}")))?;
|
||||||
|
|
||||||
info!(
|
log::info!(
|
||||||
"Daemon initialized with poll interval: {}s",
|
"Daemon initialized with poll interval: {}s",
|
||||||
config.daemon.poll_interval_sec
|
config.daemon.poll_interval_sec
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set up stats file if configured
|
// Set up stats file if configured
|
||||||
if let Some(stats_path) = &config.daemon.stats_file_path {
|
if let Some(stats_path) = &config.daemon.stats_file_path {
|
||||||
info!("Stats will be written to: {stats_path}");
|
log::info!("Stats will be written to: {stats_path}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Variables for adaptive polling
|
// Variables for adaptive polling
|
||||||
// Make sure that the poll interval is *never* zero to prevent a busy loop
|
// Make sure that the poll interval is *never* zero to prevent a busy loop
|
||||||
let mut current_poll_interval = config.daemon.poll_interval_sec.max(1);
|
let mut current_poll_interval = config.daemon.poll_interval_sec.max(1);
|
||||||
if config.daemon.poll_interval_sec == 0 {
|
if config.daemon.poll_interval_sec == 0 {
|
||||||
warn!("Poll interval is set to zero in config, using 1s minimum to prevent a busy loop");
|
log::warn!(
|
||||||
|
"Poll interval is set to zero in config, using 1s minimum to prevent a busy loop"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let mut system_history = SystemHistory::default();
|
let mut system_history = SystemHistory::default();
|
||||||
|
|
||||||
|
@ -466,7 +449,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> {
|
||||||
|
|
||||||
match monitor::collect_system_report(&config) {
|
match monitor::collect_system_report(&config) {
|
||||||
Ok(report) => {
|
Ok(report) => {
|
||||||
debug!("Collected system report, applying settings...");
|
log::debug!("Collected system report, applying settings...");
|
||||||
|
|
||||||
// Store the current state before updating history
|
// Store the current state before updating history
|
||||||
let previous_state = system_history.current_state.clone();
|
let previous_state = system_history.current_state.clone();
|
||||||
|
@ -477,24 +460,24 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> {
|
||||||
// Update the stats file if configured
|
// Update the stats file if configured
|
||||||
if let Some(stats_path) = &config.daemon.stats_file_path {
|
if let Some(stats_path) = &config.daemon.stats_file_path {
|
||||||
if let Err(e) = write_stats_file(stats_path, &report) {
|
if let Err(e) = write_stats_file(stats_path, &report) {
|
||||||
error!("Failed to write stats file: {e}");
|
log::error!("Failed to write stats file: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match engine::determine_and_apply_settings(&report, &config, None) {
|
match engine::determine_and_apply_settings(&report, &config, None) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
debug!("Successfully applied system settings");
|
log::debug!("Successfully applied system settings");
|
||||||
|
|
||||||
// If system state changed, log the new state
|
// If system state changed, log the new state
|
||||||
if system_history.current_state != previous_state {
|
if system_history.current_state != previous_state {
|
||||||
info!(
|
log::info!(
|
||||||
"System state changed to: {:?}",
|
"System state changed to: {:?}",
|
||||||
system_history.current_state
|
system_history.current_state
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error applying system settings: {e}");
|
log::error!("Error applying system settings: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -509,7 +492,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> {
|
||||||
// Store the new interval
|
// Store the new interval
|
||||||
system_history.last_computed_interval = Some(optimal_interval);
|
system_history.last_computed_interval = Some(optimal_interval);
|
||||||
|
|
||||||
debug!("Recalculated optimal interval: {optimal_interval}s");
|
log::debug!("Recalculated optimal interval: {optimal_interval}s");
|
||||||
|
|
||||||
// Don't change the interval too dramatically at once
|
// Don't change the interval too dramatically at once
|
||||||
match optimal_interval.cmp(¤t_poll_interval) {
|
match optimal_interval.cmp(¤t_poll_interval) {
|
||||||
|
@ -528,7 +511,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> {
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Log the error and stop the daemon when an invalid configuration is detected
|
// Log the error and stop the daemon when an invalid configuration is detected
|
||||||
error!("Critical configuration error: {e}");
|
log::error!("Critical configuration error: {e}");
|
||||||
running.store(false, Ordering::SeqCst);
|
running.store(false, Ordering::SeqCst);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -540,7 +523,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> {
|
||||||
config.daemon.max_poll_interval_sec,
|
config.daemon.max_poll_interval_sec,
|
||||||
);
|
);
|
||||||
|
|
||||||
debug!("Adaptive polling: set interval to {current_poll_interval}s");
|
log::debug!("Adaptive polling: set interval to {current_poll_interval}s");
|
||||||
} else {
|
} else {
|
||||||
// If adaptive polling is disabled, still apply battery-saving adjustment
|
// If adaptive polling is disabled, still apply battery-saving adjustment
|
||||||
if config.daemon.throttle_on_battery && on_battery {
|
if config.daemon.throttle_on_battery && on_battery {
|
||||||
|
@ -552,20 +535,22 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> {
|
||||||
current_poll_interval = (safe_interval * battery_multiplier)
|
current_poll_interval = (safe_interval * battery_multiplier)
|
||||||
.min(config.daemon.max_poll_interval_sec);
|
.min(config.daemon.max_poll_interval_sec);
|
||||||
|
|
||||||
debug!(
|
log::debug!(
|
||||||
"On battery power, increased poll interval to {current_poll_interval}s"
|
"On battery power, increased poll interval to {current_poll_interval}s"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Use the configured poll interval
|
// Use the configured poll interval
|
||||||
current_poll_interval = config.daemon.poll_interval_sec.max(1);
|
current_poll_interval = config.daemon.poll_interval_sec.max(1);
|
||||||
if config.daemon.poll_interval_sec == 0 {
|
if config.daemon.poll_interval_sec == 0 {
|
||||||
debug!("Using minimum poll interval of 1s instead of configured 0s");
|
log::debug!(
|
||||||
|
"Using minimum poll interval of 1s instead of configured 0s"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error collecting system report: {e}");
|
log::error!("Error collecting system report: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -574,12 +559,12 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> {
|
||||||
let poll_duration = Duration::from_secs(current_poll_interval);
|
let poll_duration = Duration::from_secs(current_poll_interval);
|
||||||
if elapsed < poll_duration {
|
if elapsed < poll_duration {
|
||||||
let sleep_time = poll_duration - elapsed;
|
let sleep_time = poll_duration - elapsed;
|
||||||
debug!("Sleeping for {}s until next cycle", sleep_time.as_secs());
|
log::debug!("Sleeping for {}s until next cycle", sleep_time.as_secs());
|
||||||
std::thread::sleep(sleep_time);
|
std::thread::sleep(sleep_time);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Daemon stopped");
|
log::info!("Daemon stopped");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ use crate::config::{AppConfig, ProfileConfig, TurboAutoSettings};
|
||||||
use crate::core::{OperationalMode, SystemReport, TurboSetting};
|
use crate::core::{OperationalMode, SystemReport, TurboSetting};
|
||||||
use crate::cpu::{self};
|
use crate::cpu::{self};
|
||||||
use crate::util::error::{ControlError, EngineError};
|
use crate::util::error::{ControlError, EngineError};
|
||||||
use log::{debug, info, warn};
|
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
|
@ -128,13 +127,13 @@ fn try_apply_feature<F, T>(
|
||||||
where
|
where
|
||||||
F: FnOnce() -> Result<T, ControlError>,
|
F: FnOnce() -> Result<T, ControlError>,
|
||||||
{
|
{
|
||||||
info!("Setting {feature_name} to '{value_description}'");
|
log::info!("Setting {feature_name} to '{value_description}'");
|
||||||
|
|
||||||
match apply_fn() {
|
match apply_fn() {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if matches!(e, ControlError::NotSupported(_)) {
|
if matches!(e, ControlError::NotSupported(_)) {
|
||||||
warn!(
|
log::warn!(
|
||||||
"{feature_name} setting is not supported on this system. Skipping {feature_name} configuration."
|
"{feature_name} setting is not supported on this system. Skipping {feature_name} configuration."
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -155,7 +154,7 @@ pub fn determine_and_apply_settings(
|
||||||
) -> Result<(), EngineError> {
|
) -> Result<(), EngineError> {
|
||||||
// First, check if there's a governor override set
|
// First, check if there's a governor override set
|
||||||
if let Some(override_governor) = cpu::get_governor_override() {
|
if let Some(override_governor) = cpu::get_governor_override() {
|
||||||
info!(
|
log::info!(
|
||||||
"Governor override is active: '{}'. Setting governor.",
|
"Governor override is active: '{}'. Setting governor.",
|
||||||
override_governor.trim()
|
override_governor.trim()
|
||||||
);
|
);
|
||||||
|
@ -182,35 +181,35 @@ pub fn determine_and_apply_settings(
|
||||||
if let Some(mode) = force_mode {
|
if let Some(mode) = force_mode {
|
||||||
match mode {
|
match mode {
|
||||||
OperationalMode::Powersave => {
|
OperationalMode::Powersave => {
|
||||||
info!("Forced Powersave mode selected. Applying 'battery' profile.");
|
log::info!("Forced Powersave mode selected. Applying 'battery' profile.");
|
||||||
selected_profile_config = &config.battery;
|
selected_profile_config = &config.battery;
|
||||||
}
|
}
|
||||||
OperationalMode::Performance => {
|
OperationalMode::Performance => {
|
||||||
info!("Forced Performance mode selected. Applying 'charger' profile.");
|
log::info!("Forced Performance mode selected. Applying 'charger' profile.");
|
||||||
selected_profile_config = &config.charger;
|
selected_profile_config = &config.charger;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Use the previously computed on_ac_power value
|
// Use the previously computed on_ac_power value
|
||||||
if on_ac_power {
|
if on_ac_power {
|
||||||
info!("On AC power, selecting Charger profile.");
|
log::info!("On AC power, selecting Charger profile.");
|
||||||
selected_profile_config = &config.charger;
|
selected_profile_config = &config.charger;
|
||||||
} else {
|
} else {
|
||||||
info!("On Battery power, selecting Battery profile.");
|
log::info!("On Battery power, selecting Battery profile.");
|
||||||
selected_profile_config = &config.battery;
|
selected_profile_config = &config.battery;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply settings from selected_profile_config
|
// Apply settings from selected_profile_config
|
||||||
if let Some(governor) = &selected_profile_config.governor {
|
if let Some(governor) = &selected_profile_config.governor {
|
||||||
info!("Setting governor to '{governor}'");
|
log::info!("Setting governor to '{governor}'");
|
||||||
// Let set_governor handle the validation
|
// Let set_governor handle the validation
|
||||||
if let Err(e) = cpu::set_governor(governor, None) {
|
if let Err(e) = cpu::set_governor(governor, None) {
|
||||||
// If the governor is not available, log a warning
|
// If the governor is not available, log a warning
|
||||||
if matches!(e, ControlError::InvalidGovernor(_))
|
if matches!(e, ControlError::InvalidGovernor(_))
|
||||||
|| matches!(e, ControlError::NotSupported(_))
|
|| matches!(e, ControlError::NotSupported(_))
|
||||||
{
|
{
|
||||||
warn!(
|
log::warn!(
|
||||||
"Configured governor '{governor}' is not available on this system. Skipping."
|
"Configured governor '{governor}' is not available on this system. Skipping."
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -220,14 +219,14 @@ pub fn determine_and_apply_settings(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(turbo_setting) = selected_profile_config.turbo {
|
if let Some(turbo_setting) = selected_profile_config.turbo {
|
||||||
info!("Setting turbo to '{turbo_setting:?}'");
|
log::info!("Setting turbo to '{turbo_setting:?}'");
|
||||||
match turbo_setting {
|
match turbo_setting {
|
||||||
TurboSetting::Auto => {
|
TurboSetting::Auto => {
|
||||||
if selected_profile_config.enable_auto_turbo {
|
if selected_profile_config.enable_auto_turbo {
|
||||||
debug!("Managing turbo in auto mode based on system conditions");
|
log::debug!("Managing turbo in auto mode based on system conditions");
|
||||||
manage_auto_turbo(report, selected_profile_config, on_ac_power)?;
|
manage_auto_turbo(report, selected_profile_config, on_ac_power)?;
|
||||||
} else {
|
} else {
|
||||||
debug!(
|
log::debug!(
|
||||||
"Superfreq's dynamic turbo management is disabled by configuration. Ensuring system uses its default behavior for automatic turbo control."
|
"Superfreq's dynamic turbo management is disabled by configuration. Ensuring system uses its default behavior for automatic turbo control."
|
||||||
);
|
);
|
||||||
// Make sure the system is set to its default automatic turbo mode.
|
// Make sure the system is set to its default automatic turbo mode.
|
||||||
|
@ -255,13 +254,13 @@ pub fn determine_and_apply_settings(
|
||||||
|
|
||||||
if let Some(min_freq) = selected_profile_config.min_freq_mhz {
|
if let Some(min_freq) = selected_profile_config.min_freq_mhz {
|
||||||
try_apply_feature("min frequency", &format!("{min_freq} MHz"), || {
|
try_apply_feature("min frequency", &format!("{min_freq} MHz"), || {
|
||||||
cpu::set_min_frequency(min_freq, None)
|
cpu::set_frequency_minimum(min_freq, None)
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(max_freq) = selected_profile_config.max_freq_mhz {
|
if let Some(max_freq) = selected_profile_config.max_freq_mhz {
|
||||||
try_apply_feature("max frequency", &format!("{max_freq} MHz"), || {
|
try_apply_feature("max frequency", &format!("{max_freq} MHz"), || {
|
||||||
cpu::set_max_frequency(max_freq, None)
|
cpu::set_frequency_maximum(max_freq, None)
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,19 +276,19 @@ pub fn determine_and_apply_settings(
|
||||||
let stop_threshold = thresholds.stop;
|
let stop_threshold = thresholds.stop;
|
||||||
|
|
||||||
if start_threshold < stop_threshold && stop_threshold <= 100 {
|
if start_threshold < stop_threshold && stop_threshold <= 100 {
|
||||||
info!("Setting battery charge thresholds: {start_threshold}-{stop_threshold}%");
|
log::info!("Setting battery charge thresholds: {start_threshold}-{stop_threshold}%");
|
||||||
match battery::set_battery_charge_thresholds(start_threshold, stop_threshold) {
|
match battery::set_battery_charge_thresholds(start_threshold, stop_threshold) {
|
||||||
Ok(()) => debug!("Battery charge thresholds set successfully"),
|
Ok(()) => log::debug!("Battery charge thresholds set successfully"),
|
||||||
Err(e) => warn!("Failed to set battery charge thresholds: {e}"),
|
Err(e) => log::warn!("Failed to set battery charge thresholds: {e}"),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!(
|
log::warn!(
|
||||||
"Invalid battery threshold values: start={start_threshold}, stop={stop_threshold}"
|
"Invalid battery threshold values: start={start_threshold}, stop={stop_threshold}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Profile settings applied successfully.");
|
log::debug!("Profile settings applied successfully.");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -346,27 +345,30 @@ fn manage_auto_turbo(
|
||||||
let enable_turbo = match (cpu_temp, avg_cpu_usage, previous_turbo_enabled) {
|
let enable_turbo = match (cpu_temp, avg_cpu_usage, previous_turbo_enabled) {
|
||||||
// If temperature is too high, disable turbo regardless of load
|
// If temperature is too high, disable turbo regardless of load
|
||||||
(Some(temp), _, _) if temp >= turbo_settings.temp_threshold_high => {
|
(Some(temp), _, _) if temp >= turbo_settings.temp_threshold_high => {
|
||||||
info!(
|
log::info!(
|
||||||
"Auto Turbo: Disabled due to high temperature ({:.1}°C >= {:.1}°C)",
|
"Auto Turbo: Disabled due to high temperature ({:.1}°C >= {:.1}°C)",
|
||||||
temp, turbo_settings.temp_threshold_high
|
temp,
|
||||||
|
turbo_settings.temp_threshold_high
|
||||||
);
|
);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
// If load is high enough, enable turbo (unless temp already caused it to disable)
|
// If load is high enough, enable turbo (unless temp already caused it to disable)
|
||||||
(_, Some(usage), _) if usage >= turbo_settings.load_threshold_high => {
|
(_, Some(usage), _) if usage >= turbo_settings.load_threshold_high => {
|
||||||
info!(
|
log::info!(
|
||||||
"Auto Turbo: Enabled due to high CPU load ({:.1}% >= {:.1}%)",
|
"Auto Turbo: Enabled due to high CPU load ({:.1}% >= {:.1}%)",
|
||||||
usage, turbo_settings.load_threshold_high
|
usage,
|
||||||
|
turbo_settings.load_threshold_high
|
||||||
);
|
);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
// If load is low, disable turbo
|
// If load is low, disable turbo
|
||||||
(_, Some(usage), _) if usage <= turbo_settings.load_threshold_low => {
|
(_, Some(usage), _) if usage <= turbo_settings.load_threshold_low => {
|
||||||
info!(
|
log::info!(
|
||||||
"Auto Turbo: Disabled due to low CPU load ({:.1}% <= {:.1}%)",
|
"Auto Turbo: Disabled due to low CPU load ({:.1}% <= {:.1}%)",
|
||||||
usage, turbo_settings.load_threshold_low
|
usage,
|
||||||
|
turbo_settings.load_threshold_low
|
||||||
);
|
);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
@ -376,7 +378,7 @@ fn manage_auto_turbo(
|
||||||
if usage > turbo_settings.load_threshold_low
|
if usage > turbo_settings.load_threshold_low
|
||||||
&& usage < turbo_settings.load_threshold_high =>
|
&& usage < turbo_settings.load_threshold_high =>
|
||||||
{
|
{
|
||||||
info!(
|
log::info!(
|
||||||
"Auto Turbo: Maintaining previous state ({}) due to intermediate load ({:.1}%)",
|
"Auto Turbo: Maintaining previous state ({}) due to intermediate load ({:.1}%)",
|
||||||
if prev_state { "enabled" } else { "disabled" },
|
if prev_state { "enabled" } else { "disabled" },
|
||||||
usage
|
usage
|
||||||
|
@ -386,7 +388,7 @@ fn manage_auto_turbo(
|
||||||
|
|
||||||
// When CPU load data is present but temperature is missing, use the same hysteresis logic
|
// When CPU load data is present but temperature is missing, use the same hysteresis logic
|
||||||
(None, Some(usage), prev_state) => {
|
(None, Some(usage), prev_state) => {
|
||||||
info!(
|
log::info!(
|
||||||
"Auto Turbo: Maintaining previous state ({}) due to missing temperature data (load: {:.1}%)",
|
"Auto Turbo: Maintaining previous state ({}) due to missing temperature data (load: {:.1}%)",
|
||||||
if prev_state { "enabled" } else { "disabled" },
|
if prev_state { "enabled" } else { "disabled" },
|
||||||
usage
|
usage
|
||||||
|
@ -396,7 +398,7 @@ fn manage_auto_turbo(
|
||||||
|
|
||||||
// When all metrics are missing, maintain the previous state
|
// When all metrics are missing, maintain the previous state
|
||||||
(None, None, prev_state) => {
|
(None, None, prev_state) => {
|
||||||
info!(
|
log::info!(
|
||||||
"Auto Turbo: Maintaining previous state ({}) due to missing all CPU metrics",
|
"Auto Turbo: Maintaining previous state ({}) due to missing all CPU metrics",
|
||||||
if prev_state { "enabled" } else { "disabled" }
|
if prev_state { "enabled" } else { "disabled" }
|
||||||
);
|
);
|
||||||
|
@ -405,7 +407,7 @@ fn manage_auto_turbo(
|
||||||
|
|
||||||
// Any other cases with partial metrics, maintain previous state for stability
|
// Any other cases with partial metrics, maintain previous state for stability
|
||||||
(_, _, prev_state) => {
|
(_, _, prev_state) => {
|
||||||
info!(
|
log::info!(
|
||||||
"Auto Turbo: Maintaining previous state ({}) due to incomplete CPU metrics",
|
"Auto Turbo: Maintaining previous state ({}) due to incomplete CPU metrics",
|
||||||
if prev_state { "enabled" } else { "disabled" }
|
if prev_state { "enabled" } else { "disabled" }
|
||||||
);
|
);
|
||||||
|
@ -429,7 +431,7 @@ fn manage_auto_turbo(
|
||||||
TurboSetting::Never
|
TurboSetting::Never
|
||||||
};
|
};
|
||||||
|
|
||||||
info!(
|
log::info!(
|
||||||
"Auto Turbo: Applying turbo change from {} to {}",
|
"Auto Turbo: Applying turbo change from {} to {}",
|
||||||
if previous_turbo_enabled {
|
if previous_turbo_enabled {
|
||||||
"enabled"
|
"enabled"
|
||||||
|
@ -441,7 +443,7 @@ fn manage_auto_turbo(
|
||||||
|
|
||||||
match cpu::set_turbo(turbo_setting) {
|
match cpu::set_turbo(turbo_setting) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
debug!(
|
log::debug!(
|
||||||
"Auto Turbo: Successfully set turbo to {}",
|
"Auto Turbo: Successfully set turbo to {}",
|
||||||
if enable_turbo { "enabled" } else { "disabled" }
|
if enable_turbo { "enabled" } else { "disabled" }
|
||||||
);
|
);
|
||||||
|
@ -450,7 +452,7 @@ fn manage_auto_turbo(
|
||||||
Err(e) => Err(EngineError::ControlError(e)),
|
Err(e) => Err(EngineError::ControlError(e)),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
debug!(
|
log::debug!(
|
||||||
"Auto Turbo: Maintaining turbo state ({}) - no change needed",
|
"Auto Turbo: Maintaining turbo state ({}) - no change needed",
|
||||||
if enable_turbo { "enabled" } else { "disabled" }
|
if enable_turbo { "enabled" } else { "disabled" }
|
||||||
);
|
);
|
||||||
|
|
869
src/main.rs
869
src/main.rs
|
@ -8,469 +8,478 @@ mod engine;
|
||||||
mod monitor;
|
mod monitor;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
use crate::config::AppConfig;
|
use anyhow::{Context, anyhow, bail};
|
||||||
use crate::core::{GovernorOverrideMode, TurboSetting};
|
use clap::Parser as _;
|
||||||
use crate::util::error::{AppError, ControlError};
|
use std::fmt::Write as _;
|
||||||
use clap::{Parser, value_parser};
|
use std::io::Write as _;
|
||||||
use env_logger::Builder;
|
use std::{io, process};
|
||||||
use log::{debug, error, info};
|
use yansi::Paint as _;
|
||||||
use std::error::Error;
|
|
||||||
use std::sync::Once;
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(clap::Parser, Debug)]
|
||||||
#[clap(author, version, about, long_about = None)]
|
#[clap(author, version, about)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
|
#[command(flatten)]
|
||||||
|
verbosity: clap_verbosity_flag::Verbosity,
|
||||||
|
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
command: Option<Commands>,
|
command: Command,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(clap::Parser, Debug)]
|
||||||
enum Commands {
|
enum Command {
|
||||||
/// Display current system information
|
/// Display information.
|
||||||
Info,
|
Info,
|
||||||
/// Run as a daemon in the background
|
|
||||||
Daemon {
|
/// Start the daemon.
|
||||||
#[clap(long)]
|
Start,
|
||||||
verbose: bool,
|
|
||||||
},
|
/// Modify attributes.
|
||||||
/// Set CPU governor
|
Set {
|
||||||
SetGovernor {
|
/// The CPUs to apply the changes to. When unspecified, will be applied to all CPUs.
|
||||||
governor: String,
|
#[arg(short = 'c', long = "for")]
|
||||||
#[clap(long)]
|
for_: Option<Vec<u32>>,
|
||||||
core_id: Option<u32>,
|
|
||||||
},
|
/// Set the CPU governor.
|
||||||
/// Force a specific governor mode persistently
|
#[arg(long)]
|
||||||
ForceGovernor {
|
governor: Option<String>, // TODO: Validate with clap for available governors.
|
||||||
/// Mode to force: performance, powersave, or reset
|
|
||||||
#[clap(value_enum)]
|
/// Set the CPU governor persistently.
|
||||||
mode: GovernorOverrideMode,
|
#[arg(long, conflicts_with = "governor")]
|
||||||
},
|
governor_persist: Option<String>, // TODO: Validate with clap for available governors.
|
||||||
/// Set turbo boost behavior
|
|
||||||
SetTurbo {
|
/// Set CPU Energy Performance Preference (EPP). Short form: --epp.
|
||||||
#[clap(value_enum)]
|
#[arg(long, alias = "epp")]
|
||||||
setting: TurboSetting,
|
energy_performance_preference: Option<String>,
|
||||||
},
|
|
||||||
/// Display comprehensive debug information
|
/// Set CPU Energy Performance Bias (EPB). Short form: --epb.
|
||||||
Debug,
|
#[arg(long, alias = "epb")]
|
||||||
/// Set Energy Performance Preference (EPP)
|
energy_performance_bias: Option<String>,
|
||||||
SetEpp {
|
|
||||||
epp: String,
|
/// Set minimum CPU frequency in MHz. Short form: --freq-min.
|
||||||
#[clap(long)]
|
#[arg(short = 'f', long, alias = "freq-min", value_parser = clap::value_parser!(u64).range(1..=10_000))]
|
||||||
core_id: Option<u32>,
|
frequency_mhz_minimum: Option<u64>,
|
||||||
},
|
|
||||||
/// Set Energy Performance Bias (EPB)
|
/// Set maximum CPU frequency in MHz. Short form: --freq-max.
|
||||||
SetEpb {
|
#[arg(short = 'F', long, alias = "freq-max", value_parser = clap::value_parser!(u64).range(1..=10_000))]
|
||||||
epb: String, // Typically 0-15
|
frequency_mhz_maximum: Option<u64>,
|
||||||
#[clap(long)]
|
|
||||||
core_id: Option<u32>,
|
/// Set turbo boost behaviour. Has to be for all CPUs.
|
||||||
},
|
#[arg(long, conflicts_with = "for_")]
|
||||||
/// Set minimum CPU frequency
|
turbo: Option<cpu::Turbo>,
|
||||||
SetMinFreq {
|
|
||||||
freq_mhz: u32,
|
/// Set ACPI platform profile. Has to be for all CPUs.
|
||||||
#[clap(long)]
|
#[arg(long, alias = "profile", conflicts_with = "for_")]
|
||||||
core_id: Option<u32>,
|
platform_profile: Option<String>,
|
||||||
},
|
|
||||||
/// Set maximum CPU frequency
|
/// Set the percentage that the power supply has to drop under for charging to start. Short form: --charge-start.
|
||||||
SetMaxFreq {
|
#[arg(short = 'p', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100), conflicts_with = "for_")]
|
||||||
freq_mhz: u32,
|
charge_threshold_start: Option<u8>,
|
||||||
#[clap(long)]
|
|
||||||
core_id: Option<u32>,
|
/// Set the percentage where charging will stop. Short form: --charge-end.
|
||||||
},
|
#[arg(short = 'P', long, alias = "charge-end", value_parser = clap::value_parser!(u8).range(0..=100), conflicts_with = "for_")]
|
||||||
/// Set ACPI platform profile
|
charge_threshold_end: Option<u8>,
|
||||||
SetPlatformProfile { profile: String },
|
|
||||||
/// Set battery charge thresholds to extend battery lifespan
|
|
||||||
SetBatteryThresholds {
|
|
||||||
/// Percentage at which charging starts (when below this value)
|
|
||||||
#[clap(value_parser = value_parser!(u8).range(0..=99))]
|
|
||||||
start_threshold: u8,
|
|
||||||
/// Percentage at which charging stops (when it reaches this value)
|
|
||||||
#[clap(value_parser = value_parser!(u8).range(1..=100))]
|
|
||||||
stop_threshold: u8,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), AppError> {
|
fn real_main() -> anyhow::Result<()> {
|
||||||
// Initialize logger once for the entire application
|
|
||||||
init_logger();
|
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
// Load configuration first, as it might be needed by the monitor module
|
env_logger::Builder::new()
|
||||||
// E.g., for ignored power supplies
|
.filter_level(cli.verbosity.log_level_filter())
|
||||||
let config = match config::load_config() {
|
.format_timestamp(None)
|
||||||
Ok(cfg) => cfg,
|
.format_module_path(false)
|
||||||
Err(e) => {
|
.init();
|
||||||
error!("Error loading configuration: {e}. Using default values.");
|
|
||||||
// Proceed with default config if loading fails
|
let config = config::load_config().context("failed to load config")?;
|
||||||
AppConfig::default()
|
|
||||||
|
match cli.command {
|
||||||
|
Command::Info => todo!(),
|
||||||
|
|
||||||
|
Command::Start => {
|
||||||
|
daemon::run_daemon(config)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
let command_result: Result<(), AppError> = match cli.command {
|
Command::Set {
|
||||||
// TODO: This will be moved to a different module in the future.
|
for_,
|
||||||
Some(Commands::Info) => match monitor::collect_system_report(&config) {
|
governor,
|
||||||
Ok(report) => {
|
governor_persist,
|
||||||
// Format section headers with proper centering
|
energy_performance_preference,
|
||||||
let format_section = |title: &str| {
|
energy_performance_bias,
|
||||||
let title_len = title.len();
|
frequency_mhz_minimum,
|
||||||
let total_width = title_len + 8; // 8 is for padding (4 on each side)
|
frequency_mhz_maximum,
|
||||||
let separator = "═".repeat(total_width);
|
turbo,
|
||||||
|
platform_profile,
|
||||||
|
charge_threshold_start,
|
||||||
|
charge_threshold_end,
|
||||||
|
} => {
|
||||||
|
let cpus = match for_ {
|
||||||
|
Some(cpus) => cpus,
|
||||||
|
None => cpu::get_real_cpus()?,
|
||||||
|
};
|
||||||
|
|
||||||
println!("\n╔{separator}╗");
|
for cpu in cpus {
|
||||||
|
if let Some(governor) = governor.as_ref() {
|
||||||
|
cpu::set_governor(governor, cpu)?;
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate centering
|
if let Some(epp) = energy_performance_preference.as_ref() {
|
||||||
println!("║ {title} ║");
|
cpu::set_epp(epp, cpu)?;
|
||||||
|
}
|
||||||
|
|
||||||
println!("╚{separator}╝");
|
if let Some(epb) = energy_performance_bias.as_ref() {
|
||||||
};
|
cpu::set_epb(epb, cpu)?;
|
||||||
|
}
|
||||||
|
|
||||||
format_section("System Information");
|
if let Some(mhz_minimum) = frequency_mhz_minimum {
|
||||||
println!("CPU Model: {}", report.system_info.cpu_model);
|
cpu::set_frequency_minimum(mhz_minimum, cpu)?;
|
||||||
println!("Architecture: {}", report.system_info.architecture);
|
}
|
||||||
println!(
|
|
||||||
"Linux Distribution: {}",
|
|
||||||
report.system_info.linux_distribution
|
|
||||||
);
|
|
||||||
|
|
||||||
// Format timestamp in a readable way
|
if let Some(mhz_maximum) = frequency_mhz_maximum {
|
||||||
println!("Current Time: {}", jiff::Timestamp::now());
|
cpu::set_frequency_maximum(mhz_maximum, cpu)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
format_section("CPU Global Info");
|
if let Some(turbo) = turbo {
|
||||||
println!(
|
cpu::set_turbo(turbo)?;
|
||||||
"Current Governor: {}",
|
}
|
||||||
report
|
|
||||||
.cpu_global
|
|
||||||
.current_governor
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or("N/A")
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"Available Governors: {}", // 21 length baseline
|
|
||||||
report.cpu_global.available_governors.join(", ")
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"Turbo Status: {}",
|
|
||||||
match report.cpu_global.turbo_status {
|
|
||||||
Some(true) => "Enabled",
|
|
||||||
Some(false) => "Disabled",
|
|
||||||
None => "Unknown",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
println!(
|
if let Some(platform_profile) = platform_profile.as_ref() {
|
||||||
"EPP: {}",
|
cpu::set_platform_profile(platform_profile)?;
|
||||||
report.cpu_global.epp.as_deref().unwrap_or("N/A")
|
}
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"EPB: {}",
|
|
||||||
report.cpu_global.epb.as_deref().unwrap_or("N/A")
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"Platform Profile: {}",
|
|
||||||
report
|
|
||||||
.cpu_global
|
|
||||||
.platform_profile
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or("N/A")
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"CPU Temperature: {}",
|
|
||||||
report.cpu_global.average_temperature_celsius.map_or_else(
|
|
||||||
|| "N/A (No sensor detected)".to_string(),
|
|
||||||
|t| format!("{t:.1}°C")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
format_section("CPU Core Info");
|
// TODO: This is like this because [`cpu`] doesn't expose
|
||||||
|
// a way of setting them individually. Will clean this up
|
||||||
|
// after that is cleaned.
|
||||||
|
if charge_threshold_start.is_some() || charge_threshold_end.is_some() {
|
||||||
|
let charge_threshold_start = charge_threshold_start.ok_or_else(|| {
|
||||||
|
anyhow!("both charge thresholds should be given at the same time")
|
||||||
|
})?;
|
||||||
|
let charge_threshold_end = charge_threshold_end.ok_or_else(|| {
|
||||||
|
anyhow!("both charge thresholds should be given at the same time")
|
||||||
|
})?;
|
||||||
|
|
||||||
// Get max core ID length for padding
|
if charge_threshold_start >= charge_threshold_end {
|
||||||
let max_core_id_len = report
|
bail!(
|
||||||
.cpu_cores
|
"charge start threshold (given as {charge_threshold_start}) must be less than stop threshold (given as {charge_threshold_end})"
|
||||||
.last()
|
|
||||||
.map_or(1, |core| core.core_id.to_string().len());
|
|
||||||
|
|
||||||
// Table headers
|
|
||||||
println!(
|
|
||||||
" {:>width$} │ {:^10} │ {:^10} │ {:^10} │ {:^7} │ {:^9}",
|
|
||||||
"Core",
|
|
||||||
"Current",
|
|
||||||
"Min",
|
|
||||||
"Max",
|
|
||||||
"Usage",
|
|
||||||
"Temp",
|
|
||||||
width = max_core_id_len + 4
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
" {:─>width$}──┼─{:─^10}─┼─{:─^10}─┼─{:─^10}─┼─{:─^7}─┼─{:─^9}",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
width = max_core_id_len + 4
|
|
||||||
);
|
|
||||||
|
|
||||||
for core_info in &report.cpu_cores {
|
|
||||||
// Format frequencies: if current > max, show in a special way
|
|
||||||
let current_freq = match core_info.current_frequency_mhz {
|
|
||||||
Some(freq) => {
|
|
||||||
let max_freq = core_info.max_frequency_mhz.unwrap_or(0);
|
|
||||||
if freq > max_freq && max_freq > 0 {
|
|
||||||
// Special format for boosted frequencies
|
|
||||||
format!("{freq}*")
|
|
||||||
} else {
|
|
||||||
format!("{freq}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => "N/A".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// CPU core display
|
|
||||||
println!(
|
|
||||||
" Core {:<width$} │ {:>10} │ {:>10} │ {:>10} │ {:>7} │ {:>9}",
|
|
||||||
core_info.core_id,
|
|
||||||
format!("{} MHz", current_freq),
|
|
||||||
format!(
|
|
||||||
"{} MHz",
|
|
||||||
core_info
|
|
||||||
.min_frequency_mhz
|
|
||||||
.map_or_else(|| "N/A".to_string(), |f| f.to_string())
|
|
||||||
),
|
|
||||||
format!(
|
|
||||||
"{} MHz",
|
|
||||||
core_info
|
|
||||||
.max_frequency_mhz
|
|
||||||
.map_or_else(|| "N/A".to_string(), |f| f.to_string())
|
|
||||||
),
|
|
||||||
format!(
|
|
||||||
"{}%",
|
|
||||||
core_info
|
|
||||||
.usage_percent
|
|
||||||
.map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}"))
|
|
||||||
),
|
|
||||||
format!(
|
|
||||||
"{}°C",
|
|
||||||
core_info
|
|
||||||
.temperature_celsius
|
|
||||||
.map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}"))
|
|
||||||
),
|
|
||||||
width = max_core_id_len
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only display battery info for systems that have real batteries
|
battery::set_battery_charge_thresholds(
|
||||||
// Skip this section entirely on desktop systems
|
charge_threshold_start,
|
||||||
if !report.batteries.is_empty() {
|
charge_threshold_end,
|
||||||
let has_real_batteries = report.batteries.iter().any(|b| {
|
)?;
|
||||||
// Check if any battery has actual battery data
|
|
||||||
// (as opposed to peripherals like wireless mice)
|
|
||||||
b.capacity_percent.is_some() || b.power_rate_watts.is_some()
|
|
||||||
});
|
|
||||||
|
|
||||||
if has_real_batteries {
|
|
||||||
format_section("Battery Info");
|
|
||||||
for battery_info in &report.batteries {
|
|
||||||
// Check if this appears to be a real system battery
|
|
||||||
if battery_info.capacity_percent.is_some()
|
|
||||||
|| battery_info.power_rate_watts.is_some()
|
|
||||||
{
|
|
||||||
let power_status = if battery_info.ac_connected {
|
|
||||||
"Connected to AC"
|
|
||||||
} else {
|
|
||||||
"Running on Battery"
|
|
||||||
};
|
|
||||||
|
|
||||||
println!("Battery {}:", battery_info.name);
|
|
||||||
println!(" Power Status: {power_status}");
|
|
||||||
println!(
|
|
||||||
" State: {}",
|
|
||||||
battery_info.charging_state.as_deref().unwrap_or("Unknown")
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(capacity) = battery_info.capacity_percent {
|
|
||||||
println!(" Capacity: {capacity}%");
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(power) = battery_info.power_rate_watts {
|
|
||||||
let direction = if power >= 0.0 {
|
|
||||||
"charging"
|
|
||||||
} else {
|
|
||||||
"discharging"
|
|
||||||
};
|
|
||||||
println!(
|
|
||||||
" Power Rate: {:.2} W ({})",
|
|
||||||
power.abs(),
|
|
||||||
direction
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display charge thresholds if available
|
|
||||||
if battery_info.charge_start_threshold.is_some()
|
|
||||||
|| battery_info.charge_stop_threshold.is_some()
|
|
||||||
{
|
|
||||||
println!(
|
|
||||||
" Charge Thresholds: {}-{}",
|
|
||||||
battery_info
|
|
||||||
.charge_start_threshold
|
|
||||||
.map_or_else(|| "N/A".to_string(), |t| t.to_string()),
|
|
||||||
battery_info
|
|
||||||
.charge_stop_threshold
|
|
||||||
.map_or_else(|| "N/A".to_string(), |t| t.to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
format_section("System Load");
|
|
||||||
println!(
|
|
||||||
"Load Average (1m): {:.2}",
|
|
||||||
report.system_load.load_avg_1min
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"Load Average (5m): {:.2}",
|
|
||||||
report.system_load.load_avg_5min
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"Load Average (15m): {:.2}",
|
|
||||||
report.system_load.load_avg_15min
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
Err(e) => Err(AppError::Monitor(e)),
|
|
||||||
},
|
|
||||||
Some(Commands::SetGovernor { governor, core_id }) => {
|
|
||||||
cpu::set_governor(&governor, core_id).map_err(AppError::Control)
|
|
||||||
}
|
|
||||||
Some(Commands::ForceGovernor { mode }) => {
|
|
||||||
cpu::force_governor(mode).map_err(AppError::Control)
|
|
||||||
}
|
|
||||||
Some(Commands::SetTurbo { setting }) => cpu::set_turbo(setting).map_err(AppError::Control),
|
|
||||||
Some(Commands::SetEpp { epp, core_id }) => {
|
|
||||||
cpu::set_epp(&epp, core_id).map_err(AppError::Control)
|
|
||||||
}
|
|
||||||
Some(Commands::SetEpb { epb, core_id }) => {
|
|
||||||
cpu::set_epb(&epb, core_id).map_err(AppError::Control)
|
|
||||||
}
|
|
||||||
Some(Commands::SetMinFreq { freq_mhz, core_id }) => {
|
|
||||||
// Basic validation for reasonable CPU frequency values
|
|
||||||
validate_freq(freq_mhz, "Minimum")?;
|
|
||||||
cpu::set_min_frequency(freq_mhz, core_id).map_err(AppError::Control)
|
|
||||||
}
|
|
||||||
Some(Commands::SetMaxFreq { freq_mhz, core_id }) => {
|
|
||||||
// Basic validation for reasonable CPU frequency values
|
|
||||||
validate_freq(freq_mhz, "Maximum")?;
|
|
||||||
cpu::set_max_frequency(freq_mhz, core_id).map_err(AppError::Control)
|
|
||||||
}
|
|
||||||
Some(Commands::SetPlatformProfile { profile }) => {
|
|
||||||
// Get available platform profiles and validate early if possible
|
|
||||||
match cpu::get_platform_profiles() {
|
|
||||||
Ok(available_profiles) => {
|
|
||||||
if available_profiles.contains(&profile) {
|
|
||||||
info!("Setting platform profile to '{profile}'");
|
|
||||||
cpu::set_platform_profile(&profile).map_err(AppError::Control)
|
|
||||||
} else {
|
|
||||||
error!(
|
|
||||||
"Invalid platform profile: '{}'. Available profiles: {}",
|
|
||||||
profile,
|
|
||||||
available_profiles.join(", ")
|
|
||||||
);
|
|
||||||
Err(AppError::Generic(format!(
|
|
||||||
"Invalid platform profile: '{}'. Available profiles: {}",
|
|
||||||
profile,
|
|
||||||
available_profiles.join(", ")
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_e) => {
|
|
||||||
// If we can't get profiles (e.g., feature not supported), pass through to the function
|
|
||||||
cpu::set_platform_profile(&profile).map_err(AppError::Control)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(Commands::SetBatteryThresholds {
|
|
||||||
start_threshold,
|
|
||||||
stop_threshold,
|
|
||||||
}) => {
|
|
||||||
// We only need to check if start < stop since the range validation is handled by Clap
|
|
||||||
if start_threshold >= stop_threshold {
|
|
||||||
error!(
|
|
||||||
"Start threshold ({start_threshold}) must be less than stop threshold ({stop_threshold})"
|
|
||||||
);
|
|
||||||
Err(AppError::Generic(format!(
|
|
||||||
"Start threshold ({start_threshold}) must be less than stop threshold ({stop_threshold})"
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
info!(
|
|
||||||
"Setting battery thresholds: start at {start_threshold}%, stop at {stop_threshold}%"
|
|
||||||
);
|
|
||||||
battery::set_battery_charge_thresholds(start_threshold, stop_threshold)
|
|
||||||
.map_err(AppError::Control)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(Commands::Daemon { verbose }) => daemon::run_daemon(config, verbose),
|
|
||||||
Some(Commands::Debug) => cli::debug::run_debug(&config),
|
|
||||||
None => {
|
|
||||||
info!("Welcome to superfreq! Use --help for commands.");
|
|
||||||
debug!("Current effective configuration: {config:?}");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This will be moved to a different module in the future.
|
||||||
|
// Some(Command::Info) => match monitor::collect_system_report(&config) {
|
||||||
|
// Ok(report) => {
|
||||||
|
// // Format section headers with proper centering
|
||||||
|
// let format_section = |title: &str| {
|
||||||
|
// let title_len = title.len();
|
||||||
|
// let total_width = title_len + 8; // 8 is for padding (4 on each side)
|
||||||
|
// let separator = "═".repeat(total_width);
|
||||||
|
|
||||||
|
// println!("\n╔{separator}╗");
|
||||||
|
|
||||||
|
// // Calculate centering
|
||||||
|
// println!("║ {title} ║");
|
||||||
|
|
||||||
|
// println!("╚{separator}╝");
|
||||||
|
// };
|
||||||
|
|
||||||
|
// format_section("System Information");
|
||||||
|
// println!("CPU Model: {}", report.system_info.cpu_model);
|
||||||
|
// println!("Architecture: {}", report.system_info.architecture);
|
||||||
|
// println!(
|
||||||
|
// "Linux Distribution: {}",
|
||||||
|
// report.system_info.linux_distribution
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // Format timestamp in a readable way
|
||||||
|
// println!("Current Time: {}", jiff::Timestamp::now());
|
||||||
|
|
||||||
|
// format_section("CPU Global Info");
|
||||||
|
// println!(
|
||||||
|
// "Current Governor: {}",
|
||||||
|
// report
|
||||||
|
// .cpu_global
|
||||||
|
// .current_governor
|
||||||
|
// .as_deref()
|
||||||
|
// .unwrap_or("N/A")
|
||||||
|
// );
|
||||||
|
// println!(
|
||||||
|
// "Available Governors: {}", // 21 length baseline
|
||||||
|
// report.cpu_global.available_governors.join(", ")
|
||||||
|
// );
|
||||||
|
// println!(
|
||||||
|
// "Turbo Status: {}",
|
||||||
|
// match report.cpu_global.turbo_status {
|
||||||
|
// Some(true) => "Enabled",
|
||||||
|
// Some(false) => "Disabled",
|
||||||
|
// None => "Unknown",
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
|
||||||
|
// println!(
|
||||||
|
// "EPP: {}",
|
||||||
|
// report.cpu_global.epp.as_deref().unwrap_or("N/A")
|
||||||
|
// );
|
||||||
|
// println!(
|
||||||
|
// "EPB: {}",
|
||||||
|
// report.cpu_global.epb.as_deref().unwrap_or("N/A")
|
||||||
|
// );
|
||||||
|
// println!(
|
||||||
|
// "Platform Profile: {}",
|
||||||
|
// report
|
||||||
|
// .cpu_global
|
||||||
|
// .platform_profile
|
||||||
|
// .as_deref()
|
||||||
|
// .unwrap_or("N/A")
|
||||||
|
// );
|
||||||
|
// println!(
|
||||||
|
// "CPU Temperature: {}",
|
||||||
|
// report.cpu_global.average_temperature_celsius.map_or_else(
|
||||||
|
// || "N/A (No sensor detected)".to_string(),
|
||||||
|
// |t| format!("{t:.1}°C")
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
|
||||||
|
// format_section("CPU Core Info");
|
||||||
|
|
||||||
|
// // Get max core ID length for padding
|
||||||
|
// let max_core_id_len = report
|
||||||
|
// .cpu_cores
|
||||||
|
// .last()
|
||||||
|
// .map_or(1, |core| core.core_id.to_string().len());
|
||||||
|
|
||||||
|
// // Table headers
|
||||||
|
// println!(
|
||||||
|
// " {:>width$} │ {:^10} │ {:^10} │ {:^10} │ {:^7} │ {:^9}",
|
||||||
|
// "Core",
|
||||||
|
// "Current",
|
||||||
|
// "Min",
|
||||||
|
// "Max",
|
||||||
|
// "Usage",
|
||||||
|
// "Temp",
|
||||||
|
// width = max_core_id_len + 4
|
||||||
|
// );
|
||||||
|
// println!(
|
||||||
|
// " {:─>width$}──┼─{:─^10}─┼─{:─^10}─┼─{:─^10}─┼─{:─^7}─┼─{:─^9}",
|
||||||
|
// "",
|
||||||
|
// "",
|
||||||
|
// "",
|
||||||
|
// "",
|
||||||
|
// "",
|
||||||
|
// "",
|
||||||
|
// width = max_core_id_len + 4
|
||||||
|
// );
|
||||||
|
|
||||||
|
// for core_info in &report.cpu_cores {
|
||||||
|
// // Format frequencies: if current > max, show in a special way
|
||||||
|
// let current_freq = match core_info.current_frequency_mhz {
|
||||||
|
// Some(freq) => {
|
||||||
|
// let max_freq = core_info.max_frequency_mhz.unwrap_or(0);
|
||||||
|
// if freq > max_freq && max_freq > 0 {
|
||||||
|
// // Special format for boosted frequencies
|
||||||
|
// format!("{freq}*")
|
||||||
|
// } else {
|
||||||
|
// format!("{freq}")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// None => "N/A".to_string(),
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // CPU core display
|
||||||
|
// println!(
|
||||||
|
// " Core {:<width$} │ {:>10} │ {:>10} │ {:>10} │ {:>7} │ {:>9}",
|
||||||
|
// core_info.core_id,
|
||||||
|
// format!("{} MHz", current_freq),
|
||||||
|
// format!(
|
||||||
|
// "{} MHz",
|
||||||
|
// core_info
|
||||||
|
// .min_frequency_mhz
|
||||||
|
// .map_or_else(|| "N/A".to_string(), |f| f.to_string())
|
||||||
|
// ),
|
||||||
|
// format!(
|
||||||
|
// "{} MHz",
|
||||||
|
// core_info
|
||||||
|
// .max_frequency_mhz
|
||||||
|
// .map_or_else(|| "N/A".to_string(), |f| f.to_string())
|
||||||
|
// ),
|
||||||
|
// format!(
|
||||||
|
// "{}%",
|
||||||
|
// core_info
|
||||||
|
// .usage_percent
|
||||||
|
// .map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}"))
|
||||||
|
// ),
|
||||||
|
// format!(
|
||||||
|
// "{}°C",
|
||||||
|
// core_info
|
||||||
|
// .temperature_celsius
|
||||||
|
// .map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}"))
|
||||||
|
// ),
|
||||||
|
// width = max_core_id_len
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Only display battery info for systems that have real batteries
|
||||||
|
// // Skip this section entirely on desktop systems
|
||||||
|
// if !report.batteries.is_empty() {
|
||||||
|
// let has_real_batteries = report.batteries.iter().any(|b| {
|
||||||
|
// // Check if any battery has actual battery data
|
||||||
|
// // (as opposed to peripherals like wireless mice)
|
||||||
|
// b.capacity_percent.is_some() || b.power_rate_watts.is_some()
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if has_real_batteries {
|
||||||
|
// format_section("Battery Info");
|
||||||
|
// for battery_info in &report.batteries {
|
||||||
|
// // Check if this appears to be a real system battery
|
||||||
|
// if battery_info.capacity_percent.is_some()
|
||||||
|
// || battery_info.power_rate_watts.is_some()
|
||||||
|
// {
|
||||||
|
// let power_status = if battery_info.ac_connected {
|
||||||
|
// "Connected to AC"
|
||||||
|
// } else {
|
||||||
|
// "Running on Battery"
|
||||||
|
// };
|
||||||
|
|
||||||
|
// println!("Battery {}:", battery_info.name);
|
||||||
|
// println!(" Power Status: {power_status}");
|
||||||
|
// println!(
|
||||||
|
// " State: {}",
|
||||||
|
// battery_info.charging_state.as_deref().unwrap_or("Unknown")
|
||||||
|
// );
|
||||||
|
|
||||||
|
// if let Some(capacity) = battery_info.capacity_percent {
|
||||||
|
// println!(" Capacity: {capacity}%");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if let Some(power) = battery_info.power_rate_watts {
|
||||||
|
// let direction = if power >= 0.0 {
|
||||||
|
// "charging"
|
||||||
|
// } else {
|
||||||
|
// "discharging"
|
||||||
|
// };
|
||||||
|
// println!(
|
||||||
|
// " Power Rate: {:.2} W ({})",
|
||||||
|
// power.abs(),
|
||||||
|
// direction
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Display charge thresholds if available
|
||||||
|
// if battery_info.charge_start_threshold.is_some()
|
||||||
|
// || battery_info.charge_stop_threshold.is_some()
|
||||||
|
// {
|
||||||
|
// println!(
|
||||||
|
// " Charge Thresholds: {}-{}",
|
||||||
|
// battery_info
|
||||||
|
// .charge_start_threshold
|
||||||
|
// .map_or_else(|| "N/A".to_string(), |t| t.to_string()),
|
||||||
|
// battery_info
|
||||||
|
// .charge_stop_threshold
|
||||||
|
// .map_or_else(|| "N/A".to_string(), |t| t.to_string())
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// format_section("System Load");
|
||||||
|
// println!(
|
||||||
|
// "Load Average (1m): {:.2}",
|
||||||
|
// report.system_load.load_avg_1min
|
||||||
|
// );
|
||||||
|
// println!(
|
||||||
|
// "Load Average (5m): {:.2}",
|
||||||
|
// report.system_load.load_avg_5min
|
||||||
|
// );
|
||||||
|
// println!(
|
||||||
|
// "Load Average (15m): {:.2}",
|
||||||
|
// report.system_load.load_avg_15min
|
||||||
|
// );
|
||||||
|
// Ok(())
|
||||||
|
// }
|
||||||
|
// Err(e) => Err(AppError::Monitor(e)),
|
||||||
|
// },
|
||||||
|
// Some(CliCommand::SetPlatformProfile { profile }) => {
|
||||||
|
// // Get available platform profiles and validate early if possible
|
||||||
|
// match cpu::get_platform_profiles() {
|
||||||
|
// Ok(available_profiles) => {
|
||||||
|
// if available_profiles.contains(&profile) {
|
||||||
|
// log::info!("Setting platform profile to '{profile}'");
|
||||||
|
// cpu::set_platform_profile(&profile).map_err(AppError::Control)
|
||||||
|
// } else {
|
||||||
|
// log::error!(
|
||||||
|
// "Invalid platform profile: '{}'. Available profiles: {}",
|
||||||
|
// profile,
|
||||||
|
// available_profiles.join(", ")
|
||||||
|
// );
|
||||||
|
// Err(AppError::Generic(format!(
|
||||||
|
// "Invalid platform profile: '{}'. Available profiles: {}",
|
||||||
|
// profile,
|
||||||
|
// available_profiles.join(", ")
|
||||||
|
// )))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// Err(_e) => {
|
||||||
|
// // If we can't get profiles (e.g., feature not supported), pass through to the function
|
||||||
|
// cpu::set_platform_profile(&profile).map_err(AppError::Control)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let Err(error) = real_main() else {
|
||||||
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = command_result {
|
let mut err = io::stderr();
|
||||||
error!("Error executing command: {e}");
|
|
||||||
if let Some(source) = e.source() {
|
|
||||||
error!("Caused by: {source}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for permission denied errors
|
let mut message = String::new();
|
||||||
if let AppError::Control(control_error) = &e {
|
let mut chain = error.chain().rev().peekable();
|
||||||
if matches!(control_error, ControlError::PermissionDenied(_)) {
|
|
||||||
error!(
|
while let Some(error) = chain.next() {
|
||||||
"Hint: This operation may require administrator privileges (e.g., run with sudo)."
|
let _ = write!(
|
||||||
);
|
err,
|
||||||
|
"{header} ",
|
||||||
|
header = if chain.peek().is_none() {
|
||||||
|
"error:"
|
||||||
|
} else {
|
||||||
|
"cause:"
|
||||||
}
|
}
|
||||||
}
|
.red()
|
||||||
|
.bold(),
|
||||||
|
);
|
||||||
|
|
||||||
std::process::exit(1);
|
String::clear(&mut message);
|
||||||
|
let _ = write!(message, "{error}");
|
||||||
|
|
||||||
|
let mut chars = message.char_indices();
|
||||||
|
|
||||||
|
let _ = match (chars.next(), chars.next()) {
|
||||||
|
(Some((_, first)), Some((second_start, second))) if second.is_lowercase() => {
|
||||||
|
writeln!(
|
||||||
|
err,
|
||||||
|
"{first_lowercase}{rest}",
|
||||||
|
first_lowercase = first.to_lowercase(),
|
||||||
|
rest = &message[second_start..],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
writeln!(err, "{message}")
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
process::exit(1);
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize the logger for the entire application
|
|
||||||
static LOGGER_INIT: Once = Once::new();
|
|
||||||
fn init_logger() {
|
|
||||||
LOGGER_INIT.call_once(|| {
|
|
||||||
// Set default log level based on environment or default to Info
|
|
||||||
let env_log = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string());
|
|
||||||
|
|
||||||
Builder::new()
|
|
||||||
.parse_filters(&env_log)
|
|
||||||
.format_timestamp(None)
|
|
||||||
.format_module_path(false)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
debug!("Logger initialized with RUST_LOG={env_log}");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate CPU frequency input values
|
|
||||||
fn validate_freq(freq_mhz: u32, label: &str) -> Result<(), AppError> {
|
|
||||||
if freq_mhz == 0 {
|
|
||||||
error!("{label} frequency cannot be zero");
|
|
||||||
Err(AppError::Generic(format!(
|
|
||||||
"{label} frequency cannot be zero"
|
|
||||||
)))
|
|
||||||
} else if freq_mhz > 10000 {
|
|
||||||
// Extremely high value unlikely to be valid
|
|
||||||
error!("{label} frequency ({freq_mhz} MHz) is unreasonably high");
|
|
||||||
Err(AppError::Generic(format!(
|
|
||||||
"{label} frequency ({freq_mhz} MHz) is unreasonably high"
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
use crate::config::AppConfig;
|
use crate::config::AppConfig;
|
||||||
use crate::core::{BatteryInfo, CpuCoreInfo, CpuGlobalInfo, SystemInfo, SystemLoad, SystemReport};
|
use crate::core::{BatteryInfo, CpuCoreInfo, CpuGlobalInfo, SystemInfo, SystemLoad, SystemReport};
|
||||||
use crate::cpu::get_logical_core_count;
|
use crate::cpu::get_real_cpus;
|
||||||
use crate::util::error::SysMonitorError;
|
use crate::util::error::SysMonitorError;
|
||||||
use log::debug;
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fs,
|
fs,
|
||||||
|
@ -364,7 +363,7 @@ pub fn get_all_cpu_core_info() -> Result<Vec<CpuCoreInfo>> {
|
||||||
thread::sleep(Duration::from_millis(250)); // interval for CPU usage calculation
|
thread::sleep(Duration::from_millis(250)); // interval for CPU usage calculation
|
||||||
let final_cpu_times = read_all_cpu_times()?;
|
let final_cpu_times = read_all_cpu_times()?;
|
||||||
|
|
||||||
let num_cores = get_logical_core_count()
|
let num_cores = get_real_cpus()
|
||||||
.map_err(|_| SysMonitorError::ReadError("Could not get the number of cores".to_string()))?;
|
.map_err(|_| SysMonitorError::ReadError("Could not get the number of cores".to_string()))?;
|
||||||
|
|
||||||
let mut core_infos = Vec::with_capacity(num_cores as usize);
|
let mut core_infos = Vec::with_capacity(num_cores as usize);
|
||||||
|
@ -395,7 +394,7 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo {
|
||||||
let mut cpufreq_base_path_buf = PathBuf::from("/sys/devices/system/cpu/cpu0/cpufreq/");
|
let mut cpufreq_base_path_buf = PathBuf::from("/sys/devices/system/cpu/cpu0/cpufreq/");
|
||||||
|
|
||||||
if !cpufreq_base_path_buf.exists() {
|
if !cpufreq_base_path_buf.exists() {
|
||||||
let core_count = get_logical_core_count().unwrap_or_else(|e| {
|
let core_count = get_real_cpus().unwrap_or_else(|e| {
|
||||||
eprintln!("Warning: {e}");
|
eprintln!("Warning: {e}");
|
||||||
0
|
0
|
||||||
});
|
});
|
||||||
|
@ -551,7 +550,7 @@ pub fn get_battery_info(config: &AppConfig) -> Result<Vec<BatteryInfo>> {
|
||||||
if ps_type == "Battery" {
|
if ps_type == "Battery" {
|
||||||
// Skip peripheral batteries that aren't real laptop batteries
|
// Skip peripheral batteries that aren't real laptop batteries
|
||||||
if is_peripheral_battery(&ps_path, &name) {
|
if is_peripheral_battery(&ps_path, &name) {
|
||||||
debug!("Skipping peripheral battery: {name}");
|
log::debug!("Skipping peripheral battery: {name}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -598,7 +597,7 @@ pub fn get_battery_info(config: &AppConfig) -> Result<Vec<BatteryInfo>> {
|
||||||
|
|
||||||
// If we found no batteries but have power supplies, we're likely on a desktop
|
// If we found no batteries but have power supplies, we're likely on a desktop
|
||||||
if batteries.is_empty() && overall_ac_connected {
|
if batteries.is_empty() && overall_ac_connected {
|
||||||
debug!("No laptop batteries found, likely a desktop system");
|
log::debug!("No laptop batteries found, likely a desktop system");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(batteries)
|
Ok(batteries)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue