diff --git a/src/config.rs b/src/config.rs index 365970e..36cc82c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -195,10 +195,11 @@ mod expression { 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!(charging => "?charging"); - named!(on_battery => "?on-battery"); + + named!(discharging => "?discharging"); } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] @@ -225,65 +226,80 @@ pub enum Expression { #[serde(with = "expression::power_supply_discharge_rate")] PowerSupplyDischargeRate, - #[serde(with = "expression::charging")] - Charging, - #[serde(with = "expression::on_battery")] - OnBattery, + #[serde(with = "expression::discharging")] + Discharging, Boolean(bool), Number(f64), Plus { - value: Box, - plus: Box, + #[serde(rename = "value")] + a: Box, + #[serde(rename = "plus")] + b: Box, }, Minus { - value: Box, - minus: Box, + #[serde(rename = "value")] + a: Box, + #[serde(rename = "minus")] + b: Box, }, Multiply { - value: Box, - multiply: Box, + #[serde(rename = "value")] + a: Box, + #[serde(rename = "multiply")] + b: Box, }, Power { - value: Box, - power: Box, + #[serde(rename = "value")] + a: Box, + #[serde(rename = "power")] + b: Box, }, Divide { - value: Box, - divide: Box, + #[serde(rename = "value")] + a: Box, + #[serde(rename = "divide")] + b: Box, }, - #[serde(rename_all = "kebab-case")] LessThan { - value: Box, - is_less_than: Box, + #[serde(rename = "value")] + a: Box, + #[serde(rename = "is-less-than")] + b: Box, }, - #[serde(rename_all = "kebab-case")] MoreThan { - value: Box, - is_more_than: Box, + #[serde(rename = "value")] + a: Box, + #[serde(rename = "is-more-than")] + b: Box, }, - #[serde(rename_all = "kebab-case")] Equal { - value: Box, - is_equal: Box, + #[serde(rename = "value")] + a: Box, + #[serde(rename = "is-equal")] + b: Box, leeway: Box, }, And { - value: Box, - and: Box, + #[serde(rename = "value")] + a: Box, + #[serde(rename = "and")] + b: Box, }, All { all: Vec, }, Or { - value: Box, - or: Box, + #[serde(rename = "value")] + a: Box, + #[serde(rename = "or")] + b: Box, }, Any { any: Vec, @@ -318,6 +334,121 @@ impl Expression { } } +#[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 power_supply_charge: f64, + pub power_supply_discharge_rate: Option, + + pub discharging: bool, +} + +impl 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! 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 { diff --git a/src/daemon.rs b/src/daemon.rs index fc1a6a5..43cac22 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -306,121 +306,6 @@ impl Daemon { } } -impl Daemon { - fn eval(&self, expression: &config::Expression) -> anyhow::Result> { - 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<()> { assert!(config.rules.is_sorted_by_key(|rule| rule.priority)); @@ -451,8 +336,19 @@ pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> { let sleep_until = Instant::now() + daemon.polling_interval(); + 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(), + }; + for rule in &config.rules { - let Some(condition) = daemon.eval(&rule.if_)? else { + let Some(condition) = rule.if_.eval(&state)? else { continue; };