mirror of
https://github.com/RGBCube/watt
synced 2025-08-02 19:07:47 +00:00
Compare commits
58 commits
2dbc747868
...
3e235c089b
Author | SHA1 | Date | |
---|---|---|---|
3e235c089b | |||
0e8b40227b | |||
2c154cd589 | |||
917ed77255 | |||
b6dd9e78d4 | |||
a25ae59bde | |||
008e05b726 | |||
7503e235a3 | |||
c2325fa5ed | |||
1283e5be14 | |||
ee7ea6b86d | |||
303a84479c | |||
421d4aaacc | |||
fd3ae29dc5 | |||
2812baa77b | |||
a343e38d95 | |||
004b879672 | |||
4763b54c97 | |||
571f172cc2 | |||
07ca582760 | |||
99feb831a8 | |||
961d1dfcd7 | |||
d87237165b | |||
230967832b | |||
a514f1ba7a | |||
058ef997c6 | |||
1ab9aceced | |||
137f801d2b | |||
543e5a052e | |||
91cef3b8b1 | |||
c062327457 | |||
4fa59b7ed4 | |||
606cedb68a | |||
0de8105432 | |||
f3813230c5 | |||
b6d4e09c7f | |||
ca4b1dbc92 | |||
c073b640dc | |||
0d3a88be03 | |||
2995909544 | |||
2704379b42 | |||
0ed0f18bb3 | |||
a14d88cee7 | |||
004e8e2a9c | |||
6ef4da9113 | |||
6377480312 | |||
baef8af981 | |||
d0932ae78c | |||
87085f913b | |||
![]() |
da07011b02 | ||
![]() |
0f3d5d81dd | ||
![]() |
71cd443ba7 | ||
![]() |
3caaa22f3e | ||
![]() |
08c51b6296 | ||
![]() |
6b1af5cbab | ||
![]() |
7b375439bb | ||
![]() |
017793288a | ||
![]() |
55e04ea09e |
13 changed files with 733 additions and 914 deletions
116
Cargo.lock
generated
116
Cargo.lock
generated
|
@ -182,27 +182,6 @@ dependencies = [
|
|||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.3"
|
||||
|
@ -232,17 +211,6 @@ version = "1.0.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.3"
|
||||
|
@ -301,12 +269,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806"
|
||||
dependencies = [
|
||||
"jiff-static",
|
||||
"jiff-tzdb-platform",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -320,37 +286,12 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-tzdb"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524"
|
||||
|
||||
[[package]]
|
||||
name = "jiff-tzdb-platform"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
|
||||
dependencies = [
|
||||
"jiff-tzdb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.172"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.27"
|
||||
|
@ -391,12 +332,6 @@ version = "1.21.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.0"
|
||||
|
@ -430,17 +365,6 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libredox",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
|
@ -505,26 +429,6 @@ version = "0.11.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "superfreq"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"clap-verbosity-flag",
|
||||
"ctrlc",
|
||||
"derive_more",
|
||||
"dirs",
|
||||
"env_logger",
|
||||
"jiff",
|
||||
"log",
|
||||
"num_cpus",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"toml",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.101"
|
||||
|
@ -622,10 +526,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
name = "watt"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"clap-verbosity-flag",
|
||||
"ctrlc",
|
||||
"derive_more",
|
||||
"env_logger",
|
||||
"log",
|
||||
"num_cpus",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"toml",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
|
|
26
Cargo.toml
26
Cargo.toml
|
@ -1,23 +1,21 @@
|
|||
[package]
|
||||
name = "superfreq"
|
||||
name = "watt"
|
||||
description = "Modern CPU frequency and power management utility for Linux"
|
||||
version = "0.3.2"
|
||||
version = "0.4.0"
|
||||
edition = "2024"
|
||||
authors = ["NotAShelf <raf@notashelf.dev>"]
|
||||
authors = ["NotAShelf <raf@notashelf.dev>", "RGBCube <git@rgbcu.be>"]
|
||||
rust-version = "1.85"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
toml = "0.8"
|
||||
dirs = "6.0"
|
||||
clap = { version = "4.0", features = ["derive", "env"] }
|
||||
num_cpus = "1.16"
|
||||
ctrlc = "3.4"
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
thiserror = "2.0"
|
||||
anyhow = "1.0"
|
||||
jiff = "0.2.13"
|
||||
clap = { version = "4.0", features = ["derive", "env"] }
|
||||
clap-verbosity-flag = "3.0.2"
|
||||
yansi = { version = "1.0.1", features = ["detect-env", "detect-tty"] }
|
||||
ctrlc = "3.4"
|
||||
derive_more = { version = "2.0.1", features = ["full"] }
|
||||
env_logger = "0.11"
|
||||
log = "0.4"
|
||||
num_cpus = "1.16"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "2.0"
|
||||
toml = "0.8"
|
||||
yansi = { version = "1.0.1", features = ["detect-env", "detect-tty"] }
|
||||
|
|
51
build.rs
Normal file
51
build.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const MULTICALL_NAMES: &[&str] = &["cpu", "power"];
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
|
||||
let target = out_dir
|
||||
.parent() // target/debug/build/<pkg>-<hash>/out
|
||||
.and_then(|p| p.parent()) // target/debug/build/<pkg>-<hash>
|
||||
.and_then(|p| p.parent()) // target/debug/
|
||||
.ok_or("failed to find target directory")?;
|
||||
|
||||
let main_binary_name = env::var("CARGO_PKG_NAME")?;
|
||||
|
||||
let main_binary_path = target.join(&main_binary_name);
|
||||
|
||||
let mut errored = false;
|
||||
|
||||
for name in MULTICALL_NAMES {
|
||||
let hardlink_path = target.join(name);
|
||||
|
||||
if hardlink_path.exists() {
|
||||
if hardlink_path.is_dir() {
|
||||
fs::remove_dir_all(&hardlink_path)?;
|
||||
} else {
|
||||
fs::remove_file(&hardlink_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(error) = fs::hard_link(&main_binary_path, &hardlink_path) {
|
||||
println!(
|
||||
"cargo:warning=failed to create hard link '{path}': {error}",
|
||||
path = hardlink_path.display(),
|
||||
);
|
||||
errored = true;
|
||||
}
|
||||
}
|
||||
|
||||
if 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(())
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
[[rule]]
|
||||
priority = 0
|
||||
if = { value = "%cpu-usage", is-more-than = 0.7 }
|
||||
|
|
35
flake.nix
35
flake.nix
|
@ -6,21 +6,38 @@
|
|||
nixpkgs,
|
||||
...
|
||||
} @ inputs: let
|
||||
forAllSystems = nixpkgs.lib.genAttrs ["x86_64-linux"];
|
||||
pkgsForEach = nixpkgs.legacyPackages;
|
||||
forAllSystems = nixpkgs.lib.genAttrs ["x86_64-linux" "aarch64-linux"];
|
||||
pkgsForEach = forAllSystems (system:
|
||||
import nixpkgs {
|
||||
localSystem.system = system;
|
||||
overlays = [self.overlays.default];
|
||||
});
|
||||
in {
|
||||
packages = forAllSystems (system: {
|
||||
superfreq = pkgsForEach.${system}.callPackage ./nix/package.nix {};
|
||||
default = self.packages.${system}.superfreq;
|
||||
});
|
||||
overlays = {
|
||||
superfreq = final: _: {
|
||||
superfreq = final.callPackage ./nix/package.nix {};
|
||||
};
|
||||
default = self.overlays.superfreq;
|
||||
};
|
||||
|
||||
devShells = forAllSystems (system: {
|
||||
default = pkgsForEach.${system}.callPackage ./nix/shell.nix {};
|
||||
});
|
||||
packages =
|
||||
nixpkgs.lib.mapAttrs (system: pkgs: {
|
||||
inherit (pkgs) superfreq;
|
||||
default = self.packages.${system}.superfreq;
|
||||
})
|
||||
pkgsForEach;
|
||||
|
||||
devShells =
|
||||
nixpkgs.lib.mapAttrs (system: pkgs: {
|
||||
default = pkgs.callPackage ./nix/shell.nix {};
|
||||
})
|
||||
pkgsForEach;
|
||||
|
||||
nixosModules = {
|
||||
superfreq = import ./nix/module.nix inputs;
|
||||
default = self.nixosModules.superfreq;
|
||||
};
|
||||
|
||||
formatter = forAllSystems (system: nixpkgs.legacyPackages.${system}.alejandra);
|
||||
};
|
||||
}
|
||||
|
|
295
src/config.rs
295
src/config.rs
|
@ -155,92 +155,151 @@ impl PowerDelta {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
|
||||
#[serde(untagged, rename_all = "kebab-case")]
|
||||
macro_rules! named {
|
||||
($variant:ident => $value:literal) => {
|
||||
pub mod $variant {
|
||||
pub fn serialize<S: serde::Serializer>(serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_str($value)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D: serde::Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<(), D::Error> {
|
||||
struct Visitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for Visitor {
|
||||
type Value = ();
|
||||
|
||||
fn expecting(&self, writer: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
writer.write_str(concat!("\"", $value, "\""))
|
||||
}
|
||||
|
||||
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
|
||||
if value != $value {
|
||||
return Err(E::invalid_value(serde::de::Unexpected::Str(value), &self));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(Visitor)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mod expression {
|
||||
named!(cpu_usage => "%cpu-usage");
|
||||
named!(cpu_usage_volatility => "$cpu-usage-volatility");
|
||||
named!(cpu_temperature => "$cpu-temperature");
|
||||
named!(cpu_temperature_volatility => "$cpu-temperature-volatility");
|
||||
named!(cpu_idle_seconds => "$cpu-idle-seconds");
|
||||
|
||||
named!(power_supply_charge => "%power-supply-charge");
|
||||
named!(power_supply_discharge_rate => "%power-supply-discharge-rate");
|
||||
|
||||
named!(discharging => "?discharging");
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum Expression {
|
||||
#[serde(rename = "%cpu-usage")]
|
||||
#[serde(with = "expression::cpu_usage")]
|
||||
CpuUsage,
|
||||
|
||||
#[serde(rename = "$cpu-usage-volatility")]
|
||||
#[serde(with = "expression::cpu_usage_volatility")]
|
||||
CpuUsageVolatility,
|
||||
|
||||
#[serde(rename = "$cpu-temperature")]
|
||||
#[serde(with = "expression::cpu_temperature")]
|
||||
CpuTemperature,
|
||||
|
||||
#[serde(rename = "$cpu-temperature-volatility")]
|
||||
#[serde(with = "expression::cpu_temperature_volatility")]
|
||||
CpuTemperatureVolatility,
|
||||
|
||||
#[serde(rename = "$cpu-idle-seconds")]
|
||||
#[serde(with = "expression::cpu_idle_seconds")]
|
||||
CpuIdleSeconds,
|
||||
|
||||
#[serde(rename = "%power-supply-charge")]
|
||||
#[serde(with = "expression::power_supply_charge")]
|
||||
PowerSupplyCharge,
|
||||
|
||||
#[serde(rename = "%power-supply-discharge-rate")]
|
||||
#[serde(with = "expression::power_supply_discharge_rate")]
|
||||
PowerSupplyDischargeRate,
|
||||
|
||||
#[serde(rename = "?charging")]
|
||||
Charging,
|
||||
#[serde(rename = "?on-battery")]
|
||||
OnBattery,
|
||||
#[serde(with = "expression::discharging")]
|
||||
Discharging,
|
||||
|
||||
#[serde(rename = "#false")]
|
||||
False,
|
||||
|
||||
#[default]
|
||||
#[serde(rename = "#true")]
|
||||
True,
|
||||
Boolean(bool),
|
||||
|
||||
Number(f64),
|
||||
|
||||
Plus {
|
||||
value: Box<Expression>,
|
||||
plus: Box<Expression>,
|
||||
#[serde(rename = "value")]
|
||||
a: Box<Expression>,
|
||||
#[serde(rename = "plus")]
|
||||
b: Box<Expression>,
|
||||
},
|
||||
Minus {
|
||||
value: Box<Expression>,
|
||||
minus: Box<Expression>,
|
||||
#[serde(rename = "value")]
|
||||
a: Box<Expression>,
|
||||
#[serde(rename = "minus")]
|
||||
b: Box<Expression>,
|
||||
},
|
||||
Multiply {
|
||||
value: Box<Expression>,
|
||||
multiply: Box<Expression>,
|
||||
#[serde(rename = "value")]
|
||||
a: Box<Expression>,
|
||||
#[serde(rename = "multiply")]
|
||||
b: Box<Expression>,
|
||||
},
|
||||
Power {
|
||||
value: Box<Expression>,
|
||||
power: Box<Expression>,
|
||||
#[serde(rename = "value")]
|
||||
a: Box<Expression>,
|
||||
#[serde(rename = "power")]
|
||||
b: Box<Expression>,
|
||||
},
|
||||
Divide {
|
||||
value: Box<Expression>,
|
||||
divide: Box<Expression>,
|
||||
#[serde(rename = "value")]
|
||||
a: Box<Expression>,
|
||||
#[serde(rename = "divide")]
|
||||
b: Box<Expression>,
|
||||
},
|
||||
|
||||
LessThan {
|
||||
value: Box<Expression>,
|
||||
is_less_than: Box<Expression>,
|
||||
#[serde(rename = "value")]
|
||||
a: Box<Expression>,
|
||||
#[serde(rename = "is-less-than")]
|
||||
b: Box<Expression>,
|
||||
},
|
||||
|
||||
MoreThan {
|
||||
value: Box<Expression>,
|
||||
is_more_than: Box<Expression>,
|
||||
#[serde(rename = "value")]
|
||||
a: Box<Expression>,
|
||||
#[serde(rename = "is-more-than")]
|
||||
b: Box<Expression>,
|
||||
},
|
||||
|
||||
Equal {
|
||||
value: Box<Expression>,
|
||||
is_equal: Box<Expression>,
|
||||
#[serde(rename = "value")]
|
||||
a: Box<Expression>,
|
||||
#[serde(rename = "is-equal")]
|
||||
b: Box<Expression>,
|
||||
leeway: Box<Expression>,
|
||||
},
|
||||
|
||||
And {
|
||||
value: Box<Expression>,
|
||||
and: Box<Expression>,
|
||||
#[serde(rename = "value")]
|
||||
a: Box<Expression>,
|
||||
#[serde(rename = "and")]
|
||||
b: Box<Expression>,
|
||||
},
|
||||
All {
|
||||
all: Vec<Expression>,
|
||||
},
|
||||
|
||||
Or {
|
||||
value: Box<Expression>,
|
||||
or: Box<Expression>,
|
||||
#[serde(rename = "value")]
|
||||
a: Box<Expression>,
|
||||
#[serde(rename = "or")]
|
||||
b: Box<Expression>,
|
||||
},
|
||||
Any {
|
||||
any: Vec<Expression>,
|
||||
|
@ -251,25 +310,164 @@ pub enum Expression {
|
|||
},
|
||||
}
|
||||
|
||||
impl Default for Expression {
|
||||
fn default() -> Self {
|
||||
Self::Boolean(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl Expression {
|
||||
pub fn as_number(&self) -> anyhow::Result<f64> {
|
||||
let Self::Number(number) = self else {
|
||||
bail!("tried to cast '{self:?}' to a number, failed")
|
||||
};
|
||||
|
||||
Ok(*number)
|
||||
}
|
||||
|
||||
pub fn as_boolean(&self) -> anyhow::Result<bool> {
|
||||
let Self::Boolean(boolean) = self else {
|
||||
bail!("tried to cast '{self:?}' to a boolean, failed")
|
||||
};
|
||||
|
||||
Ok(*boolean)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct EvalState {
|
||||
pub cpu_usage: f64,
|
||||
pub cpu_usage_volatility: Option<f64>,
|
||||
pub cpu_temperature: f64,
|
||||
pub cpu_temperature_volatility: Option<f64>,
|
||||
pub cpu_idle_seconds: f64,
|
||||
|
||||
pub power_supply_charge: f64,
|
||||
pub power_supply_discharge_rate: Option<f64>,
|
||||
|
||||
pub discharging: bool,
|
||||
}
|
||||
|
||||
impl Expression {
|
||||
pub fn eval(&self, state: &EvalState) -> anyhow::Result<Option<Expression>> {
|
||||
use Expression::*;
|
||||
|
||||
macro_rules! try_ok {
|
||||
($expression:expr) => {
|
||||
match $expression {
|
||||
Some(value) => value,
|
||||
None => return Ok(None),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! eval {
|
||||
($expression:expr) => {
|
||||
try_ok!($expression.eval(state)?)
|
||||
};
|
||||
}
|
||||
|
||||
// [e8dax09]: This may be look inefficient, and it definitely isn't optimal,
|
||||
// but expressions in rules are usually so small that it doesn't matter or
|
||||
// make a perceiveable performance difference.
|
||||
//
|
||||
// We also want to be strict, instead of lazy in binary operations, because
|
||||
// we want to catch type errors immediately.
|
||||
//
|
||||
// FIXME: We currently cannot catch errors that will happen when propagating None.
|
||||
// You can have a type error go uncaught on first startup by using $cpu-usage-volatility
|
||||
// incorrectly, for example.
|
||||
Ok(Some(match self {
|
||||
CpuUsage => Number(state.cpu_usage),
|
||||
CpuUsageVolatility => Number(try_ok!(state.cpu_usage_volatility)),
|
||||
CpuTemperature => Number(state.cpu_temperature),
|
||||
CpuTemperatureVolatility => Number(try_ok!(state.cpu_temperature_volatility)),
|
||||
CpuIdleSeconds => Number(state.cpu_idle_seconds),
|
||||
|
||||
PowerSupplyCharge => Number(state.cpu_idle_seconds),
|
||||
PowerSupplyDischargeRate => Number(try_ok!(state.power_supply_discharge_rate)),
|
||||
|
||||
Discharging => Boolean(state.discharging),
|
||||
|
||||
literal @ (Boolean(_) | Number(_)) => literal.clone(),
|
||||
|
||||
Plus { a, b } => Number(eval!(a).as_number()? + eval!(b).as_number()?),
|
||||
Minus { a, b } => Number(eval!(a).as_number()? - eval!(b).as_number()?),
|
||||
Multiply { a, b } => Number(eval!(a).as_number()? * eval!(b).as_number()?),
|
||||
Power { a, b } => Number(eval!(a).as_number()?.powf(eval!(b).as_number()?)),
|
||||
Divide { a, b } => Number(eval!(a).as_number()? / eval!(b).as_number()?),
|
||||
|
||||
LessThan { a, b } => Boolean(eval!(a).as_number()? < eval!(b).as_number()?),
|
||||
MoreThan { a, b } => Boolean(eval!(a).as_number()? > eval!(b).as_number()?),
|
||||
Equal { a, b, leeway } => {
|
||||
let a = eval!(a).as_number()?;
|
||||
let b = eval!(b).as_number()?;
|
||||
let leeway = eval!(leeway).as_number()?;
|
||||
|
||||
let minimum = a - leeway;
|
||||
let maximum = a + leeway;
|
||||
|
||||
Boolean(minimum < b && b < maximum)
|
||||
}
|
||||
|
||||
And { a, b } => {
|
||||
let a = eval!(a).as_boolean()?;
|
||||
let b = eval!(b).as_boolean()?;
|
||||
|
||||
Boolean(a && b)
|
||||
}
|
||||
All { all } => {
|
||||
let mut result = true;
|
||||
|
||||
for value in all {
|
||||
let value = eval!(value).as_boolean()?;
|
||||
|
||||
result = result && value;
|
||||
}
|
||||
|
||||
Boolean(result)
|
||||
}
|
||||
Or { a, b } => {
|
||||
let a = eval!(a).as_boolean()?;
|
||||
let b = eval!(b).as_boolean()?;
|
||||
|
||||
Boolean(a || b)
|
||||
}
|
||||
Any { any } => {
|
||||
let mut result = false;
|
||||
|
||||
for value in any {
|
||||
let value = eval!(value).as_boolean()?;
|
||||
|
||||
result = result || value;
|
||||
}
|
||||
|
||||
Boolean(result)
|
||||
}
|
||||
Not { not } => Boolean(!eval!(not).as_boolean()?),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
|
||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||
pub struct Rule {
|
||||
priority: u8,
|
||||
pub priority: u8,
|
||||
|
||||
#[serde(default, rename = "if", skip_serializing_if = "is_default")]
|
||||
if_: Expression,
|
||||
pub condition: Expression,
|
||||
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
cpu: CpuDelta,
|
||||
pub cpu: CpuDelta,
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
power: PowerDelta,
|
||||
pub power: PowerDelta,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct DaemonConfig {
|
||||
#[serde(rename = "rule")]
|
||||
rules: Vec<Rule>,
|
||||
pub rules: Vec<Rule>,
|
||||
}
|
||||
|
||||
impl DaemonConfig {
|
||||
|
@ -278,7 +476,8 @@ impl DaemonConfig {
|
|||
format!("failed to read config from '{path}'", path = path.display())
|
||||
})?;
|
||||
|
||||
let config: Self = toml::from_str(&contents).context("failed to parse config file")?;
|
||||
let mut config: Self = toml::from_str(&contents)
|
||||
.with_context(|| format!("failed to parse file at '{path}'", path = path.display(),))?;
|
||||
|
||||
{
|
||||
let mut priorities = Vec::with_capacity(config.rules.len());
|
||||
|
@ -292,6 +491,10 @@ impl DaemonConfig {
|
|||
}
|
||||
}
|
||||
|
||||
config.rules.sort_by_key(|rule| rule.priority);
|
||||
|
||||
log::debug!("loaded config: {config:#?}");
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
|
51
src/core.rs
51
src/core.rs
|
@ -1,51 +0,0 @@
|
|||
pub struct SystemInfo {
|
||||
// Overall system details
|
||||
pub cpu_model: String,
|
||||
}
|
||||
|
||||
pub struct CpuCoreInfo {
|
||||
// Per-core data
|
||||
pub core_id: u32,
|
||||
pub temperature_celsius: Option<f32>,
|
||||
}
|
||||
|
||||
pub struct CpuGlobalInfo {
|
||||
// System-wide CPU settings
|
||||
pub epp: Option<String>, // Energy Performance Preference
|
||||
pub epb: Option<String>, // Energy Performance Bias
|
||||
pub average_temperature_celsius: Option<f32>, // Average temperature across all cores
|
||||
}
|
||||
|
||||
pub struct BatteryInfo {
|
||||
// Battery status (AC connected, charging state, capacity, power rate, charge start/stop thresholds if available).
|
||||
pub name: String,
|
||||
pub ac_connected: bool,
|
||||
pub charging_state: Option<String>, // e.g., "Charging", "Discharging", "Full"
|
||||
pub capacity_percent: Option<u8>,
|
||||
pub power_rate_watts: Option<f32>, // positive for charging, negative for discharging
|
||||
pub charge_start_threshold: Option<u8>,
|
||||
pub charge_stop_threshold: Option<u8>,
|
||||
}
|
||||
|
||||
pub struct SystemLoad {
|
||||
// System load averages.
|
||||
pub load_avg_1min: f32,
|
||||
pub load_avg_5min: f32,
|
||||
pub load_avg_15min: f32,
|
||||
}
|
||||
|
||||
pub struct SystemReport {
|
||||
// Now combine all the above for a snapshot of the system state.
|
||||
pub system_info: SystemInfo,
|
||||
pub cpu_cores: Vec<CpuCoreInfo>,
|
||||
pub cpu_global: CpuGlobalInfo,
|
||||
pub batteries: Vec<BatteryInfo>,
|
||||
pub system_load: SystemLoad,
|
||||
pub timestamp: std::time::SystemTime, // so we know when the report was generated
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum OperationalMode {
|
||||
Powersave,
|
||||
Performance,
|
||||
}
|
73
src/cpu.rs
73
src/cpu.rs
|
@ -1,14 +1,14 @@
|
|||
use anyhow::{Context, bail};
|
||||
use yansi::Paint as _;
|
||||
|
||||
use std::{cell::OnceCell, collections::HashMap, fmt, string::ToString};
|
||||
use std::{cell::OnceCell, collections::HashMap, fmt, mem, rc::Rc, string::ToString};
|
||||
|
||||
use crate::fs;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq)]
|
||||
pub struct CpuRescanCache {
|
||||
stat: OnceCell<HashMap<u32, CpuStat>>,
|
||||
temperatures: OnceCell<HashMap<u32, f64>>,
|
||||
info: OnceCell<HashMap<u32, Rc<HashMap<String, String>>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
@ -64,6 +64,7 @@ pub struct Cpu {
|
|||
pub epb: Option<String>,
|
||||
|
||||
pub stat: CpuStat,
|
||||
pub info: Option<Rc<HashMap<String, String>>>,
|
||||
|
||||
pub temperature: Option<f64>,
|
||||
}
|
||||
|
@ -105,6 +106,7 @@ impl Cpu {
|
|||
softirq: 0,
|
||||
steal: 0,
|
||||
},
|
||||
info: None,
|
||||
|
||||
temperature: None,
|
||||
};
|
||||
|
@ -172,7 +174,7 @@ impl Cpu {
|
|||
}
|
||||
|
||||
self.rescan_stat(cache)?;
|
||||
self.rescan_temperature(cache)?;
|
||||
self.rescan_info(cache)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -347,56 +349,53 @@ impl Cpu {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn rescan_temperature(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> {
|
||||
fn rescan_info(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> {
|
||||
// OnceCell::get_or_try_init is unstable. Cope:
|
||||
let temperatures = match cache.temperatures.get() {
|
||||
Some(temperature) => temperature,
|
||||
let info = match cache.info.get() {
|
||||
Some(stat) => stat,
|
||||
|
||||
None => {
|
||||
const PATH: &str = "/sys/class/hwmon";
|
||||
let content = fs::read("/proc/cpuinfo")
|
||||
.context("failed to read CPU info")?
|
||||
.context("/proc/cpuinfo does not exist")?;
|
||||
|
||||
let temperatures = HashMap::new();
|
||||
let mut info = HashMap::new();
|
||||
let mut current_number = None;
|
||||
let mut current_data = HashMap::new();
|
||||
|
||||
for entry in fs::read_dir(PATH)
|
||||
.with_context(|| format!("failed to read hardware information from '{PATH}'"))?
|
||||
.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;
|
||||
macro_rules! try_save_data {
|
||||
() => {
|
||||
if let Some(number) = current_number.take() {
|
||||
info.insert(number, Rc::new(mem::take(&mut current_data)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
match &*name {
|
||||
// Intel CPU temperature driver
|
||||
"coretemp" => todo!(),
|
||||
for line in content.lines() {
|
||||
let parts = line.splitn(2, ':').collect::<Vec<_>>();
|
||||
|
||||
// AMD CPU temperature driver
|
||||
// TODO: 'zenergy' can also report those stats, I think?
|
||||
"k10temp" | "zenpower" | "amdgpu" => todo!(),
|
||||
if parts.len() == 2 {
|
||||
let key = parts[0].trim();
|
||||
let value = parts[1].trim();
|
||||
|
||||
// Other CPU temperature drivers
|
||||
_ if name.contains("cpu") || name.contains("temp") => todo!(),
|
||||
if key == "processor" {
|
||||
try_save_data!();
|
||||
|
||||
_ => {}
|
||||
current_number = value.parse::<u32>().ok();
|
||||
} else {
|
||||
current_data.insert(key.to_owned(), value.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cache.temperatures.set(temperatures).unwrap();
|
||||
cache.temperatures.get().unwrap()
|
||||
try_save_data!();
|
||||
|
||||
cache.info.set(info).unwrap();
|
||||
cache.info.get().unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
self.temperature = temperatures.get(&self.number).copied();
|
||||
self.info = info.get(&self.number).cloned();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
170
src/daemon.rs
170
src/daemon.rs
|
@ -1,16 +1,18 @@
|
|||
use std::{
|
||||
collections::VecDeque,
|
||||
ops,
|
||||
cell::LazyCell,
|
||||
collections::{HashMap, VecDeque},
|
||||
ops::Deref,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
use crate::config;
|
||||
use crate::{config, system};
|
||||
|
||||
/// Calculate the idle time multiplier based on system idle time.
|
||||
///
|
||||
|
@ -42,8 +44,8 @@ struct Daemon {
|
|||
/// The last computed polling interval.
|
||||
last_polling_interval: Option<Duration>,
|
||||
|
||||
/// Whether if we are charging right now.
|
||||
charging: bool,
|
||||
/// The system state.
|
||||
system: system::System,
|
||||
|
||||
/// CPU usage and temperature log.
|
||||
cpu_log: VecDeque<CpuLog>,
|
||||
|
@ -52,6 +54,56 @@ struct Daemon {
|
|||
power_supply_log: VecDeque<PowerSupplyLog>,
|
||||
}
|
||||
|
||||
impl Daemon {
|
||||
fn rescan(&mut self) -> anyhow::Result<()> {
|
||||
self.system.rescan()?;
|
||||
|
||||
while self.cpu_log.len() > 99 {
|
||||
self.cpu_log.pop_front();
|
||||
}
|
||||
|
||||
self.cpu_log.push_back(CpuLog {
|
||||
at: Instant::now(),
|
||||
|
||||
usage: self
|
||||
.system
|
||||
.cpus
|
||||
.iter()
|
||||
.map(|cpu| cpu.stat.usage())
|
||||
.sum::<f64>()
|
||||
/ self.system.cpus.len() as f64,
|
||||
|
||||
temperature: self.system.cpu_temperatures.values().sum::<f64>()
|
||||
/ self.system.cpu_temperatures.len() as f64,
|
||||
});
|
||||
|
||||
let at = Instant::now();
|
||||
|
||||
let (charge_sum, charge_nr) =
|
||||
self.system
|
||||
.power_supplies
|
||||
.iter()
|
||||
.fold((0.0, 0u32), |(sum, count), power_supply| {
|
||||
if let Some(charge_percent) = power_supply.charge_percent {
|
||||
(sum + charge_percent, count + 1)
|
||||
} else {
|
||||
(sum, count)
|
||||
}
|
||||
});
|
||||
|
||||
while self.power_supply_log.len() > 99 {
|
||||
self.power_supply_log.pop_front();
|
||||
}
|
||||
|
||||
self.power_supply_log.push_back(PowerSupplyLog {
|
||||
at,
|
||||
charge: charge_sum / charge_nr as f64,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct CpuLog {
|
||||
at: Instant,
|
||||
|
||||
|
@ -63,8 +115,6 @@ struct CpuLog {
|
|||
}
|
||||
|
||||
struct CpuVolatility {
|
||||
at: ops::Range<Instant>,
|
||||
|
||||
usage: f64,
|
||||
|
||||
temperature: f64,
|
||||
|
@ -72,6 +122,17 @@ struct CpuVolatility {
|
|||
|
||||
impl Daemon {
|
||||
fn cpu_volatility(&self) -> Option<CpuVolatility> {
|
||||
let recent_log_count = self
|
||||
.cpu_log
|
||||
.iter()
|
||||
.rev()
|
||||
.take_while(|log| log.at.elapsed() < Duration::from_secs(5 * 60))
|
||||
.count();
|
||||
|
||||
if recent_log_count < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.cpu_log.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
@ -91,8 +152,6 @@ impl Daemon {
|
|||
}
|
||||
|
||||
Some(CpuVolatility {
|
||||
at: self.cpu_log.front().unwrap().at..self.cpu_log.back().unwrap().at,
|
||||
|
||||
usage: usage_change_sum / change_count as f64,
|
||||
temperature: temperature_change_sum / change_count as f64,
|
||||
})
|
||||
|
@ -134,6 +193,13 @@ struct PowerSupplyLog {
|
|||
}
|
||||
|
||||
impl Daemon {
|
||||
fn discharging(&self) -> bool {
|
||||
self.system
|
||||
.power_supplies
|
||||
.iter()
|
||||
.any(|power_supply| power_supply.charge_state.as_deref() == Some("Discharging"))
|
||||
}
|
||||
|
||||
/// Calculates the discharge rate, returns a number between 0 and 1.
|
||||
///
|
||||
/// The discharge rate is averaged per hour.
|
||||
|
@ -181,7 +247,7 @@ impl Daemon {
|
|||
let mut interval = Duration::from_secs(5);
|
||||
|
||||
// We are on battery, so we must be more conservative with our polling.
|
||||
if !self.charging {
|
||||
if self.discharging() {
|
||||
match self.power_supply_discharge_rate() {
|
||||
Some(discharge_rate) => {
|
||||
if discharge_rate > 0.2 {
|
||||
|
@ -243,6 +309,8 @@ impl Daemon {
|
|||
}
|
||||
|
||||
pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> {
|
||||
assert!(config.rules.is_sorted_by_key(|rule| rule.priority));
|
||||
|
||||
log::info!("starting daemon...");
|
||||
|
||||
let cancelled = Arc::new(AtomicBool::new(false));
|
||||
|
@ -254,7 +322,87 @@ pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> {
|
|||
})
|
||||
.context("failed to set Ctrl-C handler")?;
|
||||
|
||||
while !cancelled.load(Ordering::SeqCst) {}
|
||||
let mut daemon = Daemon {
|
||||
last_user_activity: Instant::now(),
|
||||
|
||||
last_polling_interval: None,
|
||||
|
||||
system: system::System::new()?,
|
||||
|
||||
cpu_log: VecDeque::new(),
|
||||
power_supply_log: VecDeque::new(),
|
||||
};
|
||||
|
||||
while !cancelled.load(Ordering::SeqCst) {
|
||||
daemon.rescan()?;
|
||||
|
||||
let sleep_until = Instant::now() + daemon.polling_interval();
|
||||
|
||||
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(),
|
||||
};
|
||||
|
||||
let mut cpu_delta_for = HashMap::<u32, config::CpuDelta>::new();
|
||||
let all_cpus = LazyCell::new(|| (0..num_cpus::get() as u32).collect::<Vec<_>>());
|
||||
|
||||
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() {
|
||||
delta.apply()?;
|
||||
}
|
||||
|
||||
if let Some(delay) = sleep_until.checked_duration_since(Instant::now()) {
|
||||
thread::sleep(delay);
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("exiting...");
|
||||
|
||||
|
|
|
@ -1,426 +0,0 @@
|
|||
use anyhow::Context;
|
||||
use anyhow::bail;
|
||||
|
||||
use crate::config::AppConfig;
|
||||
use crate::core::SystemReport;
|
||||
use crate::engine;
|
||||
use crate::monitor;
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Tracks historical system data for "advanced" adaptive polling
|
||||
#[derive(Debug)]
|
||||
struct SystemHistory {
|
||||
/// Last several CPU usage measurements
|
||||
cpu_usage_history: VecDeque<f32>,
|
||||
/// Last several temperature readings
|
||||
temperature_history: VecDeque<f32>,
|
||||
/// Time of last detected user activity
|
||||
last_user_activity: Instant,
|
||||
/// Previous battery percentage (to calculate discharge rate)
|
||||
last_battery_percentage: Option<f32>,
|
||||
/// Timestamp of last battery reading
|
||||
last_battery_timestamp: Option<Instant>,
|
||||
/// Battery discharge rate (%/hour)
|
||||
battery_discharge_rate: Option<f32>,
|
||||
/// Time spent in each system state
|
||||
state_durations: std::collections::HashMap<SystemState, Duration>,
|
||||
/// Last time a state transition happened
|
||||
last_state_change: Instant,
|
||||
/// Current system state
|
||||
current_state: SystemState,
|
||||
/// Last computed optimal polling interval
|
||||
last_computed_interval: Option<u64>,
|
||||
}
|
||||
|
||||
impl SystemHistory {
|
||||
/// Update system history with new report data
|
||||
fn update(&mut self, report: &SystemReport) {
|
||||
// Update CPU usage history
|
||||
if !report.cpu_cores.is_empty() {
|
||||
let mut total_usage: f32 = 0.0;
|
||||
let mut core_count: usize = 0;
|
||||
|
||||
for core in &report.cpu_cores {
|
||||
if let Some(usage) = core.usage_percent {
|
||||
total_usage += usage;
|
||||
core_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if core_count > 0 {
|
||||
let avg_usage = total_usage / core_count as f32;
|
||||
|
||||
// Keep only the last 5 measurements
|
||||
if self.cpu_usage_history.len() >= 5 {
|
||||
self.cpu_usage_history.pop_front();
|
||||
}
|
||||
self.cpu_usage_history.push_back(avg_usage);
|
||||
|
||||
// Update last_user_activity if CPU usage indicates activity
|
||||
// Consider significant CPU usage or sudden change as user activity
|
||||
if avg_usage > 20.0
|
||||
|| (self.cpu_usage_history.len() > 1
|
||||
&& (avg_usage - self.cpu_usage_history[self.cpu_usage_history.len() - 2])
|
||||
.abs()
|
||||
> 15.0)
|
||||
{
|
||||
self.last_user_activity = Instant::now();
|
||||
log::debug!("User activity detected based on CPU usage");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update temperature history
|
||||
if let Some(temp) = report.cpu_global.average_temperature_celsius {
|
||||
if self.temperature_history.len() >= 5 {
|
||||
self.temperature_history.pop_front();
|
||||
}
|
||||
self.temperature_history.push_back(temp);
|
||||
|
||||
// Significant temperature increase can indicate user activity
|
||||
if self.temperature_history.len() > 1 {
|
||||
let temp_change =
|
||||
temp - self.temperature_history[self.temperature_history.len() - 2];
|
||||
if temp_change > 5.0 {
|
||||
// 5°C rise in temperature
|
||||
self.last_user_activity = Instant::now();
|
||||
log::debug!("User activity detected based on temperature change");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update battery discharge rate
|
||||
if let Some(battery) = report.batteries.first() {
|
||||
// Reset when we are charging or have just connected AC
|
||||
if battery.ac_connected {
|
||||
// Reset discharge tracking but continue updating the rest of
|
||||
// the history so we still detect activity/load changes on AC.
|
||||
self.battery_discharge_rate = None;
|
||||
self.last_battery_percentage = None;
|
||||
self.last_battery_timestamp = None;
|
||||
}
|
||||
|
||||
if let Some(current_percentage) = battery.capacity_percent {
|
||||
let current_percent = f32::from(current_percentage);
|
||||
|
||||
if let (Some(last_percentage), Some(last_timestamp)) =
|
||||
(self.last_battery_percentage, self.last_battery_timestamp)
|
||||
{
|
||||
let elapsed_hours = last_timestamp.elapsed().as_secs_f32() / 3600.0;
|
||||
// Only calculate discharge rate if at least 30 seconds have passed
|
||||
// and we're not on AC power
|
||||
if elapsed_hours > 0.0083 && !battery.ac_connected {
|
||||
// 0.0083 hours = 30 seconds
|
||||
// Calculate discharge rate in percent per hour
|
||||
let percent_change = last_percentage - current_percent;
|
||||
if percent_change > 0.0 {
|
||||
// Only if battery is discharging
|
||||
let hourly_rate = percent_change / elapsed_hours;
|
||||
// Clamp the discharge rate to a reasonable maximum value (100%/hour)
|
||||
let clamped_rate = hourly_rate.min(100.0);
|
||||
self.battery_discharge_rate = Some(clamped_rate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.last_battery_percentage = Some(current_percent);
|
||||
self.last_battery_timestamp = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
// Update system state tracking
|
||||
let new_state = determine_system_state(report, self);
|
||||
if new_state != self.current_state {
|
||||
// Record time spent in previous state
|
||||
let time_in_state = self.last_state_change.elapsed();
|
||||
*self
|
||||
.state_durations
|
||||
.entry(self.current_state.clone())
|
||||
.or_insert(Duration::ZERO) += time_in_state;
|
||||
|
||||
// State changes (except to Idle) likely indicate user activity
|
||||
if new_state != SystemState::Idle && new_state != SystemState::LowLoad {
|
||||
self.last_user_activity = Instant::now();
|
||||
log::debug!("User activity detected based on system state change to {new_state:?}");
|
||||
}
|
||||
|
||||
// Update state
|
||||
self.current_state = new_state;
|
||||
self.last_state_change = Instant::now();
|
||||
}
|
||||
|
||||
// Check for significant load changes
|
||||
if report.system_load.load_avg_1min > 1.0 {
|
||||
self.last_user_activity = Instant::now();
|
||||
log::debug!("User activity detected based on system load");
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate CPU usage volatility (how much it's changing)
|
||||
fn get_cpu_volatility(&self) -> f32 {
|
||||
if self.cpu_usage_history.len() < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut sum_of_changes = 0.0;
|
||||
for i in 1..self.cpu_usage_history.len() {
|
||||
sum_of_changes += (self.cpu_usage_history[i] - self.cpu_usage_history[i - 1]).abs();
|
||||
}
|
||||
|
||||
sum_of_changes / (self.cpu_usage_history.len() - 1) as f32
|
||||
}
|
||||
|
||||
/// Calculate temperature volatility
|
||||
fn get_temperature_volatility(&self) -> f32 {
|
||||
if self.temperature_history.len() < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut sum_of_changes = 0.0;
|
||||
for i in 1..self.temperature_history.len() {
|
||||
sum_of_changes += (self.temperature_history[i] - self.temperature_history[i - 1]).abs();
|
||||
}
|
||||
|
||||
sum_of_changes / (self.temperature_history.len() - 1) as f32
|
||||
}
|
||||
|
||||
/// Determine if the system appears to be idle
|
||||
fn is_system_idle(&self) -> bool {
|
||||
if self.cpu_usage_history.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// System considered idle if the average CPU usage of last readings is below 10%
|
||||
let recent_avg =
|
||||
self.cpu_usage_history.iter().sum::<f32>() / self.cpu_usage_history.len() as f32;
|
||||
recent_avg < 10.0 && self.get_cpu_volatility() < 5.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the daemon
|
||||
pub fn run_daemon(config: AppConfig) -> anyhow::Result<()> {
|
||||
log::info!("Starting superfreq daemon...");
|
||||
|
||||
// Validate critical configuration values before proceeding
|
||||
validate_poll_intervals(
|
||||
config.daemon.min_poll_interval_sec,
|
||||
config.daemon.max_poll_interval_sec,
|
||||
)?;
|
||||
|
||||
// Create a flag that will be set to true when a signal is received
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
let r = running.clone();
|
||||
|
||||
// Set up signal handlers
|
||||
ctrlc::set_handler(move || {
|
||||
log::info!("Received shutdown signal, exiting...");
|
||||
r.store(false, Ordering::SeqCst);
|
||||
})
|
||||
.context("failed to set Ctrl-C handler")?;
|
||||
|
||||
log::info!(
|
||||
"Daemon initialized with poll interval: {}s",
|
||||
config.daemon.poll_interval_sec
|
||||
);
|
||||
|
||||
// Set up stats file if configured
|
||||
if let Some(stats_path) = &config.daemon.stats_file_path {
|
||||
log::info!("Stats will be written to: {stats_path}");
|
||||
}
|
||||
|
||||
// Variables for adaptive polling
|
||||
// Make sure that the poll interval is *never* zero to prevent a busy loop
|
||||
let mut current_poll_interval = config.daemon.poll_interval_sec.max(1);
|
||||
if config.daemon.poll_interval_sec == 0 {
|
||||
log::warn!(
|
||||
"Poll interval is set to zero in config, using 1s minimum to prevent a busy loop"
|
||||
);
|
||||
}
|
||||
let mut system_history = SystemHistory::default();
|
||||
|
||||
// Main loop
|
||||
while running.load(Ordering::SeqCst) {
|
||||
let start_time = Instant::now();
|
||||
|
||||
match monitor::collect_system_report(&config) {
|
||||
Ok(report) => {
|
||||
log::debug!("Collected system report, applying settings...");
|
||||
|
||||
// Store the current state before updating history
|
||||
let previous_state = system_history.current_state.clone();
|
||||
|
||||
// Update system history with new data
|
||||
system_history.update(&report);
|
||||
|
||||
// Update the stats file if configured
|
||||
if let Some(stats_path) = &config.daemon.stats_file_path {
|
||||
if let Err(e) = write_stats_file(stats_path, &report) {
|
||||
log::error!("Failed to write stats file: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
match engine::determine_and_apply_settings(&report, &config, None) {
|
||||
Ok(()) => {
|
||||
log::debug!("Successfully applied system settings");
|
||||
|
||||
// If system state changed, log the new state
|
||||
if system_history.current_state != previous_state {
|
||||
log::info!(
|
||||
"System state changed to: {:?}",
|
||||
system_history.current_state
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error applying system settings: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we're on battery
|
||||
let on_battery = !report.batteries.is_empty()
|
||||
&& report.batteries.first().is_some_and(|b| !b.ac_connected);
|
||||
|
||||
// Calculate optimal polling interval if adaptive polling is enabled
|
||||
if config.daemon.adaptive_interval {
|
||||
match system_history.calculate_optimal_interval(&config, on_battery) {
|
||||
Ok(optimal_interval) => {
|
||||
// Store the new interval
|
||||
system_history.last_computed_interval = Some(optimal_interval);
|
||||
|
||||
log::debug!("Recalculated optimal interval: {optimal_interval}s");
|
||||
|
||||
// Don't change the interval too dramatically at once
|
||||
match optimal_interval.cmp(¤t_poll_interval) {
|
||||
std::cmp::Ordering::Greater => {
|
||||
current_poll_interval =
|
||||
(current_poll_interval + optimal_interval) / 2;
|
||||
}
|
||||
std::cmp::Ordering::Less => {
|
||||
current_poll_interval = current_poll_interval
|
||||
- ((current_poll_interval - optimal_interval) / 2).max(1);
|
||||
}
|
||||
std::cmp::Ordering::Equal => {
|
||||
// No change needed when they're equal
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// Log the error and stop the daemon when an invalid configuration is detected
|
||||
log::error!("Critical configuration error: {e}");
|
||||
running.store(false, Ordering::SeqCst);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that we respect the (user) configured min and max limits
|
||||
current_poll_interval = current_poll_interval.clamp(
|
||||
config.daemon.min_poll_interval_sec,
|
||||
config.daemon.max_poll_interval_sec,
|
||||
);
|
||||
|
||||
log::debug!("Adaptive polling: set interval to {current_poll_interval}s");
|
||||
} else {
|
||||
// If adaptive polling is disabled, still apply battery-saving adjustment
|
||||
if config.daemon.throttle_on_battery && on_battery {
|
||||
let battery_multiplier = 2; // poll half as often on battery
|
||||
|
||||
// We need to make sure `poll_interval_sec` is *at least* 1
|
||||
// before multiplying.
|
||||
let safe_interval = config.daemon.poll_interval_sec.max(1);
|
||||
current_poll_interval = (safe_interval * battery_multiplier)
|
||||
.min(config.daemon.max_poll_interval_sec);
|
||||
|
||||
log::debug!(
|
||||
"On battery power, increased poll interval to {current_poll_interval}s"
|
||||
);
|
||||
} else {
|
||||
// Use the configured poll interval
|
||||
current_poll_interval = config.daemon.poll_interval_sec.max(1);
|
||||
if config.daemon.poll_interval_sec == 0 {
|
||||
log::debug!(
|
||||
"Using minimum poll interval of 1s instead of configured 0s"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error collecting system report: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// Sleep for the remaining time in the poll interval
|
||||
let elapsed = start_time.elapsed();
|
||||
let poll_duration = Duration::from_secs(current_poll_interval);
|
||||
if elapsed < poll_duration {
|
||||
let sleep_time = poll_duration - elapsed;
|
||||
log::debug!("Sleeping for {}s until next cycle", sleep_time.as_secs());
|
||||
std::thread::sleep(sleep_time);
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("Daemon stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Simplified system state used for determining when to adjust polling interval
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)]
|
||||
enum SystemState {
|
||||
#[default]
|
||||
Unknown,
|
||||
OnAC,
|
||||
OnBattery,
|
||||
HighLoad,
|
||||
LowLoad,
|
||||
HighTemp,
|
||||
Idle,
|
||||
}
|
||||
|
||||
/// Determine the current system state for adaptive polling
|
||||
fn determine_system_state(report: &SystemReport, history: &SystemHistory) -> SystemState {
|
||||
// Check power state first
|
||||
if !report.batteries.is_empty() {
|
||||
if let Some(battery) = report.batteries.first() {
|
||||
if battery.ac_connected {
|
||||
return SystemState::OnAC;
|
||||
}
|
||||
return SystemState::OnBattery;
|
||||
}
|
||||
}
|
||||
|
||||
// No batteries means desktop, so always AC
|
||||
if report.batteries.is_empty() {
|
||||
return SystemState::OnAC;
|
||||
}
|
||||
|
||||
// Check temperature
|
||||
if let Some(temp) = report.cpu_global.average_temperature_celsius {
|
||||
if temp > 80.0 {
|
||||
return SystemState::HighTemp;
|
||||
}
|
||||
}
|
||||
|
||||
// Check load first, as high load should take precedence over idle state
|
||||
let avg_load = report.system_load.load_avg_1min;
|
||||
if avg_load > 3.0 {
|
||||
return SystemState::HighLoad;
|
||||
}
|
||||
|
||||
// Check idle state only if we don't have high load
|
||||
if history.is_system_idle() {
|
||||
return SystemState::Idle;
|
||||
}
|
||||
|
||||
// Check for low load
|
||||
if avg_load < 0.5 {
|
||||
return SystemState::LowLoad;
|
||||
}
|
||||
|
||||
// Default case
|
||||
SystemState::Unknown
|
||||
}
|
67
src/main.rs
67
src/main.rs
|
@ -21,30 +21,52 @@ use yansi::Paint as _;
|
|||
#[derive(clap::Parser, Debug)]
|
||||
#[clap(author, version, about)]
|
||||
struct Cli {
|
||||
#[command(flatten)]
|
||||
verbosity: clap_verbosity_flag::Verbosity,
|
||||
|
||||
#[clap(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(clap::Parser, Debug)]
|
||||
#[clap(multicall = true)]
|
||||
enum Command {
|
||||
/// Display information.
|
||||
Info,
|
||||
/// Watt daemon.
|
||||
Watt {
|
||||
#[command(flatten)]
|
||||
verbosity: clap_verbosity_flag::Verbosity,
|
||||
|
||||
/// Start the daemon.
|
||||
Start {
|
||||
/// The daemon config path.
|
||||
#[arg(long, env = "WATT_CONFIG")]
|
||||
config: PathBuf,
|
||||
},
|
||||
|
||||
/// Modify CPU attributes.
|
||||
CpuSet(config::CpuDelta),
|
||||
/// CPU metadata and modification utility.
|
||||
Cpu {
|
||||
#[command(flatten)]
|
||||
verbosity: clap_verbosity_flag::Verbosity,
|
||||
|
||||
#[clap(subcommand)]
|
||||
command: CpuCommand,
|
||||
},
|
||||
|
||||
/// Power supply metadata and modification utility.
|
||||
Power {
|
||||
#[command(flatten)]
|
||||
verbosity: clap_verbosity_flag::Verbosity,
|
||||
|
||||
#[clap(subcommand)]
|
||||
command: PowerCommand,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(clap::Parser, Debug)]
|
||||
enum CpuCommand {
|
||||
/// Modify CPU attributes.
|
||||
Set(config::CpuDelta),
|
||||
}
|
||||
|
||||
#[derive(clap::Parser, Debug)]
|
||||
enum PowerCommand {
|
||||
/// Modify power supply attributes.
|
||||
PowerSet(config::PowerDelta),
|
||||
Set(config::PowerDelta),
|
||||
}
|
||||
|
||||
fn real_main() -> anyhow::Result<()> {
|
||||
|
@ -52,24 +74,33 @@ fn real_main() -> anyhow::Result<()> {
|
|||
|
||||
yansi::whenever(yansi::Condition::TTY_AND_COLOR);
|
||||
|
||||
let (Command::Watt { verbosity, .. }
|
||||
| Command::Cpu { verbosity, .. }
|
||||
| Command::Power { verbosity, .. }) = cli.command;
|
||||
|
||||
env_logger::Builder::new()
|
||||
.filter_level(cli.verbosity.log_level_filter())
|
||||
.filter_level(verbosity.log_level_filter())
|
||||
.format_timestamp(None)
|
||||
.format_module_path(false)
|
||||
.init();
|
||||
|
||||
match cli.command {
|
||||
Command::Info => todo!(),
|
||||
|
||||
Command::Start { config } => {
|
||||
let config = config::DaemonConfig::load_from(&config)
|
||||
.context("failed to load daemon config file")?;
|
||||
Command::Watt { config, .. } => {
|
||||
let config =
|
||||
config::DaemonConfig::load_from(&config).context("failed to load daemon config")?;
|
||||
|
||||
daemon::run(config)
|
||||
}
|
||||
|
||||
Command::CpuSet(delta) => delta.apply(),
|
||||
Command::PowerSet(delta) => delta.apply(),
|
||||
Command::Cpu {
|
||||
command: CpuCommand::Set(delta),
|
||||
..
|
||||
} => delta.apply(),
|
||||
|
||||
Command::Power {
|
||||
command: PowerCommand::Set(delta),
|
||||
..
|
||||
} => delta.apply(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
223
src/monitor.rs
223
src/monitor.rs
|
@ -1,203 +1,24 @@
|
|||
use crate::config::AppConfig;
|
||||
use crate::core::{BatteryInfo, CpuCoreInfo, CpuGlobalInfo, SystemInfo, SystemLoad, SystemReport};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
thread,
|
||||
time::Duration,
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
pub fn get_system_info() -> SystemInfo {
|
||||
let cpu_model = get_cpu_model().unwrap_or_else(|_| "Unknown".to_string());
|
||||
|
||||
SystemInfo { cpu_model }
|
||||
}
|
||||
|
||||
pub fn get_cpu_core_info(core_id: u32) -> anyhow::Result<CpuCoreInfo> {
|
||||
// Temperature detection.
|
||||
// Should be generic enough to be able to support for multiple hardware sensors
|
||||
// with the possibility of extending later down the road.
|
||||
let mut temperature_celsius: Option<f32> = None;
|
||||
|
||||
// Search for temperature in hwmon devices
|
||||
if let Ok(hwmon_dir) = fs::read_dir("/sys/class/hwmon") {
|
||||
for hw_entry in hwmon_dir.flatten() {
|
||||
let hw_path = hw_entry.path();
|
||||
|
||||
// Check hwmon driver name
|
||||
if let Ok(name) = read_sysfs_file_trimmed(hw_path.join("name")) {
|
||||
// Intel CPU temperature driver
|
||||
if name == "coretemp" {
|
||||
if let Some(temp) = get_temperature_for_core(&hw_path, core_id, "Core") {
|
||||
temperature_celsius = Some(temp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// AMD CPU temperature driver
|
||||
// TODO: 'zenergy' can also report those stats, I think?
|
||||
else if name == "k10temp" || name == "zenpower" || name == "amdgpu" {
|
||||
// AMD's k10temp doesn't always label cores individually
|
||||
// First try to find core-specific temps
|
||||
if let Some(temp) = get_temperature_for_core(&hw_path, core_id, "Tdie") {
|
||||
temperature_celsius = Some(temp);
|
||||
break;
|
||||
}
|
||||
|
||||
// Try Tctl temperature (CPU control temp)
|
||||
if let Some(temp) = get_generic_sensor_temperature(&hw_path, "Tctl") {
|
||||
temperature_celsius = Some(temp);
|
||||
break;
|
||||
}
|
||||
|
||||
// Try CPU temperature
|
||||
if let Some(temp) = get_generic_sensor_temperature(&hw_path, "CPU") {
|
||||
temperature_celsius = Some(temp);
|
||||
break;
|
||||
}
|
||||
|
||||
// Fall back to any available temperature input without a specific label
|
||||
temperature_celsius = get_fallback_temperature(&hw_path);
|
||||
if temperature_celsius.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Other CPU temperature drivers
|
||||
else if name.contains("cpu") || name.contains("temp") {
|
||||
// Try to find a label that matches this core
|
||||
if let Some(temp) = get_temperature_for_core(&hw_path, core_id, "Core") {
|
||||
temperature_celsius = Some(temp);
|
||||
break;
|
||||
}
|
||||
|
||||
// Fall back to any temperature reading if specific core not found
|
||||
temperature_celsius = get_fallback_temperature(&hw_path);
|
||||
if temperature_celsius.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try /sys/devices/platform paths for thermal zones as a last resort
|
||||
if temperature_celsius.is_none() {
|
||||
if let Ok(thermal_zones) = fs::read_dir("/sys/devices/virtual/thermal") {
|
||||
for entry in thermal_zones.flatten() {
|
||||
let zone_path = entry.path();
|
||||
let name = entry.file_name().into_string().unwrap_or_default();
|
||||
// if temperature_celsius.is_none() {
|
||||
// if let Ok(thermal_zones) = fs::read_dir("/sys/devices/virtual/thermal") {
|
||||
// for entry in thermal_zones.flatten() {
|
||||
// let zone_path = entry.path();
|
||||
// let name = entry.file_name().into_string().unwrap_or_default();
|
||||
|
||||
if name.starts_with("thermal_zone") {
|
||||
// Try to match by type
|
||||
if let Ok(zone_type) = read_sysfs_file_trimmed(zone_path.join("type")) {
|
||||
if zone_type.contains("cpu")
|
||||
|| zone_type.contains("x86")
|
||||
|| zone_type.contains("core")
|
||||
{
|
||||
if let Ok(temp_mc) = read_sysfs_value::<i32>(zone_path.join("temp")) {
|
||||
temperature_celsius = Some(temp_mc as f32 / 1000.0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CpuCoreInfo {
|
||||
core_id,
|
||||
temperature_celsius,
|
||||
})
|
||||
}
|
||||
|
||||
/// Finds core-specific temperature
|
||||
fn get_temperature_for_core(hw_path: &Path, core_id: u32, label_prefix: &str) -> Option<f32> {
|
||||
for i in 1..=32 {
|
||||
// Increased range to handle systems with many sensors
|
||||
let label_path = hw_path.join(format!("temp{i}_label"));
|
||||
let input_path = hw_path.join(format!("temp{i}_input"));
|
||||
|
||||
if label_path.exists() && input_path.exists() {
|
||||
if let Ok(label) = read_sysfs_file_trimmed(&label_path) {
|
||||
// Match various common label formats:
|
||||
// "Core X", "core X", "Core-X", "CPU Core X", etc.
|
||||
let core_pattern = format!("{label_prefix} {core_id}");
|
||||
let alt_pattern = format!("{label_prefix}-{core_id}");
|
||||
|
||||
if label.eq_ignore_ascii_case(&core_pattern)
|
||||
|| label.eq_ignore_ascii_case(&alt_pattern)
|
||||
|| label
|
||||
.to_lowercase()
|
||||
.contains(&format!("core {core_id}").to_lowercase())
|
||||
{
|
||||
if let Ok(temp_mc) = read_sysfs_value::<i32>(&input_path) {
|
||||
return Some(temp_mc as f32 / 1000.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// Finds generic sensor temperatures by label
|
||||
fn get_generic_sensor_temperature(hw_path: &Path, label_name: &str) -> Option<f32> {
|
||||
for i in 1..=32 {
|
||||
let label_path = hw_path.join(format!("temp{i}_label"));
|
||||
let input_path = hw_path.join(format!("temp{i}_input"));
|
||||
|
||||
if label_path.exists() && input_path.exists() {
|
||||
if let Ok(label) = read_sysfs_file_trimmed(&label_path) {
|
||||
if label.eq_ignore_ascii_case(label_name)
|
||||
|| label.to_lowercase().contains(&label_name.to_lowercase())
|
||||
{
|
||||
if let Ok(temp_mc) = read_sysfs_value::<i32>(&input_path) {
|
||||
return Some(temp_mc as f32 / 1000.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if !label_path.exists() && input_path.exists() {
|
||||
// Some sensors might not have labels but still have valid temp inputs
|
||||
if let Ok(temp_mc) = read_sysfs_value::<i32>(&input_path) {
|
||||
return Some(temp_mc as f32 / 1000.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// Fallback to any temperature reading from a sensor
|
||||
fn get_fallback_temperature(hw_path: &Path) -> Option<f32> {
|
||||
for i in 1..=32 {
|
||||
let input_path = hw_path.join(format!("temp{i}_input"));
|
||||
|
||||
if input_path.exists() {
|
||||
if let Ok(temp_mc) = read_sysfs_value::<i32>(&input_path) {
|
||||
return Some(temp_mc as f32 / 1000.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_cpu_model() -> anyhow::Result<String> {
|
||||
let path = Path::new("/proc/cpuinfo");
|
||||
let content = fs::read_to_string(path).map_err(|_| {
|
||||
SysMonitorError::ReadError(format!("Cannot read contents of {}.", path.display()))
|
||||
})?;
|
||||
|
||||
for line in content.lines() {
|
||||
if line.starts_with("model name") {
|
||||
if let Some(val) = line.split(':').nth(1) {
|
||||
let cpu_model = val.trim().to_string();
|
||||
return Ok(cpu_model);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(SysMonitorError::ParseError(
|
||||
"Could not find CPU model name in /proc/cpuinfo.".to_string(),
|
||||
))
|
||||
}
|
||||
// if name.starts_with("thermal_zone") {
|
||||
// // Try to match by type
|
||||
// if let Ok(zone_type) = read_sysfs_file_trimmed(zone_path.join("type")) {
|
||||
// if zone_type.contains("cpu")
|
||||
// || zone_type.contains("x86")
|
||||
// || zone_type.contains("core")
|
||||
// {
|
||||
// if let Ok(temp_mc) = read_sysfs_value::<i32>(zone_path.join("temp")) {
|
||||
// temperature_celsius = Some(temp_mc as f32 / 1000.0);
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
|
111
src/system.rs
111
src/system.rs
|
@ -1,3 +1,5 @@
|
|||
use std::{collections::HashMap, path::Path};
|
||||
|
||||
use anyhow::{Context, bail};
|
||||
|
||||
use crate::{cpu, fs, power_supply};
|
||||
|
@ -10,6 +12,8 @@ pub struct System {
|
|||
pub load_average_15min: f64,
|
||||
|
||||
pub cpus: Vec<cpu::Cpu>,
|
||||
pub cpu_temperatures: HashMap<u32, f64>,
|
||||
|
||||
pub power_supplies: Vec<power_supply::PowerSupply>,
|
||||
}
|
||||
|
||||
|
@ -19,6 +23,8 @@ impl System {
|
|||
is_ac: false,
|
||||
|
||||
cpus: Vec::new(),
|
||||
cpu_temperatures: HashMap::new(),
|
||||
|
||||
power_supplies: Vec::new(),
|
||||
|
||||
load_average_1min: 0.0,
|
||||
|
@ -44,6 +50,111 @@ impl System {
|
|||
|| self.is_desktop()?;
|
||||
|
||||
self.rescan_load_average()?;
|
||||
self.rescan_temperatures()?;
|
||||
|
||||
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)
|
||||
.with_context(|| format!("failed to read hardware information from '{PATH}'"))?
|
||||
.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)?;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
continue;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
// 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("tdie")
|
||||
.trim_start_matches("Tdie")
|
||||
.trim_start()
|
||||
.trim_start_matches("tctl")
|
||||
.trim_start_matches("Tctl")
|
||||
.trim_start()
|
||||
.trim_start_matches("-")
|
||||
.trim();
|
||||
|
||||
let Ok(number) = number.parse::<u32>() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(temperature_mc) = fs::read_n::<i64>(&input_path).with_context(|| {
|
||||
format!(
|
||||
"failed to read CPU temperature from '{path}'",
|
||||
path = input_path.display(),
|
||||
)
|
||||
})?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
temperatures.insert(number, temperature_mc as f64 / 1000.0);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue