mirror of
https://github.com/RGBCube/superfreq
synced 2025-07-27 17:07:44 +00:00
daemon: add eval
This commit is contained in:
parent
c2325fa5ed
commit
7503e235a3
3 changed files with 245 additions and 22 deletions
|
@ -155,7 +155,7 @@ impl PowerDelta {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||||
#[serde(untagged, rename_all = "kebab-case")]
|
#[serde(untagged, rename_all = "kebab-case")]
|
||||||
pub enum Expression {
|
pub enum Expression {
|
||||||
#[serde(rename = "%cpu-usage")]
|
#[serde(rename = "%cpu-usage")]
|
||||||
|
@ -184,12 +184,7 @@ pub enum Expression {
|
||||||
#[serde(rename = "?on-battery")]
|
#[serde(rename = "?on-battery")]
|
||||||
OnBattery,
|
OnBattery,
|
||||||
|
|
||||||
#[serde(rename = "#false")]
|
Boolean(bool),
|
||||||
False,
|
|
||||||
|
|
||||||
#[default]
|
|
||||||
#[serde(rename = "#true")]
|
|
||||||
True,
|
|
||||||
|
|
||||||
Number(f64),
|
Number(f64),
|
||||||
|
|
||||||
|
@ -251,25 +246,49 @@ pub enum 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(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
|
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
|
||||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||||
pub struct Rule {
|
pub struct Rule {
|
||||||
priority: u8,
|
pub priority: u8,
|
||||||
|
|
||||||
#[serde(default, rename = "if", skip_serializing_if = "is_default")]
|
#[serde(default, rename = "if", skip_serializing_if = "is_default")]
|
||||||
if_: Expression,
|
pub if_: Expression,
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "is_default")]
|
#[serde(default, skip_serializing_if = "is_default")]
|
||||||
cpu: CpuDelta,
|
pub cpu: CpuDelta,
|
||||||
#[serde(default, skip_serializing_if = "is_default")]
|
#[serde(default, skip_serializing_if = "is_default")]
|
||||||
power: PowerDelta,
|
pub power: PowerDelta,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
|
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
|
||||||
#[serde(default, rename_all = "kebab-case")]
|
#[serde(default, rename_all = "kebab-case")]
|
||||||
pub struct DaemonConfig {
|
pub struct DaemonConfig {
|
||||||
#[serde(rename = "rule")]
|
#[serde(rename = "rule")]
|
||||||
rules: Vec<Rule>,
|
pub rules: Vec<Rule>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DaemonConfig {
|
impl DaemonConfig {
|
||||||
|
@ -278,7 +297,7 @@ impl DaemonConfig {
|
||||||
format!("failed to read config from '{path}'", path = path.display())
|
format!("failed to read config from '{path}'", path = path.display())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let config: Self = toml::from_str(&contents).context("failed to parse config file")?;
|
let mut config: Self = toml::from_str(&contents).context("failed to parse config file")?;
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut priorities = Vec::with_capacity(config.rules.len());
|
let mut priorities = Vec::with_capacity(config.rules.len());
|
||||||
|
@ -292,6 +311,8 @@ impl DaemonConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.rules.sort_by_key(|rule| rule.priority);
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
219
src/daemon.rs
219
src/daemon.rs
|
@ -5,6 +5,7 @@ use std::{
|
||||||
Arc,
|
Arc,
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
},
|
},
|
||||||
|
thread,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -42,8 +43,8 @@ struct Daemon {
|
||||||
/// The last computed polling interval.
|
/// The last computed polling interval.
|
||||||
last_polling_interval: Option<Duration>,
|
last_polling_interval: Option<Duration>,
|
||||||
|
|
||||||
/// Whether if we are charging right now.
|
/// The system state.
|
||||||
charging: bool,
|
system: system::System,
|
||||||
|
|
||||||
/// CPU usage and temperature log.
|
/// CPU usage and temperature log.
|
||||||
cpu_log: VecDeque<CpuLog>,
|
cpu_log: VecDeque<CpuLog>,
|
||||||
|
@ -52,6 +53,56 @@ struct Daemon {
|
||||||
power_supply_log: VecDeque<PowerSupplyLog>,
|
power_supply_log: VecDeque<PowerSupplyLog>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Daemon {
|
||||||
|
fn rescan(&mut self) -> anyhow::Result<()> {
|
||||||
|
self.system.rescan()?;
|
||||||
|
|
||||||
|
while self.cpu_log.len() > 99 {
|
||||||
|
self.cpu_log.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cpu_log.push_back(CpuLog {
|
||||||
|
at: Instant::now(),
|
||||||
|
|
||||||
|
usage: self
|
||||||
|
.system
|
||||||
|
.cpus
|
||||||
|
.iter()
|
||||||
|
.map(|cpu| cpu.stat.usage())
|
||||||
|
.sum::<f64>()
|
||||||
|
/ self.system.cpus.len() as f64,
|
||||||
|
|
||||||
|
temperature: self.system.cpu_temperatures.values().sum::<f64>()
|
||||||
|
/ self.system.cpu_temperatures.len() as f64,
|
||||||
|
});
|
||||||
|
|
||||||
|
let at = Instant::now();
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
while self.power_supply_log.len() > 99 {
|
||||||
|
self.power_supply_log.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.power_supply_log.push_back(PowerSupplyLog {
|
||||||
|
at,
|
||||||
|
charge: charge_sum / charge_nr as f64,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct CpuLog {
|
struct CpuLog {
|
||||||
at: Instant,
|
at: Instant,
|
||||||
|
|
||||||
|
@ -134,12 +185,19 @@ struct PowerSupplyLog {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Daemon {
|
impl Daemon {
|
||||||
|
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.
|
/// Calculates the discharge rate, returns a number between 0 and 1.
|
||||||
///
|
///
|
||||||
/// The discharge rate is averaged per hour.
|
/// The discharge rate is averaged per hour.
|
||||||
/// So a return value of Some(0.3) means the battery has been
|
/// So a return value of Some(0.3) means the battery has been
|
||||||
/// discharging 30% per hour.
|
/// discharging 30% per hour.
|
||||||
fn power_supply_discharge_rate(&mut self) -> Option<f64> {
|
fn power_supply_discharge_rate(&self) -> Option<f64> {
|
||||||
let mut last_charge = None;
|
let mut last_charge = None;
|
||||||
|
|
||||||
// A list of increasing charge percentages.
|
// A list of increasing charge percentages.
|
||||||
|
@ -159,9 +217,7 @@ impl Daemon {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
self.charging = discharging.len() < 2;
|
if discharging.len() < 2 {
|
||||||
|
|
||||||
if self.charging {
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,7 +239,7 @@ impl Daemon {
|
||||||
let mut interval = Duration::from_secs(5);
|
let mut interval = Duration::from_secs(5);
|
||||||
|
|
||||||
// We are on battery, so we must be more conservative with our polling.
|
// We are on battery, so we must be more conservative with our polling.
|
||||||
if !self.charging {
|
if self.discharging() {
|
||||||
match self.power_supply_discharge_rate() {
|
match self.power_supply_discharge_rate() {
|
||||||
Some(discharge_rate) => {
|
Some(discharge_rate) => {
|
||||||
if discharge_rate > 0.2 {
|
if discharge_rate > 0.2 {
|
||||||
|
@ -244,7 +300,124 @@ impl Daemon {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Daemon {
|
||||||
|
fn eval(&self, expression: &config::Expression) -> anyhow::Result<Option<config::Expression>> {
|
||||||
|
use config::Expression::*;
|
||||||
|
|
||||||
|
macro_rules! try_ok {
|
||||||
|
($expression:expr) => {
|
||||||
|
match $expression {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return Ok(None),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(match expression {
|
||||||
|
CpuUsage => Number(self.cpu_log.back().unwrap().usage),
|
||||||
|
CpuUsageVolatility => Number(try_ok!(self.cpu_volatility()).usage),
|
||||||
|
CpuTemperature => Number(self.cpu_log.back().unwrap().temperature),
|
||||||
|
CpuTemperatureVolatility => Number(try_ok!(self.cpu_volatility()).temperature),
|
||||||
|
CpuIdleSeconds => Number(self.last_user_activity.elapsed().as_secs_f64()),
|
||||||
|
PowerSupplyCharge => Number(self.power_supply_log.back().unwrap().charge),
|
||||||
|
PowerSupplyDischargeRate => Number(try_ok!(self.power_supply_discharge_rate())),
|
||||||
|
|
||||||
|
Charging => Boolean(!self.discharging()),
|
||||||
|
OnBattery => Boolean(self.discharging()),
|
||||||
|
|
||||||
|
literal @ Boolean(_) | literal @ Number(_) => literal.clone(),
|
||||||
|
|
||||||
|
Plus { value, plus } => Number(
|
||||||
|
try_ok!(self.eval(value)?).as_number()? + try_ok!(self.eval(plus)?).as_number()?,
|
||||||
|
),
|
||||||
|
Minus { value, minus } => Number(
|
||||||
|
try_ok!(self.eval(value)?).as_number()? - try_ok!(self.eval(minus)?).as_number()?,
|
||||||
|
),
|
||||||
|
Multiply { value, multiply } => Number(
|
||||||
|
try_ok!(self.eval(value)?).as_number()?
|
||||||
|
* try_ok!(self.eval(multiply)?).as_number()?,
|
||||||
|
),
|
||||||
|
Power { value, power } => Number(
|
||||||
|
try_ok!(self.eval(value)?)
|
||||||
|
.as_number()?
|
||||||
|
.powf(try_ok!(self.eval(power)?).as_number()?),
|
||||||
|
),
|
||||||
|
Divide { value, divide } => Number(
|
||||||
|
try_ok!(self.eval(value)?).as_number()?
|
||||||
|
/ try_ok!(self.eval(divide)?).as_number()?,
|
||||||
|
),
|
||||||
|
|
||||||
|
LessThan {
|
||||||
|
value,
|
||||||
|
is_less_than,
|
||||||
|
} => Boolean(
|
||||||
|
try_ok!(self.eval(value)?).as_number()?
|
||||||
|
< try_ok!(self.eval(is_less_than)?).as_number()?,
|
||||||
|
),
|
||||||
|
MoreThan {
|
||||||
|
value,
|
||||||
|
is_more_than,
|
||||||
|
} => Boolean(
|
||||||
|
try_ok!(self.eval(value)?).as_number()?
|
||||||
|
> try_ok!(self.eval(is_more_than)?).as_number()?,
|
||||||
|
),
|
||||||
|
Equal {
|
||||||
|
value,
|
||||||
|
is_equal,
|
||||||
|
leeway,
|
||||||
|
} => {
|
||||||
|
let value = try_ok!(self.eval(value)?).as_number()?;
|
||||||
|
let leeway = try_ok!(self.eval(leeway)?).as_number()?;
|
||||||
|
|
||||||
|
let is_equal = try_ok!(self.eval(is_equal)?).as_number()?;
|
||||||
|
|
||||||
|
let minimum = value - leeway;
|
||||||
|
let maximum = value + leeway;
|
||||||
|
|
||||||
|
Boolean(minimum < is_equal && is_equal < maximum)
|
||||||
|
}
|
||||||
|
|
||||||
|
And { value, and } => Boolean(
|
||||||
|
try_ok!(self.eval(value)?).as_boolean()?
|
||||||
|
&& try_ok!(self.eval(and)?).as_boolean()?,
|
||||||
|
),
|
||||||
|
All { all } => {
|
||||||
|
let mut result = true;
|
||||||
|
|
||||||
|
for value in all {
|
||||||
|
result = result && try_ok!(self.eval(value)?).as_boolean()?;
|
||||||
|
|
||||||
|
if !result {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Boolean(result)
|
||||||
|
}
|
||||||
|
Or { value, or } => Boolean(
|
||||||
|
try_ok!(self.eval(value)?).as_boolean()? || try_ok!(self.eval(or)?).as_boolean()?,
|
||||||
|
),
|
||||||
|
Any { any } => {
|
||||||
|
let mut result = false;
|
||||||
|
|
||||||
|
for value in any {
|
||||||
|
result = result || try_ok!(self.eval(value)?).as_boolean()?;
|
||||||
|
|
||||||
|
if result {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Boolean(result)
|
||||||
|
}
|
||||||
|
Not { not } => Boolean(!try_ok!(self.eval(not)?).as_boolean()?),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> {
|
pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> {
|
||||||
|
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));
|
||||||
|
@ -256,9 +429,37 @@ pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> {
|
||||||
})
|
})
|
||||||
.context("failed to set Ctrl-C handler")?;
|
.context("failed to set Ctrl-C handler")?;
|
||||||
|
|
||||||
let mut system = system::System::new()?;
|
let mut daemon = Daemon {
|
||||||
|
last_user_activity: Instant::now(),
|
||||||
|
|
||||||
while !cancelled.load(Ordering::SeqCst) {}
|
last_polling_interval: None,
|
||||||
|
|
||||||
|
system: system::System::new()?,
|
||||||
|
|
||||||
|
cpu_log: VecDeque::new(),
|
||||||
|
power_supply_log: VecDeque::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
while !cancelled.load(Ordering::SeqCst) {
|
||||||
|
daemon.rescan()?;
|
||||||
|
|
||||||
|
let sleep_until = Instant::now() + daemon.polling_interval();
|
||||||
|
|
||||||
|
for rule in &config.rules {
|
||||||
|
let Some(condition) = daemon.eval(&rule.if_)? else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if condition.as_boolean()? {
|
||||||
|
rule.cpu.apply()?;
|
||||||
|
rule.power.apply()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(delay) = sleep_until.checked_duration_since(Instant::now()) {
|
||||||
|
thread::sleep(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log::info!("exiting...");
|
log::info!("exiting...");
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,7 @@ impl System {
|
||||||
|| self.is_desktop()?;
|
|| self.is_desktop()?;
|
||||||
|
|
||||||
self.rescan_load_average()?;
|
self.rescan_load_average()?;
|
||||||
|
self.rescan_temperatures()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue