diff --git a/.rustfmt.toml b/.rustfmt.toml deleted file mode 100644 index df184f2..0000000 --- a/.rustfmt.toml +++ /dev/null @@ -1,30 +0,0 @@ -# Taken from https://github.com/cull-os/carcass. -# Modified to have 2 space indents and 80 line width. - -# float_literal_trailing_zero = "Always" # TODO: Warning for some reason? -condense_wildcard_suffixes = true -doc_comment_code_block_width = 80 -edition = "2024" # Keep in sync with Cargo.toml. -enum_discrim_align_threshold = 60 -force_explicit_abi = false -force_multiline_blocks = true -format_code_in_doc_comments = true -format_macro_matchers = true -format_strings = true -group_imports = "StdExternalCrate" -hex_literal_case = "Upper" -imports_granularity = "Crate" -imports_layout = "Vertical" -inline_attribute_width = 60 -match_block_trailing_comma = true -max_width = 80 -newline_style = "Unix" -normalize_comments = true -normalize_doc_attributes = true -overflow_delimited_expr = true -struct_field_align_threshold = 60 -tab_spaces = 2 -unstable_features = true -use_field_init_shorthand = true -use_try_shorthand = true -wrap_comments = true diff --git a/.taplo.toml b/.taplo.toml deleted file mode 100644 index 9abeaee..0000000 --- a/.taplo.toml +++ /dev/null @@ -1,15 +0,0 @@ -# Taken from https://github.com/cull-os/carcass. - -[formatting] -align_entries = true -column_width = 100 -compact_arrays = false -reorder_inline_tables = true -reorder_keys = true - -[[rule]] -include = [ "**/Cargo.toml" ] -keys = [ "package" ] - -[rule.formatting] -reorder_keys = false diff --git a/Cargo.toml b/Cargo.toml index 12f2395..ecc84ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,21 +1,21 @@ [package] -name = "watt" -description = "Modern CPU frequency and power management utility for Linux" -version = "0.4.0" -edition = "2024" -authors = [ "NotAShelf ", "RGBCube " ] +name = "watt" +description = "Modern CPU frequency and power management utility for Linux" +version = "0.4.0" +edition = "2024" +authors = ["NotAShelf ", "RGBCube "] rust-version = "1.85" [dependencies] -anyhow = "1.0" -clap = { version = "4.0", features = [ "derive", "env" ] } +anyhow = "1.0" +clap = { version = "4.0", features = ["derive", "env"] } clap-verbosity-flag = "3.0.2" -ctrlc = "3.4" -derive_more = { version = "2.0.1", features = [ "full" ] } -env_logger = "0.11" -log = "0.4" -num_cpus = "1.16" -serde = { version = "1.0", features = [ "derive" ] } -thiserror = "2.0" -toml = "0.8" -yansi = { version = "1.0.1", features = [ "detect-env", "detect-tty" ] } +ctrlc = "3.4" +derive_more = { version = "2.0.1", features = ["full"] } +env_logger = "0.11" +log = "0.4" +num_cpus = "1.16" +serde = { version = "1.0", features = ["derive"] } +thiserror = "2.0" +toml = "0.8" +yansi = { version = "1.0.1", features = ["detect-env", "detect-tty"] } diff --git a/build.rs b/build.rs index 4eaf79b..5cc203d 100644 --- a/build.rs +++ b/build.rs @@ -1,57 +1,51 @@ -use std::{ - env, - fs, - path::PathBuf, -}; +use std::env; +use std::fs; +use std::path::PathBuf; const MULTICALL_NAMES: &[&str] = &["cpu", "power"]; fn main() -> Result<(), Box> { - println!("cargo:rerun-if-changed=build.rs"); - println!("cargo:rerun-if-changed=target"); + println!("cargo:rerun-if-changed=build.rs"); - let out_dir = PathBuf::from(env::var("OUT_DIR")?); - let target = out_dir + let out_dir = PathBuf::from(env::var("OUT_DIR")?); + let target = out_dir .parent() // target/debug/build/-/out .and_then(|p| p.parent()) // target/debug/build/- .and_then(|p| p.parent()) // target/debug/ .ok_or("failed to find target directory")?; - let main_binary_name = env::var("CARGO_PKG_NAME")?; + let main_binary_name = env::var("CARGO_PKG_NAME")?; - let main_binary_path = target.join(&main_binary_name); + let main_binary_path = target.join(&main_binary_name); - let mut errored = false; + let mut errored = false; - for name in MULTICALL_NAMES { - let hardlink_path = target.join(name); + for name in MULTICALL_NAMES { + let hardlink_path = target.join(name); - if hardlink_path.exists() { - if hardlink_path.is_dir() { - fs::remove_dir_all(&hardlink_path)?; - } else { - fs::remove_file(&hardlink_path)?; - } + if hardlink_path.exists() { + if hardlink_path.is_dir() { + fs::remove_dir_all(&hardlink_path)?; + } else { + fs::remove_file(&hardlink_path)?; + } + } + + if let Err(error) = fs::hard_link(&main_binary_path, &hardlink_path) { + println!( + "cargo:warning=failed to create hard link '{path}': {error}", + path = hardlink_path.display(), + ); + errored = true; + } } - if let Err(error) = fs::hard_link(&main_binary_path, &hardlink_path) { - println!( - "cargo:warning=failed to create hard link '{path}': {error}", - path = hardlink_path.display(), - ); - errored = true; + if errored { + println!( + "cargo:warning=this often happens because the target binary isn't built yet, try running `cargo build` again" + ); + println!("cargo:warning=keep in mind that this is for development purposes only"); } - } - if errored { - println!( - "cargo:warning=this often happens because the target binary isn't built \ - yet, try running `cargo build` again" - ); - println!( - "cargo:warning=keep in mind that this is for development purposes only" - ); - } - - Ok(()) + Ok(()) } diff --git a/config.toml b/config.toml index 293c9e8..ce2dd33 100644 --- a/config.toml +++ b/config.toml @@ -5,101 +5,107 @@ # Emergency thermal protection (highest priority). [[rule]] +priority = 100 +if = { value = "$cpu-temperature", is-more-than = 85.0 } +cpu.governor = "powersave" cpu.energy-performance-preference = "power" -cpu.frequency-mhz-maximum = 2000 -cpu.governor = "powersave" -cpu.turbo = false -if = { value = "$cpu-temperature", is-more-than = 85.0 } -priority = 100 +cpu.frequency-mhz-maximum = 2000 +cpu.turbo = false # Critical battery preservation. [[rule]] +priority = 90 +if.all = [ + "?discharging", + { value = "%power-supply-charge", is-less-than = 0.3 }, +] +cpu.governor = "powersave" cpu.energy-performance-preference = "power" -cpu.frequency-mhz-maximum = 800 # More aggressive below critical threshold. -cpu.governor = "powersave" -cpu.turbo = false -if.all = [ "?discharging", { value = "%power-supply-charge", is-less-than = 0.3 } ] -power.platform-profile = "low-power" -priority = 90 +cpu.frequency-mhz-maximum = 800 # More aggressive below critical threshold. +cpu.turbo = false +power.platform-profile = "low-power" # High performance mode for sustained high load. [[rule]] -cpu.energy-performance-preference = "performance" -cpu.governor = "performance" -cpu.turbo = true +priority = 80 if.all = [ { value = "%cpu-usage", is-more-than = 0.8 }, { value = "$cpu-idle-seconds", is-less-than = 30.0 }, { value = "$cpu-temperature", is-less-than = 75.0 }, ] -priority = 80 +cpu.governor = "performance" +cpu.energy-performance-preference = "performance" +cpu.turbo = true # Performance mode when not discharging. [[rule]] -cpu.energy-performance-bias = "balance_performance" -cpu.energy-performance-preference = "performance" -cpu.governor = "performance" -cpu.turbo = true +priority = 70 if.all = [ { not = "?discharging" }, { value = "%cpu-usage", is-more-than = 0.1 }, { value = "$cpu-temperature", is-less-than = 80.0 }, ] -priority = 70 +cpu.governor = "performance" +cpu.energy-performance-preference = "performance" +cpu.energy-performance-bias = "balance_performance" +cpu.turbo = true # Moderate performance for medium load. [[rule]] -cpu.energy-performance-preference = "balance_performance" -cpu.governor = "schedutil" +priority = 60 if.all = [ { value = "%cpu-usage", is-more-than = 0.4 }, { value = "%cpu-usage", is-less-than = 0.8 }, ] -priority = 60 +cpu.governor = "schedutil" +cpu.energy-performance-preference = "balance_performance" # Power saving during low activity. [[rule]] -cpu.energy-performance-preference = "power" -cpu.governor = "powersave" -cpu.turbo = false +priority = 50 if.all = [ { value = "%cpu-usage", is-less-than = 0.2 }, { value = "$cpu-idle-seconds", is-more-than = 60.0 }, ] -priority = 50 +cpu.governor = "powersave" +cpu.energy-performance-preference = "power" +cpu.turbo = false # Extended idle power optimization. [[rule]] +priority = 40 +if = { value = "$cpu-idle-seconds", is-more-than = 300.0 } +cpu.governor = "powersave" cpu.energy-performance-preference = "power" -cpu.frequency-mhz-maximum = 1600 -cpu.governor = "powersave" -cpu.turbo = false -if = { value = "$cpu-idle-seconds", is-more-than = 300.0 } -priority = 40 +cpu.frequency-mhz-maximum = 1600 +cpu.turbo = false # Battery conservation when discharging. [[rule]] +priority = 30 +if.all = [ + "?discharging", + { value = "%power-supply-charge", is-less-than = 0.5 }, +] +cpu.governor = "powersave" cpu.energy-performance-preference = "power" -cpu.frequency-mhz-maximum = 2000 -cpu.governor = "powersave" -cpu.turbo = false -if.all = [ "?discharging", { value = "%power-supply-charge", is-less-than = 0.5 } ] -power.platform-profile = "low-power" -priority = 30 +cpu.frequency-mhz-maximum = 2000 +cpu.turbo = false +power.platform-profile = "low-power" # General battery mode. [[rule]] -cpu.energy-performance-bias = "balance_power" +priority = 20 +if = "?discharging" +cpu.governor = "powersave" cpu.energy-performance-preference = "power" -cpu.frequency-mhz-maximum = 1800 -cpu.frequency-mhz-minimum = 200 -cpu.governor = "powersave" -cpu.turbo = false -if = "?discharging" -priority = 20 +cpu.energy-performance-bias = "balance_power" +cpu.frequency-mhz-maximum = 1800 +cpu.frequency-mhz-minimum = 200 +cpu.turbo = false # Balanced performance for general use. Default fallback rule. [[rule]] +priority = 0 +cpu.governor = "schedutil" cpu.energy-performance-preference = "balance_performance" -cpu.governor = "schedutil" -priority = 0 diff --git a/src/config.rs b/src/config.rs index 4342a62..e474f89 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,580 +1,526 @@ -use std::{ - fs, - path::Path, -}; +use std::{fs, path::Path}; -use anyhow::{ - Context, - bail, -}; -use serde::{ - Deserialize, - Serialize, -}; +use anyhow::{Context, bail}; +use serde::{Deserialize, Serialize}; -use crate::{ - cpu, - power_supply, -}; +use crate::{cpu, power_supply}; fn is_default(value: &T) -> bool { - *value == T::default() + *value == T::default() } -#[derive( - Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq, -)] +#[derive(Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq)] #[serde(deny_unknown_fields, default, rename_all = "kebab-case")] pub struct CpuDelta { - /// The CPUs to apply the changes to. When unspecified, will be applied to - /// all CPUs. - #[arg(short = 'c', long = "for")] - #[serde(rename = "for", skip_serializing_if = "is_default")] - pub for_: Option>, + /// The CPUs to apply the changes to. When unspecified, will be applied to all CPUs. + #[arg(short = 'c', long = "for")] + #[serde(rename = "for", skip_serializing_if = "is_default")] + pub for_: Option>, - /// Set the CPU governor. - #[arg(short = 'g', long)] - #[serde(skip_serializing_if = "is_default")] - pub governor: Option, /* TODO: Validate with clap for available - * governors. */ + /// Set the CPU governor. + #[arg(short = 'g', long)] + #[serde(skip_serializing_if = "is_default")] + pub governor: Option, // TODO: Validate with clap for available governors. - /// Set CPU Energy Performance Preference (EPP). Short form: --epp. - #[arg(short = 'p', long, alias = "epp")] - #[serde(skip_serializing_if = "is_default")] - pub energy_performance_preference: Option, /* TODO: Validate with - * clap for available - * governors. */ + /// Set CPU Energy Performance Preference (EPP). Short form: --epp. + #[arg(short = 'p', long, alias = "epp")] + #[serde(skip_serializing_if = "is_default")] + pub energy_performance_preference: Option, // TODO: Validate with clap for available governors. - /// Set CPU Energy Performance Bias (EPB). Short form: --epb. - #[arg(short = 'b', long, alias = "epb")] - #[serde(skip_serializing_if = "is_default")] - pub energy_performance_bias: Option, /* TODO: Validate with clap for available governors. */ + /// Set CPU Energy Performance Bias (EPB). Short form: --epb. + #[arg(short = 'b', long, alias = "epb")] + #[serde(skip_serializing_if = "is_default")] + pub energy_performance_bias: Option, // TODO: Validate with clap for available governors. - /// Set minimum CPU frequency in MHz. Short form: --freq-min. - #[arg(short = 'f', long, alias = "freq-min", value_parser = clap::value_parser!(u64).range(1..=10_000))] - #[serde(skip_serializing_if = "is_default")] - pub frequency_mhz_minimum: Option, + /// Set minimum CPU frequency in MHz. Short form: --freq-min. + #[arg(short = 'f', long, alias = "freq-min", value_parser = clap::value_parser!(u64).range(1..=10_000))] + #[serde(skip_serializing_if = "is_default")] + pub frequency_mhz_minimum: Option, - /// Set maximum CPU frequency in MHz. Short form: --freq-max. - #[arg(short = 'F', long, alias = "freq-max", value_parser = clap::value_parser!(u64).range(1..=10_000))] - #[serde(skip_serializing_if = "is_default")] - pub frequency_mhz_maximum: Option, + /// Set maximum CPU frequency in MHz. Short form: --freq-max. + #[arg(short = 'F', long, alias = "freq-max", value_parser = clap::value_parser!(u64).range(1..=10_000))] + #[serde(skip_serializing_if = "is_default")] + pub frequency_mhz_maximum: Option, - /// Set turbo boost behaviour. Has to be for all CPUs. - #[arg(short = 't', long, conflicts_with = "for_")] - #[serde(skip_serializing_if = "is_default")] - pub turbo: Option, + /// Set turbo boost behaviour. Has to be for all CPUs. + #[arg(short = 't', long, conflicts_with = "for_")] + #[serde(skip_serializing_if = "is_default")] + pub turbo: Option, } impl CpuDelta { - pub fn apply(&self) -> anyhow::Result<()> { - let mut cpus = match &self.for_ { - Some(numbers) => { - let mut cpus = Vec::with_capacity(numbers.len()); - let cache = cpu::CpuRescanCache::default(); + pub fn apply(&self) -> anyhow::Result<()> { + let mut cpus = match &self.for_ { + Some(numbers) => { + let mut cpus = Vec::with_capacity(numbers.len()); + let cache = cpu::CpuRescanCache::default(); - for &number in numbers { - cpus.push(cpu::Cpu::new(number, &cache)?); + for &number in numbers { + cpus.push(cpu::Cpu::new(number, &cache)?); + } + + cpus + } + None => cpu::Cpu::all().context("failed to get all CPUs and their information")?, + }; + + for cpu in &mut cpus { + if let Some(governor) = self.governor.as_ref() { + cpu.set_governor(governor)?; + } + + if let Some(epp) = self.energy_performance_preference.as_ref() { + cpu.set_epp(epp)?; + } + + if let Some(epb) = self.energy_performance_bias.as_ref() { + cpu.set_epb(epb)?; + } + + if let Some(mhz_minimum) = self.frequency_mhz_minimum { + cpu.set_frequency_mhz_minimum(mhz_minimum)?; + } + + if let Some(mhz_maximum) = self.frequency_mhz_maximum { + cpu.set_frequency_mhz_maximum(mhz_maximum)?; + } } - cpus - }, - None => { - cpu::Cpu::all() - .context("failed to get all CPUs and their information")? - }, - }; + if let Some(turbo) = self.turbo { + cpu::Cpu::set_turbo(turbo)?; + } - for cpu in &mut cpus { - if let Some(governor) = self.governor.as_ref() { - cpu.set_governor(governor)?; - } - - if let Some(epp) = self.energy_performance_preference.as_ref() { - cpu.set_epp(epp)?; - } - - if let Some(epb) = self.energy_performance_bias.as_ref() { - cpu.set_epb(epb)?; - } - - if let Some(mhz_minimum) = self.frequency_mhz_minimum { - cpu.set_frequency_mhz_minimum(mhz_minimum)?; - } - - if let Some(mhz_maximum) = self.frequency_mhz_maximum { - cpu.set_frequency_mhz_maximum(mhz_maximum)?; - } + Ok(()) } - - if let Some(turbo) = self.turbo { - cpu::Cpu::set_turbo(turbo)?; - } - - Ok(()) - } } -#[derive( - Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq, -)] +#[derive(Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq)] #[serde(deny_unknown_fields, default, rename_all = "kebab-case")] pub struct PowerDelta { - /// The power supplies to apply the changes to. When unspecified, will be - /// applied to all power supplies. - #[arg(short = 'p', long = "for")] - #[serde(rename = "for", skip_serializing_if = "is_default")] - pub for_: Option>, + /// The power supplies to apply the changes to. When unspecified, will be applied to all power supplies. + #[arg(short = 'p', long = "for")] + #[serde(rename = "for", skip_serializing_if = "is_default")] + pub for_: Option>, - /// Set the percentage that the power supply has to drop under for charging - /// to start. Short form: --charge-start. - #[arg(short = 'c', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100))] - #[serde(skip_serializing_if = "is_default")] - pub charge_threshold_start: Option, + /// Set the percentage that the power supply has to drop under for charging to start. Short form: --charge-start. + #[arg(short = 'c', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100))] + #[serde(skip_serializing_if = "is_default")] + pub charge_threshold_start: Option, - /// Set the percentage where charging will stop. Short form: --charge-end. - #[arg(short = 'C', long, alias = "charge-end", value_parser = clap::value_parser!(u8).range(0..=100))] - #[serde(skip_serializing_if = "is_default")] - pub charge_threshold_end: Option, + /// Set the percentage where charging will stop. Short form: --charge-end. + #[arg(short = 'C', long, alias = "charge-end", value_parser = clap::value_parser!(u8).range(0..=100))] + #[serde(skip_serializing_if = "is_default")] + pub charge_threshold_end: Option, - /// Set ACPI platform profile. Has to be for all power supplies. - #[arg(short = 'f', long, alias = "profile", conflicts_with = "for_")] - #[serde(skip_serializing_if = "is_default")] - pub platform_profile: Option, + /// Set ACPI platform profile. Has to be for all power supplies. + #[arg(short = 'f', long, alias = "profile", conflicts_with = "for_")] + #[serde(skip_serializing_if = "is_default")] + pub platform_profile: Option, } impl PowerDelta { - pub fn apply(&self) -> anyhow::Result<()> { - let mut power_supplies = match &self.for_ { - Some(names) => { - let mut power_supplies = Vec::with_capacity(names.len()); + pub fn apply(&self) -> anyhow::Result<()> { + let mut power_supplies = match &self.for_ { + Some(names) => { + let mut power_supplies = Vec::with_capacity(names.len()); - for name in names { - power_supplies - .push(power_supply::PowerSupply::from_name(name.clone())?); + for name in names { + power_supplies.push(power_supply::PowerSupply::from_name(name.clone())?); + } + + power_supplies + } + + None => power_supply::PowerSupply::all()? + .into_iter() + .filter(|power_supply| power_supply.threshold_config.is_some()) + .collect(), + }; + + for power_supply in &mut power_supplies { + if let Some(threshold_start) = self.charge_threshold_start { + power_supply.set_charge_threshold_start(threshold_start as f64 / 100.0)?; + } + + if let Some(threshold_end) = self.charge_threshold_end { + power_supply.set_charge_threshold_end(threshold_end as f64 / 100.0)?; + } } - power_supplies - }, + if let Some(platform_profile) = self.platform_profile.as_ref() { + power_supply::PowerSupply::set_platform_profile(platform_profile)?; + } - None => { - power_supply::PowerSupply::all()? - .into_iter() - .filter(|power_supply| power_supply.threshold_config.is_some()) - .collect() - }, - }; - - for power_supply in &mut power_supplies { - if let Some(threshold_start) = self.charge_threshold_start { - power_supply - .set_charge_threshold_start(threshold_start as f64 / 100.0)?; - } - - if let Some(threshold_end) = self.charge_threshold_end { - power_supply.set_charge_threshold_end(threshold_end as f64 / 100.0)?; - } + Ok(()) } - - if let Some(platform_profile) = self.platform_profile.as_ref() { - power_supply::PowerSupply::set_platform_profile(platform_profile)?; - } - - Ok(()) - } } macro_rules! named { - ($variant:ident => $value:literal) => { - pub mod $variant { - pub fn serialize( - serializer: S, - ) -> Result { - serializer.serialize_str($value) - } - - pub fn deserialize<'de, D: serde::Deserializer<'de>>( - deserializer: D, - ) -> Result<(), D::Error> { - struct Visitor; - - impl<'de> serde::de::Visitor<'de> for Visitor { - type Value = (); - - fn expecting( - &self, - writer: &mut std::fmt::Formatter, - ) -> std::fmt::Result { - writer.write_str(concat!("\"", $value, "\"")) - } - - fn visit_str( - self, - value: &str, - ) -> Result { - if value != $value { - return Err(E::invalid_value( - serde::de::Unexpected::Str(value), - &self, - )); + ($variant:ident => $value:literal) => { + pub mod $variant { + pub fn serialize(serializer: S) -> Result { + serializer.serialize_str($value) } - Ok(()) - } - } + pub fn deserialize<'de, D: serde::Deserializer<'de>>( + deserializer: D, + ) -> Result<(), D::Error> { + struct Visitor; - deserializer.deserialize_str(Visitor) - } - } - }; + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = (); + + fn expecting(&self, writer: &mut std::fmt::Formatter) -> std::fmt::Result { + writer.write_str(concat!("\"", $value, "\"")) + } + + fn visit_str(self, value: &str) -> Result { + if value != $value { + return Err(E::invalid_value(serde::de::Unexpected::Str(value), &self)); + } + + Ok(()) + } + } + + deserializer.deserialize_str(Visitor) + } + } + }; } mod expression { - named!(cpu_usage => "%cpu-usage"); - named!(cpu_usage_volatility => "$cpu-usage-volatility"); - named!(cpu_temperature => "$cpu-temperature"); - named!(cpu_temperature_volatility => "$cpu-temperature-volatility"); - named!(cpu_idle_seconds => "$cpu-idle-seconds"); + named!(cpu_usage => "%cpu-usage"); + named!(cpu_usage_volatility => "$cpu-usage-volatility"); + named!(cpu_temperature => "$cpu-temperature"); + named!(cpu_temperature_volatility => "$cpu-temperature-volatility"); + named!(cpu_idle_seconds => "$cpu-idle-seconds"); - named!(power_supply_charge => "%power-supply-charge"); - named!(power_supply_discharge_rate => "%power-supply-discharge-rate"); + named!(power_supply_charge => "%power-supply-charge"); + named!(power_supply_discharge_rate => "%power-supply-discharge-rate"); - named!(discharging => "?discharging"); + named!(discharging => "?discharging"); } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(untagged)] pub enum Expression { - #[serde(with = "expression::cpu_usage")] - CpuUsage, + #[serde(with = "expression::cpu_usage")] + CpuUsage, - #[serde(with = "expression::cpu_usage_volatility")] - CpuUsageVolatility, + #[serde(with = "expression::cpu_usage_volatility")] + CpuUsageVolatility, - #[serde(with = "expression::cpu_temperature")] - CpuTemperature, + #[serde(with = "expression::cpu_temperature")] + CpuTemperature, - #[serde(with = "expression::cpu_temperature_volatility")] - CpuTemperatureVolatility, + #[serde(with = "expression::cpu_temperature_volatility")] + CpuTemperatureVolatility, - #[serde(with = "expression::cpu_idle_seconds")] - CpuIdleSeconds, + #[serde(with = "expression::cpu_idle_seconds")] + CpuIdleSeconds, - #[serde(with = "expression::power_supply_charge")] - PowerSupplyCharge, + #[serde(with = "expression::power_supply_charge")] + PowerSupplyCharge, - #[serde(with = "expression::power_supply_discharge_rate")] - PowerSupplyDischargeRate, + #[serde(with = "expression::power_supply_discharge_rate")] + PowerSupplyDischargeRate, - #[serde(with = "expression::discharging")] - Discharging, + #[serde(with = "expression::discharging")] + Discharging, - Boolean(bool), + Boolean(bool), - Number(f64), + Number(f64), - Plus { - #[serde(rename = "value")] - a: Box, - #[serde(rename = "plus")] - b: Box, - }, - Minus { - #[serde(rename = "value")] - a: Box, - #[serde(rename = "minus")] - b: Box, - }, - Multiply { - #[serde(rename = "value")] - a: Box, - #[serde(rename = "multiply")] - b: Box, - }, - Power { - #[serde(rename = "value")] - a: Box, - #[serde(rename = "power")] - b: Box, - }, - Divide { - #[serde(rename = "value")] - a: Box, - #[serde(rename = "divide")] - b: Box, - }, + Plus { + #[serde(rename = "value")] + a: Box, + #[serde(rename = "plus")] + b: Box, + }, + Minus { + #[serde(rename = "value")] + a: Box, + #[serde(rename = "minus")] + b: Box, + }, + Multiply { + #[serde(rename = "value")] + a: Box, + #[serde(rename = "multiply")] + b: Box, + }, + Power { + #[serde(rename = "value")] + a: Box, + #[serde(rename = "power")] + b: Box, + }, + Divide { + #[serde(rename = "value")] + a: Box, + #[serde(rename = "divide")] + b: Box, + }, - LessThan { - #[serde(rename = "value")] - a: Box, - #[serde(rename = "is-less-than")] - b: Box, - }, - MoreThan { - #[serde(rename = "value")] - a: Box, - #[serde(rename = "is-more-than")] - b: Box, - }, + LessThan { + #[serde(rename = "value")] + a: Box, + #[serde(rename = "is-less-than")] + b: Box, + }, + MoreThan { + #[serde(rename = "value")] + a: Box, + #[serde(rename = "is-more-than")] + b: Box, + }, - Equal { - #[serde(rename = "value")] - a: Box, - #[serde(rename = "is-equal")] - b: Box, - leeway: Box, - }, + Equal { + #[serde(rename = "value")] + a: Box, + #[serde(rename = "is-equal")] + b: Box, + leeway: Box, + }, - And { - #[serde(rename = "value")] - a: Box, - #[serde(rename = "and")] - b: Box, - }, - All { - all: Vec, - }, + And { + #[serde(rename = "value")] + a: Box, + #[serde(rename = "and")] + b: Box, + }, + All { + all: Vec, + }, - Or { - #[serde(rename = "value")] - a: Box, - #[serde(rename = "or")] - b: Box, - }, - Any { - any: Vec, - }, + Or { + #[serde(rename = "value")] + a: Box, + #[serde(rename = "or")] + b: Box, + }, + Any { + any: Vec, + }, - Not { - not: Box, - }, + Not { + not: Box, + }, } impl Default for Expression { - fn default() -> Self { - Self::Boolean(true) - } + fn default() -> Self { + Self::Boolean(true) + } } impl Expression { - pub fn as_number(&self) -> anyhow::Result { - let Self::Number(number) = self else { - bail!("tried to cast '{self:?}' to a number, failed") - }; + pub fn as_number(&self) -> anyhow::Result { + let Self::Number(number) = self else { + bail!("tried to cast '{self:?}' to a number, failed") + }; - Ok(*number) - } + Ok(*number) + } - pub fn as_boolean(&self) -> anyhow::Result { - let Self::Boolean(boolean) = self else { - bail!("tried to cast '{self:?}' to a boolean, failed") - }; + pub fn as_boolean(&self) -> anyhow::Result { + let Self::Boolean(boolean) = self else { + bail!("tried to cast '{self:?}' to a boolean, failed") + }; - Ok(*boolean) - } + Ok(*boolean) + } } #[derive(Debug, Clone, PartialEq)] pub struct EvalState { - pub cpu_usage: f64, - pub cpu_usage_volatility: Option, - pub cpu_temperature: f64, - pub cpu_temperature_volatility: Option, - pub cpu_idle_seconds: f64, + pub cpu_usage: f64, + pub cpu_usage_volatility: Option, + pub cpu_temperature: f64, + pub cpu_temperature_volatility: Option, + pub cpu_idle_seconds: f64, - pub power_supply_charge: f64, - pub power_supply_discharge_rate: Option, + pub power_supply_charge: f64, + pub power_supply_discharge_rate: Option, - pub discharging: bool, + pub discharging: bool, } impl Expression { - pub fn eval(&self, state: &EvalState) -> anyhow::Result> { - use Expression::*; + pub fn eval(&self, state: &EvalState) -> anyhow::Result> { + use Expression::*; - macro_rules! try_ok { - ($expression:expr) => { - match $expression { - Some(value) => value, - None => return Ok(None), + macro_rules! try_ok { + ($expression:expr) => { + match $expression { + Some(value) => value, + None => return Ok(None), + } + }; } - }; + + macro_rules! eval { + ($expression:expr) => { + try_ok!($expression.eval(state)?) + }; + } + + // [e8dax09]: This may be look inefficient, and it definitely isn't optimal, + // but expressions in rules are usually so small that it doesn't matter or + // make a perceiveable performance difference. + // + // We also want to be strict, instead of lazy in binary operations, because + // we want to catch type errors immediately. + // + // FIXME: We currently cannot catch errors that will happen when propagating None. + // You can have a type error go uncaught on first startup by using $cpu-usage-volatility + // incorrectly, for example. + Ok(Some(match self { + CpuUsage => Number(state.cpu_usage), + CpuUsageVolatility => Number(try_ok!(state.cpu_usage_volatility)), + CpuTemperature => Number(state.cpu_temperature), + CpuTemperatureVolatility => Number(try_ok!(state.cpu_temperature_volatility)), + CpuIdleSeconds => Number(state.cpu_idle_seconds), + + PowerSupplyCharge => Number(state.cpu_idle_seconds), + PowerSupplyDischargeRate => Number(try_ok!(state.power_supply_discharge_rate)), + + Discharging => Boolean(state.discharging), + + literal @ (Boolean(_) | Number(_)) => literal.clone(), + + Plus { a, b } => Number(eval!(a).as_number()? + eval!(b).as_number()?), + Minus { a, b } => Number(eval!(a).as_number()? - eval!(b).as_number()?), + Multiply { a, b } => Number(eval!(a).as_number()? * eval!(b).as_number()?), + Power { a, b } => Number(eval!(a).as_number()?.powf(eval!(b).as_number()?)), + Divide { a, b } => Number(eval!(a).as_number()? / eval!(b).as_number()?), + + LessThan { a, b } => Boolean(eval!(a).as_number()? < eval!(b).as_number()?), + MoreThan { a, b } => Boolean(eval!(a).as_number()? > eval!(b).as_number()?), + Equal { a, b, leeway } => { + let a = eval!(a).as_number()?; + let b = eval!(b).as_number()?; + let leeway = eval!(leeway).as_number()?; + + let minimum = a - leeway; + let maximum = a + leeway; + + Boolean(minimum < b && b < maximum) + } + + And { a, b } => { + let a = eval!(a).as_boolean()?; + let b = eval!(b).as_boolean()?; + + Boolean(a && b) + } + All { all } => { + let mut result = true; + + for value in all { + let value = eval!(value).as_boolean()?; + + result = result && value; + } + + Boolean(result) + } + Or { a, b } => { + let a = eval!(a).as_boolean()?; + let b = eval!(b).as_boolean()?; + + Boolean(a || b) + } + Any { any } => { + let mut result = false; + + for value in any { + let value = eval!(value).as_boolean()?; + + result = result || value; + } + + Boolean(result) + } + Not { not } => Boolean(!eval!(not).as_boolean()?), + })) } - - macro_rules! eval { - ($expression:expr) => { - try_ok!($expression.eval(state)?) - }; - } - - // [e8dax09]: This may be look inefficient, and it definitely isn't optimal, - // but expressions in rules are usually so small that it doesn't matter or - // make a perceiveable performance difference. - // - // We also want to be strict, instead of lazy in binary operations, because - // we want to catch type errors immediately. - // - // FIXME: We currently cannot catch errors that will happen when propagating - // None. You can have a type error go uncaught on first startup by using - // $cpu-usage-volatility incorrectly, for example. - Ok(Some(match self { - CpuUsage => Number(state.cpu_usage), - CpuUsageVolatility => Number(try_ok!(state.cpu_usage_volatility)), - CpuTemperature => Number(state.cpu_temperature), - CpuTemperatureVolatility => { - Number(try_ok!(state.cpu_temperature_volatility)) - }, - CpuIdleSeconds => Number(state.cpu_idle_seconds), - - PowerSupplyCharge => Number(state.cpu_idle_seconds), - PowerSupplyDischargeRate => { - Number(try_ok!(state.power_supply_discharge_rate)) - }, - - Discharging => Boolean(state.discharging), - - literal @ (Boolean(_) | Number(_)) => literal.clone(), - - Plus { a, b } => Number(eval!(a).as_number()? + eval!(b).as_number()?), - Minus { a, b } => Number(eval!(a).as_number()? - eval!(b).as_number()?), - Multiply { a, b } => { - Number(eval!(a).as_number()? * eval!(b).as_number()?) - }, - Power { a, b } => { - Number(eval!(a).as_number()?.powf(eval!(b).as_number()?)) - }, - Divide { a, b } => Number(eval!(a).as_number()? / eval!(b).as_number()?), - - LessThan { a, b } => { - Boolean(eval!(a).as_number()? < eval!(b).as_number()?) - }, - MoreThan { a, b } => { - Boolean(eval!(a).as_number()? > eval!(b).as_number()?) - }, - Equal { a, b, leeway } => { - let a = eval!(a).as_number()?; - let b = eval!(b).as_number()?; - let leeway = eval!(leeway).as_number()?; - - let minimum = a - leeway; - let maximum = a + leeway; - - Boolean(minimum < b && b < maximum) - }, - - And { a, b } => { - let a = eval!(a).as_boolean()?; - let b = eval!(b).as_boolean()?; - - Boolean(a && b) - }, - All { all } => { - let mut result = true; - - for value in all { - let value = eval!(value).as_boolean()?; - - result = result && value; - } - - Boolean(result) - }, - Or { a, b } => { - let a = eval!(a).as_boolean()?; - let b = eval!(b).as_boolean()?; - - Boolean(a || b) - }, - Any { any } => { - let mut result = false; - - for value in any { - let value = eval!(value).as_boolean()?; - - result = result || value; - } - - Boolean(result) - }, - Not { not } => Boolean(!eval!(not).as_boolean()?), - })) - } } #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] pub struct Rule { - pub priority: u8, + pub priority: u8, - #[serde(default, rename = "if", skip_serializing_if = "is_default")] - pub condition: Expression, + #[serde(default, rename = "if", skip_serializing_if = "is_default")] + pub condition: Expression, - #[serde(default, skip_serializing_if = "is_default")] - pub cpu: CpuDelta, - #[serde(default, skip_serializing_if = "is_default")] - pub power: PowerDelta, + #[serde(default, skip_serializing_if = "is_default")] + pub cpu: CpuDelta, + #[serde(default, skip_serializing_if = "is_default")] + pub power: PowerDelta, } #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] #[serde(default, rename_all = "kebab-case")] pub struct DaemonConfig { - #[serde(rename = "rule")] - pub rules: Vec, + #[serde(rename = "rule")] + pub rules: Vec, } impl DaemonConfig { - const DEFAULT: &str = include_str!("../config.toml"); + const DEFAULT: &str = include_str!("../config.toml"); - pub fn load_from(path: Option<&Path>) -> anyhow::Result { - let contents = if let Some(path) = path { - log::debug!("loading config from '{path}'", path = path.display()); + pub fn load_from(path: Option<&Path>) -> anyhow::Result { + let contents = if let Some(path) = path { + log::debug!("loading config from '{path}'", path = path.display()); - &fs::read_to_string(path).with_context(|| { - format!("failed to read config from '{path}'", path = path.display()) - })? - } else { - log::debug!( - "loading default config from embedded toml:\n{config}", - config = Self::DEFAULT, - ); + &fs::read_to_string(path).with_context(|| { + format!("failed to read config from '{path}'", path = path.display()) + })? + } else { + log::debug!( + "loading default config from embedded toml:\n{config}", + config = Self::DEFAULT, + ); - Self::DEFAULT - }; + Self::DEFAULT + }; - let mut config: Self = toml::from_str(contents).with_context(|| { - path.map_or( - "failed to parse builtin default config, this is a bug".to_owned(), - |p| format!("failed to parse file at '{path}'", path = p.display()), - ) - })?; + let mut config: Self = toml::from_str(contents).with_context(|| { + path.map_or( + "failed to parse builtin default config, this is a bug".to_owned(), + |p| format!("failed to parse file at '{path}'", path = p.display()), + ) + })?; - { - let mut priorities = Vec::with_capacity(config.rules.len()); + { + let mut priorities = Vec::with_capacity(config.rules.len()); - for rule in &config.rules { - if priorities.contains(&rule.priority) { - bail!("each config rule must have a different priority") + for rule in &config.rules { + if priorities.contains(&rule.priority) { + bail!("each config rule must have a different priority") + } + + priorities.push(rule.priority); + } } - priorities.push(rule.priority); - } + // This is just for debug traces. + if log::max_level() >= log::LevelFilter::Debug { + if config.rules.is_sorted_by_key(|rule| rule.priority) { + log::debug!("config rules are sorted by increasing priority, not doing anything"); + } else { + log::debug!("config rules aren't sorted by priority, sorting"); + } + } + + config.rules.sort_by_key(|rule| rule.priority); + + log::debug!("loaded config: {config:#?}"); + + Ok(config) } - - // This is just for debug traces. - if log::max_level() >= log::LevelFilter::Debug { - if config.rules.is_sorted_by_key(|rule| rule.priority) { - log::debug!( - "config rules are sorted by increasing priority, not doing anything" - ); - } else { - log::debug!("config rules aren't sorted by priority, sorting"); - } - } - - config.rules.sort_by_key(|rule| rule.priority); - - log::debug!("loaded config: {config:#?}"); - - Ok(config) - } } diff --git a/src/cpu.rs b/src/cpu.rs index 5cfca4f..e0e00e2 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1,697 +1,641 @@ -use std::{ - cell::OnceCell, - collections::HashMap, - fmt, - mem, - rc::Rc, - string::ToString, -}; - -use anyhow::{ - Context, - bail, -}; +use anyhow::{Context, bail}; use yansi::Paint as _; +use std::{cell::OnceCell, collections::HashMap, fmt, mem, rc::Rc, string::ToString}; + use crate::fs; #[derive(Default, Debug, Clone, PartialEq)] pub struct CpuRescanCache { - stat: OnceCell>, - info: OnceCell>>>, + stat: OnceCell>, + info: OnceCell>>>, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CpuStat { - pub user: u64, - pub nice: u64, - pub system: u64, - pub idle: u64, - pub iowait: u64, - pub irq: u64, - pub softirq: u64, - pub steal: u64, + pub user: u64, + pub nice: u64, + pub system: u64, + pub idle: u64, + pub iowait: u64, + pub irq: u64, + pub softirq: u64, + pub steal: u64, } impl CpuStat { - pub fn total(&self) -> u64 { - self.user - + self.nice - + self.system - + self.idle - + self.iowait - + self.irq - + self.softirq - + self.steal - } + pub fn total(&self) -> u64 { + self.user + + self.nice + + self.system + + self.idle + + self.iowait + + self.irq + + self.softirq + + self.steal + } - pub fn idle(&self) -> u64 { - self.idle + self.iowait - } + pub fn idle(&self) -> u64 { + self.idle + self.iowait + } - pub fn usage(&self) -> f64 { - 1.0 - self.idle() as f64 / self.total() as f64 - } + pub fn usage(&self) -> f64 { + 1.0 - self.idle() as f64 / self.total() as f64 + } } #[derive(Debug, Clone, PartialEq)] pub struct Cpu { - pub number: u32, + pub number: u32, - pub has_cpufreq: bool, + pub has_cpufreq: bool, - pub available_governors: Vec, - pub governor: Option, + pub available_governors: Vec, + pub governor: Option, - pub frequency_mhz: Option, - pub frequency_mhz_minimum: Option, - pub frequency_mhz_maximum: Option, + pub frequency_mhz: Option, + pub frequency_mhz_minimum: Option, + pub frequency_mhz_maximum: Option, - pub available_epps: Vec, - pub epp: Option, + pub available_epps: Vec, + pub epp: Option, - pub available_epbs: Vec, - pub epb: Option, + pub available_epbs: Vec, + pub epb: Option, - pub stat: CpuStat, - pub info: Option>>, + pub stat: CpuStat, + pub info: Option>>, - pub temperature: Option, + pub temperature: Option, } impl fmt::Display for Cpu { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let number = self.number.cyan(); + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let number = self.number.cyan(); - write!(f, "CPU {number}") - } + write!(f, "CPU {number}") + } } impl Cpu { - pub fn new(number: u32, cache: &CpuRescanCache) -> anyhow::Result { - let mut cpu = Self { - number, - has_cpufreq: false, + pub fn new(number: u32, cache: &CpuRescanCache) -> anyhow::Result { + let mut cpu = Self { + number, + has_cpufreq: false, - available_governors: Vec::new(), - governor: None, + available_governors: Vec::new(), + governor: None, - frequency_mhz: None, - frequency_mhz_minimum: None, - frequency_mhz_maximum: None, + frequency_mhz: None, + frequency_mhz_minimum: None, + frequency_mhz_maximum: None, - available_epps: Vec::new(), - epp: None, + available_epps: Vec::new(), + epp: None, - available_epbs: Vec::new(), - epb: None, + available_epbs: Vec::new(), + epb: None, - stat: CpuStat { - user: 0, - nice: 0, - system: 0, - idle: 0, - iowait: 0, - irq: 0, - softirq: 0, - steal: 0, - }, - info: None, - - temperature: None, - }; - cpu.rescan(cache)?; - - Ok(cpu) - } - - /// Get all CPUs. - pub fn all() -> anyhow::Result> { - const PATH: &str = "/sys/devices/system/cpu"; - - let mut cpus = vec![]; - let cache = CpuRescanCache::default(); - - for entry in fs::read_dir(PATH) - .context("failed to read CPU entries")? - .with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))? - { - let entry = - entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; - - let entry_file_name = entry.file_name(); - - let Some(name) = entry_file_name.to_str() else { - continue; - }; - - let Some(cpu_prefix_removed) = name.strip_prefix("cpu") else { - continue; - }; - - // Has to match "cpu{N}". - let Ok(number) = cpu_prefix_removed.parse() else { - continue; - }; - - cpus.push(Self::new(number, &cache)?); - } - - // Fall back if sysfs iteration above fails to find any cpufreq CPUs. - if cpus.is_empty() { - for number in 0..num_cpus::get() as u32 { - cpus.push(Self::new(number, &cache)?); - } - } - - Ok(cpus) - } - - /// Rescan CPU, tuning local copy of settings. - pub fn rescan(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { - let Self { number, .. } = self; - - if !fs::exists(format!("/sys/devices/system/cpu/cpu{number}")) { - bail!("{self} does not exist"); - } - - self.has_cpufreq = - fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); - - if self.has_cpufreq { - self.rescan_governor()?; - self.rescan_frequency()?; - self.rescan_epp()?; - self.rescan_epb()?; - } - - self.rescan_stat(cache)?; - self.rescan_info(cache)?; - - Ok(()) - } - - fn rescan_governor(&mut self) -> anyhow::Result<()> { - let Self { number, .. } = *self; - - self.available_governors = 'available_governors: { - let Some(content) = fs::read(format!( - "/sys/devices/system/cpu/cpu{number}/cpufreq/\ - scaling_available_governors" - )) - .with_context(|| format!("failed to read {self} available governors"))? - else { - break 'available_governors Vec::new(); - }; - - content - .split_whitespace() - .map(ToString::to_string) - .collect() - }; - - self.governor = Some( - fs::read(format!( - "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_governor" - )) - .with_context(|| format!("failed to read {self} scaling governor"))? - .with_context(|| format!("failed to find {self} scaling governor"))?, - ); - - Ok(()) - } - - fn rescan_frequency(&mut self) -> anyhow::Result<()> { - let Self { number, .. } = *self; - - let frequency_khz = fs::read_n::(format!( - "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_cur_freq" - )) - .with_context(|| format!("failed to parse {self} frequency"))? - .with_context(|| format!("failed to find {self} frequency"))?; - let frequency_khz_minimum = fs::read_n::(format!( - "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" - )) - .with_context(|| format!("failed to parse {self} frequency minimum"))? - .with_context(|| format!("failed to find {self} frequency"))?; - let frequency_khz_maximum = fs::read_n::(format!( - "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq" - )) - .with_context(|| format!("failed to parse {self} frequency maximum"))? - .with_context(|| format!("failed to find {self} frequency"))?; - - self.frequency_mhz = Some(frequency_khz / 1000); - self.frequency_mhz_minimum = Some(frequency_khz_minimum / 1000); - self.frequency_mhz_maximum = Some(frequency_khz_maximum / 1000); - - Ok(()) - } - - fn rescan_epp(&mut self) -> anyhow::Result<()> { - let Self { number, .. } = *self; - - self.available_epps = 'available_epps: { - let Some(content) = fs::read(format!( - "/sys/devices/system/cpu/cpu{number}/cpufreq/\ - energy_performance_available_preferences" - )) - .with_context(|| format!("failed to read {self} available EPPs"))? - else { - break 'available_epps Vec::new(); - }; - - content - .split_whitespace() - .map(ToString::to_string) - .collect() - }; - - self.epp = Some( - fs::read(format!( - "/sys/devices/system/cpu/cpu{number}/cpufreq/\ - energy_performance_preference" - )) - .with_context(|| format!("failed to read {self} EPP"))? - .with_context(|| format!("failed to find {self} EPP"))?, - ); - - Ok(()) - } - - fn rescan_epb(&mut self) -> anyhow::Result<()> { - let Self { number, .. } = self; - - self.available_epbs = if self.has_cpufreq { - vec![ - "1".to_owned(), - "2".to_owned(), - "3".to_owned(), - "4".to_owned(), - "5".to_owned(), - "6".to_owned(), - "7".to_owned(), - "8".to_owned(), - "9".to_owned(), - "10".to_owned(), - "11".to_owned(), - "12".to_owned(), - "13".to_owned(), - "14".to_owned(), - "15".to_owned(), - "performance".to_owned(), - "balance-performance".to_owned(), - "balance_performance".to_owned(), // Alternative form with underscore. - "balance-power".to_owned(), - "balance_power".to_owned(), // Alternative form with underscore. - "power".to_owned(), - ] - } else { - Vec::new() - }; - - self.epb = Some( - fs::read(format!( - "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias" - )) - .with_context(|| format!("failed to read {self} EPB"))? - .with_context(|| format!("failed to find {self} EPB"))?, - ); - - Ok(()) - } - - fn rescan_stat(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { - // OnceCell::get_or_try_init is unstable. Cope: - let stat = match cache.stat.get() { - Some(stat) => stat, - - None => { - let content = fs::read("/proc/stat") - .context("failed to read CPU stat")? - .context("/proc/stat does not exist")?; - - cache - .stat - .set(HashMap::from_iter(content.lines().skip(1).filter_map( - |line| { - let mut parts = line.strip_prefix("cpu")?.split_whitespace(); - - let number = parts.next()?.parse().ok()?; - - let stat = CpuStat { - user: parts.next()?.parse().ok()?, - nice: parts.next()?.parse().ok()?, - system: parts.next()?.parse().ok()?, - idle: parts.next()?.parse().ok()?, - iowait: parts.next()?.parse().ok()?, - irq: parts.next()?.parse().ok()?, - softirq: parts.next()?.parse().ok()?, - steal: parts.next()?.parse().ok()?, - }; - - Some((number, stat)) + stat: CpuStat { + user: 0, + nice: 0, + system: 0, + idle: 0, + iowait: 0, + irq: 0, + softirq: 0, + steal: 0, }, - ))) - .unwrap(); + info: None, - cache.stat.get().unwrap() - }, - }; + temperature: None, + }; + cpu.rescan(cache)?; - self.stat = stat - .get(&self.number) - .with_context(|| format!("failed to get stat of {self}"))? - .clone(); + Ok(cpu) + } - Ok(()) - } + /// Get all CPUs. + pub fn all() -> anyhow::Result> { + const PATH: &str = "/sys/devices/system/cpu"; - fn rescan_info(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { - // OnceCell::get_or_try_init is unstable. Cope: - let info = match cache.info.get() { - Some(stat) => stat, + let mut cpus = vec![]; + let cache = CpuRescanCache::default(); - None => { - let content = fs::read("/proc/cpuinfo") - .context("failed to read CPU info")? - .context("/proc/cpuinfo does not exist")?; + for entry in fs::read_dir(PATH) + .with_context(|| format!("failed to read CPU entries from '{PATH}'"))? + .with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))? + { + let entry = entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; - let mut info = HashMap::new(); - let mut current_number = None; - let mut current_data = HashMap::new(); + let entry_file_name = entry.file_name(); - macro_rules! try_save_data { - () => { - if let Some(number) = current_number.take() { - info.insert(number, Rc::new(mem::take(&mut current_data))); - } - }; + let Some(name) = entry_file_name.to_str() else { + continue; + }; + + let Some(cpu_prefix_removed) = name.strip_prefix("cpu") else { + continue; + }; + + // Has to match "cpu{N}". + let Ok(number) = cpu_prefix_removed.parse() else { + continue; + }; + + cpus.push(Self::new(number, &cache)?); } - for line in content.lines() { - let parts = line.splitn(2, ':').collect::>(); - - if parts.len() == 2 { - let key = parts[0].trim(); - let value = parts[1].trim(); - - if key == "processor" { - try_save_data!(); - - current_number = value.parse::().ok(); - } else { - current_data.insert(key.to_owned(), value.to_owned()); + // Fall back if sysfs iteration above fails to find any cpufreq CPUs. + if cpus.is_empty() { + for number in 0..num_cpus::get() as u32 { + cpus.push(Self::new(number, &cache)?); } - } } - try_save_data!(); - - cache.info.set(info).unwrap(); - cache.info.get().unwrap() - }, - }; - - self.info = info.get(&self.number).cloned(); - - Ok(()) - } - - pub fn set_governor(&mut self, governor: &str) -> anyhow::Result<()> { - let Self { - number, - available_governors: ref governors, - .. - } = *self; - - if !governors - .iter() - .any(|avail_governor| avail_governor == governor) - { - bail!( - "governor '{governor}' is not available for {self}. available \ - governors: {governors}", - governors = governors.join(", "), - ); + Ok(cpus) } - fs::write( - format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_governor"), - governor, - ) - .with_context(|| { - format!( - "this probably means that {self} doesn't exist or doesn't support \ - changing governors" - ) - })?; + /// Rescan CPU, tuning local copy of settings. + pub fn rescan(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { + let Self { number, .. } = self; - self.governor = Some(governor.to_owned()); + if !fs::exists(format!("/sys/devices/system/cpu/cpu{number}")) { + bail!("{self} does not exist"); + } - Ok(()) - } + self.has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); - pub fn set_epp(&mut self, epp: &str) -> anyhow::Result<()> { - let Self { - number, - available_epps: ref epps, - .. - } = *self; + if self.has_cpufreq { + self.rescan_governor()?; + self.rescan_frequency()?; + self.rescan_epp()?; + self.rescan_epb()?; + } - if !epps.iter().any(|avail_epp| avail_epp == epp) { - bail!( - "EPP value '{epp}' is not available for {self}. available EPP values: \ - {epps}", - epps = epps.join(", "), - ); + self.rescan_stat(cache)?; + self.rescan_info(cache)?; + + Ok(()) } - fs::write( - format!( - "/sys/devices/system/cpu/cpu{number}/cpufreq/\ - energy_performance_preference" - ), - epp, - ) - .with_context(|| { - format!( - "this probably means that {self} doesn't exist or doesn't support \ - changing EPP" - ) - })?; + fn rescan_governor(&mut self) -> anyhow::Result<()> { + let Self { number, .. } = *self; - self.epp = Some(epp.to_owned()); + self.available_governors = 'available_governors: { + let Some(content) = fs::read(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_available_governors" + )) + .with_context(|| format!("failed to read {self} available governors"))? + else { + break 'available_governors Vec::new(); + }; - Ok(()) - } + content + .split_whitespace() + .map(ToString::to_string) + .collect() + }; - pub fn set_epb(&mut self, epb: &str) -> anyhow::Result<()> { - let Self { - number, - available_epbs: ref epbs, - .. - } = *self; + self.governor = Some( + fs::read(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_governor" + )) + .with_context(|| format!("failed to read {self} scaling governor"))? + .with_context(|| format!("failed to find {self} scaling governor"))?, + ); - if !epbs.iter().any(|avail_epb| avail_epb == epb) { - bail!( - "EPB value '{epb}' is not available for {self}. available EPB values: \ - {valid}", - valid = epbs.join(", "), - ); + Ok(()) } - fs::write( - format!( - "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias" - ), - epb, - ) - .with_context(|| { - format!( - "this probably means that {self} doesn't exist or doesn't support \ - changing EPB" - ) - })?; + fn rescan_frequency(&mut self) -> anyhow::Result<()> { + let Self { number, .. } = *self; - self.epb = Some(epb.to_owned()); + let frequency_khz = fs::read_n::(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_cur_freq" + )) + .with_context(|| format!("failed to parse {self} frequency"))? + .with_context(|| format!("failed to find {self} frequency"))?; + let frequency_khz_minimum = fs::read_n::(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" + )) + .with_context(|| format!("failed to parse {self} frequency minimum"))? + .with_context(|| format!("failed to find {self} frequency"))?; + let frequency_khz_maximum = fs::read_n::(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq" + )) + .with_context(|| format!("failed to parse {self} frequency maximum"))? + .with_context(|| format!("failed to find {self} frequency"))?; - Ok(()) - } + self.frequency_mhz = Some(frequency_khz / 1000); + self.frequency_mhz_minimum = Some(frequency_khz_minimum / 1000); + self.frequency_mhz_maximum = Some(frequency_khz_maximum / 1000); - pub fn set_frequency_mhz_minimum( - &mut self, - frequency_mhz: u64, - ) -> anyhow::Result<()> { - let Self { number, .. } = *self; - - self.validate_frequency_mhz_minimum(frequency_mhz)?; - - // We use u64 for the intermediate calculation to prevent overflow - let frequency_khz = frequency_mhz * 1000; - let frequency_khz = frequency_khz.to_string(); - - fs::write( - format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq"), - &frequency_khz, - ) - .with_context(|| { - format!( - "this probably means that {self} doesn't exist or doesn't support \ - changing minimum frequency" - ) - })?; - - self.frequency_mhz_minimum = Some(frequency_mhz); - - Ok(()) - } - - fn validate_frequency_mhz_minimum( - &self, - new_frequency_mhz: u64, - ) -> anyhow::Result<()> { - let Self { number, .. } = self; - - let Some(minimum_frequency_khz) = fs::read_n::(format!( - "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" - )) - .with_context(|| format!("failed to read {self} minimum frequency"))? - else { - // Just let it pass if we can't find anything. - return Ok(()); - }; - - if new_frequency_mhz * 1000 < minimum_frequency_khz { - bail!( - "new minimum frequency ({new_frequency_mhz} MHz) cannot be lower than \ - the minimum frequency ({} MHz) for {self}", - minimum_frequency_khz / 1000, - ); + Ok(()) } - Ok(()) - } + fn rescan_epp(&mut self) -> anyhow::Result<()> { + let Self { number, .. } = *self; - pub fn set_frequency_mhz_maximum( - &mut self, - frequency_mhz: u64, - ) -> anyhow::Result<()> { - let Self { number, .. } = *self; + self.available_epps = 'available_epps: { + let Some(content) = fs::read(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_available_preferences" + )).with_context(|| format!("failed to read {self} available EPPs"))? else { + break 'available_epps Vec::new(); + }; - self.validate_frequency_mhz_maximum(frequency_mhz)?; + content + .split_whitespace() + .map(ToString::to_string) + .collect() + }; - // We use u64 for the intermediate calculation to prevent overflow - let frequency_khz = frequency_mhz * 1000; - let frequency_khz = frequency_khz.to_string(); + self.epp = Some( + fs::read(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_preference" + )) + .with_context(|| format!("failed to read {self} EPP"))? + .with_context(|| format!("failed to find {self} EPP"))?, + ); - fs::write( - format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq"), - &frequency_khz, - ) - .with_context(|| { - format!( - "this probably means that {self} doesn't exist or doesn't support \ - changing maximum frequency" - ) - })?; - - self.frequency_mhz_maximum = Some(frequency_mhz); - - Ok(()) - } - - fn validate_frequency_mhz_maximum( - &self, - new_frequency_mhz: u64, - ) -> anyhow::Result<()> { - let Self { number, .. } = self; - - let Some(maximum_frequency_khz) = fs::read_n::(format!( - "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq" - )) - .with_context(|| format!("failed to read {self} maximum frequency"))? - else { - // Just let it pass if we can't find anything. - return Ok(()); - }; - - if new_frequency_mhz * 1000 > maximum_frequency_khz { - bail!( - "new maximum frequency ({new_frequency_mhz} MHz) cannot be higher \ - than the maximum frequency ({} MHz) for {self}", - maximum_frequency_khz / 1000, - ); + Ok(()) } - Ok(()) - } + fn rescan_epb(&mut self) -> anyhow::Result<()> { + let Self { number, .. } = self; - pub fn set_turbo(on: bool) -> anyhow::Result<()> { - let value_boost = match on { - true => "1", // boost = 1 means turbo is enabled. - false => "0", // boost = 0 means turbo is disabled. - }; + self.available_epbs = if self.has_cpufreq { + vec![ + "1".to_owned(), + "2".to_owned(), + "3".to_owned(), + "4".to_owned(), + "5".to_owned(), + "6".to_owned(), + "7".to_owned(), + "8".to_owned(), + "9".to_owned(), + "10".to_owned(), + "11".to_owned(), + "12".to_owned(), + "13".to_owned(), + "14".to_owned(), + "15".to_owned(), + "performance".to_owned(), + "balance-performance".to_owned(), + "balance_performance".to_owned(), // Alternative form with underscore. + "balance-power".to_owned(), + "balance_power".to_owned(), // Alternative form with underscore. + "power".to_owned(), + ] + } else { + Vec::new() + }; - let value_boost_negated = match on { - true => "0", // no_turbo = 0 means turbo is enabled. - false => "1", // no_turbo = 1 means turbo is disabled. - }; + self.epb = Some( + fs::read(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias" + )) + .with_context(|| format!("failed to read {self} EPB"))? + .with_context(|| format!("failed to find {self} EPB"))?, + ); - // AMD specific paths - let amd_boost_path = "/sys/devices/system/cpu/amd_pstate/cpufreq/boost"; - let msr_boost_path = - "/sys/devices/system/cpu/cpufreq/amd_pstate_enable_boost"; - - // Path priority (from most to least specific) - let intel_boost_path_negated = - "/sys/devices/system/cpu/intel_pstate/no_turbo"; - let generic_boost_path = "/sys/devices/system/cpu/cpufreq/boost"; - - // Try each boost control path in order of specificity - if fs::write(intel_boost_path_negated, value_boost_negated).is_ok() { - return Ok(()); - } - if fs::write(amd_boost_path, value_boost).is_ok() { - return Ok(()); - } - if fs::write(msr_boost_path, value_boost).is_ok() { - return Ok(()); - } - if fs::write(generic_boost_path, value_boost).is_ok() { - return Ok(()); + Ok(()) } - // Also try per-core cpufreq boost for some AMD systems. - if Self::all()?.iter().any(|cpu| { - let Cpu { number, .. } = cpu; + fn rescan_stat(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { + // OnceCell::get_or_try_init is unstable. Cope: + let stat = match cache.stat.get() { + Some(stat) => stat, - fs::write( - format!("/sys/devices/system/cpu/cpu{number}/cpufreq/boost"), - value_boost, - ) - .is_ok() - }) { - return Ok(()); + None => { + let content = fs::read("/proc/stat") + .context("failed to read CPU stat")? + .context("/proc/stat does not exist")?; + + cache + .stat + .set(HashMap::from_iter(content.lines().skip(1).filter_map( + |line| { + let mut parts = line.strip_prefix("cpu")?.split_whitespace(); + + let number = parts.next()?.parse().ok()?; + + let stat = CpuStat { + user: parts.next()?.parse().ok()?, + nice: parts.next()?.parse().ok()?, + system: parts.next()?.parse().ok()?, + idle: parts.next()?.parse().ok()?, + iowait: parts.next()?.parse().ok()?, + irq: parts.next()?.parse().ok()?, + softirq: parts.next()?.parse().ok()?, + steal: parts.next()?.parse().ok()?, + }; + + Some((number, stat)) + }, + ))) + .unwrap(); + + cache.stat.get().unwrap() + } + }; + + self.stat = stat + .get(&self.number) + .with_context(|| format!("failed to get stat of {self}"))? + .clone(); + + Ok(()) } - bail!("no supported CPU boost control mechanism found"); - } + fn rescan_info(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { + // OnceCell::get_or_try_init is unstable. Cope: + let info = match cache.info.get() { + Some(stat) => stat, - pub fn turbo() -> anyhow::Result> { - if let Some(content) = - fs::read_n::("/sys/devices/system/cpu/intel_pstate/no_turbo") - .context("failed to read CPU turbo boost status")? - { - return Ok(Some(content == 0)); + None => { + let content = fs::read("/proc/cpuinfo") + .context("failed to read CPU info")? + .context("/proc/cpuinfo does not exist")?; + + let mut info = HashMap::new(); + let mut current_number = None; + let mut current_data = HashMap::new(); + + macro_rules! try_save_data { + () => { + if let Some(number) = current_number.take() { + info.insert(number, Rc::new(mem::take(&mut current_data))); + } + }; + } + + for line in content.lines() { + let parts = line.splitn(2, ':').collect::>(); + + if parts.len() == 2 { + let key = parts[0].trim(); + let value = parts[1].trim(); + + if key == "processor" { + try_save_data!(); + + current_number = value.parse::().ok(); + } else { + current_data.insert(key.to_owned(), value.to_owned()); + } + } + } + + try_save_data!(); + + cache.info.set(info).unwrap(); + cache.info.get().unwrap() + } + }; + + self.info = info.get(&self.number).cloned(); + + Ok(()) } - if let Some(content) = - fs::read_n::("/sys/devices/system/cpu/cpufreq/boost") - .context("failed to read CPU turbo boost status")? - { - return Ok(Some(content == 1)); + pub fn set_governor(&mut self, governor: &str) -> anyhow::Result<()> { + let Self { + number, + available_governors: ref governors, + .. + } = *self; + + if !governors + .iter() + .any(|avail_governor| avail_governor == governor) + { + bail!( + "governor '{governor}' is not available for {self}. available governors: {governors}", + governors = governors.join(", "), + ); + } + + fs::write( + format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_governor"), + governor, + ) + .with_context(|| { + format!( + "this probably means that {self} doesn't exist or doesn't support changing governors" + ) + })?; + + self.governor = Some(governor.to_owned()); + + Ok(()) } - Ok(None) - } + pub fn set_epp(&mut self, epp: &str) -> anyhow::Result<()> { + let Self { + number, + available_epps: ref epps, + .. + } = *self; + + if !epps.iter().any(|avail_epp| avail_epp == epp) { + bail!( + "EPP value '{epp}' is not available for {self}. available EPP values: {epps}", + epps = epps.join(", "), + ); + } + + fs::write( + format!("/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_preference"), + epp, + ) + .with_context(|| { + format!("this probably means that {self} doesn't exist or doesn't support changing EPP") + })?; + + self.epp = Some(epp.to_owned()); + + Ok(()) + } + + pub fn set_epb(&mut self, epb: &str) -> anyhow::Result<()> { + let Self { + number, + available_epbs: ref epbs, + .. + } = *self; + + if !epbs.iter().any(|avail_epb| avail_epb == epb) { + bail!( + "EPB value '{epb}' is not available for {self}. available EPB values: {valid}", + valid = epbs.join(", "), + ); + } + + fs::write( + format!("/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias"), + epb, + ) + .with_context(|| { + format!("this probably means that {self} doesn't exist or doesn't support changing EPB") + })?; + + self.epb = Some(epb.to_owned()); + + Ok(()) + } + + pub fn set_frequency_mhz_minimum(&mut self, frequency_mhz: u64) -> anyhow::Result<()> { + let Self { number, .. } = *self; + + self.validate_frequency_mhz_minimum(frequency_mhz)?; + + // We use u64 for the intermediate calculation to prevent overflow + let frequency_khz = frequency_mhz * 1000; + let frequency_khz = frequency_khz.to_string(); + + fs::write( + format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq"), + &frequency_khz, + ) + .with_context(|| { + format!("this probably means that {self} doesn't exist or doesn't support changing minimum frequency") + })?; + + self.frequency_mhz_minimum = Some(frequency_mhz); + + Ok(()) + } + + fn validate_frequency_mhz_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { + let Self { number, .. } = self; + + let Some(minimum_frequency_khz) = fs::read_n::(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" + )) + .with_context(|| format!("failed to read {self} minimum frequency"))? + else { + // Just let it pass if we can't find anything. + return Ok(()); + }; + + if new_frequency_mhz * 1000 < minimum_frequency_khz { + bail!( + "new minimum frequency ({new_frequency_mhz} MHz) cannot be lower than the minimum frequency ({} MHz) for {self}", + minimum_frequency_khz / 1000, + ); + } + + Ok(()) + } + + pub fn set_frequency_mhz_maximum(&mut self, frequency_mhz: u64) -> anyhow::Result<()> { + let Self { number, .. } = *self; + + self.validate_frequency_mhz_maximum(frequency_mhz)?; + + // We use u64 for the intermediate calculation to prevent overflow + let frequency_khz = frequency_mhz * 1000; + let frequency_khz = frequency_khz.to_string(); + + fs::write( + format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq"), + &frequency_khz, + ) + .with_context(|| { + format!("this probably means that {self} doesn't exist or doesn't support changing maximum frequency") + })?; + + self.frequency_mhz_maximum = Some(frequency_mhz); + + Ok(()) + } + + fn validate_frequency_mhz_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { + let Self { number, .. } = self; + + let Some(maximum_frequency_khz) = fs::read_n::(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq" + )) + .with_context(|| format!("failed to read {self} maximum frequency"))? + else { + // Just let it pass if we can't find anything. + return Ok(()); + }; + + if new_frequency_mhz * 1000 > maximum_frequency_khz { + bail!( + "new maximum frequency ({new_frequency_mhz} MHz) cannot be higher than the maximum frequency ({} MHz) for {self}", + maximum_frequency_khz / 1000, + ); + } + + Ok(()) + } + + pub fn set_turbo(on: bool) -> anyhow::Result<()> { + let value_boost = match on { + true => "1", // boost = 1 means turbo is enabled. + false => "0", // boost = 0 means turbo is disabled. + }; + + let value_boost_negated = match on { + true => "0", // no_turbo = 0 means turbo is enabled. + false => "1", // no_turbo = 1 means turbo is disabled. + }; + + // AMD specific paths + let amd_boost_path = "/sys/devices/system/cpu/amd_pstate/cpufreq/boost"; + let msr_boost_path = "/sys/devices/system/cpu/cpufreq/amd_pstate_enable_boost"; + + // Path priority (from most to least specific) + let intel_boost_path_negated = "/sys/devices/system/cpu/intel_pstate/no_turbo"; + let generic_boost_path = "/sys/devices/system/cpu/cpufreq/boost"; + + // Try each boost control path in order of specificity + if fs::write(intel_boost_path_negated, value_boost_negated).is_ok() { + return Ok(()); + } + if fs::write(amd_boost_path, value_boost).is_ok() { + return Ok(()); + } + if fs::write(msr_boost_path, value_boost).is_ok() { + return Ok(()); + } + if fs::write(generic_boost_path, value_boost).is_ok() { + return Ok(()); + } + + // Also try per-core cpufreq boost for some AMD systems. + if Self::all()?.iter().any(|cpu| { + let Cpu { number, .. } = cpu; + + fs::write( + format!("/sys/devices/system/cpu/cpu{number}/cpufreq/boost"), + value_boost, + ) + .is_ok() + }) { + return Ok(()); + } + + bail!("no supported CPU boost control mechanism found"); + } + + pub fn turbo() -> anyhow::Result> { + if let Some(content) = fs::read_n::("/sys/devices/system/cpu/intel_pstate/no_turbo") + .context("failed to read CPU turbo boost status")? + { + return Ok(Some(content == 0)); + } + + if let Some(content) = fs::read_n::("/sys/devices/system/cpu/cpufreq/boost") + .context("failed to read CPU turbo boost status")? + { + return Ok(Some(content == 1)); + } + + Ok(None) + } } diff --git a/src/daemon.rs b/src/daemon.rs index 08997e6..d8ee2e6 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,29 +1,17 @@ use std::{ - cell::LazyCell, - collections::{ - HashMap, - VecDeque, - }, - sync::{ - Arc, - atomic::{ - AtomicBool, - Ordering, + cell::LazyCell, + collections::{HashMap, VecDeque}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, }, - }, - thread, - time::{ - Duration, - Instant, - }, + thread, + time::{Duration, Instant}, }; use anyhow::Context; -use crate::{ - config, - system, -}; +use crate::{config, system}; /// Calculate the idle time multiplier based on system idle time. /// @@ -31,433 +19,419 @@ use crate::{ /// - For idle times < 2 minutes: Linear interpolation from 1.0 to 2.0 /// - For idle times >= 2 minutes: Logarithmic scaling (1.0 + log2(minutes)) fn idle_multiplier(idle_for: Duration) -> f64 { - let factor = match idle_for.as_secs() < 120 { - // Less than 2 minutes. - // Linear interpolation from 1.0 (at 0s) to 2.0 (at 120s) - true => (idle_for.as_secs() as f64) / 120.0, + let factor = match idle_for.as_secs() < 120 { + // Less than 2 minutes. + // Linear interpolation from 1.0 (at 0s) to 2.0 (at 120s) + true => (idle_for.as_secs() as f64) / 120.0, - // 2 minutes or more. - // Logarithmic scaling: 1.0 + log2(minutes) - false => { - let idle_minutes = idle_for.as_secs() as f64 / 60.0; - idle_minutes.log2() - }, - }; + // 2 minutes or more. + // Logarithmic scaling: 1.0 + log2(minutes) + false => { + let idle_minutes = idle_for.as_secs() as f64 / 60.0; + idle_minutes.log2() + } + }; - // Clamp the multiplier to avoid excessive delays. - (1.0 + factor).clamp(1.0, 5.0) + // Clamp the multiplier to avoid excessive delays. + (1.0 + factor).clamp(1.0, 5.0) } #[derive(Debug)] struct Daemon { - /// Last time when there was user activity. - last_user_activity: Instant, + /// Last time when there was user activity. + last_user_activity: Instant, - /// The last computed polling delay. - last_polling_delay: Option, + /// The last computed polling delay. + last_polling_delay: Option, - /// The system state. - system: system::System, + /// The system state. + system: system::System, - /// CPU usage and temperature log. - cpu_log: VecDeque, + /// CPU usage and temperature log. + cpu_log: VecDeque, - /// Power supply status log. - power_supply_log: VecDeque, + /// Power supply status log. + power_supply_log: VecDeque, } impl Daemon { - fn rescan(&mut self) -> anyhow::Result<()> { - self.system.rescan()?; + fn rescan(&mut self) -> anyhow::Result<()> { + self.system.rescan()?; - log::debug!("appending daemon logs..."); + log::debug!("appending daemon logs..."); - let at = Instant::now(); + let at = Instant::now(); - while self.cpu_log.len() > 100 { - log::debug!("daemon CPU log was too long, popping element"); - self.cpu_log.pop_front(); + while self.cpu_log.len() > 100 { + log::debug!("daemon CPU log was too long, popping element"); + self.cpu_log.pop_front(); + } + + let cpu_log = CpuLog { + at, + + usage: self + .system + .cpus + .iter() + .map(|cpu| cpu.stat.usage()) + .sum::() + / self.system.cpus.len() as f64, + + temperature: self.system.cpu_temperatures.values().sum::() + / self.system.cpu_temperatures.len() as f64, + }; + log::debug!("appending CPU log item: {cpu_log:?}"); + self.cpu_log.push_back(cpu_log); + + while self.power_supply_log.len() > 100 { + log::debug!("daemon power supply log was too long, popping element"); + self.power_supply_log.pop_front(); + } + + let power_supply_log = PowerSupplyLog { + at, + charge: { + let (charge_sum, charge_nr) = self.system.power_supplies.iter().fold( + (0.0, 0u32), + |(sum, count), power_supply| { + if let Some(charge_percent) = power_supply.charge_percent { + (sum + charge_percent, count + 1) + } else { + (sum, count) + } + }, + ); + + charge_sum / charge_nr as f64 + }, + }; + log::debug!("appending power supply log item: {power_supply_log:?}"); + self.power_supply_log.push_back(power_supply_log); + + Ok(()) } - - let cpu_log = CpuLog { - at, - - usage: self - .system - .cpus - .iter() - .map(|cpu| cpu.stat.usage()) - .sum::() - / self.system.cpus.len() as f64, - - temperature: self.system.cpu_temperatures.values().sum::() - / self.system.cpu_temperatures.len() as f64, - }; - log::debug!("appending CPU log item: {cpu_log:?}"); - self.cpu_log.push_back(cpu_log); - - while self.power_supply_log.len() > 100 { - log::debug!("daemon power supply log was too long, popping element"); - self.power_supply_log.pop_front(); - } - - let power_supply_log = PowerSupplyLog { - at, - charge: { - let (charge_sum, charge_nr) = self.system.power_supplies.iter().fold( - (0.0, 0u32), - |(sum, count), power_supply| { - if let Some(charge_percent) = power_supply.charge_percent { - (sum + charge_percent, count + 1) - } else { - (sum, count) - } - }, - ); - - charge_sum / charge_nr as f64 - }, - }; - log::debug!("appending power supply log item: {power_supply_log:?}"); - self.power_supply_log.push_back(power_supply_log); - - Ok(()) - } } #[derive(Debug)] struct CpuLog { - at: Instant, + at: Instant, - /// CPU usage between 0-1, a percentage. - usage: f64, + /// CPU usage between 0-1, a percentage. + usage: f64, - /// CPU temperature in celsius. - temperature: f64, + /// CPU temperature in celsius. + temperature: f64, } #[derive(Debug)] struct CpuVolatility { - usage: f64, + usage: f64, - temperature: f64, + temperature: f64, } impl Daemon { - fn cpu_volatility(&self) -> Option { - let recent_log_count = self - .cpu_log - .iter() - .rev() - .take_while(|log| log.at.elapsed() < Duration::from_secs(5 * 60)) - .count(); + fn cpu_volatility(&self) -> Option { + let recent_log_count = self + .cpu_log + .iter() + .rev() + .take_while(|log| log.at.elapsed() < Duration::from_secs(5 * 60)) + .count(); - if recent_log_count < 2 { - return None; + if recent_log_count < 2 { + return None; + } + + if self.cpu_log.len() < 2 { + return None; + } + + let change_count = self.cpu_log.len() - 1; + + let mut usage_change_sum = 0.0; + let mut temperature_change_sum = 0.0; + + for index in 0..change_count { + let usage_change = self.cpu_log[index + 1].usage - self.cpu_log[index].usage; + usage_change_sum += usage_change.abs(); + + let temperature_change = + self.cpu_log[index + 1].temperature - self.cpu_log[index].temperature; + temperature_change_sum += temperature_change.abs(); + } + + Some(CpuVolatility { + usage: usage_change_sum / change_count as f64, + temperature: temperature_change_sum / change_count as f64, + }) } - if self.cpu_log.len() < 2 { - return None; + fn is_cpu_idle(&self) -> bool { + let recent_log_count = self + .cpu_log + .iter() + .rev() + .take_while(|log| log.at.elapsed() < Duration::from_secs(5 * 60)) + .count(); + + if recent_log_count < 2 { + return false; + } + + let recent_average = self + .cpu_log + .iter() + .rev() + .take(recent_log_count) + .map(|log| log.usage) + .sum::() + / recent_log_count as f64; + + recent_average < 0.1 + && self + .cpu_volatility() + .is_none_or(|volatility| volatility.usage < 0.05) } - - let change_count = self.cpu_log.len() - 1; - - let mut usage_change_sum = 0.0; - let mut temperature_change_sum = 0.0; - - for index in 0..change_count { - let usage_change = - self.cpu_log[index + 1].usage - self.cpu_log[index].usage; - usage_change_sum += usage_change.abs(); - - let temperature_change = - self.cpu_log[index + 1].temperature - self.cpu_log[index].temperature; - temperature_change_sum += temperature_change.abs(); - } - - Some(CpuVolatility { - usage: usage_change_sum / change_count as f64, - temperature: temperature_change_sum / change_count as f64, - }) - } - - fn is_cpu_idle(&self) -> bool { - let recent_log_count = self - .cpu_log - .iter() - .rev() - .take_while(|log| log.at.elapsed() < Duration::from_secs(5 * 60)) - .count(); - - if recent_log_count < 2 { - return false; - } - - let recent_average = self - .cpu_log - .iter() - .rev() - .take(recent_log_count) - .map(|log| log.usage) - .sum::() - / recent_log_count as f64; - - recent_average < 0.1 - && self - .cpu_volatility() - .is_none_or(|volatility| volatility.usage < 0.05) - } } #[derive(Debug)] struct PowerSupplyLog { - at: Instant, + at: Instant, - /// Charge 0-1, as a percentage. - charge: f64, + /// Charge 0-1, as a percentage. + charge: f64, } impl Daemon { - fn discharging(&self) -> bool { - self.system.power_supplies.iter().any(|power_supply| { - power_supply.charge_state.as_deref() == Some("Discharging") - }) - } + fn discharging(&self) -> bool { + self.system + .power_supplies + .iter() + .any(|power_supply| power_supply.charge_state.as_deref() == Some("Discharging")) + } - /// Calculates the discharge rate, returns a number between 0 and 1. - /// - /// The discharge rate is averaged per hour. - /// So a return value of Some(0.3) means the battery has been - /// discharging 30% per hour. - fn power_supply_discharge_rate(&self) -> Option { - let mut last_charge = None; + /// Calculates the discharge rate, returns a number between 0 and 1. + /// + /// The discharge rate is averaged per hour. + /// So a return value of Some(0.3) means the battery has been + /// discharging 30% per hour. + fn power_supply_discharge_rate(&self) -> Option { + let mut last_charge = None; - // A list of increasing charge percentages. - let discharging: Vec<&PowerSupplyLog> = self - .power_supply_log - .iter() - .rev() - .take_while(move |log| { - let Some(last_charge_value) = last_charge else { - last_charge = Some(log.charge); - return true; + // A list of increasing charge percentages. + let discharging: Vec<&PowerSupplyLog> = self + .power_supply_log + .iter() + .rev() + .take_while(move |log| { + let Some(last_charge_value) = last_charge else { + last_charge = Some(log.charge); + return true; + }; + + last_charge = Some(log.charge); + + log.charge > last_charge_value + }) + .collect(); + + if discharging.len() < 2 { + return None; + } + + // Start of discharging. Has the most charge. + let start = discharging.last().unwrap(); + // End of discharging, very close to now. Has the least charge. + let end = discharging.first().unwrap(); + + let discharging_duration_seconds = (start.at - end.at).as_secs_f64(); + let discharging_duration_hours = discharging_duration_seconds / 60.0 / 60.0; + let discharged = start.charge - end.charge; + + Some(discharged / discharging_duration_hours) + } +} + +impl Daemon { + fn polling_delay(&mut self) -> Duration { + let mut delay = Duration::from_secs(5); + + // We are on battery, so we must be more conservative with our polling. + if self.discharging() { + match self.power_supply_discharge_rate() { + Some(discharge_rate) => { + if discharge_rate > 0.2 { + delay *= 3; + } else if discharge_rate > 0.1 { + delay *= 2; + } else { + // *= 1.5; + delay /= 2; + delay *= 3; + } + } + + // If we can't determine the discharge rate, that means that + // we were very recently started. Which is user activity. + None => { + delay *= 2; + } + } + } + + if self.is_cpu_idle() { + let idle_for = self.last_user_activity.elapsed(); + + if idle_for > Duration::from_secs(30) { + let factor = idle_multiplier(idle_for); + + log::debug!( + "system has been idle for {seconds} seconds (approx {minutes} minutes), applying idle factor: {factor:.2}x", + seconds = idle_for.as_secs(), + minutes = idle_for.as_secs() / 60, + ); + + delay = Duration::from_secs_f64(delay.as_secs_f64() * factor); + } + } + + if let Some(volatility) = self.cpu_volatility() { + if volatility.usage > 0.1 || volatility.temperature > 0.02 { + delay = (delay / 2).max(Duration::from_secs(1)); + } + } + + let delay = match self.last_polling_delay { + Some(last_delay) => Duration::from_secs_f64( + // 30% of current computed delay, 70% of last delay. + delay.as_secs_f64() * 0.3 + last_delay.as_secs_f64() * 0.7, + ), + + None => delay, }; - last_charge = Some(log.charge); + let delay = Duration::from_secs_f64(delay.as_secs_f64().clamp(1.0, 30.0)); - log.charge > last_charge_value - }) - .collect(); + self.last_polling_delay = Some(delay); - if discharging.len() < 2 { - return None; + delay } - - // Start of discharging. Has the most charge. - let start = discharging.last().unwrap(); - // End of discharging, very close to now. Has the least charge. - let end = discharging.first().unwrap(); - - let discharging_duration_seconds = (start.at - end.at).as_secs_f64(); - let discharging_duration_hours = discharging_duration_seconds / 60.0 / 60.0; - let discharged = start.charge - end.charge; - - Some(discharged / discharging_duration_hours) - } -} - -impl Daemon { - fn polling_delay(&mut self) -> Duration { - let mut delay = Duration::from_secs(5); - - // We are on battery, so we must be more conservative with our polling. - if self.discharging() { - match self.power_supply_discharge_rate() { - Some(discharge_rate) => { - if discharge_rate > 0.2 { - delay *= 3; - } else if discharge_rate > 0.1 { - delay *= 2; - } else { - // *= 1.5; - delay /= 2; - delay *= 3; - } - }, - - // If we can't determine the discharge rate, that means that - // we were very recently started. Which is user activity. - None => { - delay *= 2; - }, - } - } - - if self.is_cpu_idle() { - let idle_for = self.last_user_activity.elapsed(); - - if idle_for > Duration::from_secs(30) { - let factor = idle_multiplier(idle_for); - - log::debug!( - "system has been idle for {seconds} seconds (approx {minutes} \ - minutes), applying idle factor: {factor:.2}x", - seconds = idle_for.as_secs(), - minutes = idle_for.as_secs() / 60, - ); - - delay = Duration::from_secs_f64(delay.as_secs_f64() * factor); - } - } - - if let Some(volatility) = self.cpu_volatility() { - if volatility.usage > 0.1 || volatility.temperature > 0.02 { - delay = (delay / 2).max(Duration::from_secs(1)); - } - } - - let delay = match self.last_polling_delay { - Some(last_delay) => { - Duration::from_secs_f64( - // 30% of current computed delay, 70% of last delay. - delay.as_secs_f64() * 0.3 + last_delay.as_secs_f64() * 0.7, - ) - }, - - None => delay, - }; - - let delay = Duration::from_secs_f64(delay.as_secs_f64().clamp(1.0, 30.0)); - - self.last_polling_delay = Some(delay); - - delay - } } pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> { - assert!(config.rules.is_sorted_by_key(|rule| rule.priority)); + assert!(config.rules.is_sorted_by_key(|rule| rule.priority)); - log::info!("starting daemon..."); + log::info!("starting daemon..."); - let cancelled = Arc::new(AtomicBool::new(false)); + let cancelled = Arc::new(AtomicBool::new(false)); - log::debug!("setting ctrl-c handler..."); - let cancelled_ = Arc::clone(&cancelled); - ctrlc::set_handler(move || { - log::info!("received shutdown signal"); - cancelled_.store(true, Ordering::SeqCst); - }) - .context("failed to set ctrl-c handler")?; + log::debug!("setting ctrl-c handler..."); + let cancelled_ = Arc::clone(&cancelled); + ctrlc::set_handler(move || { + log::info!("received shutdown signal"); + cancelled_.store(true, Ordering::SeqCst); + }) + .context("failed to set ctrl-c handler")?; - let mut daemon = Daemon { - last_user_activity: Instant::now(), + let mut daemon = Daemon { + last_user_activity: Instant::now(), - last_polling_delay: None, + last_polling_delay: None, - system: system::System::new()?, + system: system::System::new()?, - cpu_log: VecDeque::new(), - power_supply_log: VecDeque::new(), - }; - - while !cancelled.load(Ordering::SeqCst) { - daemon.rescan()?; - - let delay = daemon.polling_delay(); - log::info!( - "next poll will be in {seconds} seconds or {minutes} minutes, possibly \ - delayed if application of rules takes more than the polling delay", - seconds = delay.as_secs_f64(), - minutes = delay.as_secs_f64() / 60.0, - ); - - log::debug!("filtering rules and applying them..."); - - let start = Instant::now(); - - let state = config::EvalState { - cpu_usage: daemon.cpu_log.back().unwrap().usage, - cpu_usage_volatility: daemon.cpu_volatility().map(|vol| vol.usage), - cpu_temperature: daemon.cpu_log.back().unwrap().temperature, - cpu_temperature_volatility: daemon - .cpu_volatility() - .map(|vol| vol.temperature), - cpu_idle_seconds: daemon - .last_user_activity - .elapsed() - .as_secs_f64(), - power_supply_charge: daemon - .power_supply_log - .back() - .unwrap() - .charge, - power_supply_discharge_rate: daemon.power_supply_discharge_rate(), - discharging: daemon.discharging(), + cpu_log: VecDeque::new(), + power_supply_log: VecDeque::new(), }; - let mut cpu_delta_for = HashMap::::new(); - let all_cpus = - LazyCell::new(|| (0..num_cpus::get() as u32).collect::>()); + while !cancelled.load(Ordering::SeqCst) { + daemon.rescan()?; - for rule in &config.rules { - let Some(condition) = rule.condition.eval(&state)? else { - continue; - }; + let delay = daemon.polling_delay(); + log::info!( + "next poll will be in {seconds} seconds or {minutes} minutes, possibly delayed if application of rules takes more than the polling delay", + seconds = delay.as_secs_f64(), + minutes = delay.as_secs_f64() / 60.0, + ); - let cpu_for = rule.cpu.for_.as_ref().unwrap_or_else(|| &*all_cpus); + log::debug!("filtering rules and applying them..."); - for cpu in cpu_for { - let delta = cpu_delta_for.entry(*cpu).or_default(); + let start = Instant::now(); - delta.for_ = Some(vec![*cpu]); + let state = config::EvalState { + cpu_usage: daemon.cpu_log.back().unwrap().usage, + cpu_usage_volatility: daemon.cpu_volatility().map(|vol| vol.usage), + cpu_temperature: daemon.cpu_log.back().unwrap().temperature, + cpu_temperature_volatility: daemon.cpu_volatility().map(|vol| vol.temperature), + cpu_idle_seconds: daemon.last_user_activity.elapsed().as_secs_f64(), + power_supply_charge: daemon.power_supply_log.back().unwrap().charge, + power_supply_discharge_rate: daemon.power_supply_discharge_rate(), + discharging: daemon.discharging(), + }; - if let Some(governor) = rule.cpu.governor.as_ref() { - delta.governor = Some(governor.clone()); + let mut cpu_delta_for = HashMap::::new(); + let all_cpus = LazyCell::new(|| (0..num_cpus::get() as u32).collect::>()); + + for rule in &config.rules { + let Some(condition) = rule.condition.eval(&state)? else { + continue; + }; + + let cpu_for = rule.cpu.for_.as_ref().unwrap_or_else(|| &*all_cpus); + + for cpu in cpu_for { + let delta = cpu_delta_for.entry(*cpu).or_default(); + + delta.for_ = Some(vec![*cpu]); + + if let Some(governor) = rule.cpu.governor.as_ref() { + delta.governor = Some(governor.clone()); + } + + if let Some(epp) = rule.cpu.energy_performance_preference.as_ref() { + delta.energy_performance_preference = Some(epp.clone()); + } + + if let Some(epb) = rule.cpu.energy_performance_bias.as_ref() { + delta.energy_performance_bias = Some(epb.clone()); + } + + if let Some(mhz_minimum) = rule.cpu.frequency_mhz_minimum { + delta.frequency_mhz_minimum = Some(mhz_minimum); + } + + if let Some(mhz_maximum) = rule.cpu.frequency_mhz_maximum { + delta.frequency_mhz_maximum = Some(mhz_maximum); + } + + if let Some(turbo) = rule.cpu.turbo { + delta.turbo = Some(turbo); + } + } + + // TODO: Also merge this into one like CPU. + if condition.as_boolean()? { + rule.power.apply()?; + } } - if let Some(epp) = rule.cpu.energy_performance_preference.as_ref() { - delta.energy_performance_preference = Some(epp.clone()); + for delta in cpu_delta_for.values() { + delta.apply()?; } - if let Some(epb) = rule.cpu.energy_performance_bias.as_ref() { - delta.energy_performance_bias = Some(epb.clone()); - } + let elapsed = start.elapsed(); + log::info!( + "filtered and applied rules in {seconds} seconds or {minutes} minutes", + seconds = elapsed.as_secs_f64(), + minutes = elapsed.as_secs_f64() / 60.0, + ); - if let Some(mhz_minimum) = rule.cpu.frequency_mhz_minimum { - delta.frequency_mhz_minimum = Some(mhz_minimum); - } - - if let Some(mhz_maximum) = rule.cpu.frequency_mhz_maximum { - delta.frequency_mhz_maximum = Some(mhz_maximum); - } - - if let Some(turbo) = rule.cpu.turbo { - delta.turbo = Some(turbo); - } - } - - // TODO: Also merge this into one like CPU. - if condition.as_boolean()? { - rule.power.apply()?; - } + thread::sleep(delay.saturating_sub(elapsed)); } - for delta in cpu_delta_for.values() { - delta.apply()?; - } + log::info!("stopping polling loop and thus daemon..."); - let elapsed = start.elapsed(); - log::info!( - "filtered and applied rules in {seconds} seconds or {minutes} minutes", - seconds = elapsed.as_secs_f64(), - minutes = elapsed.as_secs_f64() / 60.0, - ); - - thread::sleep(delay.saturating_sub(elapsed)); - } - - log::info!("stopping polling loop and thus daemon..."); - - Ok(()) + Ok(()) } diff --git a/src/engine.rs b/src/engine.rs new file mode 100644 index 0000000..b9c7b01 --- /dev/null +++ b/src/engine.rs @@ -0,0 +1,465 @@ +use crate::config::{AppConfig, ProfileConfig, TurboAutoSettings}; +use crate::core::{OperationalMode, SystemReport}; +use crate::cpu::{self}; +use crate::power_supply; +use std::sync::OnceLock; +use std::sync::atomic::{AtomicBool, Ordering}; + +/// Track turbo boost state for AC and battery power modes +struct TurboHysteresisStates { + /// State for when on AC power + charger: TurboHysteresis, + /// State for when on battery power + battery: TurboHysteresis, +} + +impl TurboHysteresisStates { + const fn new() -> Self { + Self { + charger: TurboHysteresis::new(), + battery: TurboHysteresis::new(), + } + } + + const fn get_for_power_state(&self, is_on_ac: bool) -> &TurboHysteresis { + if is_on_ac { + &self.charger + } else { + &self.battery + } + } +} + +static TURBO_STATES: OnceLock = OnceLock::new(); + +/// Get or initialize the global turbo states +fn get_turbo_states() -> &'static TurboHysteresisStates { + TURBO_STATES.get_or_init(TurboHysteresisStates::new) +} + +/// Manage turbo boost hysteresis state. +/// Contains the state needed to implement hysteresis +/// for the dynamic turbo management feature +struct TurboHysteresis { + /// Whether turbo was enabled in the previous cycle + previous_state: AtomicBool, + /// Whether the hysteresis state has been initialized + initialized: AtomicBool, +} + +impl TurboHysteresis { + const fn new() -> Self { + Self { + previous_state: AtomicBool::new(false), + initialized: AtomicBool::new(false), + } + } + + /// Get the previous turbo state, if initialized + fn get_previous_state(&self) -> Option { + if self.initialized.load(Ordering::Acquire) { + Some(self.previous_state.load(Ordering::Acquire)) + } else { + None + } + } + + /// Initialize the state with a specific value if not already initialized + /// Only one thread should be able to initialize the state + fn initialize_with(&self, initial_state: bool) -> bool { + // First, try to atomically change initialized from false to true + // Only one thread can win the initialization race + match self.initialized.compare_exchange( + false, // expected: not initialized + true, // desired: mark as initialized + Ordering::Release, // success: release for memory visibility + Ordering::Acquire, // failure: just need to acquire the current value + ) { + Ok(_) => { + // We won the race to initialize + // Now it's safe to set the initial state since we know we're the only + // thread that has successfully marked this as initialized + self.previous_state.store(initial_state, Ordering::Release); + initial_state + } + Err(_) => { + // Another thread already initialized it. + // Just read the current state value that was set by the winning thread + self.previous_state.load(Ordering::Acquire) + } + } + } + + /// Update the turbo state for hysteresis + fn update_state(&self, new_state: bool) { + // First store the new state, then mark as initialized + // With this, any thread seeing initialized=true will also see the correct state + self.previous_state.store(new_state, Ordering::Release); + + // Already initialized, no need for compare_exchange + if self.initialized.load(Ordering::Relaxed) { + return; + } + + // Otherwise, try to set initialized=true (but only if it was false) + self.initialized + .compare_exchange( + false, // expected: not initialized + true, // desired: mark as initialized + Ordering::Release, // success: release for memory visibility + Ordering::Relaxed, // failure: we don't care about the current value on failure + ) + .ok(); // Ignore the result. If it fails, it means another thread already initialized it + } +} + +/// Try applying a CPU feature and handle common error cases. Centralizes the where we +/// previously did: +/// 1. Try to apply a feature setting +/// 2. If not supported, log a warning and continue +/// 3. If other error, propagate the error +fn try_apply_feature anyhow::Result<()>, T>( + feature_name: &str, + value_description: &str, + apply_fn: F, +) -> anyhow::Result<()> { + log::info!("Setting {feature_name} to '{value_description}'"); + + apply_fn() +} + +/// Determines the appropriate CPU profile based on power status or forced mode, +/// and applies the settings (via helpers defined in the `cpu` module) +pub fn determine_and_apply_settings( + report: &SystemReport, + config: &AppConfig, + force_mode: Option, +) -> anyhow::Result<()> { + // // First, check if there's a governor override set + // if let Some(override_governor) = cpu::get_governor_override() { + // log::info!( + // "Governor override is active: '{}'. Setting governor.", + // override_governor.trim() + // ); + + // // Apply the override governor setting + // try_apply_feature("override governor", override_governor.trim(), || { + // cpu::set_governor(override_governor.trim(), None) + // })?; + // } + + // Determine AC/Battery status once, early in the function + // For desktops (no batteries), we should always use the AC power profile + // For laptops, we check if all batteries report connected to AC + let on_ac_power = if report.batteries.is_empty() { + // No batteries means desktop/server, always on AC + true + } else { + // Check if all batteries report AC connected + report.batteries.iter().all(|b| b.ac_connected) + }; + + let selected_profile_config: &ProfileConfig; + + if let Some(mode) = force_mode { + match mode { + OperationalMode::Powersave => { + log::info!("Forced Powersave mode selected. Applying 'battery' profile."); + selected_profile_config = &config.battery; + } + OperationalMode::Performance => { + log::info!("Forced Performance mode selected. Applying 'charger' profile."); + selected_profile_config = &config.charger; + } + } + } else { + // Use the previously computed on_ac_power value + if on_ac_power { + log::info!("On AC power, selecting Charger profile."); + selected_profile_config = &config.charger; + } else { + log::info!("On Battery power, selecting Battery profile."); + selected_profile_config = &config.battery; + } + } + + // Apply settings from selected_profile_config + if let Some(governor) = &selected_profile_config.governor { + log::info!("Setting governor to '{governor}'"); + for cpu in cpu::Cpu::all()? { + // Let set_governor handle the validation + if let Err(error) = cpu.set_governor(governor) { + // If the governor is not available, log a warning + log::warn!("{error}"); + } + } + } + + if let Some(turbo_setting) = selected_profile_config.turbo { + log::info!("Setting turbo to '{turbo_setting:?}'"); + match turbo_setting { + TurboSetting::Auto => { + if selected_profile_config.enable_auto_turbo { + log::debug!("Managing turbo in auto mode based on system conditions"); + manage_auto_turbo(report, selected_profile_config, on_ac_power)?; + } else { + log::debug!( + "Watt'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. + // This is important if turbo was previously forced off. + try_apply_feature("Turbo boost", "system default (Auto)", || { + cpu::set_turbo(TurboSetting::Auto) + })?; + } + } + _ => { + try_apply_feature("Turbo boost", &format!("{turbo_setting:?}"), || { + cpu::set_turbo(turbo_setting) + })?; + } + } + } + + if let Some(epp) = &selected_profile_config.epp { + try_apply_feature("EPP", epp, || cpu::set_epp(epp, None))?; + } + + if let Some(epb) = &selected_profile_config.epb { + try_apply_feature("EPB", epb, || cpu::set_epb(epb, None))?; + } + + if let Some(min_freq) = selected_profile_config.min_freq_mhz { + try_apply_feature("min frequency", &format!("{min_freq} MHz"), || { + cpu::set_frequency_minimum(min_freq, None) + })?; + } + + if let Some(max_freq) = selected_profile_config.max_freq_mhz { + try_apply_feature("max frequency", &format!("{max_freq} MHz"), || { + cpu::set_frequency_maximum(max_freq, None) + })?; + } + + if let Some(profile) = &selected_profile_config.platform_profile { + try_apply_feature("platform profile", profile, || { + cpu::set_platform_profile(profile) + })?; + } + + // Set battery charge thresholds if configured + if let Some(thresholds) = &selected_profile_config.battery_charge_thresholds { + let start_threshold = thresholds.start; + let stop_threshold = thresholds.stop; + + if start_threshold < stop_threshold && stop_threshold <= 100 { + log::info!("Setting battery charge thresholds: {start_threshold}-{stop_threshold}%"); + match power_supply::set_battery_charge_thresholds(start_threshold, stop_threshold) { + Ok(()) => log::debug!("Battery charge thresholds set successfully"), + Err(e) => log::warn!("Failed to set battery charge thresholds: {e}"), + } + } else { + log::warn!( + "Invalid battery threshold values: start={start_threshold}, stop={stop_threshold}" + ); + } + } + + log::debug!("Profile settings applied successfully."); + + Ok(()) +} + +fn manage_auto_turbo( + report: &SystemReport, + config: &ProfileConfig, + on_ac_power: bool, +) -> anyhow::Result<()> { + // Get the auto turbo settings from the config + let turbo_settings = &config.turbo_auto_settings; + + // Validate the complete configuration to ensure it's usable + validate_turbo_auto_settings(turbo_settings)?; + + // 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 + } + }; + + // Get the previous state or initialize with the configured initial state + let previous_turbo_enabled = { + let turbo_states = get_turbo_states(); + let hysteresis = turbo_states.get_for_power_state(on_ac_power); + if let Some(state) = hysteresis.get_previous_state() { + state + } else { + // Initialize with the configured initial state and return it + hysteresis.initialize_with(turbo_settings.initial_turbo_state) + } + }; + + // Decision logic for enabling/disabling turbo with hysteresis + let enable_turbo = match (cpu_temp, avg_cpu_usage, previous_turbo_enabled) { + // If temperature is too high, disable turbo regardless of load + (Some(temp), _, _) if temp >= turbo_settings.temp_threshold_high => { + log::info!( + "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 => { + log::info!( + "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 => { + log::info!( + "Auto Turbo: Disabled due to low CPU load ({:.1}% <= {:.1}%)", + usage, + turbo_settings.load_threshold_low + ); + false + } + + // In intermediate load range, maintain previous state (hysteresis) + (_, Some(usage), prev_state) + if usage > turbo_settings.load_threshold_low + && usage < turbo_settings.load_threshold_high => + { + log::info!( + "Auto Turbo: Maintaining previous state ({}) due to intermediate load ({:.1}%)", + if prev_state { "enabled" } else { "disabled" }, + usage + ); + prev_state + } + + // When CPU load data is present but temperature is missing, use the same hysteresis logic + (None, Some(usage), prev_state) => { + log::info!( + "Auto Turbo: Maintaining previous state ({}) due to missing temperature data (load: {:.1}%)", + if prev_state { "enabled" } else { "disabled" }, + usage + ); + prev_state + } + + // When all metrics are missing, maintain the previous state + (None, None, prev_state) => { + log::info!( + "Auto Turbo: Maintaining previous state ({}) due to missing all CPU metrics", + if prev_state { "enabled" } else { "disabled" } + ); + prev_state + } + + // Any other cases with partial metrics, maintain previous state for stability + (_, _, prev_state) => { + log::info!( + "Auto Turbo: Maintaining previous state ({}) due to incomplete CPU metrics", + if prev_state { "enabled" } else { "disabled" } + ); + prev_state + } + }; + + // Save the current state for next time + { + let turbo_states = get_turbo_states(); + let hysteresis = turbo_states.get_for_power_state(on_ac_power); + hysteresis.update_state(enable_turbo); + } + + // Only apply the setting if the state has changed + let changed = previous_turbo_enabled != enable_turbo; + if changed { + let turbo_setting = if enable_turbo { + TurboSetting::Always + } else { + TurboSetting::Never + }; + + log::info!( + "Auto Turbo: Applying turbo change from {} to {}", + if previous_turbo_enabled { + "enabled" + } else { + "disabled" + }, + if enable_turbo { "enabled" } else { "disabled" } + ); + + match cpu::set_turbo(turbo_setting) { + Ok(()) => { + log::debug!( + "Auto Turbo: Successfully set turbo to {}", + if enable_turbo { "enabled" } else { "disabled" } + ); + Ok(()) + } + Err(e) => Err(EngineError::ControlError(e)), + } + } else { + log::debug!( + "Auto Turbo: Maintaining turbo state ({}) - no change needed", + if enable_turbo { "enabled" } else { "disabled" } + ); + Ok(()) + } +} + +fn validate_turbo_auto_settings(settings: &TurboAutoSettings) -> Result<(), EngineError> { + if settings.load_threshold_high <= settings.load_threshold_low + || settings.load_threshold_high > 100.0 + || settings.load_threshold_high < 0.0 + || settings.load_threshold_low < 0.0 + || settings.load_threshold_low > 100.0 + { + return Err(EngineError::ConfigurationError( + "Invalid turbo auto settings: load thresholds must be between 0 % and 100 % with high > low" + .to_string(), + )); + } + + // Validate temperature threshold (realistic range for CPU temps in Celsius) + // TODO: different CPUs have different temperature thresholds. While 110 is a good example + // "extreme" case, the upper barrier might be *lower* for some devices. We'll want to fix + // this eventually, or make it configurable. + if settings.temp_threshold_high <= 0.0 || settings.temp_threshold_high > 110.0 { + return Err(EngineError::ConfigurationError( + "Invalid turbo auto settings: temperature threshold must be between 0°C and 110°C" + .to_string(), + )); + } + + Ok(()) +} diff --git a/src/fs.rs b/src/fs.rs index 852cdf4..3192e4d 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,80 +1,65 @@ -use std::{ - error, - fs, - io, - path::Path, - str, -}; +use std::{error, fs, io, path::Path, str}; use anyhow::Context; pub fn exists(path: impl AsRef) -> bool { - let path = path.as_ref(); + let path = path.as_ref(); - path.exists() + path.exists() } pub fn read_dir(path: impl AsRef) -> anyhow::Result> { - let path = path.as_ref(); + let path = path.as_ref(); - match fs::read_dir(path) { - Ok(entries) => Ok(Some(entries)), + match fs::read_dir(path) { + Ok(entries) => Ok(Some(entries)), - Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), - Err(error) => { - Err(error).context(format!( - "failed to read directory '{path}'", - path = path.display() - )) - }, - } + Err(error) => Err(error).context(format!( + "failed to read directory '{path}'", + path = path.display() + )), + } } pub fn read(path: impl AsRef) -> anyhow::Result> { - let path = path.as_ref(); + let path = path.as_ref(); - match fs::read_to_string(path) { - Ok(string) => Ok(Some(string.trim().to_owned())), + match fs::read_to_string(path) { + Ok(string) => Ok(Some(string.trim().to_owned())), - Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), - Err(error) => { - Err(error) - .context(format!("failed to read '{path}", path = path.display())) - }, - } + Err(error) => Err(error).context(format!("failed to read '{path}", path = path.display())), + } } -pub fn read_n( - path: impl AsRef, -) -> anyhow::Result> +pub fn read_n(path: impl AsRef) -> anyhow::Result> where - N::Err: error::Error + Send + Sync + 'static, + N::Err: error::Error + Send + Sync + 'static, { - let path = path.as_ref(); + let path = path.as_ref(); - match read(path)? { - Some(content) => { - Ok(Some(content.trim().parse().with_context(|| { - format!( - "failed to parse contents of '{path}' as a unsigned number", - path = path.display(), - ) - })?)) - }, + match read(path)? { + Some(content) => Ok(Some(content.trim().parse().with_context(|| { + format!( + "failed to parse contents of '{path}' as a unsigned number", + path = path.display(), + ) + })?)), - None => Ok(None), - } + None => Ok(None), + } } pub fn write(path: impl AsRef, value: &str) -> anyhow::Result<()> { - let path = path.as_ref(); + let path = path.as_ref(); - fs::write(path, value).with_context(|| { - format!( - "failed to write '{value}' to '{path}'", - path = path.display(), - ) - }) + fs::write(path, value).with_context(|| { + format!( + "failed to write '{value}' to '{path}'", + path = path.display(), + ) + }) } diff --git a/src/main.rs b/src/main.rs index 0ec780c..bc25436 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,149 +10,143 @@ mod daemon; // mod engine; // mod monitor; -use std::{ - fmt::Write as _, - io, - io::Write as _, - path::PathBuf, - process, -}; - use anyhow::Context; use clap::Parser as _; +use std::fmt::Write as _; +use std::io::Write as _; +use std::path::PathBuf; +use std::{io, process}; use yansi::Paint as _; #[derive(clap::Parser, Debug)] #[clap(author, version, about)] struct Cli { - #[clap(subcommand)] - command: Command, + #[clap(subcommand)] + command: Command, } #[derive(clap::Parser, Debug)] #[clap(multicall = true)] enum Command { - /// Watt daemon. - Watt { - #[command(flatten)] - verbosity: clap_verbosity_flag::Verbosity, + /// Watt daemon. + Watt { + #[command(flatten)] + verbosity: clap_verbosity_flag::Verbosity, - /// The daemon config path. - #[arg(long, env = "WATT_CONFIG")] - config: Option, - }, + /// The daemon config path. + #[arg(long, env = "WATT_CONFIG")] + config: Option, + }, - /// CPU metadata and modification utility. - Cpu { - #[command(flatten)] - verbosity: clap_verbosity_flag::Verbosity, + /// CPU metadata and modification utility. + Cpu { + #[command(flatten)] + verbosity: clap_verbosity_flag::Verbosity, - #[clap(subcommand)] - command: CpuCommand, - }, + #[clap(subcommand)] + command: CpuCommand, + }, - /// Power supply metadata and modification utility. - Power { - #[command(flatten)] - verbosity: clap_verbosity_flag::Verbosity, + /// Power supply metadata and modification utility. + Power { + #[command(flatten)] + verbosity: clap_verbosity_flag::Verbosity, - #[clap(subcommand)] - command: PowerCommand, - }, + #[clap(subcommand)] + command: PowerCommand, + }, } #[derive(clap::Parser, Debug)] enum CpuCommand { - /// Modify CPU attributes. - Set(config::CpuDelta), + /// Modify CPU attributes. + Set(config::CpuDelta), } #[derive(clap::Parser, Debug)] enum PowerCommand { - /// Modify power supply attributes. - Set(config::PowerDelta), + /// Modify power supply attributes. + Set(config::PowerDelta), } fn real_main() -> anyhow::Result<()> { - let cli = Cli::parse(); + let cli = Cli::parse(); - yansi::whenever(yansi::Condition::TTY_AND_COLOR); + yansi::whenever(yansi::Condition::TTY_AND_COLOR); - let (Command::Watt { verbosity, .. } - | Command::Cpu { verbosity, .. } - | Command::Power { verbosity, .. }) = cli.command; + let (Command::Watt { verbosity, .. } + | Command::Cpu { verbosity, .. } + | Command::Power { verbosity, .. }) = cli.command; - env_logger::Builder::new() - .filter_level(verbosity.log_level_filter()) - .format_timestamp(None) - .format_module_path(false) - .init(); + env_logger::Builder::new() + .filter_level(verbosity.log_level_filter()) + .format_timestamp(None) + .format_module_path(false) + .init(); - match cli.command { - Command::Watt { config, .. } => { - let config = config::DaemonConfig::load_from(config.as_deref()) - .context("failed to load daemon config")?; + match cli.command { + Command::Watt { config, .. } => { + let config = config::DaemonConfig::load_from(config.as_deref()) + .context("failed to load daemon config")?; - daemon::run(config) - }, + daemon::run(config) + } - Command::Cpu { - command: CpuCommand::Set(delta), - .. - } => delta.apply(), + Command::Cpu { + command: CpuCommand::Set(delta), + .. + } => delta.apply(), - Command::Power { - command: PowerCommand::Set(delta), - .. - } => delta.apply(), - } + Command::Power { + command: PowerCommand::Set(delta), + .. + } => delta.apply(), + } } fn main() { - let Err(error) = real_main() else { - return; - }; - - let mut err = io::stderr(); - - let mut message = String::new(); - let mut chain = error.chain().rev().peekable(); - - while let Some(error) = chain.next() { - let _ = write!( - err, - "{header} ", - header = if chain.peek().is_none() { - "error:" - } else { - "cause:" - } - .red() - .bold(), - ); - - String::clear(&mut message); - let _ = write!(message, "{error}"); - - let mut chars = message.char_indices(); - - let _ = match (chars.next(), chars.next()) { - (Some((_, first)), Some((second_start, second))) - if second.is_lowercase() => - { - writeln!( - err, - "{first_lowercase}{rest}", - first_lowercase = first.to_lowercase(), - rest = &message[second_start..], - ) - }, - - _ => { - writeln!(err, "{message}") - }, + let Err(error) = real_main() else { + return; }; - } - process::exit(1); + let mut err = io::stderr(); + + let mut message = String::new(); + let mut chain = error.chain().rev().peekable(); + + while let Some(error) = chain.next() { + let _ = write!( + err, + "{header} ", + header = if chain.peek().is_none() { + "error:" + } else { + "cause:" + } + .red() + .bold(), + ); + + String::clear(&mut message); + let _ = write!(message, "{error}"); + + let mut chars = message.char_indices(); + + let _ = match (chars.next(), chars.next()) { + (Some((_, first)), Some((second_start, second))) if second.is_lowercase() => { + writeln!( + err, + "{first_lowercase}{rest}", + first_lowercase = first.to_lowercase(), + rest = &message[second_start..], + ) + } + + _ => { + writeln!(err, "{message}") + } + }; + } + + process::exit(1); } diff --git a/src/monitor.rs b/src/monitor.rs new file mode 100644 index 0000000..e4ff659 --- /dev/null +++ b/src/monitor.rs @@ -0,0 +1,24 @@ +// Try /sys/devices/platform paths for thermal zones as a last resort +// if temperature_celsius.is_none() { +// if let Ok(thermal_zones) = fs::read_dir("/sys/devices/virtual/thermal") { +// for entry in thermal_zones.flatten() { +// let zone_path = entry.path(); +// let name = entry.file_name().into_string().unwrap_or_default(); + +// if name.starts_with("thermal_zone") { +// // Try to match by type +// if let Ok(zone_type) = read_sysfs_file_trimmed(zone_path.join("type")) { +// if zone_type.contains("cpu") +// || zone_type.contains("x86") +// || zone_type.contains("core") +// { +// if let Ok(temp_mc) = read_sysfs_value::(zone_path.join("temp")) { +// temperature_celsius = Some(temp_mc as f32 / 1000.0); +// break; +// } +// } +// } +// } +// } +// } +// } diff --git a/src/power_supply.rs b/src/power_supply.rs index 3bd03f2..2155c29 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -1,413 +1,370 @@ -use std::{ - fmt, - path::{ - Path, - PathBuf, - }, -}; - -use anyhow::{ - Context, - anyhow, - bail, -}; +use anyhow::{Context, anyhow, bail}; use yansi::Paint as _; +use std::{ + fmt, + path::{Path, PathBuf}, +}; + use crate::fs; /// Represents a pattern of path suffixes used to control charge thresholds /// for different device vendors. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PowerSupplyThresholdConfig { - pub manufacturer: &'static str, - pub path_start: &'static str, - pub path_end: &'static str, + pub manufacturer: &'static str, + pub path_start: &'static str, + pub path_end: &'static str, } /// Power supply threshold configs. const POWER_SUPPLY_THRESHOLD_CONFIGS: &[PowerSupplyThresholdConfig] = &[ - PowerSupplyThresholdConfig { - manufacturer: "Standard", - path_start: "charge_control_start_threshold", - path_end: "charge_control_end_threshold", - }, - PowerSupplyThresholdConfig { - manufacturer: "ASUS", - path_start: "charge_control_start_percentage", - path_end: "charge_control_end_percentage", - }, - // Combine Huawei and ThinkPad since they use identical paths. - PowerSupplyThresholdConfig { - manufacturer: "ThinkPad/Huawei", - path_start: "charge_start_threshold", - path_end: "charge_stop_threshold", - }, - // Framework laptop support. - PowerSupplyThresholdConfig { - manufacturer: "Framework", - path_start: "charge_behaviour_start_threshold", - path_end: "charge_behaviour_end_threshold", - }, + PowerSupplyThresholdConfig { + manufacturer: "Standard", + path_start: "charge_control_start_threshold", + path_end: "charge_control_end_threshold", + }, + PowerSupplyThresholdConfig { + manufacturer: "ASUS", + path_start: "charge_control_start_percentage", + path_end: "charge_control_end_percentage", + }, + // Combine Huawei and ThinkPad since they use identical paths. + PowerSupplyThresholdConfig { + manufacturer: "ThinkPad/Huawei", + path_start: "charge_start_threshold", + path_end: "charge_stop_threshold", + }, + // Framework laptop support. + PowerSupplyThresholdConfig { + manufacturer: "Framework", + path_start: "charge_behaviour_start_threshold", + path_end: "charge_behaviour_end_threshold", + }, ]; /// Represents a power supply that supports charge threshold control. #[derive(Debug, Clone, PartialEq)] pub struct PowerSupply { - pub name: String, - pub path: PathBuf, + pub name: String, + pub path: PathBuf, - pub type_: String, - pub is_from_peripheral: bool, + pub type_: String, + pub is_from_peripheral: bool, - pub charge_state: Option, - pub charge_percent: Option, + pub charge_state: Option, + pub charge_percent: Option, - pub charge_threshold_start: f64, - pub charge_threshold_end: f64, + pub charge_threshold_start: f64, + pub charge_threshold_end: f64, - pub drain_rate_watts: Option, + pub drain_rate_watts: Option, - pub threshold_config: Option, + pub threshold_config: Option, } impl PowerSupply { - pub fn is_ac(&self) -> bool { - !self.is_from_peripheral - && matches!( - &*self.type_, - "Mains" | "USB_PD_DRP" | "USB_PD" | "USB_DCP" | "USB_CDP" | "USB_ACA" - ) - || self.type_.starts_with("AC") - || self.type_.contains("ACAD") - || self.type_.contains("ADP") - } + pub fn is_ac(&self) -> bool { + !self.is_from_peripheral + && matches!( + &*self.type_, + "Mains" | "USB_PD_DRP" | "USB_PD" | "USB_DCP" | "USB_CDP" | "USB_ACA" + ) + || self.type_.starts_with("AC") + || self.type_.contains("ACAD") + || self.type_.contains("ADP") + } } impl fmt::Display for PowerSupply { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "power supply '{name}'", name = self.name.yellow())?; + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "power supply '{name}'", name = self.name.yellow())?; - if let Some(config) = self.threshold_config.as_ref() { - write!( - f, - " from manufacturer '{manufacturer}'", - manufacturer = config.manufacturer.green(), - )?; + if let Some(config) = self.threshold_config.as_ref() { + write!( + f, + " from manufacturer '{manufacturer}'", + manufacturer = config.manufacturer.green(), + )?; + } + + Ok(()) } - - Ok(()) - } } const POWER_SUPPLY_PATH: &str = "/sys/class/power_supply"; impl PowerSupply { - pub fn from_name(name: String) -> anyhow::Result { - let mut power_supply = Self { - path: Path::new(POWER_SUPPLY_PATH).join(&name), - name, - type_: String::new(), + pub fn from_name(name: String) -> anyhow::Result { + let mut power_supply = Self { + path: Path::new(POWER_SUPPLY_PATH).join(&name), + name, + type_: String::new(), - charge_state: None, - charge_percent: None, + charge_state: None, + charge_percent: None, - charge_threshold_start: 0.0, - charge_threshold_end: 1.0, + charge_threshold_start: 0.0, + charge_threshold_end: 1.0, - drain_rate_watts: None, + drain_rate_watts: None, - is_from_peripheral: false, + is_from_peripheral: false, - threshold_config: None, - }; - - power_supply.rescan()?; - - Ok(power_supply) - } - - pub fn from_path(path: PathBuf) -> anyhow::Result { - let mut power_supply = PowerSupply { - name: path - .file_name() - .with_context(|| { - format!("failed to get file name of '{path}'", path = path.display(),) - })? - .to_string_lossy() - .to_string(), - - path, - type_: String::new(), - - charge_state: None, - charge_percent: None, - - charge_threshold_start: 0.0, - charge_threshold_end: 1.0, - - drain_rate_watts: None, - - is_from_peripheral: false, - - threshold_config: None, - }; - - power_supply.rescan()?; - - Ok(power_supply) - } - - pub fn all() -> anyhow::Result> { - let mut power_supplies = Vec::new(); - - for entry in fs::read_dir(POWER_SUPPLY_PATH) - .context("failed to read power supply entries")? - .with_context(|| { - format!("'{POWER_SUPPLY_PATH}' doesn't exist, are you on linux?") - })? - { - let entry = match entry { - Ok(entry) => entry, - - Err(error) => { - log::warn!("failed to read power supply entry: {error}"); - continue; - }, - }; - - power_supplies.push(PowerSupply::from_path(entry.path())?); - } - - Ok(power_supplies) - } - - pub fn rescan(&mut self) -> anyhow::Result<()> { - if !self.path.exists() { - bail!("{self} does not exist"); - } - - self.type_ = { - let type_path = self.path.join("type"); - - fs::read(&type_path) - .with_context(|| { - format!("failed to read '{path}'", path = type_path.display()) - })? - .with_context(|| { - format!("'{path}' doesn't exist", path = type_path.display()) - })? - }; - - self.is_from_peripheral = 'is_from_peripheral: { - let name_lower = self.name.to_lowercase(); - - // Common peripheral battery names. - if name_lower.contains("mouse") - || name_lower.contains("keyboard") - || name_lower.contains("trackpad") - || name_lower.contains("gamepad") - || name_lower.contains("controller") - || name_lower.contains("headset") - || name_lower.contains("headphone") - { - break 'is_from_peripheral true; - } - - // Small capacity batteries are likely not laptop batteries. - if let Some(energy_full) = - fs::read_n::(self.path.join("energy_full")).with_context(|| { - format!("failed to read the max charge {self} can hold") - })? - { - // Most laptop batteries are at least 20,000,000 µWh (20 Wh). - // Peripheral batteries are typically much smaller. - if energy_full < 10_000_000 { - // 10 Wh in µWh. - break 'is_from_peripheral true; - } - } - // Check for model name that indicates a peripheral - if let Some(model) = fs::read(self.path.join("model_name")) - .with_context(|| format!("failed to read the model name of {self}"))? - { - if model.contains("bluetooth") || model.contains("wireless") { - break 'is_from_peripheral true; - } - } - - false - }; - - if self.type_ == "Battery" { - self.charge_state = fs::read(self.path.join("status")) - .with_context(|| format!("failed to read {self} charge status"))?; - - self.charge_percent = fs::read_n::(self.path.join("capacity")) - .with_context(|| format!("failed to read {self} charge percent"))? - .map(|percent| percent as f64 / 100.0); - - self.charge_threshold_start = - fs::read_n::(self.path.join("charge_control_start_threshold")) - .with_context(|| { - format!("failed to read {self} charge threshold start") - })? - .map_or(0.0, |percent| percent as f64 / 100.0); - - self.charge_threshold_end = - fs::read_n::(self.path.join("charge_control_end_threshold")) - .with_context(|| { - format!("failed to read {self} charge threshold end") - })? - .map_or(100.0, |percent| percent as f64 / 100.0); - - self.drain_rate_watts = - match fs::read_n::(self.path.join("power_now")) - .with_context(|| format!("failed to read {self} power drain"))? - { - Some(drain) => Some(drain as f64), - - None => { - let current_ua = - fs::read_n::(self.path.join("current_now")) - .with_context(|| format!("failed to read {self} current"))?; - - let voltage_uv = - fs::read_n::(self.path.join("voltage_now")) - .with_context(|| format!("failed to read {self} voltage"))?; - - current_ua.zip(voltage_uv).map(|(current, voltage)| { - // Power (W) = Voltage (V) * Current (A) - // (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W - current as f64 * voltage as f64 / 1e12 - }) - }, + threshold_config: None, }; - self.threshold_config = POWER_SUPPLY_THRESHOLD_CONFIGS - .iter() - .find(|config| { - self.path.join(config.path_start).exists() - && self.path.join(config.path_end).exists() - }) - .copied(); + power_supply.rescan()?; + + Ok(power_supply) } - Ok(()) - } + pub fn from_path(path: PathBuf) -> anyhow::Result { + let mut power_supply = PowerSupply { + name: path + .file_name() + .with_context(|| { + format!("failed to get file name of '{path}'", path = path.display(),) + })? + .to_string_lossy() + .to_string(), - pub fn charge_threshold_path_start(&self) -> Option { - self - .threshold_config - .map(|config| self.path.join(config.path_start)) - } + path, + type_: String::new(), - pub fn charge_threshold_path_end(&self) -> Option { - self - .threshold_config - .map(|config| self.path.join(config.path_end)) - } + charge_state: None, + charge_percent: None, - pub fn set_charge_threshold_start( - &mut self, - charge_threshold_start: f64, - ) -> anyhow::Result<()> { - fs::write( - &self.charge_threshold_path_start().ok_or_else(|| { - anyhow!( - "power supply '{name}' does not support changing charge threshold \ - levels", - name = self.name, - ) - })?, - &((charge_threshold_start * 100.0) as u8).to_string(), - ) - .with_context(|| { - format!("failed to set charge threshold start for {self}") - })?; + charge_threshold_start: 0.0, + charge_threshold_end: 1.0, - self.charge_threshold_start = charge_threshold_start; + drain_rate_watts: None, - log::info!( - "set battery threshold start for {self} to {charge_threshold_start}%" - ); + is_from_peripheral: false, - Ok(()) - } + threshold_config: None, + }; - pub fn set_charge_threshold_end( - &mut self, - charge_threshold_end: f64, - ) -> anyhow::Result<()> { - fs::write( - &self.charge_threshold_path_end().ok_or_else(|| { - anyhow!( - "power supply '{name}' does not support changing charge threshold \ - levels", - name = self.name, - ) - })?, - &((charge_threshold_end * 100.0) as u8).to_string(), - ) - .with_context(|| { - format!("failed to set charge threshold end for {self}") - })?; + power_supply.rescan()?; - self.charge_threshold_end = charge_threshold_end; - - log::info!( - "set battery threshold end for {self} to {charge_threshold_end}%" - ); - - Ok(()) - } - - pub fn get_available_platform_profiles() -> anyhow::Result> { - let path = "/sys/firmware/acpi/platform_profile_choices"; - - let Some(content) = fs::read(path) - .context("failed to read available ACPI platform profiles")? - else { - return Ok(Vec::new()); - }; - - Ok( - content - .split_whitespace() - .map(ToString::to_string) - .collect(), - ) - } - - /// Sets the platform profile. - /// This changes the system performance, temperature, fan, and other hardware - /// related characteristics. - /// - /// Also see [`The Kernel docs`] for this. - /// - /// [`The Kernel docs`]: - pub fn set_platform_profile(profile: &str) -> anyhow::Result<()> { - let profiles = Self::get_available_platform_profiles()?; - - if !profiles - .iter() - .any(|avail_profile| avail_profile == profile) - { - bail!( - "profile '{profile}' is not available for system. valid profiles: \ - {profiles}", - profiles = profiles.join(", "), - ); + Ok(power_supply) } - fs::write("/sys/firmware/acpi/platform_profile", profile).context( - "this probably means that your system does not support changing ACPI \ - profiles", - ) - } + pub fn all() -> anyhow::Result> { + let mut power_supplies = Vec::new(); - pub fn platform_profile() -> anyhow::Result { - fs::read("/sys/firmware/acpi/platform_profile") - .context("failed to read platform profile")? - .context("failed to find platform profile") - } + for entry in fs::read_dir(POWER_SUPPLY_PATH) + .with_context(|| format!("failed to read '{POWER_SUPPLY_PATH}'"))? + .with_context(|| format!("'{POWER_SUPPLY_PATH}' doesn't exist, are you on linux?"))? + { + let entry = match entry { + Ok(entry) => entry, + + Err(error) => { + log::warn!("failed to read power supply entry: {error}"); + continue; + } + }; + + power_supplies.push(PowerSupply::from_path(entry.path())?); + } + + Ok(power_supplies) + } + + pub fn rescan(&mut self) -> anyhow::Result<()> { + if !self.path.exists() { + bail!("{self} does not exist"); + } + + self.type_ = { + let type_path = self.path.join("type"); + + fs::read(&type_path) + .with_context(|| format!("failed to read '{path}'", path = type_path.display()))? + .with_context(|| format!("'{path}' doesn't exist", path = type_path.display()))? + }; + + self.is_from_peripheral = 'is_from_peripheral: { + let name_lower = self.name.to_lowercase(); + + // Common peripheral battery names. + if name_lower.contains("mouse") + || name_lower.contains("keyboard") + || name_lower.contains("trackpad") + || name_lower.contains("gamepad") + || name_lower.contains("controller") + || name_lower.contains("headset") + || name_lower.contains("headphone") + { + break 'is_from_peripheral true; + } + + // Small capacity batteries are likely not laptop batteries. + if let Some(energy_full) = fs::read_n::(self.path.join("energy_full")) + .with_context(|| format!("failed to read the max charge {self} can hold"))? + { + // Most laptop batteries are at least 20,000,000 µWh (20 Wh). + // Peripheral batteries are typically much smaller. + if energy_full < 10_000_000 { + // 10 Wh in µWh. + break 'is_from_peripheral true; + } + } + // Check for model name that indicates a peripheral + if let Some(model) = fs::read(self.path.join("model_name")) + .with_context(|| format!("failed to read the model name of {self}"))? + { + if model.contains("bluetooth") || model.contains("wireless") { + break 'is_from_peripheral true; + } + } + + false + }; + + if self.type_ == "Battery" { + self.charge_state = fs::read(self.path.join("status")) + .with_context(|| format!("failed to read {self} charge status"))?; + + self.charge_percent = fs::read_n::(self.path.join("capacity")) + .with_context(|| format!("failed to read {self} charge percent"))? + .map(|percent| percent as f64 / 100.0); + + self.charge_threshold_start = + fs::read_n::(self.path.join("charge_control_start_threshold")) + .with_context(|| format!("failed to read {self} charge threshold start"))? + .map_or(0.0, |percent| percent as f64 / 100.0); + + self.charge_threshold_end = + fs::read_n::(self.path.join("charge_control_end_threshold")) + .with_context(|| format!("failed to read {self} charge threshold end"))? + .map_or(100.0, |percent| percent as f64 / 100.0); + + self.drain_rate_watts = match fs::read_n::(self.path.join("power_now")) + .with_context(|| format!("failed to read {self} power drain"))? + { + Some(drain) => Some(drain as f64), + + None => { + let current_ua = fs::read_n::(self.path.join("current_now")) + .with_context(|| format!("failed to read {self} current"))?; + + let voltage_uv = fs::read_n::(self.path.join("voltage_now")) + .with_context(|| format!("failed to read {self} voltage"))?; + + current_ua.zip(voltage_uv).map(|(current, voltage)| { + // Power (W) = Voltage (V) * Current (A) + // (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W + current as f64 * voltage as f64 / 1e12 + }) + } + }; + + self.threshold_config = POWER_SUPPLY_THRESHOLD_CONFIGS + .iter() + .find(|config| { + self.path.join(config.path_start).exists() + && self.path.join(config.path_end).exists() + }) + .copied(); + } + + Ok(()) + } + + pub fn charge_threshold_path_start(&self) -> Option { + self.threshold_config + .map(|config| self.path.join(config.path_start)) + } + + pub fn charge_threshold_path_end(&self) -> Option { + self.threshold_config + .map(|config| self.path.join(config.path_end)) + } + + pub fn set_charge_threshold_start( + &mut self, + charge_threshold_start: f64, + ) -> anyhow::Result<()> { + fs::write( + &self.charge_threshold_path_start().ok_or_else(|| { + anyhow!( + "power supply '{name}' does not support changing charge threshold levels", + name = self.name, + ) + })?, + &((charge_threshold_start * 100.0) as u8).to_string(), + ) + .with_context(|| format!("failed to set charge threshold start for {self}"))?; + + self.charge_threshold_start = charge_threshold_start; + + log::info!("set battery threshold start for {self} to {charge_threshold_start}%"); + + Ok(()) + } + + pub fn set_charge_threshold_end(&mut self, charge_threshold_end: f64) -> anyhow::Result<()> { + fs::write( + &self.charge_threshold_path_end().ok_or_else(|| { + anyhow!( + "power supply '{name}' does not support changing charge threshold levels", + name = self.name, + ) + })?, + &((charge_threshold_end * 100.0) as u8).to_string(), + ) + .with_context(|| format!("failed to set charge threshold end for {self}"))?; + + self.charge_threshold_end = charge_threshold_end; + + log::info!("set battery threshold end for {self} to {charge_threshold_end}%"); + + Ok(()) + } + + pub fn get_available_platform_profiles() -> anyhow::Result> { + let path = "/sys/firmware/acpi/platform_profile_choices"; + + let Some(content) = + fs::read(path).context("failed to read available ACPI platform profiles")? + else { + return Ok(Vec::new()); + }; + + Ok(content + .split_whitespace() + .map(ToString::to_string) + .collect()) + } + + /// Sets the platform profile. + /// This changes the system performance, temperature, fan, and other hardware replated characteristics. + /// + /// Also see [`The Kernel docs`] for this. + /// + /// [`The Kernel docs`]: + pub fn set_platform_profile(profile: &str) -> anyhow::Result<()> { + let profiles = Self::get_available_platform_profiles()?; + + if !profiles + .iter() + .any(|avail_profile| avail_profile == profile) + { + bail!( + "profile '{profile}' is not available for system. valid profiles: {profiles}", + profiles = profiles.join(", "), + ); + } + + fs::write("/sys/firmware/acpi/platform_profile", profile) + .context("this probably means that your system does not support changing ACPI profiles") + } + + pub fn platform_profile() -> anyhow::Result { + fs::read("/sys/firmware/acpi/platform_profile") + .context("failed to read platform profile")? + .context("failed to find platform profile") + } } diff --git a/src/system.rs b/src/system.rs index 768142c..322e0e4 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,413 +1,322 @@ -use std::{ - collections::HashMap, - path::Path, - time::Instant, -}; +use std::{collections::HashMap, path::Path, time::Instant}; -use anyhow::{ - Context, - bail, -}; +use anyhow::{Context, bail}; -use crate::{ - cpu, - fs, - power_supply, -}; +use crate::{cpu, fs, power_supply}; #[derive(Debug)] pub struct System { - pub is_ac: bool, + pub is_ac: bool, - pub load_average_1min: f64, - pub load_average_5min: f64, - pub load_average_15min: f64, + pub load_average_1min: f64, + pub load_average_5min: f64, + pub load_average_15min: f64, - pub cpus: Vec, - pub cpu_temperatures: HashMap, + pub cpus: Vec, + pub cpu_temperatures: HashMap, - pub power_supplies: Vec, + pub power_supplies: Vec, } impl System { - pub fn new() -> anyhow::Result { - let mut system = Self { - is_ac: false, + pub fn new() -> anyhow::Result { + let mut system = Self { + is_ac: false, - cpus: Vec::new(), - cpu_temperatures: HashMap::new(), + cpus: Vec::new(), + cpu_temperatures: HashMap::new(), - power_supplies: Vec::new(), + power_supplies: Vec::new(), - load_average_1min: 0.0, - load_average_5min: 0.0, - load_average_15min: 0.0, - }; - - system.rescan()?; - - Ok(system) - } - - pub fn rescan(&mut self) -> anyhow::Result<()> { - log::debug!("rescanning view of system hardware..."); - - { - let start = Instant::now(); - self.cpus = cpu::Cpu::all().context("failed to scan CPUs")?; - log::debug!( - "rescanned all CPUs in {millis}ms", - millis = start.elapsed().as_millis(), - ); - } - - { - let start = Instant::now(); - self.power_supplies = power_supply::PowerSupply::all() - .context("failed to scan power supplies")?; - log::debug!( - "rescanned all power supplies in {millis}ms", - millis = start.elapsed().as_millis(), - ); - } - - self.is_ac = self - .power_supplies - .iter() - .any(|power_supply| power_supply.is_ac()) - || { - log::debug!( - "checking whether if this device is a desktop to determine if it is \ - AC as no power supplies are" - ); - - let start = Instant::now(); - let is_desktop = self.is_desktop()?; - log::debug!( - "checked if is a desktop in {millis}ms", - millis = start.elapsed().as_millis(), - ); - - log::debug!( - "scan result: {elaborate}", - elaborate = if is_desktop { - "is a desktop, therefore is AC" - } else { - "not a desktop, and not AC" - }, - ); - - is_desktop - }; - - { - let start = Instant::now(); - self.rescan_load_average()?; - log::debug!( - "rescanned load average in {millis}ms", - millis = start.elapsed().as_millis(), - ); - } - - { - let start = Instant::now(); - self.rescan_temperatures()?; - log::debug!( - "rescanned temperatures in {millis}ms", - millis = start.elapsed().as_millis(), - ); - } - - Ok(()) - } - - fn rescan_temperatures(&mut self) -> anyhow::Result<()> { - const PATH: &str = "/sys/class/hwmon"; - - let mut temperatures = HashMap::new(); - - for entry in fs::read_dir(PATH) - .context("failed to read hardware information")? - .with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))? - { - let entry = - entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; - - let entry_path = entry.path(); - - let Some(name) = - fs::read(entry_path.join("name")).with_context(|| { - format!( - "failed to read name of hardware entry at '{path}'", - path = entry_path.display(), - ) - })? - else { - continue; - }; - - match &*name { - // TODO: 'zenergy' can also report those stats, I think? - "coretemp" | "k10temp" | "zenpower" | "amdgpu" => { - Self::get_temperatures(&entry_path, &mut temperatures)?; - }, - - // Other CPU temperature drivers. - _ if name.contains("cpu") || name.contains("temp") => { - Self::get_temperatures(&entry_path, &mut temperatures)?; - }, - - _ => {}, - } - } - - if temperatures.is_empty() { - const PATH: &str = "/sys/devices/virtual/thermal"; - - log::debug!( - "failed to get CPU temperature information by using hwmon, falling \ - back to '{PATH}'" - ); - - let Some(thermal_zones) = - fs::read_dir(PATH).context("failed to read thermal information")? - else { - return Ok(()); - }; - - let mut counter = 0; - - for entry in thermal_zones { - let entry = - entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; - - let entry_path = entry.path(); - - let entry_name = entry.file_name(); - let entry_name = entry_name.to_string_lossy(); - - if !entry_name.starts_with("thermal_zone") { - continue; - } - - let Some(entry_type) = - fs::read(entry_path.join("type")).with_context(|| { - format!( - "failed to read type of zone at '{path}'", - path = entry_path.display(), - ) - })? - else { - continue; + load_average_1min: 0.0, + load_average_5min: 0.0, + load_average_15min: 0.0, }; - if !entry_type.contains("cpu") - && !entry_type.contains("x86") - && !entry_type.contains("core") + system.rescan()?; + + Ok(system) + } + + pub fn rescan(&mut self) -> anyhow::Result<()> { + log::debug!("rescanning view of system hardware..."); + { - continue; + let start = Instant::now(); + self.cpus = cpu::Cpu::all().context("failed to scan CPUs")?; + log::debug!( + "rescanned all CPUs in {millis}ms", + millis = start.elapsed().as_millis(), + ); } - let Some(temperature_mc) = fs::read_n::(entry_path.join("temp")) - .with_context(|| { - format!( - "failed to read temperature of zone at '{path}'", - path = entry_path.display(), - ) - })? - else { - continue; - }; + { + let start = Instant::now(); + self.power_supplies = + power_supply::PowerSupply::all().context("failed to scan power supplies")?; + log::debug!( + "rescanned all power supplies in {millis}ms", + millis = start.elapsed().as_millis(), + ); + } - // Magic value to see that it is from the thermal zones. - temperatures.insert(777 + counter, temperature_mc as f64 / 1000.0); - counter += 1; - } + self.is_ac = self + .power_supplies + .iter() + .any(|power_supply| power_supply.is_ac()) + || { + log::debug!( + "checking whether if this device is a desktop to determine if it is AC as no power supplies are" + ); + + let start = Instant::now(); + let is_desktop = self.is_desktop()?; + log::debug!( + "checked if is a desktop in {millis}ms", + millis = start.elapsed().as_millis(), + ); + + log::debug!( + "scan result: {elaborate}", + elaborate = if is_desktop { + "is a desktop, therefore is AC" + } else { + "not a desktop, and not AC" + }, + ); + + is_desktop + }; + + { + let start = Instant::now(); + self.rescan_load_average()?; + log::debug!( + "rescanned load average in {millis}ms", + millis = start.elapsed().as_millis(), + ); + } + + { + let start = Instant::now(); + self.rescan_temperatures()?; + log::debug!( + "rescanned temperatures in {millis}ms", + millis = start.elapsed().as_millis(), + ); + } + + Ok(()) } - self.cpu_temperatures = temperatures; + fn rescan_temperatures(&mut self) -> anyhow::Result<()> { + const PATH: &str = "/sys/class/hwmon"; - Ok(()) - } + let mut temperatures = HashMap::new(); - fn get_temperatures( - device_path: &Path, - temperatures: &mut HashMap, - ) -> anyhow::Result<()> { - // Increased range to handle systems with many sensors. - for i in 1..=96 { - let label_path = device_path.join(format!("temp{i}_label")); - let input_path = device_path.join(format!("temp{i}_input")); + for entry in fs::read_dir(PATH) + .with_context(|| format!("failed to read hardware information from '{PATH}'"))? + .with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))? + { + let entry = entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; - if !label_path.exists() || !input_path.exists() { - log::debug!( - "{label_path} or {input_path} doesn't exist, skipping temp label", - label_path = label_path.display(), - input_path = input_path.display(), - ); - continue; - } + let entry_path = entry.path(); - log::debug!( - "{label_path} or {input_path} exists, scanning temp label...", - label_path = label_path.display(), - input_path = input_path.display(), - ); + let Some(name) = fs::read(entry_path.join("name")).with_context(|| { + format!( + "failed to read name of hardware entry at '{path}'", + path = entry_path.display(), + ) + })? + else { + continue; + }; - let Some(label) = fs::read(&label_path).with_context(|| { - format!( - "failed to read hardware hardware device label from '{path}'", - path = label_path.display(), - ) - })? - else { - continue; - }; - log::debug!("label content: {label}"); + match &*name { + // TODO: 'zenergy' can also report those stats, I think? + "coretemp" | "k10temp" | "zenpower" | "amdgpu" => { + Self::get_temperatures(&entry_path, &mut temperatures)?; + } - // Match various common label formats: - // "Core X", "core X", "Core-X", "CPU Core X", etc. - let number = label - .trim_start_matches("cpu") - .trim_start_matches("CPU") - .trim_start() - .trim_start_matches("core") - .trim_start_matches("Core") - .trim_start() - .trim_start_matches("Tctl") - .trim_start_matches("Tdie") - .trim_start_matches("Tccd") - .trim_start_matches(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']) - .trim_start() - .trim_start_matches("-") - .trim(); + // Other CPU temperature drivers. + _ if name.contains("cpu") || name.contains("temp") => { + Self::get_temperatures(&entry_path, &mut temperatures)?; + } - log::debug!( - "stripped 'Core' or similar identifier prefix of label content: \ - {number}" - ); + _ => {} + } + } - let Ok(number) = number.parse::() else { - log::debug!("stripped content not a valid number, skipping"); - continue; - }; - log::debug!( - "stripped content is a valid number, taking it as the core number" - ); - log::debug!( + self.cpu_temperatures = temperatures; + + Ok(()) + } + + fn get_temperatures( + device_path: &Path, + temperatures: &mut HashMap, + ) -> anyhow::Result<()> { + // Increased range to handle systems with many sensors. + for i in 1..=96 { + let label_path = device_path.join(format!("temp{i}_label")); + let input_path = device_path.join(format!("temp{i}_input")); + + if !label_path.exists() || !input_path.exists() { + log::debug!( + "{label_path} or {input_path} doesn't exist, skipping temp label", + label_path = label_path.display(), + input_path = input_path.display(), + ); + continue; + } + + log::debug!( + "{label_path} or {input_path} exists, scanning temp label...", + label_path = label_path.display(), + input_path = input_path.display(), + ); + + let Some(label) = fs::read(&label_path).with_context(|| { + format!( + "failed to read hardware hardware device label from '{path}'", + path = label_path.display(), + ) + })? + else { + continue; + }; + log::debug!("label content: {number}"); + + // Match various common label formats: + // "Core X", "core X", "Core-X", "CPU Core X", etc. + let number = label + .trim_start_matches("cpu") + .trim_start_matches("CPU") + .trim_start() + .trim_start_matches("core") + .trim_start_matches("Core") + .trim_start() + .trim_start_matches("Tctl") + .trim_start_matches("Tdie") + .trim_start_matches("Tccd") + .trim_start_matches(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']) + .trim_start() + .trim_start_matches("-") + .trim(); + + log::debug!("stripped 'Core' or similar identifier prefix of label content: {number}"); + + let Ok(number) = number.parse::() else { + log::debug!("stripped content not a valid number, skipping"); + continue; + }; + log::debug!("stripped content is a valid number, taking it as the core number"); + log::debug!( "it is fine if this number doesn't seem accurate due to CPU binning, see a more detailed explanation at: https://rgbcu.be/blog/why-cores" ); - let Some(temperature_mc) = - fs::read_n::(&input_path).with_context(|| { - format!( - "failed to read CPU temperature from '{path}'", - path = input_path.display(), - ) - })? - else { - continue; - }; - log::debug!( - "temperature content: {celsius} celsius", - celsius = temperature_mc as f64 / 1000.0 - ); + let Some(temperature_mc) = fs::read_n::(&input_path).with_context(|| { + format!( + "failed to read CPU temperature from '{path}'", + path = input_path.display(), + ) + })? + else { + continue; + }; + log::debug!( + "temperature content: {celcius} celcius", + celcius = temperature_mc as f64 / 1000.0 + ); - temperatures.insert(number, temperature_mc as f64 / 1000.0); + temperatures.insert(number, temperature_mc as f64 / 1000.0); + } + + Ok(()) } - Ok(()) - } + fn is_desktop(&mut self) -> anyhow::Result { + log::debug!("checking chassis type to determine if we are a desktop"); + if let Some(chassis_type) = + fs::read("/sys/class/dmi/id/chassis_type").context("failed to read chassis type")? + { + // 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower, + // 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In One, + // 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis, + // 31=Convertible Laptop + match chassis_type.trim() { + // Desktop form factors. + "3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => { + log::debug!("chassis is a desktop form factor, short circuting true"); + return Ok(true); + } - fn is_desktop(&mut self) -> anyhow::Result { - log::debug!("checking chassis type to determine if we are a desktop"); - if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type") - .context("failed to read chassis type")? - { - // 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower, - // 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In - // One, 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main - // Server Chassis, 31=Convertible Laptop - match chassis_type.trim() { - // Desktop form factors. - "3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => { - log::debug!("chassis is a desktop form factor, short circuting true"); - return Ok(true); - }, + // Laptop form factors. + "9" | "10" | "14" | "31" => { + log::debug!("chassis is a laptop form factor, short circuting false"); + return Ok(false); + } - // Laptop form factors. - "9" | "10" | "14" | "31" => { - log::debug!("chassis is a laptop form factor, short circuting false"); - return Ok(false); - }, + // Unknown, continue with other checks + _ => log::debug!("unknown chassis type"), + } + } - // Unknown, continue with other checks - _ => log::debug!("unknown chassis type"), - } + // Check battery-specific ACPI paths that laptops typically have + let laptop_acpi_paths = [ + "/sys/class/power_supply/BAT0", + "/sys/class/power_supply/BAT1", + "/proc/acpi/battery", + ]; + + log::debug!("checking existence of ACPI paths"); + for path in laptop_acpi_paths { + if fs::exists(path) { + log::debug!("path '{path}' exists, short circuting false"); + return Ok(false); // Likely a laptop. + } + } + + log::debug!("checking if power saving paths exists"); + // Check CPU power policies, desktops often don't have these + let power_saving_exists = fs::exists("/sys/module/intel_pstate/parameters/no_hwp") + || fs::exists("/sys/devices/system/cpu/cpufreq/conservative"); + + if !power_saving_exists { + log::debug!("power saving paths do not exist, short circuting true"); + return Ok(true); // Likely a desktop. + } + + // Default to assuming desktop if we can't determine. + log::debug!("cannot determine whether if we are a desktop, defaulting to true"); + Ok(true) } - // Check battery-specific ACPI paths that laptops typically have - let laptop_acpi_paths = [ - "/sys/class/power_supply/BAT0", - "/sys/class/power_supply/BAT1", - "/proc/acpi/battery", - ]; + fn rescan_load_average(&mut self) -> anyhow::Result<()> { + let content = fs::read("/proc/loadavg") + .context("failed to read load average from '/proc/loadavg'")? + .context("'/proc/loadavg' doesn't exist, are you on linux?")?; - log::debug!("checking existence of ACPI paths"); - for path in laptop_acpi_paths { - if fs::exists(path) { - log::debug!("path '{path}' exists, short circuting false"); - return Ok(false); // Likely a laptop. - } + let mut parts = content.split_whitespace(); + + let (Some(load_average_1min), Some(load_average_5min), Some(load_average_15min)) = + (parts.next(), parts.next(), parts.next()) + else { + bail!( + "failed to parse first 3 load average entries due to there not being enough, content: {content}" + ); + }; + + self.load_average_1min = load_average_1min + .parse() + .context("failed to parse load average")?; + self.load_average_5min = load_average_5min + .parse() + .context("failed to parse load average")?; + self.load_average_15min = load_average_15min + .parse() + .context("failed to parse load average")?; + + Ok(()) } - - log::debug!("checking if power saving paths exists"); - // Check CPU power policies, desktops often don't have these - let power_saving_exists = - fs::exists("/sys/module/intel_pstate/parameters/no_hwp") - || fs::exists("/sys/devices/system/cpu/cpufreq/conservative"); - - if !power_saving_exists { - log::debug!("power saving paths do not exist, short circuting true"); - return Ok(true); // Likely a desktop. - } - - // Default to assuming desktop if we can't determine. - log::debug!( - "cannot determine whether if we are a desktop, defaulting to true" - ); - Ok(true) - } - - fn rescan_load_average(&mut self) -> anyhow::Result<()> { - let content = fs::read("/proc/loadavg") - .context("failed to read load average from '/proc/loadavg'")? - .context("'/proc/loadavg' doesn't exist, are you on linux?")?; - - let mut parts = content.split_whitespace(); - - let ( - Some(load_average_1min), - Some(load_average_5min), - Some(load_average_15min), - ) = (parts.next(), parts.next(), parts.next()) - else { - bail!( - "failed to parse first 3 load average entries due to there not being \ - enough, content: {content}" - ); - }; - - self.load_average_1min = load_average_1min - .parse() - .context("failed to parse load average")?; - self.load_average_5min = load_average_5min - .parse() - .context("failed to parse load average")?; - self.load_average_15min = load_average_15min - .parse() - .context("failed to parse load average")?; - - Ok(()) - } }