mirror of
https://github.com/RGBCube/superfreq
synced 2025-07-28 09:27:44 +00:00
580 lines
16 KiB
Rust
580 lines
16 KiB
Rust
use std::{
|
|
fs,
|
|
path::Path,
|
|
};
|
|
|
|
use anyhow::{
|
|
Context,
|
|
bail,
|
|
};
|
|
use serde::{
|
|
Deserialize,
|
|
Serialize,
|
|
};
|
|
|
|
use crate::{
|
|
cpu,
|
|
power_supply,
|
|
};
|
|
|
|
fn is_default<T: Default + PartialEq>(value: &T) -> bool {
|
|
*value == T::default()
|
|
}
|
|
|
|
#[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<Vec<u32>>,
|
|
|
|
/// Set the CPU governor.
|
|
#[arg(short = 'g', long)]
|
|
#[serde(skip_serializing_if = "is_default")]
|
|
pub governor: Option<String>, /* 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<String>, /* 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<String>, /* 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<u64>,
|
|
|
|
/// Set maximum CPU frequency in MHz. Short form: --freq-max.
|
|
#[arg(short = 'F', long, alias = "freq-max", value_parser = clap::value_parser!(u64).range(1..=10_000))]
|
|
#[serde(skip_serializing_if = "is_default")]
|
|
pub frequency_mhz_maximum: Option<u64>,
|
|
|
|
/// 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<bool>,
|
|
}
|
|
|
|
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();
|
|
|
|
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)?;
|
|
}
|
|
}
|
|
|
|
if let Some(turbo) = self.turbo {
|
|
cpu::Cpu::set_turbo(turbo)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[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<Vec<String>>,
|
|
|
|
/// 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<u8>,
|
|
|
|
/// 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<u8>,
|
|
|
|
/// 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<String>,
|
|
}
|
|
|
|
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());
|
|
|
|
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)?;
|
|
}
|
|
}
|
|
|
|
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<S: serde::Serializer>(
|
|
serializer: S,
|
|
) -> Result<S::Ok, S::Error> {
|
|
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<E: serde::de::Error>(
|
|
self,
|
|
value: &str,
|
|
) -> Result<Self::Value, E> {
|
|
if value != $value {
|
|
return Err(E::invalid_value(
|
|
serde::de::Unexpected::Str(value),
|
|
&self,
|
|
));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
deserializer.deserialize_str(Visitor)
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
mod expression {
|
|
named!(cpu_usage => "%cpu-usage");
|
|
named!(cpu_usage_volatility => "$cpu-usage-volatility");
|
|
named!(cpu_temperature => "$cpu-temperature");
|
|
named!(cpu_temperature_volatility => "$cpu-temperature-volatility");
|
|
named!(cpu_idle_seconds => "$cpu-idle-seconds");
|
|
|
|
named!(power_supply_charge => "%power-supply-charge");
|
|
named!(power_supply_discharge_rate => "%power-supply-discharge-rate");
|
|
|
|
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_volatility")]
|
|
CpuUsageVolatility,
|
|
|
|
#[serde(with = "expression::cpu_temperature")]
|
|
CpuTemperature,
|
|
|
|
#[serde(with = "expression::cpu_temperature_volatility")]
|
|
CpuTemperatureVolatility,
|
|
|
|
#[serde(with = "expression::cpu_idle_seconds")]
|
|
CpuIdleSeconds,
|
|
|
|
#[serde(with = "expression::power_supply_charge")]
|
|
PowerSupplyCharge,
|
|
|
|
#[serde(with = "expression::power_supply_discharge_rate")]
|
|
PowerSupplyDischargeRate,
|
|
|
|
#[serde(with = "expression::discharging")]
|
|
Discharging,
|
|
|
|
Boolean(bool),
|
|
|
|
Number(f64),
|
|
|
|
Plus {
|
|
#[serde(rename = "value")]
|
|
a: Box<Expression>,
|
|
#[serde(rename = "plus")]
|
|
b: Box<Expression>,
|
|
},
|
|
Minus {
|
|
#[serde(rename = "value")]
|
|
a: Box<Expression>,
|
|
#[serde(rename = "minus")]
|
|
b: Box<Expression>,
|
|
},
|
|
Multiply {
|
|
#[serde(rename = "value")]
|
|
a: Box<Expression>,
|
|
#[serde(rename = "multiply")]
|
|
b: Box<Expression>,
|
|
},
|
|
Power {
|
|
#[serde(rename = "value")]
|
|
a: Box<Expression>,
|
|
#[serde(rename = "power")]
|
|
b: Box<Expression>,
|
|
},
|
|
Divide {
|
|
#[serde(rename = "value")]
|
|
a: Box<Expression>,
|
|
#[serde(rename = "divide")]
|
|
b: Box<Expression>,
|
|
},
|
|
|
|
LessThan {
|
|
#[serde(rename = "value")]
|
|
a: Box<Expression>,
|
|
#[serde(rename = "is-less-than")]
|
|
b: Box<Expression>,
|
|
},
|
|
MoreThan {
|
|
#[serde(rename = "value")]
|
|
a: Box<Expression>,
|
|
#[serde(rename = "is-more-than")]
|
|
b: Box<Expression>,
|
|
},
|
|
|
|
Equal {
|
|
#[serde(rename = "value")]
|
|
a: Box<Expression>,
|
|
#[serde(rename = "is-equal")]
|
|
b: Box<Expression>,
|
|
leeway: Box<Expression>,
|
|
},
|
|
|
|
And {
|
|
#[serde(rename = "value")]
|
|
a: Box<Expression>,
|
|
#[serde(rename = "and")]
|
|
b: Box<Expression>,
|
|
},
|
|
All {
|
|
all: Vec<Expression>,
|
|
},
|
|
|
|
Or {
|
|
#[serde(rename = "value")]
|
|
a: Box<Expression>,
|
|
#[serde(rename = "or")]
|
|
b: Box<Expression>,
|
|
},
|
|
Any {
|
|
any: Vec<Expression>,
|
|
},
|
|
|
|
Not {
|
|
not: Box<Expression>,
|
|
},
|
|
}
|
|
|
|
impl Default for Expression {
|
|
fn default() -> Self {
|
|
Self::Boolean(true)
|
|
}
|
|
}
|
|
|
|
impl Expression {
|
|
pub fn as_number(&self) -> anyhow::Result<f64> {
|
|
let Self::Number(number) = self else {
|
|
bail!("tried to cast '{self:?}' to a number, failed")
|
|
};
|
|
|
|
Ok(*number)
|
|
}
|
|
|
|
pub fn as_boolean(&self) -> anyhow::Result<bool> {
|
|
let Self::Boolean(boolean) = self else {
|
|
bail!("tried to cast '{self:?}' to a boolean, failed")
|
|
};
|
|
|
|
Ok(*boolean)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct EvalState {
|
|
pub cpu_usage: f64,
|
|
pub cpu_usage_volatility: Option<f64>,
|
|
pub cpu_temperature: f64,
|
|
pub cpu_temperature_volatility: Option<f64>,
|
|
pub cpu_idle_seconds: f64,
|
|
|
|
pub power_supply_charge: f64,
|
|
pub power_supply_discharge_rate: Option<f64>,
|
|
|
|
pub discharging: bool,
|
|
}
|
|
|
|
impl Expression {
|
|
pub fn eval(&self, state: &EvalState) -> anyhow::Result<Option<Expression>> {
|
|
use Expression::*;
|
|
|
|
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()?),
|
|
}))
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
|
|
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
|
pub struct Rule {
|
|
pub priority: u8,
|
|
|
|
#[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,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
|
|
#[serde(default, rename_all = "kebab-case")]
|
|
pub struct DaemonConfig {
|
|
#[serde(rename = "rule")]
|
|
pub rules: Vec<Rule>,
|
|
}
|
|
|
|
impl DaemonConfig {
|
|
const DEFAULT: &str = include_str!("config.toml");
|
|
|
|
pub fn load_from(path: Option<&Path>) -> anyhow::Result<Self> {
|
|
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,
|
|
);
|
|
|
|
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 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);
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|