mirror of
https://github.com/RGBCube/superfreq
synced 2025-07-27 17:07:44 +00:00
treewide: toml and rust formatter changes
This commit is contained in:
parent
fb5ef3d3d2
commit
e9e1df90e6
12 changed files with 2456 additions and 2182 deletions
30
.rustfmt.toml
Normal file
30
.rustfmt.toml
Normal file
|
@ -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
|
15
.taplo.toml
Normal file
15
.taplo.toml
Normal file
|
@ -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
|
32
Cargo.toml
32
Cargo.toml
|
@ -1,21 +1,21 @@
|
||||||
[package]
|
[package]
|
||||||
name = "watt"
|
name = "watt"
|
||||||
description = "Modern CPU frequency and power management utility for Linux"
|
description = "Modern CPU frequency and power management utility for Linux"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["NotAShelf <raf@notashelf.dev>", "RGBCube <git@rgbcu.be>"]
|
authors = [ "NotAShelf <raf@notashelf.dev>", "RGBCube <git@rgbcu.be>" ]
|
||||||
rust-version = "1.85"
|
rust-version = "1.85"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
clap = { version = "4.0", features = ["derive", "env"] }
|
clap = { version = "4.0", features = [ "derive", "env" ] }
|
||||||
clap-verbosity-flag = "3.0.2"
|
clap-verbosity-flag = "3.0.2"
|
||||||
ctrlc = "3.4"
|
ctrlc = "3.4"
|
||||||
derive_more = { version = "2.0.1", features = ["full"] }
|
derive_more = { version = "2.0.1", features = [ "full" ] }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
num_cpus = "1.16"
|
num_cpus = "1.16"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = [ "derive" ] }
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
yansi = { version = "1.0.1", features = ["detect-env", "detect-tty"] }
|
yansi = { version = "1.0.1", features = [ "detect-env", "detect-tty" ] }
|
||||||
|
|
71
build.rs
71
build.rs
|
@ -1,52 +1,57 @@
|
||||||
use std::env;
|
use std::{
|
||||||
use std::fs;
|
env,
|
||||||
use std::path::PathBuf;
|
fs,
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
const MULTICALL_NAMES: &[&str] = &["cpu", "power"];
|
const MULTICALL_NAMES: &[&str] = &["cpu", "power"];
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
println!("cargo:rerun-if-changed=build.rs");
|
println!("cargo:rerun-if-changed=build.rs");
|
||||||
println!("cargo:rerun-if-changed=target");
|
println!("cargo:rerun-if-changed=target");
|
||||||
|
|
||||||
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
|
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
|
||||||
let target = out_dir
|
let target = out_dir
|
||||||
.parent() // target/debug/build/<pkg>-<hash>/out
|
.parent() // target/debug/build/<pkg>-<hash>/out
|
||||||
.and_then(|p| p.parent()) // target/debug/build/<pkg>-<hash>
|
.and_then(|p| p.parent()) // target/debug/build/<pkg>-<hash>
|
||||||
.and_then(|p| p.parent()) // target/debug/
|
.and_then(|p| p.parent()) // target/debug/
|
||||||
.ok_or("failed to find target directory")?;
|
.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 {
|
for name in MULTICALL_NAMES {
|
||||||
let hardlink_path = target.join(name);
|
let hardlink_path = target.join(name);
|
||||||
|
|
||||||
if hardlink_path.exists() {
|
if hardlink_path.exists() {
|
||||||
if hardlink_path.is_dir() {
|
if hardlink_path.is_dir() {
|
||||||
fs::remove_dir_all(&hardlink_path)?;
|
fs::remove_dir_all(&hardlink_path)?;
|
||||||
} else {
|
} else {
|
||||||
fs::remove_file(&hardlink_path)?;
|
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 errored {
|
if let Err(error) = fs::hard_link(&main_binary_path, &hardlink_path) {
|
||||||
println!(
|
println!(
|
||||||
"cargo:warning=this often happens because the target binary isn't built yet, try running `cargo build` again"
|
"cargo:warning=failed to create hard link '{path}': {error}",
|
||||||
);
|
path = hardlink_path.display(),
|
||||||
println!("cargo:warning=keep in mind that this is for development purposes only");
|
);
|
||||||
|
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(())
|
||||||
}
|
}
|
||||||
|
|
100
config.toml
100
config.toml
|
@ -5,107 +5,101 @@
|
||||||
|
|
||||||
# Emergency thermal protection (highest priority).
|
# Emergency thermal protection (highest priority).
|
||||||
[[rule]]
|
[[rule]]
|
||||||
priority = 100
|
|
||||||
if = { value = "$cpu-temperature", is-more-than = 85.0 }
|
|
||||||
cpu.governor = "powersave"
|
|
||||||
cpu.energy-performance-preference = "power"
|
cpu.energy-performance-preference = "power"
|
||||||
cpu.frequency-mhz-maximum = 2000
|
cpu.frequency-mhz-maximum = 2000
|
||||||
cpu.turbo = false
|
cpu.governor = "powersave"
|
||||||
|
cpu.turbo = false
|
||||||
|
if = { value = "$cpu-temperature", is-more-than = 85.0 }
|
||||||
|
priority = 100
|
||||||
|
|
||||||
# Critical battery preservation.
|
# Critical battery preservation.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
priority = 90
|
|
||||||
if.all = [
|
|
||||||
"?discharging",
|
|
||||||
{ value = "%power-supply-charge", is-less-than = 0.3 },
|
|
||||||
]
|
|
||||||
cpu.governor = "powersave"
|
|
||||||
cpu.energy-performance-preference = "power"
|
cpu.energy-performance-preference = "power"
|
||||||
cpu.frequency-mhz-maximum = 800 # More aggressive below critical threshold.
|
cpu.frequency-mhz-maximum = 800 # More aggressive below critical threshold.
|
||||||
cpu.turbo = false
|
cpu.governor = "powersave"
|
||||||
power.platform-profile = "low-power"
|
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.
|
# High performance mode for sustained high load.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
priority = 80
|
cpu.energy-performance-preference = "performance"
|
||||||
|
cpu.governor = "performance"
|
||||||
|
cpu.turbo = true
|
||||||
if.all = [
|
if.all = [
|
||||||
{ value = "%cpu-usage", is-more-than = 0.8 },
|
{ value = "%cpu-usage", is-more-than = 0.8 },
|
||||||
{ value = "$cpu-idle-seconds", is-less-than = 30.0 },
|
{ value = "$cpu-idle-seconds", is-less-than = 30.0 },
|
||||||
{ value = "$cpu-temperature", is-less-than = 75.0 },
|
{ value = "$cpu-temperature", is-less-than = 75.0 },
|
||||||
]
|
]
|
||||||
cpu.governor = "performance"
|
priority = 80
|
||||||
cpu.energy-performance-preference = "performance"
|
|
||||||
cpu.turbo = true
|
|
||||||
|
|
||||||
# Performance mode when not discharging.
|
# Performance mode when not discharging.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
priority = 70
|
cpu.energy-performance-bias = "balance_performance"
|
||||||
|
cpu.energy-performance-preference = "performance"
|
||||||
|
cpu.governor = "performance"
|
||||||
|
cpu.turbo = true
|
||||||
if.all = [
|
if.all = [
|
||||||
{ not = "?discharging" },
|
{ not = "?discharging" },
|
||||||
{ value = "%cpu-usage", is-more-than = 0.1 },
|
{ value = "%cpu-usage", is-more-than = 0.1 },
|
||||||
{ value = "$cpu-temperature", is-less-than = 80.0 },
|
{ value = "$cpu-temperature", is-less-than = 80.0 },
|
||||||
]
|
]
|
||||||
cpu.governor = "performance"
|
priority = 70
|
||||||
cpu.energy-performance-preference = "performance"
|
|
||||||
cpu.energy-performance-bias = "balance_performance"
|
|
||||||
cpu.turbo = true
|
|
||||||
|
|
||||||
# Moderate performance for medium load.
|
# Moderate performance for medium load.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
priority = 60
|
cpu.energy-performance-preference = "balance_performance"
|
||||||
|
cpu.governor = "schedutil"
|
||||||
if.all = [
|
if.all = [
|
||||||
{ value = "%cpu-usage", is-more-than = 0.4 },
|
{ value = "%cpu-usage", is-more-than = 0.4 },
|
||||||
{ value = "%cpu-usage", is-less-than = 0.8 },
|
{ value = "%cpu-usage", is-less-than = 0.8 },
|
||||||
]
|
]
|
||||||
cpu.governor = "schedutil"
|
priority = 60
|
||||||
cpu.energy-performance-preference = "balance_performance"
|
|
||||||
|
|
||||||
# Power saving during low activity.
|
# Power saving during low activity.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
priority = 50
|
cpu.energy-performance-preference = "power"
|
||||||
|
cpu.governor = "powersave"
|
||||||
|
cpu.turbo = false
|
||||||
if.all = [
|
if.all = [
|
||||||
{ value = "%cpu-usage", is-less-than = 0.2 },
|
{ value = "%cpu-usage", is-less-than = 0.2 },
|
||||||
{ value = "$cpu-idle-seconds", is-more-than = 60.0 },
|
{ value = "$cpu-idle-seconds", is-more-than = 60.0 },
|
||||||
]
|
]
|
||||||
cpu.governor = "powersave"
|
priority = 50
|
||||||
cpu.energy-performance-preference = "power"
|
|
||||||
cpu.turbo = false
|
|
||||||
|
|
||||||
# Extended idle power optimization.
|
# Extended idle power optimization.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
priority = 40
|
|
||||||
if = { value = "$cpu-idle-seconds", is-more-than = 300.0 }
|
|
||||||
cpu.governor = "powersave"
|
|
||||||
cpu.energy-performance-preference = "power"
|
cpu.energy-performance-preference = "power"
|
||||||
cpu.frequency-mhz-maximum = 1600
|
cpu.frequency-mhz-maximum = 1600
|
||||||
cpu.turbo = false
|
cpu.governor = "powersave"
|
||||||
|
cpu.turbo = false
|
||||||
|
if = { value = "$cpu-idle-seconds", is-more-than = 300.0 }
|
||||||
|
priority = 40
|
||||||
|
|
||||||
# Battery conservation when discharging.
|
# Battery conservation when discharging.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
priority = 30
|
|
||||||
if.all = [
|
|
||||||
"?discharging",
|
|
||||||
{ value = "%power-supply-charge", is-less-than = 0.5 },
|
|
||||||
]
|
|
||||||
cpu.governor = "powersave"
|
|
||||||
cpu.energy-performance-preference = "power"
|
cpu.energy-performance-preference = "power"
|
||||||
cpu.frequency-mhz-maximum = 2000
|
cpu.frequency-mhz-maximum = 2000
|
||||||
cpu.turbo = false
|
cpu.governor = "powersave"
|
||||||
power.platform-profile = "low-power"
|
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.
|
# General battery mode.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
priority = 20
|
cpu.energy-performance-bias = "balance_power"
|
||||||
if = "?discharging"
|
|
||||||
cpu.governor = "powersave"
|
|
||||||
cpu.energy-performance-preference = "power"
|
cpu.energy-performance-preference = "power"
|
||||||
cpu.energy-performance-bias = "balance_power"
|
cpu.frequency-mhz-maximum = 1800
|
||||||
cpu.frequency-mhz-maximum = 1800
|
cpu.frequency-mhz-minimum = 200
|
||||||
cpu.frequency-mhz-minimum = 200
|
cpu.governor = "powersave"
|
||||||
cpu.turbo = false
|
cpu.turbo = false
|
||||||
|
if = "?discharging"
|
||||||
|
priority = 20
|
||||||
|
|
||||||
# Balanced performance for general use. Default fallback rule.
|
# Balanced performance for general use. Default fallback rule.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
priority = 0
|
|
||||||
cpu.governor = "schedutil"
|
|
||||||
cpu.energy-performance-preference = "balance_performance"
|
cpu.energy-performance-preference = "balance_performance"
|
||||||
|
cpu.governor = "schedutil"
|
||||||
|
priority = 0
|
||||||
|
|
878
src/config.rs
878
src/config.rs
|
@ -1,526 +1,580 @@
|
||||||
use std::{fs, path::Path};
|
use std::{
|
||||||
|
fs,
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::{Context, bail};
|
use anyhow::{
|
||||||
use serde::{Deserialize, Serialize};
|
Context,
|
||||||
|
bail,
|
||||||
|
};
|
||||||
|
use serde::{
|
||||||
|
Deserialize,
|
||||||
|
Serialize,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{cpu, power_supply};
|
use crate::{
|
||||||
|
cpu,
|
||||||
|
power_supply,
|
||||||
|
};
|
||||||
|
|
||||||
fn is_default<T: Default + PartialEq>(value: &T) -> bool {
|
fn is_default<T: Default + PartialEq>(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")]
|
#[serde(deny_unknown_fields, default, rename_all = "kebab-case")]
|
||||||
pub struct CpuDelta {
|
pub struct CpuDelta {
|
||||||
/// The CPUs to apply the changes to. When unspecified, will be applied to all CPUs.
|
/// The CPUs to apply the changes to. When unspecified, will be applied to
|
||||||
#[arg(short = 'c', long = "for")]
|
/// all CPUs.
|
||||||
#[serde(rename = "for", skip_serializing_if = "is_default")]
|
#[arg(short = 'c', long = "for")]
|
||||||
pub for_: Option<Vec<u32>>,
|
#[serde(rename = "for", skip_serializing_if = "is_default")]
|
||||||
|
pub for_: Option<Vec<u32>>,
|
||||||
|
|
||||||
/// Set the CPU governor.
|
/// Set the CPU governor.
|
||||||
#[arg(short = 'g', long)]
|
#[arg(short = 'g', long)]
|
||||||
#[serde(skip_serializing_if = "is_default")]
|
#[serde(skip_serializing_if = "is_default")]
|
||||||
pub governor: Option<String>, // TODO: Validate with clap for available governors.
|
pub governor: Option<String>, /* TODO: Validate with clap for available
|
||||||
|
* governors. */
|
||||||
|
|
||||||
/// Set CPU Energy Performance Preference (EPP). Short form: --epp.
|
/// Set CPU Energy Performance Preference (EPP). Short form: --epp.
|
||||||
#[arg(short = 'p', long, alias = "epp")]
|
#[arg(short = 'p', long, alias = "epp")]
|
||||||
#[serde(skip_serializing_if = "is_default")]
|
#[serde(skip_serializing_if = "is_default")]
|
||||||
pub energy_performance_preference: Option<String>, // TODO: Validate with clap for available governors.
|
pub energy_performance_preference: Option<String>, /* TODO: Validate with
|
||||||
|
* clap for available
|
||||||
|
* governors. */
|
||||||
|
|
||||||
/// Set CPU Energy Performance Bias (EPB). Short form: --epb.
|
/// Set CPU Energy Performance Bias (EPB). Short form: --epb.
|
||||||
#[arg(short = 'b', long, alias = "epb")]
|
#[arg(short = 'b', long, alias = "epb")]
|
||||||
#[serde(skip_serializing_if = "is_default")]
|
#[serde(skip_serializing_if = "is_default")]
|
||||||
pub energy_performance_bias: Option<String>, // TODO: Validate with clap for available governors.
|
pub energy_performance_bias: Option<String>, /* TODO: Validate with clap for available governors. */
|
||||||
|
|
||||||
/// Set minimum CPU frequency in MHz. Short form: --freq-min.
|
/// 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))]
|
#[arg(short = 'f', long, alias = "freq-min", value_parser = clap::value_parser!(u64).range(1..=10_000))]
|
||||||
#[serde(skip_serializing_if = "is_default")]
|
#[serde(skip_serializing_if = "is_default")]
|
||||||
pub frequency_mhz_minimum: Option<u64>,
|
pub frequency_mhz_minimum: Option<u64>,
|
||||||
|
|
||||||
/// Set maximum CPU frequency in MHz. Short form: --freq-max.
|
/// 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))]
|
#[arg(short = 'F', long, alias = "freq-max", value_parser = clap::value_parser!(u64).range(1..=10_000))]
|
||||||
#[serde(skip_serializing_if = "is_default")]
|
#[serde(skip_serializing_if = "is_default")]
|
||||||
pub frequency_mhz_maximum: Option<u64>,
|
pub frequency_mhz_maximum: Option<u64>,
|
||||||
|
|
||||||
/// Set turbo boost behaviour. Has to be for all CPUs.
|
/// Set turbo boost behaviour. Has to be for all CPUs.
|
||||||
#[arg(short = 't', long, conflicts_with = "for_")]
|
#[arg(short = 't', long, conflicts_with = "for_")]
|
||||||
#[serde(skip_serializing_if = "is_default")]
|
#[serde(skip_serializing_if = "is_default")]
|
||||||
pub turbo: Option<bool>,
|
pub turbo: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CpuDelta {
|
impl CpuDelta {
|
||||||
pub fn apply(&self) -> anyhow::Result<()> {
|
pub fn apply(&self) -> anyhow::Result<()> {
|
||||||
let mut cpus = match &self.for_ {
|
let mut cpus = match &self.for_ {
|
||||||
Some(numbers) => {
|
Some(numbers) => {
|
||||||
let mut cpus = Vec::with_capacity(numbers.len());
|
let mut cpus = Vec::with_capacity(numbers.len());
|
||||||
let cache = cpu::CpuRescanCache::default();
|
let cache = cpu::CpuRescanCache::default();
|
||||||
|
|
||||||
for &number in numbers {
|
for &number in numbers {
|
||||||
cpus.push(cpu::Cpu::new(number, &cache)?);
|
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 {
|
cpus
|
||||||
cpu::Cpu::set_turbo(turbo)?;
|
},
|
||||||
}
|
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")]
|
#[serde(deny_unknown_fields, default, rename_all = "kebab-case")]
|
||||||
pub struct PowerDelta {
|
pub struct PowerDelta {
|
||||||
/// The power supplies to apply the changes to. When unspecified, will be applied to all power supplies.
|
/// The power supplies to apply the changes to. When unspecified, will be
|
||||||
#[arg(short = 'p', long = "for")]
|
/// applied to all power supplies.
|
||||||
#[serde(rename = "for", skip_serializing_if = "is_default")]
|
#[arg(short = 'p', long = "for")]
|
||||||
pub for_: Option<Vec<String>>,
|
#[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.
|
/// Set the percentage that the power supply has to drop under for charging
|
||||||
#[arg(short = 'c', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100))]
|
/// to start. Short form: --charge-start.
|
||||||
#[serde(skip_serializing_if = "is_default")]
|
#[arg(short = 'c', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100))]
|
||||||
pub charge_threshold_start: Option<u8>,
|
#[serde(skip_serializing_if = "is_default")]
|
||||||
|
pub charge_threshold_start: Option<u8>,
|
||||||
|
|
||||||
/// Set the percentage where charging will stop. Short form: --charge-end.
|
/// 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))]
|
#[arg(short = 'C', long, alias = "charge-end", value_parser = clap::value_parser!(u8).range(0..=100))]
|
||||||
#[serde(skip_serializing_if = "is_default")]
|
#[serde(skip_serializing_if = "is_default")]
|
||||||
pub charge_threshold_end: Option<u8>,
|
pub charge_threshold_end: Option<u8>,
|
||||||
|
|
||||||
/// Set ACPI platform profile. Has to be for all power supplies.
|
/// Set ACPI platform profile. Has to be for all power supplies.
|
||||||
#[arg(short = 'f', long, alias = "profile", conflicts_with = "for_")]
|
#[arg(short = 'f', long, alias = "profile", conflicts_with = "for_")]
|
||||||
#[serde(skip_serializing_if = "is_default")]
|
#[serde(skip_serializing_if = "is_default")]
|
||||||
pub platform_profile: Option<String>,
|
pub platform_profile: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PowerDelta {
|
impl PowerDelta {
|
||||||
pub fn apply(&self) -> anyhow::Result<()> {
|
pub fn apply(&self) -> anyhow::Result<()> {
|
||||||
let mut power_supplies = match &self.for_ {
|
let mut power_supplies = match &self.for_ {
|
||||||
Some(names) => {
|
Some(names) => {
|
||||||
let mut power_supplies = Vec::with_capacity(names.len());
|
let mut power_supplies = Vec::with_capacity(names.len());
|
||||||
|
|
||||||
for name in names {
|
for name in names {
|
||||||
power_supplies.push(power_supply::PowerSupply::from_name(name.clone())?);
|
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_supplies
|
||||||
power_supply::PowerSupply::set_platform_profile(platform_profile)?;
|
},
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
macro_rules! named {
|
||||||
($variant:ident => $value:literal) => {
|
($variant:ident => $value:literal) => {
|
||||||
pub mod $variant {
|
pub mod $variant {
|
||||||
pub fn serialize<S: serde::Serializer>(serializer: S) -> Result<S::Ok, S::Error> {
|
pub fn serialize<S: serde::Serializer>(
|
||||||
serializer.serialize_str($value)
|
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,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deserialize<'de, D: serde::Deserializer<'de>>(
|
Ok(())
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
deserializer.deserialize_str(Visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mod expression {
|
mod expression {
|
||||||
named!(cpu_usage => "%cpu-usage");
|
named!(cpu_usage => "%cpu-usage");
|
||||||
named!(cpu_usage_volatility => "$cpu-usage-volatility");
|
named!(cpu_usage_volatility => "$cpu-usage-volatility");
|
||||||
named!(cpu_temperature => "$cpu-temperature");
|
named!(cpu_temperature => "$cpu-temperature");
|
||||||
named!(cpu_temperature_volatility => "$cpu-temperature-volatility");
|
named!(cpu_temperature_volatility => "$cpu-temperature-volatility");
|
||||||
named!(cpu_idle_seconds => "$cpu-idle-seconds");
|
named!(cpu_idle_seconds => "$cpu-idle-seconds");
|
||||||
|
|
||||||
named!(power_supply_charge => "%power-supply-charge");
|
named!(power_supply_charge => "%power-supply-charge");
|
||||||
named!(power_supply_discharge_rate => "%power-supply-discharge-rate");
|
named!(power_supply_discharge_rate => "%power-supply-discharge-rate");
|
||||||
|
|
||||||
named!(discharging => "?discharging");
|
named!(discharging => "?discharging");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum Expression {
|
pub enum Expression {
|
||||||
#[serde(with = "expression::cpu_usage")]
|
#[serde(with = "expression::cpu_usage")]
|
||||||
CpuUsage,
|
CpuUsage,
|
||||||
|
|
||||||
#[serde(with = "expression::cpu_usage_volatility")]
|
#[serde(with = "expression::cpu_usage_volatility")]
|
||||||
CpuUsageVolatility,
|
CpuUsageVolatility,
|
||||||
|
|
||||||
#[serde(with = "expression::cpu_temperature")]
|
#[serde(with = "expression::cpu_temperature")]
|
||||||
CpuTemperature,
|
CpuTemperature,
|
||||||
|
|
||||||
#[serde(with = "expression::cpu_temperature_volatility")]
|
#[serde(with = "expression::cpu_temperature_volatility")]
|
||||||
CpuTemperatureVolatility,
|
CpuTemperatureVolatility,
|
||||||
|
|
||||||
#[serde(with = "expression::cpu_idle_seconds")]
|
#[serde(with = "expression::cpu_idle_seconds")]
|
||||||
CpuIdleSeconds,
|
CpuIdleSeconds,
|
||||||
|
|
||||||
#[serde(with = "expression::power_supply_charge")]
|
#[serde(with = "expression::power_supply_charge")]
|
||||||
PowerSupplyCharge,
|
PowerSupplyCharge,
|
||||||
|
|
||||||
#[serde(with = "expression::power_supply_discharge_rate")]
|
#[serde(with = "expression::power_supply_discharge_rate")]
|
||||||
PowerSupplyDischargeRate,
|
PowerSupplyDischargeRate,
|
||||||
|
|
||||||
#[serde(with = "expression::discharging")]
|
#[serde(with = "expression::discharging")]
|
||||||
Discharging,
|
Discharging,
|
||||||
|
|
||||||
Boolean(bool),
|
Boolean(bool),
|
||||||
|
|
||||||
Number(f64),
|
Number(f64),
|
||||||
|
|
||||||
Plus {
|
Plus {
|
||||||
#[serde(rename = "value")]
|
#[serde(rename = "value")]
|
||||||
a: Box<Expression>,
|
a: Box<Expression>,
|
||||||
#[serde(rename = "plus")]
|
#[serde(rename = "plus")]
|
||||||
b: Box<Expression>,
|
b: Box<Expression>,
|
||||||
},
|
},
|
||||||
Minus {
|
Minus {
|
||||||
#[serde(rename = "value")]
|
#[serde(rename = "value")]
|
||||||
a: Box<Expression>,
|
a: Box<Expression>,
|
||||||
#[serde(rename = "minus")]
|
#[serde(rename = "minus")]
|
||||||
b: Box<Expression>,
|
b: Box<Expression>,
|
||||||
},
|
},
|
||||||
Multiply {
|
Multiply {
|
||||||
#[serde(rename = "value")]
|
#[serde(rename = "value")]
|
||||||
a: Box<Expression>,
|
a: Box<Expression>,
|
||||||
#[serde(rename = "multiply")]
|
#[serde(rename = "multiply")]
|
||||||
b: Box<Expression>,
|
b: Box<Expression>,
|
||||||
},
|
},
|
||||||
Power {
|
Power {
|
||||||
#[serde(rename = "value")]
|
#[serde(rename = "value")]
|
||||||
a: Box<Expression>,
|
a: Box<Expression>,
|
||||||
#[serde(rename = "power")]
|
#[serde(rename = "power")]
|
||||||
b: Box<Expression>,
|
b: Box<Expression>,
|
||||||
},
|
},
|
||||||
Divide {
|
Divide {
|
||||||
#[serde(rename = "value")]
|
#[serde(rename = "value")]
|
||||||
a: Box<Expression>,
|
a: Box<Expression>,
|
||||||
#[serde(rename = "divide")]
|
#[serde(rename = "divide")]
|
||||||
b: Box<Expression>,
|
b: Box<Expression>,
|
||||||
},
|
},
|
||||||
|
|
||||||
LessThan {
|
LessThan {
|
||||||
#[serde(rename = "value")]
|
#[serde(rename = "value")]
|
||||||
a: Box<Expression>,
|
a: Box<Expression>,
|
||||||
#[serde(rename = "is-less-than")]
|
#[serde(rename = "is-less-than")]
|
||||||
b: Box<Expression>,
|
b: Box<Expression>,
|
||||||
},
|
},
|
||||||
MoreThan {
|
MoreThan {
|
||||||
#[serde(rename = "value")]
|
#[serde(rename = "value")]
|
||||||
a: Box<Expression>,
|
a: Box<Expression>,
|
||||||
#[serde(rename = "is-more-than")]
|
#[serde(rename = "is-more-than")]
|
||||||
b: Box<Expression>,
|
b: Box<Expression>,
|
||||||
},
|
},
|
||||||
|
|
||||||
Equal {
|
Equal {
|
||||||
#[serde(rename = "value")]
|
#[serde(rename = "value")]
|
||||||
a: Box<Expression>,
|
a: Box<Expression>,
|
||||||
#[serde(rename = "is-equal")]
|
#[serde(rename = "is-equal")]
|
||||||
b: Box<Expression>,
|
b: Box<Expression>,
|
||||||
leeway: Box<Expression>,
|
leeway: Box<Expression>,
|
||||||
},
|
},
|
||||||
|
|
||||||
And {
|
And {
|
||||||
#[serde(rename = "value")]
|
#[serde(rename = "value")]
|
||||||
a: Box<Expression>,
|
a: Box<Expression>,
|
||||||
#[serde(rename = "and")]
|
#[serde(rename = "and")]
|
||||||
b: Box<Expression>,
|
b: Box<Expression>,
|
||||||
},
|
},
|
||||||
All {
|
All {
|
||||||
all: Vec<Expression>,
|
all: Vec<Expression>,
|
||||||
},
|
},
|
||||||
|
|
||||||
Or {
|
Or {
|
||||||
#[serde(rename = "value")]
|
#[serde(rename = "value")]
|
||||||
a: Box<Expression>,
|
a: Box<Expression>,
|
||||||
#[serde(rename = "or")]
|
#[serde(rename = "or")]
|
||||||
b: Box<Expression>,
|
b: Box<Expression>,
|
||||||
},
|
},
|
||||||
Any {
|
Any {
|
||||||
any: Vec<Expression>,
|
any: Vec<Expression>,
|
||||||
},
|
},
|
||||||
|
|
||||||
Not {
|
Not {
|
||||||
not: Box<Expression>,
|
not: Box<Expression>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Expression {
|
impl Default for Expression {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::Boolean(true)
|
Self::Boolean(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Expression {
|
impl Expression {
|
||||||
pub fn as_number(&self) -> anyhow::Result<f64> {
|
pub fn as_number(&self) -> anyhow::Result<f64> {
|
||||||
let Self::Number(number) = self else {
|
let Self::Number(number) = self else {
|
||||||
bail!("tried to cast '{self:?}' to a number, failed")
|
bail!("tried to cast '{self:?}' to a number, failed")
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(*number)
|
Ok(*number)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_boolean(&self) -> anyhow::Result<bool> {
|
pub fn as_boolean(&self) -> anyhow::Result<bool> {
|
||||||
let Self::Boolean(boolean) = self else {
|
let Self::Boolean(boolean) = self else {
|
||||||
bail!("tried to cast '{self:?}' to a boolean, failed")
|
bail!("tried to cast '{self:?}' to a boolean, failed")
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(*boolean)
|
Ok(*boolean)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct EvalState {
|
pub struct EvalState {
|
||||||
pub cpu_usage: f64,
|
pub cpu_usage: f64,
|
||||||
pub cpu_usage_volatility: Option<f64>,
|
pub cpu_usage_volatility: Option<f64>,
|
||||||
pub cpu_temperature: f64,
|
pub cpu_temperature: f64,
|
||||||
pub cpu_temperature_volatility: Option<f64>,
|
pub cpu_temperature_volatility: Option<f64>,
|
||||||
pub cpu_idle_seconds: f64,
|
pub cpu_idle_seconds: f64,
|
||||||
|
|
||||||
pub power_supply_charge: f64,
|
pub power_supply_charge: f64,
|
||||||
pub power_supply_discharge_rate: Option<f64>,
|
pub power_supply_discharge_rate: Option<f64>,
|
||||||
|
|
||||||
pub discharging: bool,
|
pub discharging: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Expression {
|
impl Expression {
|
||||||
pub fn eval(&self, state: &EvalState) -> anyhow::Result<Option<Expression>> {
|
pub fn eval(&self, state: &EvalState) -> anyhow::Result<Option<Expression>> {
|
||||||
use Expression::*;
|
use Expression::*;
|
||||||
|
|
||||||
macro_rules! try_ok {
|
macro_rules! try_ok {
|
||||||
($expression:expr) => {
|
($expression:expr) => {
|
||||||
match $expression {
|
match $expression {
|
||||||
Some(value) => value,
|
Some(value) => value,
|
||||||
None => return Ok(None),
|
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)]
|
#[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 {
|
||||||
pub priority: u8,
|
pub priority: u8,
|
||||||
|
|
||||||
#[serde(default, rename = "if", skip_serializing_if = "is_default")]
|
#[serde(default, rename = "if", skip_serializing_if = "is_default")]
|
||||||
pub condition: Expression,
|
pub condition: Expression,
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "is_default")]
|
#[serde(default, skip_serializing_if = "is_default")]
|
||||||
pub cpu: CpuDelta,
|
pub cpu: CpuDelta,
|
||||||
#[serde(default, skip_serializing_if = "is_default")]
|
#[serde(default, skip_serializing_if = "is_default")]
|
||||||
pub 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")]
|
||||||
pub rules: Vec<Rule>,
|
pub rules: Vec<Rule>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DaemonConfig {
|
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<Self> {
|
pub fn load_from(path: Option<&Path>) -> anyhow::Result<Self> {
|
||||||
let contents = if let Some(path) = path {
|
let contents = if let Some(path) = path {
|
||||||
log::debug!("loading config from '{path}'", path = path.display());
|
log::debug!("loading config from '{path}'", path = path.display());
|
||||||
|
|
||||||
&fs::read_to_string(path).with_context(|| {
|
&fs::read_to_string(path).with_context(|| {
|
||||||
format!("failed to read config from '{path}'", path = path.display())
|
format!("failed to read config from '{path}'", path = path.display())
|
||||||
})?
|
})?
|
||||||
} else {
|
} else {
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"loading default config from embedded toml:\n{config}",
|
"loading default config from embedded toml:\n{config}",
|
||||||
config = Self::DEFAULT,
|
config = Self::DEFAULT,
|
||||||
);
|
);
|
||||||
|
|
||||||
Self::DEFAULT
|
Self::DEFAULT
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut config: Self = toml::from_str(contents).with_context(|| {
|
let mut config: Self = toml::from_str(contents).with_context(|| {
|
||||||
path.map_or(
|
path.map_or(
|
||||||
"failed to parse builtin default config, this is a bug".to_owned(),
|
"failed to parse builtin default config, this is a bug".to_owned(),
|
||||||
|p| format!("failed to parse file at '{path}'", path = p.display()),
|
|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 {
|
for rule in &config.rules {
|
||||||
if priorities.contains(&rule.priority) {
|
if priorities.contains(&rule.priority) {
|
||||||
bail!("each config rule must have a different priority")
|
bail!("each config rule must have a different priority")
|
||||||
}
|
|
||||||
|
|
||||||
priorities.push(rule.priority);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is just for debug traces.
|
priorities.push(rule.priority);
|
||||||
if log::max_level() >= log::LevelFilter::Debug {
|
}
|
||||||
if config.rules.is_sorted_by_key(|rule| rule.priority) {
|
|
||||||
log::debug!("config rules are sorted by increasing priority, not doing anything");
|
|
||||||
} else {
|
|
||||||
log::debug!("config rules aren't sorted by priority, sorting");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.rules.sort_by_key(|rule| rule.priority);
|
|
||||||
|
|
||||||
log::debug!("loaded config: {config:#?}");
|
|
||||||
|
|
||||||
Ok(config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is just for debug traces.
|
||||||
|
if log::max_level() >= log::LevelFilter::Debug {
|
||||||
|
if config.rules.is_sorted_by_key(|rule| rule.priority) {
|
||||||
|
log::debug!(
|
||||||
|
"config rules are sorted by increasing priority, not doing anything"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log::debug!("config rules aren't sorted by priority, sorting");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.rules.sort_by_key(|rule| rule.priority);
|
||||||
|
|
||||||
|
log::debug!("loaded config: {config:#?}");
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
1144
src/cpu.rs
1144
src/cpu.rs
File diff suppressed because it is too large
Load diff
722
src/daemon.rs
722
src/daemon.rs
|
@ -1,17 +1,29 @@
|
||||||
use std::{
|
use std::{
|
||||||
cell::LazyCell,
|
cell::LazyCell,
|
||||||
collections::{HashMap, VecDeque},
|
collections::{
|
||||||
sync::{
|
HashMap,
|
||||||
Arc,
|
VecDeque,
|
||||||
atomic::{AtomicBool, Ordering},
|
},
|
||||||
|
sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{
|
||||||
|
AtomicBool,
|
||||||
|
Ordering,
|
||||||
},
|
},
|
||||||
thread,
|
},
|
||||||
time::{Duration, Instant},
|
thread,
|
||||||
|
time::{
|
||||||
|
Duration,
|
||||||
|
Instant,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
|
||||||
use crate::{config, system};
|
use crate::{
|
||||||
|
config,
|
||||||
|
system,
|
||||||
|
};
|
||||||
|
|
||||||
/// Calculate the idle time multiplier based on system idle time.
|
/// 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: Linear interpolation from 1.0 to 2.0
|
||||||
/// - For idle times >= 2 minutes: Logarithmic scaling (1.0 + log2(minutes))
|
/// - For idle times >= 2 minutes: Logarithmic scaling (1.0 + log2(minutes))
|
||||||
fn idle_multiplier(idle_for: Duration) -> f64 {
|
fn idle_multiplier(idle_for: Duration) -> f64 {
|
||||||
let factor = match idle_for.as_secs() < 120 {
|
let factor = match idle_for.as_secs() < 120 {
|
||||||
// Less than 2 minutes.
|
// Less than 2 minutes.
|
||||||
// Linear interpolation from 1.0 (at 0s) to 2.0 (at 120s)
|
// Linear interpolation from 1.0 (at 0s) to 2.0 (at 120s)
|
||||||
true => (idle_for.as_secs() as f64) / 120.0,
|
true => (idle_for.as_secs() as f64) / 120.0,
|
||||||
|
|
||||||
// 2 minutes or more.
|
// 2 minutes or more.
|
||||||
// Logarithmic scaling: 1.0 + log2(minutes)
|
// Logarithmic scaling: 1.0 + log2(minutes)
|
||||||
false => {
|
false => {
|
||||||
let idle_minutes = idle_for.as_secs() as f64 / 60.0;
|
let idle_minutes = idle_for.as_secs() as f64 / 60.0;
|
||||||
idle_minutes.log2()
|
idle_minutes.log2()
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clamp the multiplier to avoid excessive delays.
|
// Clamp the multiplier to avoid excessive delays.
|
||||||
(1.0 + factor).clamp(1.0, 5.0)
|
(1.0 + factor).clamp(1.0, 5.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Daemon {
|
struct Daemon {
|
||||||
/// Last time when there was user activity.
|
/// Last time when there was user activity.
|
||||||
last_user_activity: Instant,
|
last_user_activity: Instant,
|
||||||
|
|
||||||
/// The last computed polling delay.
|
/// The last computed polling delay.
|
||||||
last_polling_delay: Option<Duration>,
|
last_polling_delay: Option<Duration>,
|
||||||
|
|
||||||
/// The system state.
|
/// The system state.
|
||||||
system: system::System,
|
system: system::System,
|
||||||
|
|
||||||
/// CPU usage and temperature log.
|
/// CPU usage and temperature log.
|
||||||
cpu_log: VecDeque<CpuLog>,
|
cpu_log: VecDeque<CpuLog>,
|
||||||
|
|
||||||
/// Power supply status log.
|
/// Power supply status log.
|
||||||
power_supply_log: VecDeque<PowerSupplyLog>,
|
power_supply_log: VecDeque<PowerSupplyLog>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Daemon {
|
impl Daemon {
|
||||||
fn rescan(&mut self) -> anyhow::Result<()> {
|
fn rescan(&mut self) -> anyhow::Result<()> {
|
||||||
self.system.rescan()?;
|
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 {
|
while self.cpu_log.len() > 100 {
|
||||||
log::debug!("daemon CPU log was too long, popping element");
|
log::debug!("daemon CPU log was too long, popping element");
|
||||||
self.cpu_log.pop_front();
|
self.cpu_log.pop_front();
|
||||||
}
|
|
||||||
|
|
||||||
let cpu_log = CpuLog {
|
|
||||||
at,
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
log::debug!("appending CPU log item: {cpu_log:?}");
|
|
||||||
self.cpu_log.push_back(cpu_log);
|
|
||||||
|
|
||||||
while self.power_supply_log.len() > 100 {
|
|
||||||
log::debug!("daemon power supply log was too long, popping element");
|
|
||||||
self.power_supply_log.pop_front();
|
|
||||||
}
|
|
||||||
|
|
||||||
let power_supply_log = PowerSupplyLog {
|
|
||||||
at,
|
|
||||||
charge: {
|
|
||||||
let (charge_sum, charge_nr) = self.system.power_supplies.iter().fold(
|
|
||||||
(0.0, 0u32),
|
|
||||||
|(sum, count), power_supply| {
|
|
||||||
if let Some(charge_percent) = power_supply.charge_percent {
|
|
||||||
(sum + charge_percent, count + 1)
|
|
||||||
} else {
|
|
||||||
(sum, count)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
charge_sum / charge_nr as f64
|
|
||||||
},
|
|
||||||
};
|
|
||||||
log::debug!("appending power supply log item: {power_supply_log:?}");
|
|
||||||
self.power_supply_log.push_back(power_supply_log);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cpu_log = CpuLog {
|
||||||
|
at,
|
||||||
|
|
||||||
|
usage: self
|
||||||
|
.system
|
||||||
|
.cpus
|
||||||
|
.iter()
|
||||||
|
.map(|cpu| cpu.stat.usage())
|
||||||
|
.sum::<f64>()
|
||||||
|
/ self.system.cpus.len() as f64,
|
||||||
|
|
||||||
|
temperature: self.system.cpu_temperatures.values().sum::<f64>()
|
||||||
|
/ 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)]
|
#[derive(Debug)]
|
||||||
struct CpuLog {
|
struct CpuLog {
|
||||||
at: Instant,
|
at: Instant,
|
||||||
|
|
||||||
/// CPU usage between 0-1, a percentage.
|
/// CPU usage between 0-1, a percentage.
|
||||||
usage: f64,
|
usage: f64,
|
||||||
|
|
||||||
/// CPU temperature in celsius.
|
/// CPU temperature in celsius.
|
||||||
temperature: f64,
|
temperature: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct CpuVolatility {
|
struct CpuVolatility {
|
||||||
usage: f64,
|
usage: f64,
|
||||||
|
|
||||||
temperature: f64,
|
temperature: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Daemon {
|
impl Daemon {
|
||||||
fn cpu_volatility(&self) -> Option<CpuVolatility> {
|
fn cpu_volatility(&self) -> Option<CpuVolatility> {
|
||||||
let recent_log_count = self
|
let recent_log_count = self
|
||||||
.cpu_log
|
.cpu_log
|
||||||
.iter()
|
.iter()
|
||||||
.rev()
|
.rev()
|
||||||
.take_while(|log| log.at.elapsed() < Duration::from_secs(5 * 60))
|
.take_while(|log| log.at.elapsed() < Duration::from_secs(5 * 60))
|
||||||
.count();
|
.count();
|
||||||
|
|
||||||
if recent_log_count < 2 {
|
if recent_log_count < 2 {
|
||||||
return None;
|
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_cpu_idle(&self) -> bool {
|
if self.cpu_log.len() < 2 {
|
||||||
let recent_log_count = self
|
return None;
|
||||||
.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::<f64>()
|
|
||||||
/ recent_log_count as f64;
|
|
||||||
|
|
||||||
recent_average < 0.1
|
|
||||||
&& self
|
|
||||||
.cpu_volatility()
|
|
||||||
.is_none_or(|volatility| volatility.usage < 0.05)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let change_count = self.cpu_log.len() - 1;
|
||||||
|
|
||||||
|
let mut usage_change_sum = 0.0;
|
||||||
|
let mut temperature_change_sum = 0.0;
|
||||||
|
|
||||||
|
for index in 0..change_count {
|
||||||
|
let usage_change =
|
||||||
|
self.cpu_log[index + 1].usage - self.cpu_log[index].usage;
|
||||||
|
usage_change_sum += usage_change.abs();
|
||||||
|
|
||||||
|
let temperature_change =
|
||||||
|
self.cpu_log[index + 1].temperature - self.cpu_log[index].temperature;
|
||||||
|
temperature_change_sum += temperature_change.abs();
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(CpuVolatility {
|
||||||
|
usage: usage_change_sum / change_count as f64,
|
||||||
|
temperature: temperature_change_sum / change_count as f64,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_cpu_idle(&self) -> bool {
|
||||||
|
let recent_log_count = self
|
||||||
|
.cpu_log
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.take_while(|log| log.at.elapsed() < Duration::from_secs(5 * 60))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
if recent_log_count < 2 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let recent_average = self
|
||||||
|
.cpu_log
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.take(recent_log_count)
|
||||||
|
.map(|log| log.usage)
|
||||||
|
.sum::<f64>()
|
||||||
|
/ recent_log_count as f64;
|
||||||
|
|
||||||
|
recent_average < 0.1
|
||||||
|
&& self
|
||||||
|
.cpu_volatility()
|
||||||
|
.is_none_or(|volatility| volatility.usage < 0.05)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct PowerSupplyLog {
|
struct PowerSupplyLog {
|
||||||
at: Instant,
|
at: Instant,
|
||||||
|
|
||||||
/// Charge 0-1, as a percentage.
|
/// Charge 0-1, as a percentage.
|
||||||
charge: f64,
|
charge: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Daemon {
|
impl Daemon {
|
||||||
fn discharging(&self) -> bool {
|
fn discharging(&self) -> bool {
|
||||||
self.system
|
self.system.power_supplies.iter().any(|power_supply| {
|
||||||
.power_supplies
|
power_supply.charge_state.as_deref() == Some("Discharging")
|
||||||
.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(&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.
|
||||||
let discharging: Vec<&PowerSupplyLog> = self
|
let discharging: Vec<&PowerSupplyLog> = self
|
||||||
.power_supply_log
|
.power_supply_log
|
||||||
.iter()
|
.iter()
|
||||||
.rev()
|
.rev()
|
||||||
.take_while(move |log| {
|
.take_while(move |log| {
|
||||||
let Some(last_charge_value) = last_charge else {
|
let Some(last_charge_value) = last_charge else {
|
||||||
last_charge = Some(log.charge);
|
last_charge = Some(log.charge);
|
||||||
return true;
|
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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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<()> {
|
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...");
|
log::debug!("setting ctrl-c handler...");
|
||||||
let cancelled_ = Arc::clone(&cancelled);
|
let cancelled_ = Arc::clone(&cancelled);
|
||||||
ctrlc::set_handler(move || {
|
ctrlc::set_handler(move || {
|
||||||
log::info!("received shutdown signal");
|
log::info!("received shutdown signal");
|
||||||
cancelled_.store(true, Ordering::SeqCst);
|
cancelled_.store(true, Ordering::SeqCst);
|
||||||
})
|
})
|
||||||
.context("failed to set ctrl-c handler")?;
|
.context("failed to set ctrl-c handler")?;
|
||||||
|
|
||||||
let mut daemon = Daemon {
|
let mut daemon = Daemon {
|
||||||
last_user_activity: Instant::now(),
|
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(),
|
cpu_log: VecDeque::new(),
|
||||||
power_supply_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) {
|
let mut cpu_delta_for = HashMap::<u32, config::CpuDelta>::new();
|
||||||
daemon.rescan()?;
|
let all_cpus =
|
||||||
|
LazyCell::new(|| (0..num_cpus::get() as u32).collect::<Vec<_>>());
|
||||||
|
|
||||||
let delay = daemon.polling_delay();
|
for rule in &config.rules {
|
||||||
log::info!(
|
let Some(condition) = rule.condition.eval(&state)? else {
|
||||||
"next poll will be in {seconds} seconds or {minutes} minutes, possibly delayed if application of rules takes more than the polling delay",
|
continue;
|
||||||
seconds = delay.as_secs_f64(),
|
};
|
||||||
minutes = delay.as_secs_f64() / 60.0,
|
|
||||||
);
|
|
||||||
|
|
||||||
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 {
|
delta.for_ = Some(vec![*cpu]);
|
||||||
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(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut cpu_delta_for = HashMap::<u32, config::CpuDelta>::new();
|
if let Some(governor) = rule.cpu.governor.as_ref() {
|
||||||
let all_cpus = LazyCell::new(|| (0..num_cpus::get() as u32).collect::<Vec<_>>());
|
delta.governor = Some(governor.clone());
|
||||||
|
|
||||||
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()?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for delta in cpu_delta_for.values() {
|
if let Some(epp) = rule.cpu.energy_performance_preference.as_ref() {
|
||||||
delta.apply()?;
|
delta.energy_performance_preference = Some(epp.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let elapsed = start.elapsed();
|
if let Some(epb) = rule.cpu.energy_performance_bias.as_ref() {
|
||||||
log::info!(
|
delta.energy_performance_bias = Some(epb.clone());
|
||||||
"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));
|
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(())
|
||||||
}
|
}
|
||||||
|
|
89
src/fs.rs
89
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;
|
use anyhow::Context;
|
||||||
|
|
||||||
pub fn exists(path: impl AsRef<Path>) -> bool {
|
pub fn exists(path: impl AsRef<Path>) -> bool {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
|
||||||
path.exists()
|
path.exists()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_dir(path: impl AsRef<Path>) -> anyhow::Result<Option<fs::ReadDir>> {
|
pub fn read_dir(path: impl AsRef<Path>) -> anyhow::Result<Option<fs::ReadDir>> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
|
||||||
match fs::read_dir(path) {
|
match fs::read_dir(path) {
|
||||||
Ok(entries) => Ok(Some(entries)),
|
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!(
|
Err(error) => {
|
||||||
"failed to read directory '{path}'",
|
Err(error).context(format!(
|
||||||
path = path.display()
|
"failed to read directory '{path}'",
|
||||||
)),
|
path = path.display()
|
||||||
}
|
))
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read(path: impl AsRef<Path>) -> anyhow::Result<Option<String>> {
|
pub fn read(path: impl AsRef<Path>) -> anyhow::Result<Option<String>> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
|
||||||
match fs::read_to_string(path) {
|
match fs::read_to_string(path) {
|
||||||
Ok(string) => Ok(Some(string.trim().to_owned())),
|
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<N: str::FromStr>(path: impl AsRef<Path>) -> anyhow::Result<Option<N>>
|
pub fn read_n<N: str::FromStr>(
|
||||||
|
path: impl AsRef<Path>,
|
||||||
|
) -> anyhow::Result<Option<N>>
|
||||||
where
|
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)? {
|
match read(path)? {
|
||||||
Some(content) => Ok(Some(content.trim().parse().with_context(|| {
|
Some(content) => {
|
||||||
format!(
|
Ok(Some(content.trim().parse().with_context(|| {
|
||||||
"failed to parse contents of '{path}' as a unsigned number",
|
format!(
|
||||||
path = path.display(),
|
"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<Path>, value: &str) -> anyhow::Result<()> {
|
pub fn write(path: impl AsRef<Path>, value: &str) -> anyhow::Result<()> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
|
||||||
fs::write(path, value).with_context(|| {
|
fs::write(path, value).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"failed to write '{value}' to '{path}'",
|
"failed to write '{value}' to '{path}'",
|
||||||
path = path.display(),
|
path = path.display(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
204
src/main.rs
204
src/main.rs
|
@ -10,143 +10,149 @@ mod daemon;
|
||||||
// mod engine;
|
// mod engine;
|
||||||
// mod monitor;
|
// mod monitor;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fmt::Write as _,
|
||||||
|
io,
|
||||||
|
io::Write as _,
|
||||||
|
path::PathBuf,
|
||||||
|
process,
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use clap::Parser as _;
|
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 _;
|
use yansi::Paint as _;
|
||||||
|
|
||||||
#[derive(clap::Parser, Debug)]
|
#[derive(clap::Parser, Debug)]
|
||||||
#[clap(author, version, about)]
|
#[clap(author, version, about)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
command: Command,
|
command: Command,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Parser, Debug)]
|
#[derive(clap::Parser, Debug)]
|
||||||
#[clap(multicall = true)]
|
#[clap(multicall = true)]
|
||||||
enum Command {
|
enum Command {
|
||||||
/// Watt daemon.
|
/// Watt daemon.
|
||||||
Watt {
|
Watt {
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
verbosity: clap_verbosity_flag::Verbosity,
|
verbosity: clap_verbosity_flag::Verbosity,
|
||||||
|
|
||||||
/// The daemon config path.
|
/// The daemon config path.
|
||||||
#[arg(long, env = "WATT_CONFIG")]
|
#[arg(long, env = "WATT_CONFIG")]
|
||||||
config: Option<PathBuf>,
|
config: Option<PathBuf>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// CPU metadata and modification utility.
|
/// CPU metadata and modification utility.
|
||||||
Cpu {
|
Cpu {
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
verbosity: clap_verbosity_flag::Verbosity,
|
verbosity: clap_verbosity_flag::Verbosity,
|
||||||
|
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
command: CpuCommand,
|
command: CpuCommand,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Power supply metadata and modification utility.
|
/// Power supply metadata and modification utility.
|
||||||
Power {
|
Power {
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
verbosity: clap_verbosity_flag::Verbosity,
|
verbosity: clap_verbosity_flag::Verbosity,
|
||||||
|
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
command: PowerCommand,
|
command: PowerCommand,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Parser, Debug)]
|
#[derive(clap::Parser, Debug)]
|
||||||
enum CpuCommand {
|
enum CpuCommand {
|
||||||
/// Modify CPU attributes.
|
/// Modify CPU attributes.
|
||||||
Set(config::CpuDelta),
|
Set(config::CpuDelta),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Parser, Debug)]
|
#[derive(clap::Parser, Debug)]
|
||||||
enum PowerCommand {
|
enum PowerCommand {
|
||||||
/// Modify power supply attributes.
|
/// Modify power supply attributes.
|
||||||
Set(config::PowerDelta),
|
Set(config::PowerDelta),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn real_main() -> anyhow::Result<()> {
|
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, .. }
|
let (Command::Watt { verbosity, .. }
|
||||||
| Command::Cpu { verbosity, .. }
|
| Command::Cpu { verbosity, .. }
|
||||||
| Command::Power { verbosity, .. }) = cli.command;
|
| Command::Power { verbosity, .. }) = cli.command;
|
||||||
|
|
||||||
env_logger::Builder::new()
|
env_logger::Builder::new()
|
||||||
.filter_level(verbosity.log_level_filter())
|
.filter_level(verbosity.log_level_filter())
|
||||||
.format_timestamp(None)
|
.format_timestamp(None)
|
||||||
.format_module_path(false)
|
.format_module_path(false)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Command::Watt { config, .. } => {
|
Command::Watt { config, .. } => {
|
||||||
let config = config::DaemonConfig::load_from(config.as_deref())
|
let config = config::DaemonConfig::load_from(config.as_deref())
|
||||||
.context("failed to load daemon config")?;
|
.context("failed to load daemon config")?;
|
||||||
|
|
||||||
daemon::run(config)
|
daemon::run(config)
|
||||||
}
|
},
|
||||||
|
|
||||||
Command::Cpu {
|
Command::Cpu {
|
||||||
command: CpuCommand::Set(delta),
|
command: CpuCommand::Set(delta),
|
||||||
..
|
..
|
||||||
} => delta.apply(),
|
} => delta.apply(),
|
||||||
|
|
||||||
Command::Power {
|
Command::Power {
|
||||||
command: PowerCommand::Set(delta),
|
command: PowerCommand::Set(delta),
|
||||||
..
|
..
|
||||||
} => delta.apply(),
|
} => delta.apply(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let Err(error) = real_main() else {
|
let Err(error) = real_main() else {
|
||||||
return;
|
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();
|
process::exit(1);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,370 +1,413 @@
|
||||||
use anyhow::{Context, anyhow, bail};
|
|
||||||
use yansi::Paint as _;
|
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fmt,
|
fmt,
|
||||||
path::{Path, PathBuf},
|
path::{
|
||||||
|
Path,
|
||||||
|
PathBuf,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use anyhow::{
|
||||||
|
Context,
|
||||||
|
anyhow,
|
||||||
|
bail,
|
||||||
|
};
|
||||||
|
use yansi::Paint as _;
|
||||||
|
|
||||||
use crate::fs;
|
use crate::fs;
|
||||||
|
|
||||||
/// Represents a pattern of path suffixes used to control charge thresholds
|
/// Represents a pattern of path suffixes used to control charge thresholds
|
||||||
/// for different device vendors.
|
/// for different device vendors.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct PowerSupplyThresholdConfig {
|
pub struct PowerSupplyThresholdConfig {
|
||||||
pub manufacturer: &'static str,
|
pub manufacturer: &'static str,
|
||||||
pub path_start: &'static str,
|
pub path_start: &'static str,
|
||||||
pub path_end: &'static str,
|
pub path_end: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Power supply threshold configs.
|
/// Power supply threshold configs.
|
||||||
const POWER_SUPPLY_THRESHOLD_CONFIGS: &[PowerSupplyThresholdConfig] = &[
|
const POWER_SUPPLY_THRESHOLD_CONFIGS: &[PowerSupplyThresholdConfig] = &[
|
||||||
PowerSupplyThresholdConfig {
|
PowerSupplyThresholdConfig {
|
||||||
manufacturer: "Standard",
|
manufacturer: "Standard",
|
||||||
path_start: "charge_control_start_threshold",
|
path_start: "charge_control_start_threshold",
|
||||||
path_end: "charge_control_end_threshold",
|
path_end: "charge_control_end_threshold",
|
||||||
},
|
},
|
||||||
PowerSupplyThresholdConfig {
|
PowerSupplyThresholdConfig {
|
||||||
manufacturer: "ASUS",
|
manufacturer: "ASUS",
|
||||||
path_start: "charge_control_start_percentage",
|
path_start: "charge_control_start_percentage",
|
||||||
path_end: "charge_control_end_percentage",
|
path_end: "charge_control_end_percentage",
|
||||||
},
|
},
|
||||||
// Combine Huawei and ThinkPad since they use identical paths.
|
// Combine Huawei and ThinkPad since they use identical paths.
|
||||||
PowerSupplyThresholdConfig {
|
PowerSupplyThresholdConfig {
|
||||||
manufacturer: "ThinkPad/Huawei",
|
manufacturer: "ThinkPad/Huawei",
|
||||||
path_start: "charge_start_threshold",
|
path_start: "charge_start_threshold",
|
||||||
path_end: "charge_stop_threshold",
|
path_end: "charge_stop_threshold",
|
||||||
},
|
},
|
||||||
// Framework laptop support.
|
// Framework laptop support.
|
||||||
PowerSupplyThresholdConfig {
|
PowerSupplyThresholdConfig {
|
||||||
manufacturer: "Framework",
|
manufacturer: "Framework",
|
||||||
path_start: "charge_behaviour_start_threshold",
|
path_start: "charge_behaviour_start_threshold",
|
||||||
path_end: "charge_behaviour_end_threshold",
|
path_end: "charge_behaviour_end_threshold",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Represents a power supply that supports charge threshold control.
|
/// Represents a power supply that supports charge threshold control.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct PowerSupply {
|
pub struct PowerSupply {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
|
||||||
pub type_: String,
|
pub type_: String,
|
||||||
pub is_from_peripheral: bool,
|
pub is_from_peripheral: bool,
|
||||||
|
|
||||||
pub charge_state: Option<String>,
|
pub charge_state: Option<String>,
|
||||||
pub charge_percent: Option<f64>,
|
pub charge_percent: Option<f64>,
|
||||||
|
|
||||||
pub charge_threshold_start: f64,
|
pub charge_threshold_start: f64,
|
||||||
pub charge_threshold_end: f64,
|
pub charge_threshold_end: f64,
|
||||||
|
|
||||||
pub drain_rate_watts: Option<f64>,
|
pub drain_rate_watts: Option<f64>,
|
||||||
|
|
||||||
pub threshold_config: Option<PowerSupplyThresholdConfig>,
|
pub threshold_config: Option<PowerSupplyThresholdConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PowerSupply {
|
impl PowerSupply {
|
||||||
pub fn is_ac(&self) -> bool {
|
pub fn is_ac(&self) -> bool {
|
||||||
!self.is_from_peripheral
|
!self.is_from_peripheral
|
||||||
&& matches!(
|
&& matches!(
|
||||||
&*self.type_,
|
&*self.type_,
|
||||||
"Mains" | "USB_PD_DRP" | "USB_PD" | "USB_DCP" | "USB_CDP" | "USB_ACA"
|
"Mains" | "USB_PD_DRP" | "USB_PD" | "USB_DCP" | "USB_CDP" | "USB_ACA"
|
||||||
)
|
)
|
||||||
|| self.type_.starts_with("AC")
|
|| self.type_.starts_with("AC")
|
||||||
|| self.type_.contains("ACAD")
|
|| self.type_.contains("ACAD")
|
||||||
|| self.type_.contains("ADP")
|
|| self.type_.contains("ADP")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for PowerSupply {
|
impl fmt::Display for PowerSupply {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "power supply '{name}'", name = self.name.yellow())?;
|
write!(f, "power supply '{name}'", name = self.name.yellow())?;
|
||||||
|
|
||||||
if let Some(config) = self.threshold_config.as_ref() {
|
if let Some(config) = self.threshold_config.as_ref() {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
" from manufacturer '{manufacturer}'",
|
" from manufacturer '{manufacturer}'",
|
||||||
manufacturer = config.manufacturer.green(),
|
manufacturer = config.manufacturer.green(),
|
||||||
)?;
|
)?;
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const POWER_SUPPLY_PATH: &str = "/sys/class/power_supply";
|
const POWER_SUPPLY_PATH: &str = "/sys/class/power_supply";
|
||||||
|
|
||||||
impl PowerSupply {
|
impl PowerSupply {
|
||||||
pub fn from_name(name: String) -> anyhow::Result<Self> {
|
pub fn from_name(name: String) -> anyhow::Result<Self> {
|
||||||
let mut power_supply = Self {
|
let mut power_supply = Self {
|
||||||
path: Path::new(POWER_SUPPLY_PATH).join(&name),
|
path: Path::new(POWER_SUPPLY_PATH).join(&name),
|
||||||
name,
|
name,
|
||||||
type_: String::new(),
|
type_: String::new(),
|
||||||
|
|
||||||
charge_state: None,
|
charge_state: None,
|
||||||
charge_percent: None,
|
charge_percent: None,
|
||||||
|
|
||||||
charge_threshold_start: 0.0,
|
charge_threshold_start: 0.0,
|
||||||
charge_threshold_end: 1.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<Self> {
|
||||||
|
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<Vec<PowerSupply>> {
|
||||||
|
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<Self> {
|
Ok(power_supplies)
|
||||||
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,
|
pub fn rescan(&mut self) -> anyhow::Result<()> {
|
||||||
type_: String::new(),
|
if !self.path.exists() {
|
||||||
|
bail!("{self} does not exist");
|
||||||
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<Vec<PowerSupply>> {
|
self.type_ = {
|
||||||
let mut power_supplies = Vec::new();
|
let type_path = self.path.join("type");
|
||||||
|
|
||||||
for entry in fs::read_dir(POWER_SUPPLY_PATH)
|
fs::read(&type_path)
|
||||||
.context("failed to read power supply entries")?
|
.with_context(|| {
|
||||||
.with_context(|| format!("'{POWER_SUPPLY_PATH}' doesn't exist, are you on linux?"))?
|
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::<u64>(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::<u64>(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::<u64>(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::<u64>(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::<i64>(self.path.join("power_now"))
|
||||||
|
.with_context(|| format!("failed to read {self} power drain"))?
|
||||||
{
|
{
|
||||||
let entry = match entry {
|
Some(drain) => Some(drain as f64),
|
||||||
Ok(entry) => entry,
|
|
||||||
|
|
||||||
Err(error) => {
|
None => {
|
||||||
log::warn!("failed to read power supply entry: {error}");
|
let current_ua =
|
||||||
continue;
|
fs::read_n::<i32>(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::<i32>(self.path.join("voltage_now"))
|
||||||
|
.with_context(|| format!("failed to read {self} voltage"))?;
|
||||||
|
|
||||||
Ok(power_supplies)
|
current_ua.zip(voltage_uv).map(|(current, voltage)| {
|
||||||
}
|
// Power (W) = Voltage (V) * Current (A)
|
||||||
|
// (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W
|
||||||
pub fn rescan(&mut self) -> anyhow::Result<()> {
|
current as f64 * voltage as f64 / 1e12
|
||||||
if !self.path.exists() {
|
})
|
||||||
bail!("{self} does not exist");
|
},
|
||||||
}
|
|
||||||
|
|
||||||
self.type_ = {
|
|
||||||
let type_path = self.path.join("type");
|
|
||||||
|
|
||||||
fs::read(&type_path)
|
|
||||||
.with_context(|| format!("failed to read '{path}'", path = type_path.display()))?
|
|
||||||
.with_context(|| format!("'{path}' doesn't exist", path = type_path.display()))?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.is_from_peripheral = 'is_from_peripheral: {
|
self.threshold_config = POWER_SUPPLY_THRESHOLD_CONFIGS
|
||||||
let name_lower = self.name.to_lowercase();
|
.iter()
|
||||||
|
.find(|config| {
|
||||||
// Common peripheral battery names.
|
self.path.join(config.path_start).exists()
|
||||||
if name_lower.contains("mouse")
|
&& self.path.join(config.path_end).exists()
|
||||||
|| name_lower.contains("keyboard")
|
})
|
||||||
|| name_lower.contains("trackpad")
|
.copied();
|
||||||
|| 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::<u64>(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::<u64>(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::<u64>(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::<u64>(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::<i64>(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::<i32>(self.path.join("current_now"))
|
|
||||||
.with_context(|| format!("failed to read {self} current"))?;
|
|
||||||
|
|
||||||
let voltage_uv = fs::read_n::<i32>(self.path.join("voltage_now"))
|
|
||||||
.with_context(|| format!("failed to read {self} voltage"))?;
|
|
||||||
|
|
||||||
current_ua.zip(voltage_uv).map(|(current, voltage)| {
|
|
||||||
// Power (W) = Voltage (V) * Current (A)
|
|
||||||
// (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W
|
|
||||||
current as f64 * voltage as f64 / 1e12
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self.threshold_config = POWER_SUPPLY_THRESHOLD_CONFIGS
|
|
||||||
.iter()
|
|
||||||
.find(|config| {
|
|
||||||
self.path.join(config.path_start).exists()
|
|
||||||
&& self.path.join(config.path_end).exists()
|
|
||||||
})
|
|
||||||
.copied();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn charge_threshold_path_start(&self) -> Option<PathBuf> {
|
Ok(())
|
||||||
self.threshold_config
|
}
|
||||||
.map(|config| self.path.join(config.path_start))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn charge_threshold_path_end(&self) -> Option<PathBuf> {
|
pub fn charge_threshold_path_start(&self) -> Option<PathBuf> {
|
||||||
self.threshold_config
|
self
|
||||||
.map(|config| self.path.join(config.path_end))
|
.threshold_config
|
||||||
}
|
.map(|config| self.path.join(config.path_start))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_charge_threshold_start(
|
pub fn charge_threshold_path_end(&self) -> Option<PathBuf> {
|
||||||
&mut self,
|
self
|
||||||
charge_threshold_start: f64,
|
.threshold_config
|
||||||
) -> anyhow::Result<()> {
|
.map(|config| self.path.join(config.path_end))
|
||||||
fs::write(
|
}
|
||||||
&self.charge_threshold_path_start().ok_or_else(|| {
|
|
||||||
anyhow!(
|
pub fn set_charge_threshold_start(
|
||||||
"power supply '{name}' does not support changing charge threshold levels",
|
&mut self,
|
||||||
name = self.name,
|
charge_threshold_start: f64,
|
||||||
)
|
) -> anyhow::Result<()> {
|
||||||
})?,
|
fs::write(
|
||||||
&((charge_threshold_start * 100.0) as u8).to_string(),
|
&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<()> {
|
pub fn set_charge_threshold_end(
|
||||||
fs::write(
|
&mut self,
|
||||||
&self.charge_threshold_path_end().ok_or_else(|| {
|
charge_threshold_end: f64,
|
||||||
anyhow!(
|
) -> anyhow::Result<()> {
|
||||||
"power supply '{name}' does not support changing charge threshold levels",
|
fs::write(
|
||||||
name = self.name,
|
&self.charge_threshold_path_end().ok_or_else(|| {
|
||||||
)
|
anyhow!(
|
||||||
})?,
|
"power supply '{name}' does not support changing charge threshold \
|
||||||
&((charge_threshold_end * 100.0) as u8).to_string(),
|
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<Vec<String>> {
|
||||||
|
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`]: <https://docs.kernel.org/userspace-api/sysfs-platform_profile.html>
|
||||||
|
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<Vec<String>> {
|
fs::write("/sys/firmware/acpi/platform_profile", profile).context(
|
||||||
let path = "/sys/firmware/acpi/platform_profile_choices";
|
"this probably means that your system does not support changing ACPI \
|
||||||
|
profiles",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
let Some(content) =
|
pub fn platform_profile() -> anyhow::Result<String> {
|
||||||
fs::read(path).context("failed to read available ACPI platform profiles")?
|
fs::read("/sys/firmware/acpi/platform_profile")
|
||||||
else {
|
.context("failed to read platform profile")?
|
||||||
return Ok(Vec::new());
|
.context("failed to find platform profile")
|
||||||
};
|
}
|
||||||
|
|
||||||
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`]: <https://docs.kernel.org/userspace-api/sysfs-platform_profile.html>
|
|
||||||
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<String> {
|
|
||||||
fs::read("/sys/firmware/acpi/platform_profile")
|
|
||||||
.context("failed to read platform profile")?
|
|
||||||
.context("failed to find platform profile")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
714
src/system.rs
714
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)]
|
#[derive(Debug)]
|
||||||
pub struct System {
|
pub struct System {
|
||||||
pub is_ac: bool,
|
pub is_ac: bool,
|
||||||
|
|
||||||
pub load_average_1min: f64,
|
pub load_average_1min: f64,
|
||||||
pub load_average_5min: f64,
|
pub load_average_5min: f64,
|
||||||
pub load_average_15min: f64,
|
pub load_average_15min: f64,
|
||||||
|
|
||||||
pub cpus: Vec<cpu::Cpu>,
|
pub cpus: Vec<cpu::Cpu>,
|
||||||
pub cpu_temperatures: HashMap<u32, f64>,
|
pub cpu_temperatures: HashMap<u32, f64>,
|
||||||
|
|
||||||
pub power_supplies: Vec<power_supply::PowerSupply>,
|
pub power_supplies: Vec<power_supply::PowerSupply>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl System {
|
impl System {
|
||||||
pub fn new() -> anyhow::Result<Self> {
|
pub fn new() -> anyhow::Result<Self> {
|
||||||
let mut system = Self {
|
let mut system = Self {
|
||||||
is_ac: false,
|
is_ac: false,
|
||||||
|
|
||||||
cpus: Vec::new(),
|
cpus: Vec::new(),
|
||||||
cpu_temperatures: HashMap::new(),
|
cpu_temperatures: HashMap::new(),
|
||||||
|
|
||||||
power_supplies: Vec::new(),
|
power_supplies: Vec::new(),
|
||||||
|
|
||||||
load_average_1min: 0.0,
|
load_average_1min: 0.0,
|
||||||
load_average_5min: 0.0,
|
load_average_5min: 0.0,
|
||||||
load_average_15min: 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::<i64>(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<()> {
|
self.cpu_temperatures = temperatures;
|
||||||
log::debug!("rescanning view of system hardware...");
|
|
||||||
|
|
||||||
{
|
Ok(())
|
||||||
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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
fn get_temperatures(
|
||||||
let start = Instant::now();
|
device_path: &Path,
|
||||||
self.power_supplies =
|
temperatures: &mut HashMap<u32, f64>,
|
||||||
power_supply::PowerSupply::all().context("failed to scan power supplies")?;
|
) -> anyhow::Result<()> {
|
||||||
log::debug!(
|
// Increased range to handle systems with many sensors.
|
||||||
"rescanned all power supplies in {millis}ms",
|
for i in 1..=96 {
|
||||||
millis = start.elapsed().as_millis(),
|
let label_path = device_path.join(format!("temp{i}_label"));
|
||||||
);
|
let input_path = device_path.join(format!("temp{i}_input"));
|
||||||
}
|
|
||||||
|
|
||||||
self.is_ac = self
|
if !label_path.exists() || !input_path.exists() {
|
||||||
.power_supplies
|
log::debug!(
|
||||||
.iter()
|
"{label_path} or {input_path} doesn't exist, skipping temp label",
|
||||||
.any(|power_supply| power_supply.is_ac())
|
label_path = label_path.display(),
|
||||||
|| {
|
input_path = input_path.display(),
|
||||||
log::debug!(
|
);
|
||||||
"checking whether if this device is a desktop to determine if it is AC as no power supplies are"
|
continue;
|
||||||
);
|
}
|
||||||
|
|
||||||
let start = Instant::now();
|
log::debug!(
|
||||||
let is_desktop = self.is_desktop()?;
|
"{label_path} or {input_path} exists, scanning temp label...",
|
||||||
log::debug!(
|
label_path = label_path.display(),
|
||||||
"checked if is a desktop in {millis}ms",
|
input_path = input_path.display(),
|
||||||
millis = start.elapsed().as_millis(),
|
);
|
||||||
);
|
|
||||||
|
|
||||||
log::debug!(
|
let Some(label) = fs::read(&label_path).with_context(|| {
|
||||||
"scan result: {elaborate}",
|
format!(
|
||||||
elaborate = if is_desktop {
|
"failed to read hardware hardware device label from '{path}'",
|
||||||
"is a desktop, therefore is AC"
|
path = label_path.display(),
|
||||||
} else {
|
)
|
||||||
"not a desktop, and not AC"
|
})?
|
||||||
},
|
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();
|
||||||
|
|
||||||
{
|
log::debug!(
|
||||||
let start = Instant::now();
|
"stripped 'Core' or similar identifier prefix of label content: \
|
||||||
self.rescan_load_average()?;
|
{number}"
|
||||||
log::debug!(
|
);
|
||||||
"rescanned load average in {millis}ms",
|
|
||||||
millis = start.elapsed().as_millis(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
let Ok(number) = number.parse::<u32>() else {
|
||||||
let start = Instant::now();
|
log::debug!("stripped content not a valid number, skipping");
|
||||||
self.rescan_temperatures()?;
|
continue;
|
||||||
log::debug!(
|
};
|
||||||
"rescanned temperatures in {millis}ms",
|
log::debug!(
|
||||||
millis = start.elapsed().as_millis(),
|
"stripped content is a valid number, taking it as the core number"
|
||||||
);
|
);
|
||||||
}
|
log::debug!(
|
||||||
|
|
||||||
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::<i64>(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<u32, f64>,
|
|
||||||
) -> 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::<u32>() 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"
|
"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::<i64>(&input_path).with_context(|| {
|
let Some(temperature_mc) =
|
||||||
format!(
|
fs::read_n::<i64>(&input_path).with_context(|| {
|
||||||
"failed to read CPU temperature from '{path}'",
|
format!(
|
||||||
path = input_path.display(),
|
"failed to read CPU temperature from '{path}'",
|
||||||
)
|
path = input_path.display(),
|
||||||
})?
|
)
|
||||||
else {
|
})?
|
||||||
continue;
|
else {
|
||||||
};
|
continue;
|
||||||
log::debug!(
|
};
|
||||||
"temperature content: {celcius} celcius",
|
log::debug!(
|
||||||
celcius = temperature_mc as f64 / 1000.0
|
"temperature content: {celsius} celsius",
|
||||||
);
|
celsius = temperature_mc as f64 / 1000.0
|
||||||
|
);
|
||||||
|
|
||||||
temperatures.insert(number, temperature_mc as f64 / 1000.0);
|
temperatures.insert(number, temperature_mc as f64 / 1000.0);
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_desktop(&mut self) -> anyhow::Result<bool> {
|
Ok(())
|
||||||
log::debug!("checking chassis type to determine if we are a desktop");
|
}
|
||||||
if let Some(chassis_type) =
|
|
||||||
fs::read("/sys/class/dmi/id/chassis_type").context("failed to read chassis type")?
|
|
||||||
{
|
|
||||||
// 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower,
|
|
||||||
// 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In One,
|
|
||||||
// 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis,
|
|
||||||
// 31=Convertible Laptop
|
|
||||||
match chassis_type.trim() {
|
|
||||||
// Desktop form factors.
|
|
||||||
"3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => {
|
|
||||||
log::debug!("chassis is a desktop form factor, short circuting true");
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Laptop form factors.
|
fn is_desktop(&mut self) -> anyhow::Result<bool> {
|
||||||
"9" | "10" | "14" | "31" => {
|
log::debug!("checking chassis type to determine if we are a desktop");
|
||||||
log::debug!("chassis is a laptop form factor, short circuting false");
|
if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type")
|
||||||
return Ok(false);
|
.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
|
// Laptop form factors.
|
||||||
_ => log::debug!("unknown chassis type"),
|
"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
|
// Unknown, continue with other checks
|
||||||
let laptop_acpi_paths = [
|
_ => log::debug!("unknown chassis type"),
|
||||||
"/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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rescan_load_average(&mut self) -> anyhow::Result<()> {
|
// Check battery-specific ACPI paths that laptops typically have
|
||||||
let content = fs::read("/proc/loadavg")
|
let laptop_acpi_paths = [
|
||||||
.context("failed to read load average from '/proc/loadavg'")?
|
"/sys/class/power_supply/BAT0",
|
||||||
.context("'/proc/loadavg' doesn't exist, are you on linux?")?;
|
"/sys/class/power_supply/BAT1",
|
||||||
|
"/proc/acpi/battery",
|
||||||
|
];
|
||||||
|
|
||||||
let mut parts = content.split_whitespace();
|
log::debug!("checking existence of ACPI paths");
|
||||||
|
for path in laptop_acpi_paths {
|
||||||
let (Some(load_average_1min), Some(load_average_5min), Some(load_average_15min)) =
|
if fs::exists(path) {
|
||||||
(parts.next(), parts.next(), parts.next())
|
log::debug!("path '{path}' exists, short circuting false");
|
||||||
else {
|
return Ok(false); // Likely a laptop.
|
||||||
bail!(
|
}
|
||||||
"failed to parse first 3 load average entries due to there not being enough, content: {content}"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
self.load_average_1min = load_average_1min
|
|
||||||
.parse()
|
|
||||||
.context("failed to parse load average")?;
|
|
||||||
self.load_average_5min = load_average_5min
|
|
||||||
.parse()
|
|
||||||
.context("failed to parse load average")?;
|
|
||||||
self.load_average_15min = load_average_15min
|
|
||||||
.parse()
|
|
||||||
.context("failed to parse load average")?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log::debug!("checking if power saving paths exists");
|
||||||
|
// Check CPU power policies, desktops often don't have these
|
||||||
|
let power_saving_exists =
|
||||||
|
fs::exists("/sys/module/intel_pstate/parameters/no_hwp")
|
||||||
|
|| fs::exists("/sys/devices/system/cpu/cpufreq/conservative");
|
||||||
|
|
||||||
|
if !power_saving_exists {
|
||||||
|
log::debug!("power saving paths do not exist, short circuting true");
|
||||||
|
return Ok(true); // Likely a desktop.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to assuming desktop if we can't determine.
|
||||||
|
log::debug!(
|
||||||
|
"cannot determine whether if we are a desktop, defaulting to true"
|
||||||
|
);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rescan_load_average(&mut self) -> anyhow::Result<()> {
|
||||||
|
let content = fs::read("/proc/loadavg")
|
||||||
|
.context("failed to read load average from '/proc/loadavg'")?
|
||||||
|
.context("'/proc/loadavg' doesn't exist, are you on linux?")?;
|
||||||
|
|
||||||
|
let mut parts = content.split_whitespace();
|
||||||
|
|
||||||
|
let (
|
||||||
|
Some(load_average_1min),
|
||||||
|
Some(load_average_5min),
|
||||||
|
Some(load_average_15min),
|
||||||
|
) = (parts.next(), parts.next(), parts.next())
|
||||||
|
else {
|
||||||
|
bail!(
|
||||||
|
"failed to parse first 3 load average entries due to there not being \
|
||||||
|
enough, content: {content}"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.load_average_1min = load_average_1min
|
||||||
|
.parse()
|
||||||
|
.context("failed to parse load average")?;
|
||||||
|
self.load_average_5min = load_average_5min
|
||||||
|
.parse()
|
||||||
|
.context("failed to parse load average")?;
|
||||||
|
self.load_average_15min = load_average_15min
|
||||||
|
.parse()
|
||||||
|
.context("failed to parse load average")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue