mirror of
https://github.com/RGBCube/superfreq
synced 2025-07-28 09:27:44 +00:00
Merge pull request #12 from NotAShelf/daemon-mode
daemon: add conflict detection and governor override
This commit is contained in:
commit
25db531651
10 changed files with 932 additions and 110 deletions
29
Cargo.lock
generated
29
Cargo.lock
generated
|
@ -64,6 +64,12 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.38"
|
version = "4.5.38"
|
||||||
|
@ -110,6 +116,16 @@ 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 = "ctrlc"
|
||||||
|
version = "3.4.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73"
|
||||||
|
dependencies = [
|
||||||
|
"nix",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs"
|
name = "dirs"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
|
@ -204,6 +220,18 @@ version = "2.7.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.30.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_cpus"
|
name = "num_cpus"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
|
@ -295,6 +323,7 @@ name = "superfreq"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
|
"ctrlc",
|
||||||
"dirs",
|
"dirs",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
@ -9,3 +9,4 @@ toml = "0.8"
|
||||||
dirs = "6.0"
|
dirs = "6.0"
|
||||||
clap = { version = "4.0", features = ["derive"] }
|
clap = { version = "4.0", features = ["derive"] }
|
||||||
num_cpus = "1.16"
|
num_cpus = "1.16"
|
||||||
|
ctrlc = "3.4"
|
||||||
|
|
212
src/config.rs
212
src/config.rs
|
@ -1,30 +1,32 @@
|
||||||
|
use crate::core::TurboSetting;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use crate::core::{OperationalMode, TurboSetting};
|
use std::path::PathBuf;
|
||||||
|
|
||||||
// Structs for configuration using serde::Deserialize
|
// Structs for configuration using serde::Deserialize
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
pub struct ProfileConfig {
|
pub struct ProfileConfig {
|
||||||
pub governor: Option<String>,
|
pub governor: Option<String>,
|
||||||
pub turbo: Option<TurboSetting>,
|
pub turbo: Option<TurboSetting>,
|
||||||
pub epp: Option<String>, // Energy Performance Preference (EPP)
|
pub epp: Option<String>, // Energy Performance Preference (EPP)
|
||||||
pub epb: Option<String>, // Energy Performance Bias (EPB) - usually an integer, but string for flexibility from sysfs
|
pub epb: Option<String>, // Energy Performance Bias (EPB) - usually an integer, but string for flexibility from sysfs
|
||||||
pub min_freq_mhz: Option<u32>,
|
pub min_freq_mhz: Option<u32>,
|
||||||
pub max_freq_mhz: Option<u32>,
|
pub max_freq_mhz: Option<u32>,
|
||||||
pub platform_profile: Option<String>,
|
pub platform_profile: Option<String>,
|
||||||
|
pub turbo_auto_settings: Option<TurboAutoSettings>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ProfileConfig {
|
impl Default for ProfileConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
ProfileConfig {
|
Self {
|
||||||
governor: Some("schedutil".to_string()), // common sensible default (?)
|
governor: Some("schedutil".to_string()), // common sensible default (?)
|
||||||
turbo: Some(TurboSetting::Auto),
|
turbo: Some(TurboSetting::Auto),
|
||||||
epp: None, // defaults depend on governor and system
|
epp: None, // defaults depend on governor and system
|
||||||
epb: None, // defaults depend on governor and system
|
epb: None, // defaults depend on governor and system
|
||||||
min_freq_mhz: None, // no override
|
min_freq_mhz: None, // no override
|
||||||
max_freq_mhz: None, // no override
|
max_freq_mhz: None, // no override
|
||||||
platform_profile: None, // no override
|
platform_profile: None, // no override
|
||||||
|
turbo_auto_settings: Some(TurboAutoSettings::default()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,9 +41,11 @@ pub struct AppConfig {
|
||||||
pub ignored_power_supplies: Option<Vec<String>>,
|
pub ignored_power_supplies: Option<Vec<String>>,
|
||||||
#[serde(default = "default_poll_interval_sec")]
|
#[serde(default = "default_poll_interval_sec")]
|
||||||
pub poll_interval_sec: u64,
|
pub poll_interval_sec: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub daemon: DaemonConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_poll_interval_sec() -> u64 {
|
const fn default_poll_interval_sec() -> u64 {
|
||||||
5
|
5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,24 +59,24 @@ pub enum ConfigError {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<std::io::Error> for ConfigError {
|
impl From<std::io::Error> for ConfigError {
|
||||||
fn from(err: std::io::Error) -> ConfigError {
|
fn from(err: std::io::Error) -> Self {
|
||||||
ConfigError::Io(err)
|
Self::Io(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<toml::de::Error> for ConfigError {
|
impl From<toml::de::Error> for ConfigError {
|
||||||
fn from(err: toml::de::Error) -> ConfigError {
|
fn from(err: toml::de::Error) -> Self {
|
||||||
ConfigError::Toml(err)
|
Self::Toml(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for ConfigError {
|
impl std::fmt::Display for ConfigError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
ConfigError::Io(e) => write!(f, "I/O error: {}", e),
|
Self::Io(e) => write!(f, "I/O error: {e}"),
|
||||||
ConfigError::Toml(e) => write!(f, "TOML parsing error: {}", e),
|
Self::Toml(e) => write!(f, "TOML parsing error: {e}"),
|
||||||
ConfigError::NoValidConfigFound => write!(f, "No valid configuration file found."),
|
Self::NoValidConfigFound => write!(f, "No valid configuration file found."),
|
||||||
ConfigError::HomeDirNotFound => write!(f, "Could not determine user home directory."),
|
Self::HomeDirNotFound => write!(f, "Could not determine user home directory."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,7 +94,9 @@ pub fn load_config() -> Result<AppConfig, ConfigError> {
|
||||||
let user_config_path = home_dir.join(".config/auto_cpufreq_rs/config.toml");
|
let user_config_path = home_dir.join(".config/auto_cpufreq_rs/config.toml");
|
||||||
config_paths.push(user_config_path);
|
config_paths.push(user_config_path);
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Warning: Could not determine home directory. User-specific config will not be loaded.");
|
eprintln!(
|
||||||
|
"Warning: Could not determine home directory. User-specific config will not be loaded."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// System-wide path
|
// System-wide path
|
||||||
|
@ -108,9 +114,23 @@ pub fn load_config() -> Result<AppConfig, ConfigError> {
|
||||||
let app_config = AppConfig {
|
let app_config = AppConfig {
|
||||||
charger: ProfileConfig::from(toml_app_config.charger),
|
charger: ProfileConfig::from(toml_app_config.charger),
|
||||||
battery: ProfileConfig::from(toml_app_config.battery),
|
battery: ProfileConfig::from(toml_app_config.battery),
|
||||||
battery_charge_thresholds: toml_app_config.battery_charge_thresholds,
|
battery_charge_thresholds: toml_app_config
|
||||||
|
.battery_charge_thresholds,
|
||||||
ignored_power_supplies: toml_app_config.ignored_power_supplies,
|
ignored_power_supplies: toml_app_config.ignored_power_supplies,
|
||||||
poll_interval_sec: toml_app_config.poll_interval_sec,
|
poll_interval_sec: toml_app_config.poll_interval_sec,
|
||||||
|
daemon: DaemonConfig {
|
||||||
|
poll_interval_sec: toml_app_config.daemon.poll_interval_sec,
|
||||||
|
adaptive_interval: toml_app_config.daemon.adaptive_interval,
|
||||||
|
min_poll_interval_sec: toml_app_config
|
||||||
|
.daemon
|
||||||
|
.min_poll_interval_sec,
|
||||||
|
max_poll_interval_sec: toml_app_config
|
||||||
|
.daemon
|
||||||
|
.max_poll_interval_sec,
|
||||||
|
throttle_on_battery: toml_app_config.daemon.throttle_on_battery,
|
||||||
|
log_level: toml_app_config.daemon.log_level,
|
||||||
|
stats_file_path: toml_app_config.daemon.stats_file_path,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
return Ok(app_config);
|
return Ok(app_config);
|
||||||
}
|
}
|
||||||
|
@ -135,6 +155,7 @@ pub fn load_config() -> Result<AppConfig, ConfigError> {
|
||||||
battery_charge_thresholds: default_toml_config.battery_charge_thresholds,
|
battery_charge_thresholds: default_toml_config.battery_charge_thresholds,
|
||||||
ignored_power_supplies: default_toml_config.ignored_power_supplies,
|
ignored_power_supplies: default_toml_config.ignored_power_supplies,
|
||||||
poll_interval_sec: default_toml_config.poll_interval_sec,
|
poll_interval_sec: default_toml_config.poll_interval_sec,
|
||||||
|
daemon: DaemonConfig::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,11 +181,13 @@ pub struct AppConfigToml {
|
||||||
pub ignored_power_supplies: Option<Vec<String>>,
|
pub ignored_power_supplies: Option<Vec<String>>,
|
||||||
#[serde(default = "default_poll_interval_sec")]
|
#[serde(default = "default_poll_interval_sec")]
|
||||||
pub poll_interval_sec: u64,
|
pub poll_interval_sec: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub daemon: DaemonConfigToml,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ProfileConfigToml {
|
impl Default for ProfileConfigToml {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
ProfileConfigToml {
|
Self {
|
||||||
governor: Some("schedutil".to_string()),
|
governor: Some("schedutil".to_string()),
|
||||||
turbo: Some("auto".to_string()),
|
turbo: Some("auto".to_string()),
|
||||||
epp: None,
|
epp: None,
|
||||||
|
@ -176,22 +199,155 @@ impl Default for ProfileConfigToml {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct TurboAutoSettings {
|
||||||
|
#[serde(default = "default_load_threshold_high")]
|
||||||
|
pub load_threshold_high: f32,
|
||||||
|
#[serde(default = "default_load_threshold_low")]
|
||||||
|
pub load_threshold_low: f32,
|
||||||
|
#[serde(default = "default_temp_threshold_high")]
|
||||||
|
pub temp_threshold_high: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default thresholds for Auto turbo mode
|
||||||
|
pub const DEFAULT_LOAD_THRESHOLD_HIGH: f32 = 70.0; // enable turbo if load is above this
|
||||||
|
pub const DEFAULT_LOAD_THRESHOLD_LOW: f32 = 30.0; // disable turbo if load is below this
|
||||||
|
pub const DEFAULT_TEMP_THRESHOLD_HIGH: f32 = 75.0; // disable turbo if temperature is above this
|
||||||
|
|
||||||
|
const fn default_load_threshold_high() -> f32 {
|
||||||
|
DEFAULT_LOAD_THRESHOLD_HIGH
|
||||||
|
}
|
||||||
|
const fn default_load_threshold_low() -> f32 {
|
||||||
|
DEFAULT_LOAD_THRESHOLD_LOW
|
||||||
|
}
|
||||||
|
const fn default_temp_threshold_high() -> f32 {
|
||||||
|
DEFAULT_TEMP_THRESHOLD_HIGH
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TurboAutoSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
load_threshold_high: DEFAULT_LOAD_THRESHOLD_HIGH,
|
||||||
|
load_threshold_low: DEFAULT_LOAD_THRESHOLD_LOW,
|
||||||
|
temp_threshold_high: DEFAULT_TEMP_THRESHOLD_HIGH,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<ProfileConfigToml> for ProfileConfig {
|
impl From<ProfileConfigToml> for ProfileConfig {
|
||||||
fn from(toml_config: ProfileConfigToml) -> Self {
|
fn from(toml_config: ProfileConfigToml) -> Self {
|
||||||
ProfileConfig {
|
Self {
|
||||||
governor: toml_config.governor,
|
governor: toml_config.governor,
|
||||||
turbo: toml_config.turbo.and_then(|s| match s.to_lowercase().as_str() {
|
turbo: toml_config
|
||||||
"always" => Some(TurboSetting::Always),
|
.turbo
|
||||||
"auto" => Some(TurboSetting::Auto),
|
.and_then(|s| match s.to_lowercase().as_str() {
|
||||||
"never" => Some(TurboSetting::Never),
|
"always" => Some(TurboSetting::Always),
|
||||||
_ => None,
|
"auto" => Some(TurboSetting::Auto),
|
||||||
}),
|
"never" => Some(TurboSetting::Never),
|
||||||
|
_ => None,
|
||||||
|
}),
|
||||||
epp: toml_config.epp,
|
epp: toml_config.epp,
|
||||||
epb: toml_config.epb,
|
epb: toml_config.epb,
|
||||||
min_freq_mhz: toml_config.min_freq_mhz,
|
min_freq_mhz: toml_config.min_freq_mhz,
|
||||||
max_freq_mhz: toml_config.max_freq_mhz,
|
max_freq_mhz: toml_config.max_freq_mhz,
|
||||||
platform_profile: toml_config.platform_profile,
|
platform_profile: toml_config.platform_profile,
|
||||||
|
turbo_auto_settings: Some(TurboAutoSettings::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct DaemonConfig {
|
||||||
|
#[serde(default = "default_poll_interval_sec")]
|
||||||
|
pub poll_interval_sec: u64,
|
||||||
|
#[serde(default = "default_adaptive_interval")]
|
||||||
|
pub adaptive_interval: bool,
|
||||||
|
#[serde(default = "default_min_poll_interval_sec")]
|
||||||
|
pub min_poll_interval_sec: u64,
|
||||||
|
#[serde(default = "default_max_poll_interval_sec")]
|
||||||
|
pub max_poll_interval_sec: u64,
|
||||||
|
#[serde(default = "default_throttle_on_battery")]
|
||||||
|
pub throttle_on_battery: bool,
|
||||||
|
#[serde(default = "default_log_level")]
|
||||||
|
pub log_level: LogLevel,
|
||||||
|
#[serde(default = "default_stats_file_path")]
|
||||||
|
pub stats_file_path: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum LogLevel {
|
||||||
|
Error,
|
||||||
|
Warning,
|
||||||
|
Info,
|
||||||
|
Debug,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DaemonConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
poll_interval_sec: default_poll_interval_sec(),
|
||||||
|
adaptive_interval: default_adaptive_interval(),
|
||||||
|
min_poll_interval_sec: default_min_poll_interval_sec(),
|
||||||
|
max_poll_interval_sec: default_max_poll_interval_sec(),
|
||||||
|
throttle_on_battery: default_throttle_on_battery(),
|
||||||
|
log_level: default_log_level(),
|
||||||
|
stats_file_path: default_stats_file_path(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_adaptive_interval() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_min_poll_interval_sec() -> u64 {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_max_poll_interval_sec() -> u64 {
|
||||||
|
30
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_throttle_on_battery() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_log_level() -> LogLevel {
|
||||||
|
LogLevel::Info
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_stats_file_path() -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct DaemonConfigToml {
|
||||||
|
#[serde(default = "default_poll_interval_sec")]
|
||||||
|
pub poll_interval_sec: u64,
|
||||||
|
#[serde(default = "default_adaptive_interval")]
|
||||||
|
pub adaptive_interval: bool,
|
||||||
|
#[serde(default = "default_min_poll_interval_sec")]
|
||||||
|
pub min_poll_interval_sec: u64,
|
||||||
|
#[serde(default = "default_max_poll_interval_sec")]
|
||||||
|
pub max_poll_interval_sec: u64,
|
||||||
|
#[serde(default = "default_throttle_on_battery")]
|
||||||
|
pub throttle_on_battery: bool,
|
||||||
|
#[serde(default = "default_log_level")]
|
||||||
|
pub log_level: LogLevel,
|
||||||
|
#[serde(default = "default_stats_file_path")]
|
||||||
|
pub stats_file_path: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DaemonConfigToml {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
poll_interval_sec: default_poll_interval_sec(),
|
||||||
|
adaptive_interval: default_adaptive_interval(),
|
||||||
|
min_poll_interval_sec: default_min_poll_interval_sec(),
|
||||||
|
max_poll_interval_sec: default_max_poll_interval_sec(),
|
||||||
|
throttle_on_battery: default_throttle_on_battery(),
|
||||||
|
log_level: default_log_level(),
|
||||||
|
stats_file_path: default_stats_file_path(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
128
src/conflict.rs
Normal file
128
src/conflict.rs
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// Represents detected conflicts with other power management services
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ConflictDetection {
|
||||||
|
/// Whether TLP service was detected
|
||||||
|
pub tlp: bool,
|
||||||
|
/// Whether GNOME Power Profiles daemon was detected
|
||||||
|
pub gnome_power: bool,
|
||||||
|
/// Whether tuned service was detected
|
||||||
|
pub tuned: bool,
|
||||||
|
/// Other power managers that were detected
|
||||||
|
pub other: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConflictDetection {
|
||||||
|
/// Returns true if any conflicts were detected
|
||||||
|
pub fn has_conflicts(&self) -> bool {
|
||||||
|
self.tlp || self.gnome_power || self.tuned || !self.other.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get formatted conflict information
|
||||||
|
pub fn get_conflict_message(&self) -> String {
|
||||||
|
if !self.has_conflicts() {
|
||||||
|
return "No conflicts detected with other power management services.".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut message =
|
||||||
|
"Potential conflicts detected with other power management services:\n".to_string();
|
||||||
|
|
||||||
|
if self.tlp {
|
||||||
|
message.push_str("- TLP service is active. This may interfere with CPU settings.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.gnome_power {
|
||||||
|
message.push_str(
|
||||||
|
"- GNOME Power Profiles daemon is active. This may override CPU/power settings.\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.tuned {
|
||||||
|
message.push_str(
|
||||||
|
"- Tuned service is active. This may conflict with CPU frequency settings.\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for other in &self.other {
|
||||||
|
message.push_str(&format!(
|
||||||
|
"- {other} is active. This may conflict with superfreq.\n"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
message.push_str("\nConsider disabling conflicting services for optimal operation.");
|
||||||
|
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect if systemctl is available
|
||||||
|
fn systemctl_exists() -> bool {
|
||||||
|
Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg("command -v systemctl")
|
||||||
|
.status()
|
||||||
|
.is_ok_and(|status| status.success())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a specific systemd service is active.
|
||||||
|
// TODO: maybe we can use some kind of a binding here
|
||||||
|
// or figure out a better detection method?
|
||||||
|
fn is_service_active(service: &str) -> bool {
|
||||||
|
if !systemctl_exists() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Command::new("systemctl")
|
||||||
|
.arg("--quiet")
|
||||||
|
.arg("is-active")
|
||||||
|
.arg(service)
|
||||||
|
.status()
|
||||||
|
.is_ok_and(|status| status.success())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check for conflicts with other power management services
|
||||||
|
pub fn detect_conflicts() -> ConflictDetection {
|
||||||
|
let mut conflicts = ConflictDetection {
|
||||||
|
tlp: false,
|
||||||
|
gnome_power: false,
|
||||||
|
tuned: false,
|
||||||
|
other: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for TLP
|
||||||
|
conflicts.tlp = is_service_active("tlp.service");
|
||||||
|
|
||||||
|
// Check for GNOME Power Profiles daemon
|
||||||
|
conflicts.gnome_power = is_service_active("power-profiles-daemon.service");
|
||||||
|
|
||||||
|
// Check for tuned
|
||||||
|
conflicts.tuned = is_service_active("tuned.service");
|
||||||
|
|
||||||
|
// Check for other common power managers
|
||||||
|
let other_services = ["thermald.service", "powertop.service"];
|
||||||
|
for service in other_services {
|
||||||
|
if is_service_active(service) {
|
||||||
|
conflicts.other.push(service.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check if TLP is installed but not running as a service
|
||||||
|
// FIXME: This will obviously not work on non-FHS distros like NixOS
|
||||||
|
// which I kinda want to prioritize. Though, since we can't actually
|
||||||
|
// predict store paths I also don't know how else we can perform this
|
||||||
|
// check...
|
||||||
|
if !conflicts.tlp
|
||||||
|
&& Path::new("/usr/share/tlp").exists()
|
||||||
|
&& Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg("tlp-stat -s 2>/dev/null | grep -q 'TLP power save = enabled'")
|
||||||
|
.status()
|
||||||
|
.is_ok_and(|status| status.success())
|
||||||
|
{
|
||||||
|
conflicts.tlp = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
conflicts
|
||||||
|
}
|
38
src/core.rs
38
src/core.rs
|
@ -1,3 +1,31 @@
|
||||||
|
use clap::ValueEnum;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, ValueEnum)]
|
||||||
|
pub enum TurboSetting {
|
||||||
|
Always, // turbo is forced on (if possible)
|
||||||
|
Auto, // system or driver controls turbo
|
||||||
|
Never, // turbo is forced off
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||||
|
pub enum GovernorOverrideMode {
|
||||||
|
Performance,
|
||||||
|
Powersave,
|
||||||
|
Reset,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for GovernorOverrideMode {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Performance => write!(f, "performance"),
|
||||||
|
Self::Powersave => write!(f, "powersave"),
|
||||||
|
Self::Reset => write!(f, "reset"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct SystemInfo {
|
pub struct SystemInfo {
|
||||||
// Overall system details
|
// Overall system details
|
||||||
pub cpu_model: String,
|
pub cpu_model: String,
|
||||||
|
@ -59,13 +87,3 @@ pub enum OperationalMode {
|
||||||
Powersave,
|
Powersave,
|
||||||
Performance,
|
Performance,
|
||||||
}
|
}
|
||||||
|
|
||||||
use clap::ValueEnum;
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, ValueEnum)]
|
|
||||||
pub enum TurboSetting {
|
|
||||||
Always, // turbo is forced on (if possible)
|
|
||||||
Auto, // system or driver controls turbo
|
|
||||||
Never, // turbo is forced off
|
|
||||||
}
|
|
||||||
|
|
77
src/cpu.rs
77
src/cpu.rs
|
@ -1,4 +1,4 @@
|
||||||
use crate::core::TurboSetting;
|
use crate::core::{GovernorOverrideMode, TurboSetting};
|
||||||
use core::str;
|
use core::str;
|
||||||
use std::{fs, io, path::Path, string::ToString};
|
use std::{fs, io, path::Path, string::ToString};
|
||||||
|
|
||||||
|
@ -276,3 +276,78 @@ pub fn get_platform_profiles() -> Result<Vec<String>> {
|
||||||
.map(ToString::to_string)
|
.map(ToString::to_string)
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Path for storing the governor override state
|
||||||
|
const GOVERNOR_OVERRIDE_PATH: &str = "/etc/superfreq/governor_override";
|
||||||
|
|
||||||
|
/// Force a specific CPU governor or reset to automatic mode
|
||||||
|
pub fn force_governor(mode: GovernorOverrideMode) -> Result<()> {
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
let dir_path = Path::new("/etc/superfreq");
|
||||||
|
if !dir_path.exists() {
|
||||||
|
fs::create_dir_all(dir_path).map_err(|e| {
|
||||||
|
if e.kind() == io::ErrorKind::PermissionDenied {
|
||||||
|
ControlError::PermissionDenied(format!(
|
||||||
|
"Permission denied creating directory: {}. Try running with sudo.",
|
||||||
|
dir_path.display()
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
ControlError::Io(e)
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
match mode {
|
||||||
|
GovernorOverrideMode::Reset => {
|
||||||
|
// Remove the override file if it exists
|
||||||
|
if Path::new(GOVERNOR_OVERRIDE_PATH).exists() {
|
||||||
|
fs::remove_file(GOVERNOR_OVERRIDE_PATH).map_err(|e| {
|
||||||
|
if e.kind() == io::ErrorKind::PermissionDenied {
|
||||||
|
ControlError::PermissionDenied(format!(
|
||||||
|
"Permission denied removing override file: {GOVERNOR_OVERRIDE_PATH}. Try running with sudo."
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
ControlError::Io(e)
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
println!(
|
||||||
|
"Governor override has been reset. Normal profile-based settings will be used."
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!("No governor override was set.");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
GovernorOverrideMode::Performance | GovernorOverrideMode::Powersave => {
|
||||||
|
// Create the override file with the selected governor
|
||||||
|
let governor = mode.to_string().to_lowercase();
|
||||||
|
fs::write(GOVERNOR_OVERRIDE_PATH, &governor).map_err(|e| {
|
||||||
|
if e.kind() == io::ErrorKind::PermissionDenied {
|
||||||
|
ControlError::PermissionDenied(format!(
|
||||||
|
"Permission denied writing to override file: {GOVERNOR_OVERRIDE_PATH}. Try running with sudo."
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
ControlError::Io(e)
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Also apply the governor immediately
|
||||||
|
set_governor(&governor, None)?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Governor override set to '{governor}'. This setting will persist across reboots."
|
||||||
|
);
|
||||||
|
println!("To reset, use: superfreq force-governor reset");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current governor override if set
|
||||||
|
pub fn get_governor_override() -> Option<String> {
|
||||||
|
if Path::new(GOVERNOR_OVERRIDE_PATH).exists() {
|
||||||
|
fs::read_to_string(GOVERNOR_OVERRIDE_PATH).ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
303
src/daemon.rs
Normal file
303
src/daemon.rs
Normal file
|
@ -0,0 +1,303 @@
|
||||||
|
use crate::config::{AppConfig, LogLevel};
|
||||||
|
use crate::conflict;
|
||||||
|
use crate::core::SystemReport;
|
||||||
|
use crate::engine;
|
||||||
|
use crate::monitor;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
/// Run the daemon
|
||||||
|
pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Set effective log level based on config and verbose flag
|
||||||
|
let effective_log_level = if verbose {
|
||||||
|
LogLevel::Debug
|
||||||
|
} else {
|
||||||
|
config.daemon.log_level
|
||||||
|
};
|
||||||
|
|
||||||
|
log_message(
|
||||||
|
&effective_log_level,
|
||||||
|
LogLevel::Info,
|
||||||
|
"Starting superfreq daemon...",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check for conflicts with other power management services
|
||||||
|
let conflicts = conflict::detect_conflicts();
|
||||||
|
if conflicts.has_conflicts() {
|
||||||
|
log_message(
|
||||||
|
&effective_log_level,
|
||||||
|
LogLevel::Warning,
|
||||||
|
&conflicts.get_conflict_message(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a flag that will be set to true when a signal is received
|
||||||
|
let running = Arc::new(AtomicBool::new(true));
|
||||||
|
let r = running.clone();
|
||||||
|
|
||||||
|
// Set up signal handlers
|
||||||
|
ctrlc::set_handler(move || {
|
||||||
|
println!("Received shutdown signal, exiting...");
|
||||||
|
r.store(false, Ordering::SeqCst);
|
||||||
|
})
|
||||||
|
.expect("Error setting Ctrl-C handler");
|
||||||
|
|
||||||
|
log_message(
|
||||||
|
&effective_log_level,
|
||||||
|
LogLevel::Info,
|
||||||
|
&format!(
|
||||||
|
"Daemon initialized with poll interval: {}s",
|
||||||
|
config.daemon.poll_interval_sec
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set up stats file if configured
|
||||||
|
if let Some(stats_path) = &config.daemon.stats_file_path {
|
||||||
|
log_message(
|
||||||
|
&effective_log_level,
|
||||||
|
LogLevel::Info,
|
||||||
|
&format!("Stats will be written to: {stats_path}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variables for adaptive polling
|
||||||
|
let mut current_poll_interval = config.daemon.poll_interval_sec;
|
||||||
|
let mut last_settings_change = Instant::now();
|
||||||
|
let mut last_system_state = SystemState::Unknown;
|
||||||
|
|
||||||
|
// Main loop
|
||||||
|
while running.load(Ordering::SeqCst) {
|
||||||
|
let start_time = Instant::now();
|
||||||
|
|
||||||
|
match monitor::collect_system_report(&config) {
|
||||||
|
Ok(report) => {
|
||||||
|
log_message(
|
||||||
|
&effective_log_level,
|
||||||
|
LogLevel::Debug,
|
||||||
|
"Collected system report, applying settings...",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Determine current system state
|
||||||
|
let current_state = determine_system_state(&report);
|
||||||
|
|
||||||
|
// Update the stats file if configured
|
||||||
|
if let Some(stats_path) = &config.daemon.stats_file_path {
|
||||||
|
if let Err(e) = write_stats_file(stats_path, &report) {
|
||||||
|
log_message(
|
||||||
|
&effective_log_level,
|
||||||
|
LogLevel::Error,
|
||||||
|
&format!("Failed to write stats file: {e}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match engine::determine_and_apply_settings(&report, &config, None) {
|
||||||
|
Ok(()) => {
|
||||||
|
log_message(
|
||||||
|
&effective_log_level,
|
||||||
|
LogLevel::Debug,
|
||||||
|
"Successfully applied system settings",
|
||||||
|
);
|
||||||
|
|
||||||
|
// If system state changed or settings were applied differently, record the time
|
||||||
|
if current_state != last_system_state {
|
||||||
|
last_settings_change = Instant::now();
|
||||||
|
last_system_state = current_state.clone();
|
||||||
|
|
||||||
|
log_message(
|
||||||
|
&effective_log_level,
|
||||||
|
LogLevel::Info,
|
||||||
|
&format!("System state changed to: {current_state:?}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log_message(
|
||||||
|
&effective_log_level,
|
||||||
|
LogLevel::Error,
|
||||||
|
&format!("Error applying system settings: {e}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust poll interval if adaptive polling is enabled
|
||||||
|
if config.daemon.adaptive_interval {
|
||||||
|
let time_since_change = last_settings_change.elapsed().as_secs();
|
||||||
|
|
||||||
|
// If we've been stable for a while, increase the interval (up to max)
|
||||||
|
if time_since_change > 60 {
|
||||||
|
current_poll_interval =
|
||||||
|
(current_poll_interval * 2).min(config.daemon.max_poll_interval_sec);
|
||||||
|
|
||||||
|
log_message(
|
||||||
|
&effective_log_level,
|
||||||
|
LogLevel::Debug,
|
||||||
|
&format!(
|
||||||
|
"Adaptive polling: increasing interval to {current_poll_interval}s"
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if time_since_change < 10 {
|
||||||
|
// If we've had recent changes, decrease the interval (down to min)
|
||||||
|
current_poll_interval =
|
||||||
|
(current_poll_interval / 2).max(config.daemon.min_poll_interval_sec);
|
||||||
|
|
||||||
|
log_message(
|
||||||
|
&effective_log_level,
|
||||||
|
LogLevel::Debug,
|
||||||
|
&format!(
|
||||||
|
"Adaptive polling: decreasing interval to {current_poll_interval}s"
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If not adaptive, use the configured poll interval
|
||||||
|
current_poll_interval = config.daemon.poll_interval_sec;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If on battery and throttling is enabled, lengthen the poll interval to save power
|
||||||
|
if config.daemon.throttle_on_battery
|
||||||
|
&& !report.batteries.is_empty()
|
||||||
|
&& report.batteries.first().is_some_and(|b| !b.ac_connected)
|
||||||
|
{
|
||||||
|
let battery_multiplier = 2; // Poll half as often on battery
|
||||||
|
current_poll_interval = (current_poll_interval * battery_multiplier)
|
||||||
|
.min(config.daemon.max_poll_interval_sec);
|
||||||
|
|
||||||
|
log_message(
|
||||||
|
&effective_log_level,
|
||||||
|
LogLevel::Debug,
|
||||||
|
"On battery power, increasing poll interval to save energy",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log_message(
|
||||||
|
&effective_log_level,
|
||||||
|
LogLevel::Error,
|
||||||
|
&format!("Error collecting system report: {e}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sleep for the remaining time in the poll interval
|
||||||
|
let elapsed = start_time.elapsed();
|
||||||
|
let poll_duration = Duration::from_secs(current_poll_interval);
|
||||||
|
if elapsed < poll_duration {
|
||||||
|
let sleep_time = poll_duration - elapsed;
|
||||||
|
log_message(
|
||||||
|
&effective_log_level,
|
||||||
|
LogLevel::Debug,
|
||||||
|
&format!("Sleeping for {}s until next cycle", sleep_time.as_secs()),
|
||||||
|
);
|
||||||
|
std::thread::sleep(sleep_time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log_message(&effective_log_level, LogLevel::Info, "Daemon stopped");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log a message based on the current log level
|
||||||
|
fn log_message(effective_level: &LogLevel, msg_level: LogLevel, message: &str) {
|
||||||
|
// Only log messages at or above the effective log level
|
||||||
|
let should_log = match effective_level {
|
||||||
|
LogLevel::Error => matches!(msg_level, LogLevel::Error),
|
||||||
|
LogLevel::Warning => matches!(msg_level, LogLevel::Error | LogLevel::Warning),
|
||||||
|
LogLevel::Info => matches!(
|
||||||
|
msg_level,
|
||||||
|
LogLevel::Error | LogLevel::Warning | LogLevel::Info
|
||||||
|
),
|
||||||
|
LogLevel::Debug => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_log {
|
||||||
|
match msg_level {
|
||||||
|
LogLevel::Error => eprintln!("ERROR: {message}"),
|
||||||
|
LogLevel::Warning => eprintln!("WARNING: {message}"),
|
||||||
|
LogLevel::Info => println!("INFO: {message}"),
|
||||||
|
LogLevel::Debug => println!("DEBUG: {message}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write current system stats to a file for --stats to read
|
||||||
|
fn write_stats_file(path: &str, report: &SystemReport) -> Result<(), std::io::Error> {
|
||||||
|
let mut file = File::create(path)?;
|
||||||
|
|
||||||
|
writeln!(file, "timestamp={:?}", report.timestamp)?;
|
||||||
|
|
||||||
|
// CPU info
|
||||||
|
writeln!(file, "governor={:?}", report.cpu_global.current_governor)?;
|
||||||
|
writeln!(file, "turbo={:?}", report.cpu_global.turbo_status)?;
|
||||||
|
|
||||||
|
if let Some(temp) = report.cpu_global.average_temperature_celsius {
|
||||||
|
writeln!(file, "cpu_temp={temp:.1}")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Battery info
|
||||||
|
if !report.batteries.is_empty() {
|
||||||
|
let battery = &report.batteries[0];
|
||||||
|
writeln!(file, "ac_power={}", battery.ac_connected)?;
|
||||||
|
if let Some(cap) = battery.capacity_percent {
|
||||||
|
writeln!(file, "battery_percent={cap}")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// System load
|
||||||
|
writeln!(file, "load_1m={:.2}", report.system_load.load_avg_1min)?;
|
||||||
|
writeln!(file, "load_5m={:.2}", report.system_load.load_avg_5min)?;
|
||||||
|
writeln!(file, "load_15m={:.2}", report.system_load.load_avg_15min)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simplified system state used for determining when to adjust polling interval
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
enum SystemState {
|
||||||
|
Unknown,
|
||||||
|
OnAC,
|
||||||
|
OnBattery,
|
||||||
|
HighLoad,
|
||||||
|
LowLoad,
|
||||||
|
HighTemp,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine the current system state for adaptive polling
|
||||||
|
fn determine_system_state(report: &SystemReport) -> SystemState {
|
||||||
|
// Check power state first
|
||||||
|
if !report.batteries.is_empty() {
|
||||||
|
if let Some(battery) = report.batteries.first() {
|
||||||
|
if battery.ac_connected {
|
||||||
|
return SystemState::OnAC;
|
||||||
|
}
|
||||||
|
return SystemState::OnBattery;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No batteries means desktop, so always AC
|
||||||
|
if report.batteries.is_empty() {
|
||||||
|
return SystemState::OnAC;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check temperature
|
||||||
|
if let Some(temp) = report.cpu_global.average_temperature_celsius {
|
||||||
|
if temp > 80.0 {
|
||||||
|
return SystemState::HighTemp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check load
|
||||||
|
let avg_load = report.system_load.load_avg_1min;
|
||||||
|
if avg_load > 3.0 {
|
||||||
|
return SystemState::HighLoad;
|
||||||
|
}
|
||||||
|
if avg_load < 0.5 {
|
||||||
|
return SystemState::LowLoad;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default case
|
||||||
|
SystemState::Unknown
|
||||||
|
}
|
129
src/engine.rs
129
src/engine.rs
|
@ -1,5 +1,5 @@
|
||||||
use crate::core::{SystemReport, OperationalMode, TurboSetting};
|
|
||||||
use crate::config::{AppConfig, ProfileConfig};
|
use crate::config::{AppConfig, ProfileConfig};
|
||||||
|
use crate::core::{OperationalMode, SystemReport, TurboSetting};
|
||||||
use crate::cpu::{self, ControlError};
|
use crate::cpu::{self, ControlError};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -10,15 +10,15 @@ pub enum EngineError {
|
||||||
|
|
||||||
impl From<ControlError> for EngineError {
|
impl From<ControlError> for EngineError {
|
||||||
fn from(err: ControlError) -> Self {
|
fn from(err: ControlError) -> Self {
|
||||||
EngineError::ControlError(err)
|
Self::ControlError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for EngineError {
|
impl std::fmt::Display for EngineError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
EngineError::ControlError(e) => write!(f, "CPU control error: {}", e),
|
Self::ControlError(e) => write!(f, "CPU control error: {e}"),
|
||||||
EngineError::ConfigurationError(s) => write!(f, "Configuration error: {}", s),
|
Self::ConfigurationError(s) => write!(f, "Configuration error: {s}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,8 +26,8 @@ impl std::fmt::Display for EngineError {
|
||||||
impl std::error::Error for EngineError {
|
impl std::error::Error for EngineError {
|
||||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
match self {
|
match self {
|
||||||
EngineError::ControlError(e) => Some(e),
|
Self::ControlError(e) => Some(e),
|
||||||
EngineError::ConfigurationError(_) => None,
|
Self::ConfigurationError(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,15 @@ pub fn determine_and_apply_settings(
|
||||||
config: &AppConfig,
|
config: &AppConfig,
|
||||||
force_mode: Option<OperationalMode>,
|
force_mode: Option<OperationalMode>,
|
||||||
) -> Result<(), EngineError> {
|
) -> Result<(), EngineError> {
|
||||||
|
// First, check if there's a governor override set
|
||||||
|
if let Some(override_governor) = cpu::get_governor_override() {
|
||||||
|
println!(
|
||||||
|
"Engine: Governor override is active: '{}'. Setting governor.",
|
||||||
|
override_governor.trim()
|
||||||
|
);
|
||||||
|
cpu::set_governor(override_governor.trim(), None)?;
|
||||||
|
}
|
||||||
|
|
||||||
let selected_profile_config: &ProfileConfig;
|
let selected_profile_config: &ProfileConfig;
|
||||||
|
|
||||||
if let Some(mode) = force_mode {
|
if let Some(mode) = force_mode {
|
||||||
|
@ -57,8 +66,8 @@ pub fn determine_and_apply_settings(
|
||||||
// If no batteries, assume AC power (desktop).
|
// If no batteries, assume AC power (desktop).
|
||||||
// Otherwise, check the ac_connected status from the (first) battery.
|
// Otherwise, check the ac_connected status from the (first) battery.
|
||||||
// XXX: This relies on the setting ac_connected in BatteryInfo being set correctly.
|
// XXX: This relies on the setting ac_connected in BatteryInfo being set correctly.
|
||||||
let on_ac_power = report.batteries.is_empty() ||
|
let on_ac_power = report.batteries.is_empty()
|
||||||
report.batteries.first().map_or(false, |b| b.ac_connected);
|
|| report.batteries.first().is_some_and(|b| b.ac_connected);
|
||||||
|
|
||||||
if on_ac_power {
|
if on_ac_power {
|
||||||
println!("Engine: On AC power, selecting Charger profile.");
|
println!("Engine: On AC power, selecting Charger profile.");
|
||||||
|
@ -74,37 +83,43 @@ pub fn determine_and_apply_settings(
|
||||||
// and we'd like to replace them with proper logging in the future.
|
// and we'd like to replace them with proper logging in the future.
|
||||||
|
|
||||||
if let Some(governor) = &selected_profile_config.governor {
|
if let Some(governor) = &selected_profile_config.governor {
|
||||||
println!("Engine: Setting governor to '{}'", governor);
|
println!("Engine: Setting governor to '{governor}'");
|
||||||
cpu::set_governor(governor, None)?;
|
cpu::set_governor(governor, None)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(turbo_setting) = selected_profile_config.turbo {
|
if let Some(turbo_setting) = selected_profile_config.turbo {
|
||||||
println!("Engine: Setting turbo to '{:?}'", turbo_setting);
|
println!("Engine: Setting turbo to '{turbo_setting:?}'");
|
||||||
cpu::set_turbo(turbo_setting)?;
|
match turbo_setting {
|
||||||
|
TurboSetting::Auto => {
|
||||||
|
println!("Engine: Managing turbo in auto mode based on system conditions");
|
||||||
|
manage_auto_turbo(report, selected_profile_config)?;
|
||||||
|
}
|
||||||
|
_ => cpu::set_turbo(turbo_setting)?,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(epp) = &selected_profile_config.epp {
|
if let Some(epp) = &selected_profile_config.epp {
|
||||||
println!("Engine: Setting EPP to '{}'", epp);
|
println!("Engine: Setting EPP to '{epp}'");
|
||||||
cpu::set_epp(epp, None)?;
|
cpu::set_epp(epp, None)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(epb) = &selected_profile_config.epb {
|
if let Some(epb) = &selected_profile_config.epb {
|
||||||
println!("Engine: Setting EPB to '{}'", epb);
|
println!("Engine: Setting EPB to '{epb}'");
|
||||||
cpu::set_epb(epb, None)?;
|
cpu::set_epb(epb, None)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(min_freq) = selected_profile_config.min_freq_mhz {
|
if let Some(min_freq) = selected_profile_config.min_freq_mhz {
|
||||||
println!("Engine: Setting min frequency to '{} MHz'", min_freq);
|
println!("Engine: Setting min frequency to '{min_freq} MHz'");
|
||||||
cpu::set_min_frequency(min_freq, None)?;
|
cpu::set_min_frequency(min_freq, None)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(max_freq) = selected_profile_config.max_freq_mhz {
|
if let Some(max_freq) = selected_profile_config.max_freq_mhz {
|
||||||
println!("Engine: Setting max frequency to '{} MHz'", max_freq);
|
println!("Engine: Setting max frequency to '{max_freq} MHz'");
|
||||||
cpu::set_max_frequency(max_freq, None)?;
|
cpu::set_max_frequency(max_freq, None)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(profile) = &selected_profile_config.platform_profile {
|
if let Some(profile) = &selected_profile_config.platform_profile {
|
||||||
println!("Engine: Setting platform profile to '{}'", profile);
|
println!("Engine: Setting platform profile to '{profile}'");
|
||||||
cpu::set_platform_profile(profile)?;
|
cpu::set_platform_profile(profile)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,3 +127,85 @@ pub fn determine_and_apply_settings(
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn manage_auto_turbo(report: &SystemReport, config: &ProfileConfig) -> Result<(), EngineError> {
|
||||||
|
// Get the auto turbo settings from the config, or use defaults
|
||||||
|
let turbo_settings = config.turbo_auto_settings.clone().unwrap_or_default();
|
||||||
|
|
||||||
|
// Get average CPU temperature and CPU load
|
||||||
|
let cpu_temp = report.cpu_global.average_temperature_celsius;
|
||||||
|
|
||||||
|
// Check if we have CPU usage data available
|
||||||
|
let avg_cpu_usage = if report.cpu_cores.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let sum: f32 = report
|
||||||
|
.cpu_cores
|
||||||
|
.iter()
|
||||||
|
.filter_map(|core| core.usage_percent)
|
||||||
|
.sum();
|
||||||
|
let count = report
|
||||||
|
.cpu_cores
|
||||||
|
.iter()
|
||||||
|
.filter(|core| core.usage_percent.is_some())
|
||||||
|
.count();
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
Some(sum / count as f32)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decision logic for enabling/disabling turbo
|
||||||
|
let enable_turbo = match (cpu_temp, avg_cpu_usage) {
|
||||||
|
// If temperature is too high, disable turbo regardless of load
|
||||||
|
(Some(temp), _) if temp >= turbo_settings.temp_threshold_high => {
|
||||||
|
println!(
|
||||||
|
"Engine: Auto Turbo: Disabled due to high temperature ({:.1}°C >= {:.1}°C)",
|
||||||
|
temp, turbo_settings.temp_threshold_high
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
// If load is high enough, enable turbo (unless temp already caused it to disable)
|
||||||
|
(_, Some(usage)) if usage >= turbo_settings.load_threshold_high => {
|
||||||
|
println!(
|
||||||
|
"Engine: Auto Turbo: Enabled due to high CPU load ({:.1}% >= {:.1}%)",
|
||||||
|
usage, turbo_settings.load_threshold_high
|
||||||
|
);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
// If load is low, disable turbo
|
||||||
|
(_, Some(usage)) if usage <= turbo_settings.load_threshold_low => {
|
||||||
|
println!(
|
||||||
|
"Engine: Auto Turbo: Disabled due to low CPU load ({:.1}% <= {:.1}%)",
|
||||||
|
usage, turbo_settings.load_threshold_low
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
// In intermediate load scenarios or if we can't determine, leave turbo in current state
|
||||||
|
// For now, we'll disable it as a safe default
|
||||||
|
_ => {
|
||||||
|
println!("Engine: Auto Turbo: Disabled (default for indeterminate state)");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply the turbo setting
|
||||||
|
let turbo_setting = if enable_turbo {
|
||||||
|
TurboSetting::Always
|
||||||
|
} else {
|
||||||
|
TurboSetting::Never
|
||||||
|
};
|
||||||
|
|
||||||
|
match cpu::set_turbo(turbo_setting) {
|
||||||
|
Ok(()) => {
|
||||||
|
println!(
|
||||||
|
"Engine: Auto Turbo: Successfully set turbo to {}",
|
||||||
|
if enable_turbo { "enabled" } else { "disabled" }
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err(EngineError::ControlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
35
src/main.rs
35
src/main.rs
|
@ -1,11 +1,13 @@
|
||||||
mod config;
|
mod config;
|
||||||
|
mod conflict;
|
||||||
mod core;
|
mod core;
|
||||||
mod cpu;
|
mod cpu;
|
||||||
|
mod daemon;
|
||||||
mod engine;
|
mod engine;
|
||||||
mod monitor;
|
mod monitor;
|
||||||
|
|
||||||
use crate::config::AppConfig;
|
use crate::config::AppConfig;
|
||||||
use crate::core::TurboSetting;
|
use crate::core::{GovernorOverrideMode, TurboSetting};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
|
@ -19,12 +21,23 @@ struct Cli {
|
||||||
enum Commands {
|
enum Commands {
|
||||||
/// Display current system information
|
/// Display current system information
|
||||||
Info,
|
Info,
|
||||||
|
/// Run as a daemon in the background
|
||||||
|
Daemon {
|
||||||
|
#[clap(long)]
|
||||||
|
verbose: bool,
|
||||||
|
},
|
||||||
/// Set CPU governor
|
/// Set CPU governor
|
||||||
SetGovernor {
|
SetGovernor {
|
||||||
governor: String,
|
governor: String,
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
core_id: Option<u32>,
|
core_id: Option<u32>,
|
||||||
},
|
},
|
||||||
|
/// Force a specific governor mode persistently
|
||||||
|
ForceGovernor {
|
||||||
|
/// Mode to force: performance, powersave, or reset
|
||||||
|
#[clap(value_enum)]
|
||||||
|
mode: GovernorOverrideMode,
|
||||||
|
},
|
||||||
/// Set turbo boost behavior
|
/// Set turbo boost behavior
|
||||||
SetTurbo {
|
SetTurbo {
|
||||||
#[clap(value_enum)]
|
#[clap(value_enum)]
|
||||||
|
@ -66,7 +79,7 @@ fn main() {
|
||||||
let config = match config::load_config() {
|
let config = match config::load_config() {
|
||||||
Ok(cfg) => cfg,
|
Ok(cfg) => cfg,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error loading configuration: {}. Using default values.", e);
|
eprintln!("Error loading configuration: {e}. Using default values.");
|
||||||
// Proceed with default config if loading fails, as per previous steps
|
// Proceed with default config if loading fails, as per previous steps
|
||||||
AppConfig::default()
|
AppConfig::default()
|
||||||
}
|
}
|
||||||
|
@ -98,7 +111,7 @@ fn main() {
|
||||||
"Average CPU Temperature: {}",
|
"Average CPU Temperature: {}",
|
||||||
report.cpu_global.average_temperature_celsius.map_or_else(
|
report.cpu_global.average_temperature_celsius.map_or_else(
|
||||||
|| "N/A (CPU temperature sensor not detected)".to_string(),
|
|| "N/A (CPU temperature sensor not detected)".to_string(),
|
||||||
|t| format!("{:.1}°C", t)
|
|t| format!("{t:.1}°C")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -118,10 +131,10 @@ fn main() {
|
||||||
.map_or_else(|| "N/A".to_string(), |f| f.to_string()),
|
.map_or_else(|| "N/A".to_string(), |f| f.to_string()),
|
||||||
core_info
|
core_info
|
||||||
.usage_percent
|
.usage_percent
|
||||||
.map_or_else(|| "N/A".to_string(), |f| format!("{:.1}", f)),
|
.map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}")),
|
||||||
core_info
|
core_info
|
||||||
.temperature_celsius
|
.temperature_celsius
|
||||||
.map_or_else(|| "N/A".to_string(), |f| format!("{:.1}", f))
|
.map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +153,7 @@ fn main() {
|
||||||
.map_or_else(|| "N/A".to_string(), |c| c.to_string()),
|
.map_or_else(|| "N/A".to_string(), |c| c.to_string()),
|
||||||
battery_info
|
battery_info
|
||||||
.power_rate_watts
|
.power_rate_watts
|
||||||
.map_or_else(|| "N/A".to_string(), |p| format!("{:.2}", p)),
|
.map_or_else(|| "N/A".to_string(), |p| format!("{p:.2}")),
|
||||||
battery_info
|
battery_info
|
||||||
.charge_start_threshold
|
.charge_start_threshold
|
||||||
.map_or_else(|| "N/A".to_string(), |t| t.to_string()),
|
.map_or_else(|| "N/A".to_string(), |t| t.to_string()),
|
||||||
|
@ -164,6 +177,9 @@ fn main() {
|
||||||
},
|
},
|
||||||
Some(Commands::SetGovernor { governor, core_id }) => cpu::set_governor(&governor, core_id)
|
Some(Commands::SetGovernor { governor, core_id }) => cpu::set_governor(&governor, core_id)
|
||||||
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>),
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>),
|
||||||
|
Some(Commands::ForceGovernor { mode }) => {
|
||||||
|
cpu::force_governor(mode).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
|
||||||
|
}
|
||||||
Some(Commands::SetTurbo { setting }) => {
|
Some(Commands::SetTurbo { setting }) => {
|
||||||
cpu::set_turbo(setting).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
|
cpu::set_turbo(setting).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
|
||||||
}
|
}
|
||||||
|
@ -183,17 +199,18 @@ fn main() {
|
||||||
}
|
}
|
||||||
Some(Commands::SetPlatformProfile { profile }) => cpu::set_platform_profile(&profile)
|
Some(Commands::SetPlatformProfile { profile }) => cpu::set_platform_profile(&profile)
|
||||||
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>),
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>),
|
||||||
|
Some(Commands::Daemon { verbose }) => daemon::run_daemon(config, verbose),
|
||||||
None => {
|
None => {
|
||||||
println!("Welcome to superfreq! Use --help for commands.");
|
println!("Welcome to superfreq! Use --help for commands.");
|
||||||
println!("Current effective configuration: {:?}", config);
|
println!("Current effective configuration: {config:?}");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = command_result {
|
if let Err(e) = command_result {
|
||||||
eprintln!("Error executing command: {}", e);
|
eprintln!("Error executing command: {e}");
|
||||||
if let Some(source) = e.source() {
|
if let Some(source) = e.source() {
|
||||||
eprintln!("Caused by: {}", source);
|
eprintln!("Caused by: {source}");
|
||||||
}
|
}
|
||||||
// TODO: Consider specific error handling for PermissionDenied from cpu here
|
// TODO: Consider specific error handling for PermissionDenied from cpu here
|
||||||
// For example, check if e.downcast_ref::<cpu::ControlError>() matches PermissionDenied
|
// For example, check if e.downcast_ref::<cpu::ControlError>() matches PermissionDenied
|
||||||
|
|
|
@ -20,21 +20,21 @@ pub enum SysMonitorError {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<io::Error> for SysMonitorError {
|
impl From<io::Error> for SysMonitorError {
|
||||||
fn from(err: io::Error) -> SysMonitorError {
|
fn from(err: io::Error) -> Self {
|
||||||
SysMonitorError::Io(err)
|
Self::Io(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for SysMonitorError {
|
impl std::fmt::Display for SysMonitorError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
SysMonitorError::Io(e) => write!(f, "I/O error: {}", e),
|
Self::Io(e) => write!(f, "I/O error: {e}"),
|
||||||
SysMonitorError::ReadError(s) => write!(f, "Failed to read sysfs path: {}", s),
|
Self::ReadError(s) => write!(f, "Failed to read sysfs path: {s}"),
|
||||||
SysMonitorError::ParseError(s) => write!(f, "Failed to parse value: {}", s),
|
Self::ParseError(s) => write!(f, "Failed to parse value: {s}"),
|
||||||
SysMonitorError::ProcStatParseError(s) => {
|
Self::ProcStatParseError(s) => {
|
||||||
write!(f, "Failed to parse /proc/stat: {}", s)
|
write!(f, "Failed to parse /proc/stat: {s}")
|
||||||
}
|
}
|
||||||
SysMonitorError::NotAvailable(s) => write!(f, "Information not available: {}", s),
|
Self::NotAvailable(s) => write!(f, "Information not available: {s}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,7 +123,7 @@ fn get_logical_core_count() -> Result<u32> {
|
||||||
// Check if it's a directory representing a core that can have cpufreq
|
// Check if it's a directory representing a core that can have cpufreq
|
||||||
if entry.path().join("cpufreq").exists() {
|
if entry.path().join("cpufreq").exists() {
|
||||||
count += 1;
|
count += 1;
|
||||||
} else if Path::new(&format!("/sys/devices/system/cpu/{}/online", name_str))
|
} else if Path::new(&format!("/sys/devices/system/cpu/{name_str}/online"))
|
||||||
.exists()
|
.exists()
|
||||||
{
|
{
|
||||||
// Fallback for cores that might not have cpufreq but are online (e.g. E-cores on some setups before driver loads)
|
// Fallback for cores that might not have cpufreq but are online (e.g. E-cores on some setups before driver loads)
|
||||||
|
@ -159,7 +159,7 @@ struct CpuTimes {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CpuTimes {
|
impl CpuTimes {
|
||||||
fn total_time(&self) -> u64 {
|
const fn total_time(&self) -> u64 {
|
||||||
self.user
|
self.user
|
||||||
+ self.nice
|
+ self.nice
|
||||||
+ self.system
|
+ self.system
|
||||||
|
@ -170,7 +170,7 @@ impl CpuTimes {
|
||||||
+ self.steal
|
+ self.steal
|
||||||
}
|
}
|
||||||
|
|
||||||
fn idle_time(&self) -> u64 {
|
const fn idle_time(&self) -> u64 {
|
||||||
self.idle + self.iowait
|
self.idle + self.iowait
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -180,20 +180,18 @@ fn read_all_cpu_times() -> Result<HashMap<u32, CpuTimes>> {
|
||||||
let mut cpu_times_map = HashMap::new();
|
let mut cpu_times_map = HashMap::new();
|
||||||
|
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
if line.starts_with("cpu") && line.chars().nth(3).map_or(false, |c| c.is_digit(10)) {
|
if line.starts_with("cpu") && line.chars().nth(3).is_some_and(|c| c.is_ascii_digit()) {
|
||||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
if parts.len() < 11 {
|
if parts.len() < 11 {
|
||||||
return Err(SysMonitorError::ProcStatParseError(format!(
|
return Err(SysMonitorError::ProcStatParseError(format!(
|
||||||
"Line too short: {}",
|
"Line too short: {line}"
|
||||||
line
|
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let core_id_str = &parts[0][3..];
|
let core_id_str = &parts[0][3..];
|
||||||
let core_id = core_id_str.parse::<u32>().map_err(|_| {
|
let core_id = core_id_str.parse::<u32>().map_err(|_| {
|
||||||
SysMonitorError::ProcStatParseError(format!(
|
SysMonitorError::ProcStatParseError(format!(
|
||||||
"Failed to parse core_id: {}",
|
"Failed to parse core_id: {core_id_str}"
|
||||||
core_id_str
|
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -270,7 +268,7 @@ pub fn get_cpu_core_info(
|
||||||
prev_times: &CpuTimes,
|
prev_times: &CpuTimes,
|
||||||
current_times: &CpuTimes,
|
current_times: &CpuTimes,
|
||||||
) -> Result<CpuCoreInfo> {
|
) -> Result<CpuCoreInfo> {
|
||||||
let cpufreq_path = PathBuf::from(format!("/sys/devices/system/cpu/cpu{}/cpufreq/", core_id));
|
let cpufreq_path = PathBuf::from(format!("/sys/devices/system/cpu/cpu{core_id}/cpufreq/"));
|
||||||
|
|
||||||
let current_frequency_mhz = read_sysfs_value::<u32>(cpufreq_path.join("scaling_cur_freq"))
|
let current_frequency_mhz = read_sysfs_value::<u32>(cpufreq_path.join("scaling_cur_freq"))
|
||||||
.map(|khz| khz / 1000)
|
.map(|khz| khz / 1000)
|
||||||
|
@ -405,21 +403,21 @@ pub fn get_cpu_core_info(
|
||||||
fn get_temperature_for_core(hw_path: &Path, core_id: u32, label_prefix: &str) -> Option<f32> {
|
fn get_temperature_for_core(hw_path: &Path, core_id: u32, label_prefix: &str) -> Option<f32> {
|
||||||
for i in 1..=32 {
|
for i in 1..=32 {
|
||||||
// Increased range to handle systems with many sensors
|
// Increased range to handle systems with many sensors
|
||||||
let label_path = hw_path.join(format!("temp{}_label", i));
|
let label_path = hw_path.join(format!("temp{i}_label"));
|
||||||
let input_path = hw_path.join(format!("temp{}_input", i));
|
let input_path = hw_path.join(format!("temp{i}_input"));
|
||||||
|
|
||||||
if label_path.exists() && input_path.exists() {
|
if label_path.exists() && input_path.exists() {
|
||||||
if let Ok(label) = read_sysfs_file_trimmed(&label_path) {
|
if let Ok(label) = read_sysfs_file_trimmed(&label_path) {
|
||||||
// Match various common label formats:
|
// Match various common label formats:
|
||||||
// "Core X", "core X", "Core-X", "CPU Core X", etc.
|
// "Core X", "core X", "Core-X", "CPU Core X", etc.
|
||||||
let core_pattern = format!("{} {}", label_prefix, core_id);
|
let core_pattern = format!("{label_prefix} {core_id}");
|
||||||
let alt_pattern = format!("{}-{}", label_prefix, core_id);
|
let alt_pattern = format!("{label_prefix}-{core_id}");
|
||||||
|
|
||||||
if label.eq_ignore_ascii_case(&core_pattern)
|
if label.eq_ignore_ascii_case(&core_pattern)
|
||||||
|| label.eq_ignore_ascii_case(&alt_pattern)
|
|| label.eq_ignore_ascii_case(&alt_pattern)
|
||||||
|| label
|
|| label
|
||||||
.to_lowercase()
|
.to_lowercase()
|
||||||
.contains(&format!("core {}", core_id).to_lowercase())
|
.contains(&format!("core {core_id}").to_lowercase())
|
||||||
{
|
{
|
||||||
if let Ok(temp_mc) = read_sysfs_value::<i32>(&input_path) {
|
if let Ok(temp_mc) = read_sysfs_value::<i32>(&input_path) {
|
||||||
return Some(temp_mc as f32 / 1000.0);
|
return Some(temp_mc as f32 / 1000.0);
|
||||||
|
@ -434,8 +432,8 @@ fn get_temperature_for_core(hw_path: &Path, core_id: u32, label_prefix: &str) ->
|
||||||
// Finds generic sensor temperatures by label
|
// Finds generic sensor temperatures by label
|
||||||
fn get_generic_sensor_temperature(hw_path: &Path, label_name: &str) -> Option<f32> {
|
fn get_generic_sensor_temperature(hw_path: &Path, label_name: &str) -> Option<f32> {
|
||||||
for i in 1..=32 {
|
for i in 1..=32 {
|
||||||
let label_path = hw_path.join(format!("temp{}_label", i));
|
let label_path = hw_path.join(format!("temp{i}_label"));
|
||||||
let input_path = hw_path.join(format!("temp{}_input", i));
|
let input_path = hw_path.join(format!("temp{i}_input"));
|
||||||
|
|
||||||
if label_path.exists() && input_path.exists() {
|
if label_path.exists() && input_path.exists() {
|
||||||
if let Ok(label) = read_sysfs_file_trimmed(&label_path) {
|
if let Ok(label) = read_sysfs_file_trimmed(&label_path) {
|
||||||
|
@ -460,7 +458,7 @@ fn get_generic_sensor_temperature(hw_path: &Path, label_name: &str) -> Option<f3
|
||||||
// Fallback to any temperature reading from a sensor
|
// Fallback to any temperature reading from a sensor
|
||||||
fn get_fallback_temperature(hw_path: &Path) -> Option<f32> {
|
fn get_fallback_temperature(hw_path: &Path) -> Option<f32> {
|
||||||
for i in 1..=32 {
|
for i in 1..=32 {
|
||||||
let input_path = hw_path.join(format!("temp{}_input", i));
|
let input_path = hw_path.join(format!("temp{i}_input"));
|
||||||
|
|
||||||
if input_path.exists() {
|
if input_path.exists() {
|
||||||
if let Ok(temp_mc) = read_sysfs_value::<i32>(&input_path) {
|
if let Ok(temp_mc) = read_sysfs_value::<i32>(&input_path) {
|
||||||
|
@ -488,12 +486,12 @@ pub fn get_all_cpu_core_info() -> Result<Vec<CpuCoreInfo>> {
|
||||||
Ok(info) => core_infos.push(info),
|
Ok(info) => core_infos.push(info),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Log or handle error for a single core, maybe push a partial info or skip
|
// Log or handle error for a single core, maybe push a partial info or skip
|
||||||
eprintln!("Error getting info for core {}: {}", core_id, e);
|
eprintln!("Error getting info for core {core_id}: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Log or handle missing times for a core
|
// Log or handle missing times for a core
|
||||||
eprintln!("Missing CPU time data for core {}", core_id);
|
eprintln!("Missing CPU time data for core {core_id}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(core_infos)
|
Ok(core_infos)
|
||||||
|
@ -511,9 +509,7 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> Result<CpuGlobalInfo> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let available_governors = if cpufreq_base.join("scaling_available_governors").exists() {
|
let available_governors = if cpufreq_base.join("scaling_available_governors").exists() {
|
||||||
read_sysfs_file_trimmed(cpufreq_base.join("scaling_available_governors"))
|
read_sysfs_file_trimmed(cpufreq_base.join("scaling_available_governors")).map_or_else(|_| vec![], |s| s.split_whitespace().map(String::from).collect())
|
||||||
.map(|s| s.split_whitespace().map(String::from).collect())
|
|
||||||
.unwrap_or_else(|_| vec![])
|
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
vec![]
|
||||||
};
|
};
|
||||||
|
@ -532,43 +528,46 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> Result<CpuGlobalInfo> {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let epp = read_sysfs_file_trimmed(cpufreq_base.join("energy_performance_preference")).ok();
|
// EPP (Energy Performance Preference)
|
||||||
|
let energy_perf_pref =
|
||||||
|
read_sysfs_file_trimmed(cpufreq_base.join("energy_performance_preference")).ok();
|
||||||
|
|
||||||
// EPB is often an integer 0-15. Reading as string for now.
|
// EPB (Energy Performance Bias)
|
||||||
let epb = read_sysfs_file_trimmed(cpufreq_base.join("energy_performance_bias")).ok();
|
let energy_perf_bias =
|
||||||
|
read_sysfs_file_trimmed(cpufreq_base.join("energy_performance_bias")).ok();
|
||||||
|
|
||||||
let platform_profile = read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile").ok();
|
let platform_profile = read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile").ok();
|
||||||
let _platform_profile_choices =
|
let _platform_profile_choices =
|
||||||
read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile_choices").ok();
|
read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile_choices").ok();
|
||||||
|
|
||||||
// Calculate average CPU temperature from the core temperatures
|
// Calculate average CPU temperature from the core temperatures
|
||||||
let average_temperature_celsius = if !cpu_cores.is_empty() {
|
let average_temperature_celsius = if cpu_cores.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
// Filter cores with temperature readings, then calculate average
|
// Filter cores with temperature readings, then calculate average
|
||||||
let cores_with_temp: Vec<&CpuCoreInfo> = cpu_cores
|
let cores_with_temp: Vec<&CpuCoreInfo> = cpu_cores
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|core| core.temperature_celsius.is_some())
|
.filter(|core| core.temperature_celsius.is_some())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if !cores_with_temp.is_empty() {
|
if cores_with_temp.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
// Sum up all temperatures and divide by count
|
// Sum up all temperatures and divide by count
|
||||||
let sum: f32 = cores_with_temp
|
let sum: f32 = cores_with_temp
|
||||||
.iter()
|
.iter()
|
||||||
.map(|core| core.temperature_celsius.unwrap())
|
.map(|core| core.temperature_celsius.unwrap())
|
||||||
.sum();
|
.sum();
|
||||||
Some(sum / cores_with_temp.len() as f32)
|
Some(sum / cores_with_temp.len() as f32)
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(CpuGlobalInfo {
|
Ok(CpuGlobalInfo {
|
||||||
current_governor,
|
current_governor,
|
||||||
available_governors,
|
available_governors,
|
||||||
turbo_status,
|
turbo_status,
|
||||||
epp,
|
epp: energy_perf_pref,
|
||||||
epb,
|
epb: energy_perf_bias,
|
||||||
platform_profile,
|
platform_profile,
|
||||||
average_temperature_celsius,
|
average_temperature_celsius,
|
||||||
})
|
})
|
||||||
|
@ -578,14 +577,13 @@ pub fn get_battery_info(config: &AppConfig) -> Result<Vec<BatteryInfo>> {
|
||||||
let mut batteries = Vec::new();
|
let mut batteries = Vec::new();
|
||||||
let power_supply_path = Path::new("/sys/class/power_supply");
|
let power_supply_path = Path::new("/sys/class/power_supply");
|
||||||
|
|
||||||
if (!power_supply_path.exists()) {
|
if !power_supply_path.exists() {
|
||||||
return Ok(batteries); // no power supply directory
|
return Ok(batteries); // no power supply directory
|
||||||
}
|
}
|
||||||
|
|
||||||
let ignored_supplies = config
|
let ignored_supplies = config
|
||||||
.ignored_power_supplies
|
.ignored_power_supplies
|
||||||
.as_ref()
|
.clone()
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
// Determine overall AC connection status
|
// Determine overall AC connection status
|
||||||
|
@ -649,7 +647,7 @@ pub fn get_battery_info(config: &AppConfig) -> Result<Vec<BatteryInfo>> {
|
||||||
if let (Some(c), Some(v)) = (current_ua, voltage_uv) {
|
if let (Some(c), Some(v)) = (current_ua, voltage_uv) {
|
||||||
// Power (W) = (Voltage (V) * Current (A))
|
// Power (W) = (Voltage (V) * Current (A))
|
||||||
// (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W
|
// (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W
|
||||||
Some((c as f64 * v as f64 / 1_000_000_000_000.0) as f32)
|
Some((f64::from(c) * f64::from(v) / 1_000_000_000_000.0) as f32)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue