diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..df184f2 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,30 @@ +# 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 new file mode 100644 index 0000000..9abeaee --- /dev/null +++ b/.taplo.toml @@ -0,0 +1,15 @@ +# 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 ecc84ed..12f2395 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 7d03f98..4eaf79b 100644 --- a/build.rs +++ b/build.rs @@ -1,52 +1,57 @@ -use std::env; -use std::fs; -use std::path::PathBuf; +use std::{ + env, + fs, + 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"); + println!("cargo:rerun-if-changed=target"); - 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 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 hardlink_path.exists() { + if hardlink_path.is_dir() { + fs::remove_dir_all(&hardlink_path)?; + } else { + fs::remove_file(&hardlink_path)?; + } } - 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 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; } + } - Ok(()) + 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(()) } diff --git a/config.toml b/config.toml index ce2dd33..293c9e8 100644 --- a/config.toml +++ b/config.toml @@ -5,107 +5,101 @@ # 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.turbo = false +cpu.frequency-mhz-maximum = 2000 +cpu.governor = "powersave" +cpu.turbo = false +if = { value = "$cpu-temperature", is-more-than = 85.0 } +priority = 100 # 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.turbo = false -power.platform-profile = "low-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 # High performance mode for sustained high load. [[rule]] -priority = 80 +cpu.energy-performance-preference = "performance" +cpu.governor = "performance" +cpu.turbo = true 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 }, ] -cpu.governor = "performance" -cpu.energy-performance-preference = "performance" -cpu.turbo = true +priority = 80 # Performance mode when not discharging. [[rule]] -priority = 70 +cpu.energy-performance-bias = "balance_performance" +cpu.energy-performance-preference = "performance" +cpu.governor = "performance" +cpu.turbo = true if.all = [ { not = "?discharging" }, { value = "%cpu-usage", is-more-than = 0.1 }, { value = "$cpu-temperature", is-less-than = 80.0 }, ] -cpu.governor = "performance" -cpu.energy-performance-preference = "performance" -cpu.energy-performance-bias = "balance_performance" -cpu.turbo = true +priority = 70 # Moderate performance for medium load. [[rule]] -priority = 60 +cpu.energy-performance-preference = "balance_performance" +cpu.governor = "schedutil" if.all = [ { value = "%cpu-usage", is-more-than = 0.4 }, { value = "%cpu-usage", is-less-than = 0.8 }, ] -cpu.governor = "schedutil" -cpu.energy-performance-preference = "balance_performance" +priority = 60 # Power saving during low activity. [[rule]] -priority = 50 +cpu.energy-performance-preference = "power" +cpu.governor = "powersave" +cpu.turbo = false if.all = [ { value = "%cpu-usage", is-less-than = 0.2 }, { value = "$cpu-idle-seconds", is-more-than = 60.0 }, ] -cpu.governor = "powersave" -cpu.energy-performance-preference = "power" -cpu.turbo = false +priority = 50 # 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.turbo = false +cpu.frequency-mhz-maximum = 1600 +cpu.governor = "powersave" +cpu.turbo = false +if = { value = "$cpu-idle-seconds", is-more-than = 300.0 } +priority = 40 # 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.turbo = false -power.platform-profile = "low-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 # General battery mode. [[rule]] -priority = 20 -if = "?discharging" -cpu.governor = "powersave" +cpu.energy-performance-bias = "balance_power" cpu.energy-performance-preference = "power" -cpu.energy-performance-bias = "balance_power" -cpu.frequency-mhz-maximum = 1800 -cpu.frequency-mhz-minimum = 200 -cpu.turbo = false +cpu.frequency-mhz-maximum = 1800 +cpu.frequency-mhz-minimum = 200 +cpu.governor = "powersave" +cpu.turbo = false +if = "?discharging" +priority = 20 # 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 e474f89..4342a62 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,526 +1,580 @@ -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)?); - } - - 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)?; - } + for &number in numbers { + cpus.push(cpu::Cpu::new(number, &cache)?); } - if let Some(turbo) = self.turbo { - cpu::Cpu::set_turbo(turbo)?; - } + cpus + }, + None => { + cpu::Cpu::all() + .context("failed to get all CPUs and their information")? + }, + }; - Ok(()) + 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)?; + } } + + 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())?); - } - - 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)?; - } + for name in names { + power_supplies + .push(power_supply::PowerSupply::from_name(name.clone())?); } - if let Some(platform_profile) = self.platform_profile.as_ref() { - power_supply::PowerSupply::set_platform_profile(platform_profile)?; - } + power_supplies + }, - Ok(()) + 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)?; + } } + + 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) + ($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, + )); } - 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)); - } - - Ok(()) - } - } - - deserializer.deserialize_str(Visitor) - } + 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") - } - - priorities.push(rule.priority); - } + for rule in &config.rules { + if priorities.contains(&rule.priority) { + bail!("each config rule must have a different 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) + 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) + } } diff --git a/src/cpu.rs b/src/cpu.rs index ef2eec6..5cfca4f 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1,641 +1,697 @@ -use anyhow::{Context, bail}; -use yansi::Paint as _; +use std::{ + cell::OnceCell, + collections::HashMap, + fmt, + mem, + rc::Rc, + string::ToString, +}; -use std::{cell::OnceCell, collections::HashMap, fmt, mem, rc::Rc, string::ToString}; +use anyhow::{ + Context, + bail, +}; +use yansi::Paint as _; 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, + 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)) }, - info: None, + ))) + .unwrap(); - temperature: None, - }; - cpu.rescan(cache)?; + cache.stat.get().unwrap() + }, + }; - Ok(cpu) - } + self.stat = stat + .get(&self.number) + .with_context(|| format!("failed to get stat of {self}"))? + .clone(); - /// Get all CPUs. - pub fn all() -> anyhow::Result> { - const PATH: &str = "/sys/devices/system/cpu"; + Ok(()) + } - let mut cpus = vec![]; - let cache = CpuRescanCache::default(); + 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, - 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}'"))?; + None => { + let content = fs::read("/proc/cpuinfo") + .context("failed to read CPU info")? + .context("/proc/cpuinfo does not exist")?; - let entry_file_name = entry.file_name(); + let mut info = HashMap::new(); + let mut current_number = None; + let mut current_data = HashMap::new(); - 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)?); + macro_rules! try_save_data { + () => { + if let Some(number) = current_number.take() { + info.insert(number, Rc::new(mem::take(&mut current_data))); } + }; } - Ok(cpus) - } + for line in content.lines() { + let parts = line.splitn(2, ':').collect::>(); - /// Rescan CPU, tuning local copy of settings. - pub fn rescan(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { - let Self { number, .. } = self; + if parts.len() == 2 { + let key = parts[0].trim(); + let value = parts[1].trim(); - if !fs::exists(format!("/sys/devices/system/cpu/cpu{number}")) { - bail!("{self} does not exist"); - } + if key == "processor" { + try_save_data!(); - 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)) - }, - ))) - .unwrap(); - - cache.stat.get().unwrap() + current_number = value.parse::().ok(); + } else { + current_data.insert(key.to_owned(), value.to_owned()); } - }; + } + } - self.stat = stat - .get(&self.number) - .with_context(|| format!("failed to get stat of {self}"))? - .clone(); + try_save_data!(); - Ok(()) + 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(", "), + ); } - 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, + 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" + ) + })?; - None => { - let content = fs::read("/proc/cpuinfo") - .context("failed to read CPU info")? - .context("/proc/cpuinfo does not exist")?; + self.governor = Some(governor.to_owned()); - let mut info = HashMap::new(); - let mut current_number = None; - let mut current_data = HashMap::new(); + Ok(()) + } - macro_rules! try_save_data { - () => { - if let Some(number) = current_number.take() { - info.insert(number, Rc::new(mem::take(&mut current_data))); - } - }; - } + pub fn set_epp(&mut self, epp: &str) -> anyhow::Result<()> { + let Self { + number, + available_epps: ref epps, + .. + } = *self; - 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 !epps.iter().any(|avail_epp| avail_epp == epp) { + bail!( + "EPP value '{epp}' is not available for {self}. available EPP values: \ + {epps}", + epps = epps.join(", "), + ); } - pub fn set_governor(&mut self, governor: &str) -> anyhow::Result<()> { - let Self { - number, - available_governors: ref governors, - .. - } = *self; + 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" + ) + })?; - if !governors - .iter() - .any(|avail_governor| avail_governor == governor) - { - bail!( - "governor '{governor}' is not available for {self}. available governors: {governors}", - governors = governors.join(", "), - ); - } + self.epp = Some(epp.to_owned()); - 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" - ) - })?; + Ok(()) + } - self.governor = Some(governor.to_owned()); + pub fn set_epb(&mut self, epb: &str) -> anyhow::Result<()> { + let Self { + number, + available_epbs: ref epbs, + .. + } = *self; - Ok(()) + 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(", "), + ); } - pub fn set_epp(&mut self, epp: &str) -> anyhow::Result<()> { - let Self { - number, - available_epps: ref epps, - .. - } = *self; + 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" + ) + })?; - 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.epb = Some(epb.to_owned()); - 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") - })?; + Ok(()) + } - self.epp = Some(epp.to_owned()); + pub fn set_frequency_mhz_minimum( + &mut self, + frequency_mhz: u64, + ) -> anyhow::Result<()> { + let Self { number, .. } = *self; - Ok(()) + 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, + ); } - pub fn set_epb(&mut self, epb: &str) -> anyhow::Result<()> { - let Self { - number, - available_epbs: ref epbs, - .. - } = *self; + Ok(()) + } - 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(", "), - ); - } + pub fn set_frequency_mhz_maximum( + &mut self, + frequency_mhz: u64, + ) -> anyhow::Result<()> { + let Self { number, .. } = *self; - 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.validate_frequency_mhz_maximum(frequency_mhz)?; - self.epb = Some(epb.to_owned()); + // We use u64 for the intermediate calculation to prevent overflow + let frequency_khz = frequency_mhz * 1000; + let frequency_khz = frequency_khz.to_string(); - Ok(()) + 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, + ); } - pub fn set_frequency_mhz_minimum(&mut self, frequency_mhz: u64) -> anyhow::Result<()> { - let Self { number, .. } = *self; + Ok(()) + } - self.validate_frequency_mhz_minimum(frequency_mhz)?; + 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. + }; - // We use u64 for the intermediate calculation to prevent overflow - let frequency_khz = frequency_mhz * 1000; - let frequency_khz = frequency_khz.to_string(); + let value_boost_negated = match on { + true => "0", // no_turbo = 0 means turbo is enabled. + false => "1", // no_turbo = 1 means turbo is disabled. + }; - 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") - })?; + // 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"; - self.frequency_mhz_minimum = Some(frequency_mhz); + // 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"; - Ok(()) + // 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(()); } - fn validate_frequency_mhz_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { - let Self { number, .. } = self; + // Also try per-core cpufreq boost for some AMD systems. + if Self::all()?.iter().any(|cpu| { + let Cpu { number, .. } = cpu; - 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(()) + fs::write( + format!("/sys/devices/system/cpu/cpu{number}/cpufreq/boost"), + value_boost, + ) + .is_ok() + }) { + return Ok(()); } - pub fn set_frequency_mhz_maximum(&mut self, frequency_mhz: u64) -> anyhow::Result<()> { - let Self { number, .. } = *self; + bail!("no supported CPU boost control mechanism found"); + } - 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(()) + 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)); } - 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(()) + 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_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) - } + Ok(None) + } } diff --git a/src/daemon.rs b/src/daemon.rs index d8ee2e6..08997e6 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,17 +1,29 @@ 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. /// @@ -19,419 +31,433 @@ use crate::{config, system}; /// - 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(); - } - - 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(()) + 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(()) + } } #[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 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 recent_log_count < 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) + 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, + }) + } + + 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; - }; - - 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, + // 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; }; - let delay = Duration::from_secs_f64(delay.as_secs_f64().clamp(1.0, 30.0)); + last_charge = Some(log.charge); - self.last_polling_delay = Some(delay); + log.charge > last_charge_value + }) + .collect(); - delay + 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, + }; + + 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(), + 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(), }; - while !cancelled.load(Ordering::SeqCst) { - daemon.rescan()?; + let mut cpu_delta_for = HashMap::::new(); + let all_cpus = + LazyCell::new(|| (0..num_cpus::get() as u32).collect::>()); - 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, - ); + for rule in &config.rules { + let Some(condition) = rule.condition.eval(&state)? else { + continue; + }; - log::debug!("filtering rules and applying them..."); + let cpu_for = rule.cpu.for_.as_ref().unwrap_or_else(|| &*all_cpus); - let start = Instant::now(); + for cpu in cpu_for { + let delta = cpu_delta_for.entry(*cpu).or_default(); - 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(), - }; + delta.for_ = Some(vec![*cpu]); - 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(governor) = rule.cpu.governor.as_ref() { + delta.governor = Some(governor.clone()); } - for delta in cpu_delta_for.values() { - delta.apply()?; + if let Some(epp) = rule.cpu.energy_performance_preference.as_ref() { + delta.energy_performance_preference = Some(epp.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(epb) = rule.cpu.energy_performance_bias.as_ref() { + delta.energy_performance_bias = Some(epb.clone()); + } - thread::sleep(delay.saturating_sub(elapsed)); + 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()?; + } } - log::info!("stopping polling loop and thus daemon..."); + for delta in cpu_delta_for.values() { + delta.apply()?; + } - Ok(()) + 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(()) } diff --git a/src/fs.rs b/src/fs.rs index 3192e4d..852cdf4 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,65 +1,80 @@ -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 bc25436..0ec780c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,143 +10,149 @@ 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 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 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); + process::exit(1); } diff --git a/src/power_supply.rs b/src/power_supply.rs index ac33e96..3bd03f2 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -1,370 +1,413 @@ -use anyhow::{Context, anyhow, bail}; -use yansi::Paint as _; - use std::{ - fmt, - path::{Path, PathBuf}, + fmt, + path::{ + Path, + PathBuf, + }, }; +use anyhow::{ + Context, + anyhow, + bail, +}; +use yansi::Paint as _; + 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(), - )?; - } - - Ok(()) + if let Some(config) = self.threshold_config.as_ref() { + write!( + f, + " from manufacturer '{manufacturer}'", + manufacturer = config.manufacturer.green(), + )?; } + + 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, - }; + threshold_config: None, + }; - power_supply.rescan()?; + power_supply.rescan()?; - Ok(power_supply) + 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())?); } - 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(), + Ok(power_supplies) + } - 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 rescan(&mut self) -> anyhow::Result<()> { + if !self.path.exists() { + bail!("{self} does not exist"); } - pub fn all() -> anyhow::Result> { - let mut power_supplies = Vec::new(); + self.type_ = { + let type_path = self.path.join("type"); - 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?"))? + 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"))? { - let entry = match entry { - Ok(entry) => entry, + Some(drain) => Some(drain as f64), - Err(error) => { - log::warn!("failed to read power supply entry: {error}"); - continue; - } - }; + None => { + let current_ua = + fs::read_n::(self.path.join("current_now")) + .with_context(|| format!("failed to read {self} current"))?; - power_supplies.push(PowerSupply::from_path(entry.path())?); - } + let voltage_uv = + fs::read_n::(self.path.join("voltage_now")) + .with_context(|| format!("failed to read {self} voltage"))?; - 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()))? + 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.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(()) + 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(); } - pub fn charge_threshold_path_start(&self) -> Option { - self.threshold_config - .map(|config| self.path.join(config.path_start)) - } + Ok(()) + } - pub fn charge_threshold_path_end(&self) -> Option { - self.threshold_config - .map(|config| self.path.join(config.path_end)) - } + pub fn charge_threshold_path_start(&self) -> Option { + self + .threshold_config + .map(|config| self.path.join(config.path_start)) + } - 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(), + 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, ) - .with_context(|| format!("failed to set charge threshold start for {self}"))?; + })?, + &((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; + self.charge_threshold_start = charge_threshold_start; - log::info!("set battery threshold start for {self} to {charge_threshold_start}%"); + log::info!( + "set battery threshold start for {self} to {charge_threshold_start}%" + ); - Ok(()) - } + 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(), + 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, ) - .with_context(|| format!("failed to set charge threshold end for {self}"))?; + })?, + &((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; + self.charge_threshold_end = charge_threshold_end; - log::info!("set battery threshold end for {self} to {charge_threshold_end}%"); + log::info!( + "set battery threshold end for {self} to {charge_threshold_end}%" + ); - Ok(()) + 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(", "), + ); } - pub fn get_available_platform_profiles() -> anyhow::Result> { - let path = "/sys/firmware/acpi/platform_profile_choices"; + fs::write("/sys/firmware/acpi/platform_profile", profile).context( + "this probably means that your system does not support changing ACPI \ + profiles", + ) + } - 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") - } + 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 af4cc38..768142c 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,383 +1,413 @@ -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, + 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; }; - system.rescan()?; + if !entry_type.contains("cpu") + && !entry_type.contains("x86") + && !entry_type.contains("core") + { + continue; + } - Ok(system) + 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; + }; + + // Magic value to see that it is from the thermal zones. + temperatures.insert(777 + counter, temperature_mc as f64 / 1000.0); + counter += 1; + } } - pub fn rescan(&mut self) -> anyhow::Result<()> { - log::debug!("rescanning view of system hardware..."); + self.cpu_temperatures = temperatures; - { - 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(), - ); - } + Ok(()) + } - { - 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(), - ); - } + 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")); - 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" - ); + 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 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!( + "{label_path} or {input_path} exists, scanning temp label...", + label_path = label_path.display(), + input_path = input_path.display(), + ); - log::debug!( - "scan result: {elaborate}", - elaborate = if is_desktop { - "is a desktop, therefore is AC" - } else { - "not a desktop, and not AC" - }, - ); + 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}"); - is_desktop - }; + // 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(); - { - let start = Instant::now(); - self.rescan_load_average()?; - log::debug!( - "rescanned load average in {millis}ms", - millis = start.elapsed().as_millis(), - ); - } + log::debug!( + "stripped 'Core' or similar identifier prefix of label content: \ + {number}" + ); - { - 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; - }; - - if !entry_type.contains("cpu") - && !entry_type.contains("x86") - && !entry_type.contains("core") - { - continue; - } - - 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; - }; - - // Magic value to see that it is from the thermal zones. - temperatures.insert(777 + counter, temperature_mc as f64 / 1000.0); - counter += 1; - } - } - - 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: {label}"); - - // 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!( + 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: {celcius} celcius", - celcius = 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: {celsius} celsius", + celsius = temperature_mc as f64 / 1000.0 + ); - temperatures.insert(number, temperature_mc as f64 / 1000.0); - } - - Ok(()) + temperatures.insert(number, temperature_mc as f64 / 1000.0); } - 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); - } + Ok(()) + } - // Laptop form factors. - "9" | "10" | "14" | "31" => { - log::debug!("chassis is a laptop form factor, short circuting false"); - return Ok(false); - } + 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); + }, - // Unknown, continue with other checks - _ => log::debug!("unknown chassis type"), - } - } + // Laptop form factors. + "9" | "10" | "14" | "31" => { + log::debug!("chassis is a laptop form factor, short circuting false"); + return Ok(false); + }, - // 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) + // Unknown, continue with other checks + _ => log::debug!("unknown chassis type"), + } } - 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?")?; + // 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", + ]; - 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 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) + } + + 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(()) + } }