mirror of
https://github.com/RGBCube/superfreq
synced 2025-08-03 04:17:45 +00:00
Compare commits
7 commits
a1502009d5
...
cc0cc23b0d
Author | SHA1 | Date | |
---|---|---|---|
cc0cc23b0d | |||
c69aba87b6 | |||
5559d08f3e | |||
d61564d5f5 | |||
d1247c1570 | |||
bc343eefd9 | |||
8f3abd1ca3 |
13 changed files with 680 additions and 1389 deletions
265
src/cli/debug.rs
265
src/cli/debug.rs
|
@ -1,265 +0,0 @@
|
||||||
use crate::config::AppConfig;
|
|
||||||
use crate::cpu;
|
|
||||||
use crate::monitor;
|
|
||||||
use crate::util::error::AppError;
|
|
||||||
use std::fs;
|
|
||||||
use std::process::{Command, Stdio};
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
/// Prints comprehensive debug information about the system
|
|
||||||
pub fn run_debug(config: &AppConfig) -> Result<(), AppError> {
|
|
||||||
println!("=== SUPERFREQ DEBUG INFORMATION ===");
|
|
||||||
println!("Version: {}", env!("CARGO_PKG_VERSION"));
|
|
||||||
|
|
||||||
// Current date and time
|
|
||||||
println!("Timestamp: {}", jiff::Timestamp::now());
|
|
||||||
|
|
||||||
// Kernel information
|
|
||||||
if let Ok(kernel_info) = get_kernel_info() {
|
|
||||||
println!("Kernel Version: {kernel_info}");
|
|
||||||
} else {
|
|
||||||
println!("Kernel Version: Unable to determine");
|
|
||||||
}
|
|
||||||
|
|
||||||
// System uptime
|
|
||||||
if let Ok(uptime) = get_system_uptime() {
|
|
||||||
println!(
|
|
||||||
"System Uptime: {} hours, {} minutes",
|
|
||||||
uptime.as_secs() / 3600,
|
|
||||||
(uptime.as_secs() % 3600) / 60
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
println!("System Uptime: Unable to determine");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get system information
|
|
||||||
match monitor::collect_system_report(config) {
|
|
||||||
Ok(report) => {
|
|
||||||
println!("\n--- SYSTEM INFORMATION ---");
|
|
||||||
println!("CPU Model: {}", report.system_info.cpu_model);
|
|
||||||
println!("Architecture: {}", report.system_info.architecture);
|
|
||||||
println!(
|
|
||||||
"Linux Distribution: {}",
|
|
||||||
report.system_info.linux_distribution
|
|
||||||
);
|
|
||||||
|
|
||||||
println!("\n--- CONFIGURATION ---");
|
|
||||||
println!("Current Configuration: {config:#?}");
|
|
||||||
|
|
||||||
// Print important sysfs paths and whether they exist
|
|
||||||
println!("\n--- SYSFS PATHS ---");
|
|
||||||
check_and_print_sysfs_path(
|
|
||||||
"/sys/devices/system/cpu/intel_pstate/no_turbo",
|
|
||||||
"Intel P-State Turbo Control",
|
|
||||||
);
|
|
||||||
check_and_print_sysfs_path(
|
|
||||||
"/sys/devices/system/cpu/cpufreq/boost",
|
|
||||||
"Generic CPU Boost Control",
|
|
||||||
);
|
|
||||||
check_and_print_sysfs_path(
|
|
||||||
"/sys/devices/system/cpu/amd_pstate/cpufreq/boost",
|
|
||||||
"AMD P-State Boost Control",
|
|
||||||
);
|
|
||||||
check_and_print_sysfs_path(
|
|
||||||
"/sys/firmware/acpi/platform_profile",
|
|
||||||
"ACPI Platform Profile Control",
|
|
||||||
);
|
|
||||||
check_and_print_sysfs_path("/sys/class/power_supply", "Power Supply Information");
|
|
||||||
|
|
||||||
println!("\n--- CPU INFORMATION ---");
|
|
||||||
println!("Current Governor: {:?}", report.cpu_global.current_governor);
|
|
||||||
println!(
|
|
||||||
"Available Governors: {}",
|
|
||||||
report.cpu_global.available_governors.join(", ")
|
|
||||||
);
|
|
||||||
println!("Turbo Status: {:?}", report.cpu_global.turbo_status);
|
|
||||||
println!(
|
|
||||||
"Energy Performance Preference (EPP): {:?}",
|
|
||||||
report.cpu_global.epp
|
|
||||||
);
|
|
||||||
println!("Energy Performance Bias (EPB): {:?}", report.cpu_global.epb);
|
|
||||||
|
|
||||||
// Add governor override information
|
|
||||||
if let Some(override_governor) = cpu::get_governor_override() {
|
|
||||||
println!("Governor Override: {}", override_governor.trim());
|
|
||||||
} else {
|
|
||||||
println!("Governor Override: None");
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("\n--- PLATFORM PROFILE ---");
|
|
||||||
println!(
|
|
||||||
"Current Platform Profile: {:?}",
|
|
||||||
report.cpu_global.platform_profile
|
|
||||||
);
|
|
||||||
match cpu::get_platform_profiles() {
|
|
||||||
Ok(profiles) => println!("Available Platform Profiles: {}", profiles.join(", ")),
|
|
||||||
Err(_) => println!("Available Platform Profiles: Not supported on this system"),
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("\n--- CPU CORES DETAIL ---");
|
|
||||||
println!("Total CPU Cores: {}", report.cpu_cores.len());
|
|
||||||
for core in &report.cpu_cores {
|
|
||||||
println!("Core {}:", core.core_id);
|
|
||||||
println!(
|
|
||||||
" Current Frequency: {} MHz",
|
|
||||||
core.current_frequency_mhz
|
|
||||||
.map_or_else(|| "N/A".to_string(), |f| f.to_string())
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
" Min Frequency: {} MHz",
|
|
||||||
core.min_frequency_mhz
|
|
||||||
.map_or_else(|| "N/A".to_string(), |f| f.to_string())
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
" Max Frequency: {} MHz",
|
|
||||||
core.max_frequency_mhz
|
|
||||||
.map_or_else(|| "N/A".to_string(), |f| f.to_string())
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
" Usage: {}%",
|
|
||||||
core.usage_percent
|
|
||||||
.map_or_else(|| "N/A".to_string(), |u| format!("{u:.1}"))
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
" Temperature: {}°C",
|
|
||||||
core.temperature_celsius
|
|
||||||
.map_or_else(|| "N/A".to_string(), |t| format!("{t:.1}"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("\n--- TEMPERATURE INFORMATION ---");
|
|
||||||
println!(
|
|
||||||
"Average CPU Temperature: {}",
|
|
||||||
report.cpu_global.average_temperature_celsius.map_or_else(
|
|
||||||
|| "N/A (CPU temperature sensor not detected)".to_string(),
|
|
||||||
|t| format!("{t:.1}°C")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
println!("\n--- BATTERY INFORMATION ---");
|
|
||||||
if report.batteries.is_empty() {
|
|
||||||
println!("No batteries found or all are ignored.");
|
|
||||||
} else {
|
|
||||||
for battery in &report.batteries {
|
|
||||||
println!("Battery: {}", battery.name);
|
|
||||||
println!(" AC Connected: {}", battery.ac_connected);
|
|
||||||
println!(
|
|
||||||
" Charging State: {}",
|
|
||||||
battery.charging_state.as_deref().unwrap_or("N/A")
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
" Capacity: {}%",
|
|
||||||
battery
|
|
||||||
.capacity_percent
|
|
||||||
.map_or_else(|| "N/A".to_string(), |c| c.to_string())
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
" Power Rate: {} W",
|
|
||||||
battery
|
|
||||||
.power_rate_watts
|
|
||||||
.map_or_else(|| "N/A".to_string(), |p| format!("{p:.2}"))
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
" Charge Start Threshold: {}",
|
|
||||||
battery
|
|
||||||
.charge_start_threshold
|
|
||||||
.map_or_else(|| "N/A".to_string(), |t| t.to_string())
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
" Charge Stop Threshold: {}",
|
|
||||||
battery
|
|
||||||
.charge_stop_threshold
|
|
||||||
.map_or_else(|| "N/A".to_string(), |t| t.to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("\n--- SYSTEM LOAD ---");
|
|
||||||
println!(
|
|
||||||
"Load Average (1 min): {:.2}",
|
|
||||||
report.system_load.load_avg_1min
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"Load Average (5 min): {:.2}",
|
|
||||||
report.system_load.load_avg_5min
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"Load Average (15 min): {:.2}",
|
|
||||||
report.system_load.load_avg_15min
|
|
||||||
);
|
|
||||||
|
|
||||||
println!("\n--- DAEMON STATUS ---");
|
|
||||||
// Simple check for daemon status - can be expanded later
|
|
||||||
let daemon_status = fs::metadata("/var/run/superfreq.pid").is_ok();
|
|
||||||
println!("Daemon Running: {daemon_status}");
|
|
||||||
|
|
||||||
// Check for systemd service status
|
|
||||||
if let Ok(systemd_status) = is_systemd_service_active("superfreq") {
|
|
||||||
println!("Systemd Service Active: {systemd_status}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(e) => Err(AppError::Monitor(e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get kernel version information
|
|
||||||
fn get_kernel_info() -> Result<String, AppError> {
|
|
||||||
let output = Command::new("uname")
|
|
||||||
.arg("-r")
|
|
||||||
.output()
|
|
||||||
.map_err(AppError::Io)?;
|
|
||||||
|
|
||||||
let kernel_version = String::from_utf8(output.stdout)
|
|
||||||
.map_err(|e| AppError::Generic(format!("Failed to parse kernel version: {e}")))?;
|
|
||||||
Ok(kernel_version.trim().to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get system uptime
|
|
||||||
fn get_system_uptime() -> Result<Duration, AppError> {
|
|
||||||
let uptime_str = fs::read_to_string("/proc/uptime").map_err(AppError::Io)?;
|
|
||||||
let uptime_secs = uptime_str
|
|
||||||
.split_whitespace()
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| AppError::Generic("Invalid format in /proc/uptime file".to_string()))?
|
|
||||||
.parse::<f64>()
|
|
||||||
.map_err(|e| AppError::Generic(format!("Failed to parse uptime from /proc/uptime: {e}")))?;
|
|
||||||
|
|
||||||
Ok(Duration::from_secs_f64(uptime_secs))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a sysfs path exists and print its status
|
|
||||||
fn check_and_print_sysfs_path(path: &str, description: &str) {
|
|
||||||
let exists = std::path::Path::new(path).exists();
|
|
||||||
println!(
|
|
||||||
"{}: {} ({})",
|
|
||||||
description,
|
|
||||||
path,
|
|
||||||
if exists { "Exists" } else { "Not Found" }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a systemd service is active
|
|
||||||
fn is_systemd_service_active(service_name: &str) -> Result<bool, AppError> {
|
|
||||||
let output = Command::new("systemctl")
|
|
||||||
.arg("is-active")
|
|
||||||
.arg(format!("{service_name}.service"))
|
|
||||||
.stdout(Stdio::piped()) // capture stdout instead of letting it print
|
|
||||||
.stderr(Stdio::null()) // redirect stderr to null
|
|
||||||
.output()
|
|
||||||
.map_err(AppError::Io)?;
|
|
||||||
|
|
||||||
// Check if the command executed successfully
|
|
||||||
if !output.status.success() {
|
|
||||||
// Command failed - service is either not found or not active
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Command executed successfully, now check the output content
|
|
||||||
let status = String::from_utf8(output.stdout)
|
|
||||||
.map_err(|e| AppError::Generic(format!("Failed to parse systemctl output: {e}")))?;
|
|
||||||
|
|
||||||
// Explicitly verify the output is "active"
|
|
||||||
Ok(status.trim() == "active")
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
pub mod debug;
|
|
|
@ -2,7 +2,9 @@
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use crate::config::types::{AppConfig, AppConfigToml, ConfigError, DaemonConfig, ProfileConfig};
|
use anyhow::Context as _;
|
||||||
|
|
||||||
|
use crate::config::types::{AppConfig, AppConfigToml, DaemonConfig, ProfileConfig};
|
||||||
|
|
||||||
/// The primary function to load application configuration from a specific path or from default locations.
|
/// The primary function to load application configuration from a specific path or from default locations.
|
||||||
///
|
///
|
||||||
|
@ -14,22 +16,23 @@ use crate::config::types::{AppConfig, AppConfigToml, ConfigError, DaemonConfig,
|
||||||
///
|
///
|
||||||
/// * `Ok(AppConfig)` - Successfully loaded configuration
|
/// * `Ok(AppConfig)` - Successfully loaded configuration
|
||||||
/// * `Err(ConfigError)` - Error loading or parsing configuration
|
/// * `Err(ConfigError)` - Error loading or parsing configuration
|
||||||
pub fn load_config() -> Result<AppConfig, ConfigError> {
|
pub fn load_config() -> anyhow::Result<AppConfig> {
|
||||||
load_config_from_path(None)
|
load_config_from_path(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load configuration from a specific path or try default paths
|
/// Load configuration from a specific path or try default paths
|
||||||
pub fn load_config_from_path(specific_path: Option<&str>) -> Result<AppConfig, ConfigError> {
|
pub fn load_config_from_path(specific_path: Option<&str>) -> anyhow::Result<AppConfig> {
|
||||||
// If a specific path is provided, only try that one
|
// If a specific path is provided, only try that one
|
||||||
if let Some(path_str) = specific_path {
|
if let Some(path_str) = specific_path {
|
||||||
let path = Path::new(path_str);
|
let path = Path::new(path_str);
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
return load_and_parse_config(path);
|
return load_and_parse_config(path);
|
||||||
}
|
}
|
||||||
return Err(ConfigError::Io(std::io::Error::new(
|
|
||||||
|
Err(std::io::Error::new(
|
||||||
std::io::ErrorKind::NotFound,
|
std::io::ErrorKind::NotFound,
|
||||||
format!("Specified config file not found: {}", path.display()),
|
format!("Specified config file not found: {}", path.display()),
|
||||||
)));
|
))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for SUPERFREQ_CONFIG environment variable
|
// Check for SUPERFREQ_CONFIG environment variable
|
||||||
|
@ -79,10 +82,16 @@ pub fn load_config_from_path(specific_path: Option<&str>) -> Result<AppConfig, C
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load and parse a configuration file
|
/// Load and parse a configuration file
|
||||||
fn load_and_parse_config(path: &Path) -> Result<AppConfig, ConfigError> {
|
fn load_and_parse_config(path: &Path) -> anyhow::Result<AppConfig> {
|
||||||
let contents = fs::read_to_string(path).map_err(ConfigError::Io)?;
|
let contents = fs::read_to_string(path).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to read config file from '{path}'",
|
||||||
|
path = path.display(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let toml_app_config = toml::from_str::<AppConfigToml>(&contents).map_err(ConfigError::Toml)?;
|
let toml_app_config =
|
||||||
|
toml::from_str::<AppConfigToml>(&contents).context("failed to parse config toml")?;
|
||||||
|
|
||||||
// Handle inheritance of values from global to profile configs
|
// Handle inheritance of values from global to profile configs
|
||||||
let mut charger_profile = toml_app_config.charger.clone();
|
let mut charger_profile = toml_app_config.charger.clone();
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
|
use anyhow::bail;
|
||||||
// Configuration types and structures for superfreq
|
// Configuration types and structures for superfreq
|
||||||
use crate::core::TurboSetting;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
/// Defines constant-returning functions used for default values.
|
/// Defines constant-returning functions used for default values.
|
||||||
/// This hopefully reduces repetition since we have way too many default functions
|
/// This hopefully reduces repetition since we have way too many
|
||||||
/// that just return constants.
|
/// default functions that just return constants.
|
||||||
macro_rules! default_const {
|
macro_rules! default_const {
|
||||||
($name:ident, $type:ty, $value:expr) => {
|
($($name:ident -> $type:ty = $value:expr;)*) => {
|
||||||
|
$(
|
||||||
const fn $name() -> $type {
|
const fn $name() -> $type {
|
||||||
$value
|
$value
|
||||||
}
|
}
|
||||||
|
)*
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,34 +22,21 @@ pub struct PowerSupplyChargeThresholds {
|
||||||
pub stop: u8,
|
pub stop: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PowerSupplyChargeThresholds {
|
impl TryFrom<(u8, u8)> for PowerSupplyChargeThresholds {
|
||||||
pub fn new(start: u8, stop: u8) -> Result<Self, ConfigError> {
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from((start, stop): (u8, u8)) -> anyhow::Result<Self> {
|
||||||
if stop == 0 {
|
if stop == 0 {
|
||||||
return Err(ConfigError::Validation(
|
bail!("stop threshold must be greater than 0%");
|
||||||
"Stop threshold must be greater than 0%".to_string(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
if start >= stop {
|
if start >= stop {
|
||||||
return Err(ConfigError::Validation(format!(
|
bail!("start threshold ({start}) must be less than stop threshold ({stop})");
|
||||||
"Start threshold ({start}) must be less than stop threshold ({stop})"
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
if stop > 100 {
|
if stop > 100 {
|
||||||
return Err(ConfigError::Validation(format!(
|
bail!("stop threshold ({stop}) cannot exceed 100%");
|
||||||
"Stop threshold ({stop}) cannot exceed 100%"
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self { start, stop })
|
Ok(PowerSupplyChargeThresholds { start, stop })
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<(u8, u8)> for PowerSupplyChargeThresholds {
|
|
||||||
type Error = ConfigError;
|
|
||||||
|
|
||||||
fn try_from(values: (u8, u8)) -> Result<Self, Self::Error> {
|
|
||||||
let (start, stop) = values;
|
|
||||||
Self::new(start, stop)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +44,7 @@ impl TryFrom<(u8, u8)> for PowerSupplyChargeThresholds {
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
pub struct ProfileConfig {
|
pub struct ProfileConfig {
|
||||||
pub governor: Option<String>,
|
pub governor: Option<String>,
|
||||||
pub turbo: Option<TurboSetting>,
|
pub turbo: Option<bool>,
|
||||||
pub epp: Option<String>, // Energy Performance Preference (EPP)
|
pub epp: Option<String>, // Energy Performance Preference (EPP)
|
||||||
pub epb: Option<String>, // Energy Performance Bias (EPB) - usually an integer, but string for flexibility from sysfs
|
pub epb: Option<String>, // Energy Performance Bias (EPB) - usually an integer, but string for flexibility from sysfs
|
||||||
pub min_freq_mhz: Option<u32>,
|
pub min_freq_mhz: Option<u32>,
|
||||||
|
@ -73,7 +62,7 @@ impl Default for ProfileConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
governor: Some("schedutil".to_string()), // common sensible default (?)
|
governor: Some("schedutil".to_string()), // common sensible default (?)
|
||||||
turbo: Some(TurboSetting::Auto),
|
turbo: None,
|
||||||
epp: None, // defaults depend on governor and system
|
epp: None, // defaults depend on governor and system
|
||||||
epb: None, // defaults depend on governor and system
|
epb: None, // defaults depend on governor and system
|
||||||
min_freq_mhz: None, // no override
|
min_freq_mhz: None, // no override
|
||||||
|
@ -97,19 +86,6 @@ pub struct AppConfig {
|
||||||
pub daemon: DaemonConfig,
|
pub daemon: DaemonConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error type for config loading
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum ConfigError {
|
|
||||||
#[error("I/O error: {0}")]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
|
|
||||||
#[error("TOML parsing error: {0}")]
|
|
||||||
Toml(#[from] toml::de::Error),
|
|
||||||
|
|
||||||
#[error("Configuration validation error: {0}")]
|
|
||||||
Validation(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Intermediate structs for TOML parsing
|
// Intermediate structs for TOML parsing
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
pub struct ProfileConfigToml {
|
pub struct ProfileConfigToml {
|
||||||
|
@ -178,22 +154,14 @@ pub const DEFAULT_LOAD_THRESHOLD_LOW: f32 = 30.0; // disable turbo if load is be
|
||||||
pub const DEFAULT_TEMP_THRESHOLD_HIGH: f32 = 75.0; // disable turbo if temperature is above this
|
pub const DEFAULT_TEMP_THRESHOLD_HIGH: f32 = 75.0; // disable turbo if temperature is above this
|
||||||
pub const DEFAULT_INITIAL_TURBO_STATE: bool = false; // by default, start with turbo disabled
|
pub const DEFAULT_INITIAL_TURBO_STATE: bool = false; // by default, start with turbo disabled
|
||||||
|
|
||||||
default_const!(
|
default_const! {
|
||||||
default_load_threshold_high,
|
default_load_threshold_high -> f32 = DEFAULT_LOAD_THRESHOLD_HIGH;
|
||||||
f32,
|
default_load_threshold_low -> f32 = DEFAULT_LOAD_THRESHOLD_LOW;
|
||||||
DEFAULT_LOAD_THRESHOLD_HIGH
|
|
||||||
);
|
default_temp_threshold_high -> f32 = DEFAULT_TEMP_THRESHOLD_HIGH;
|
||||||
default_const!(default_load_threshold_low, f32, DEFAULT_LOAD_THRESHOLD_LOW);
|
|
||||||
default_const!(
|
default_initial_turbo_state -> bool = DEFAULT_INITIAL_TURBO_STATE;
|
||||||
default_temp_threshold_high,
|
}
|
||||||
f32,
|
|
||||||
DEFAULT_TEMP_THRESHOLD_HIGH
|
|
||||||
);
|
|
||||||
default_const!(
|
|
||||||
default_initial_turbo_state,
|
|
||||||
bool,
|
|
||||||
DEFAULT_INITIAL_TURBO_STATE
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Default for TurboAutoSettings {
|
impl Default for TurboAutoSettings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
@ -213,10 +181,10 @@ impl From<ProfileConfigToml> for ProfileConfig {
|
||||||
turbo: toml_config
|
turbo: toml_config
|
||||||
.turbo
|
.turbo
|
||||||
.and_then(|s| match s.to_lowercase().as_str() {
|
.and_then(|s| match s.to_lowercase().as_str() {
|
||||||
"always" => Some(TurboSetting::Always),
|
"always" => Some(true),
|
||||||
"auto" => Some(TurboSetting::Auto),
|
"auto" => None,
|
||||||
"never" => Some(TurboSetting::Never),
|
"never" => Some(false),
|
||||||
_ => None,
|
_ => panic!("invalid turbo value: {s}, must be one of: always, auto, never"),
|
||||||
}),
|
}),
|
||||||
epp: toml_config.epp,
|
epp: toml_config.epp,
|
||||||
epb: toml_config.epb,
|
epb: toml_config.epb,
|
||||||
|
@ -270,14 +238,16 @@ impl Default for DaemonConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
default_const!(default_poll_interval_sec, u64, 5);
|
default_const! {
|
||||||
default_const!(default_adaptive_interval, bool, false);
|
default_poll_interval_sec -> u64 = 5;
|
||||||
default_const!(default_min_poll_interval_sec, u64, 1);
|
default_adaptive_interval -> bool = false;
|
||||||
default_const!(default_max_poll_interval_sec, u64, 30);
|
default_min_poll_interval_sec -> u64 = 1;
|
||||||
default_const!(default_throttle_on_battery, bool, true);
|
default_max_poll_interval_sec -> u64 = 30;
|
||||||
default_const!(default_log_level, LogLevel, LogLevel::Info);
|
default_throttle_on_battery -> bool = true;
|
||||||
default_const!(default_stats_file_path, Option<String>, None);
|
default_log_level -> LogLevel = LogLevel::Info;
|
||||||
default_const!(default_enable_auto_turbo, bool, true);
|
default_stats_file_path -> Option<String> = None;
|
||||||
|
default_enable_auto_turbo -> bool = true;
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
pub struct DaemonConfigToml {
|
pub struct DaemonConfigToml {
|
||||||
|
|
571
src/cpu.rs
571
src/cpu.rs
|
@ -1,19 +1,6 @@
|
||||||
use anyhow::{Context, bail};
|
use anyhow::{Context, bail};
|
||||||
use derive_more::Display;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use std::{fs, io, path::Path, string::ToString};
|
use std::{fmt, fs, path::Path, string::ToString};
|
||||||
|
|
||||||
// // Valid EPP (Energy Performance Preference) string values.
|
|
||||||
// const EPP_FALLBACK_VALUES: &[&str] = &[
|
|
||||||
// "default",
|
|
||||||
// "performance",
|
|
||||||
// "balance-performance",
|
|
||||||
// "balance_performance", // Alternative form with underscore.
|
|
||||||
// "balance-power",
|
|
||||||
// "balance_power", // Alternative form with underscore.
|
|
||||||
// "power",
|
|
||||||
// ];
|
|
||||||
|
|
||||||
fn exists(path: impl AsRef<Path>) -> bool {
|
fn exists(path: impl AsRef<Path>) -> bool {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
@ -41,8 +28,32 @@ fn write(path: impl AsRef<Path>, value: &str) -> anyhow::Result<()> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get real, tunable CPUs.
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub fn get_real_cpus() -> anyhow::Result<Vec<u32>> {
|
pub struct Cpu {
|
||||||
|
pub number: u32,
|
||||||
|
pub has_cpufreq: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Cpu {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let Self { number, .. } = self;
|
||||||
|
write!(f, "CPU {number}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cpu {
|
||||||
|
pub fn new(number: u32) -> anyhow::Result<Self> {
|
||||||
|
let mut cpu = Self {
|
||||||
|
number,
|
||||||
|
has_cpufreq: false,
|
||||||
|
};
|
||||||
|
cpu.rescan()?;
|
||||||
|
|
||||||
|
Ok(cpu)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all CPUs.
|
||||||
|
pub fn all() -> anyhow::Result<Vec<Cpu>> {
|
||||||
const PATH: &str = "/sys/devices/system/cpu";
|
const PATH: &str = "/sys/devices/system/cpu";
|
||||||
|
|
||||||
let mut cpus = vec![];
|
let mut cpus = vec![];
|
||||||
|
@ -62,55 +73,40 @@ pub fn get_real_cpus() -> anyhow::Result<Vec<u32>> {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Has to match "cpu{N}".
|
// Has to match "cpu{N}".
|
||||||
let Ok(cpu) = cpu_prefix_removed.parse::<u32>() else {
|
let Ok(number) = cpu_prefix_removed.parse::<u32>() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Has to match "cpu{N}/cpufreq".
|
cpus.push(Self::new(number)?);
|
||||||
if !entry.path().join("cpufreq").exists() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
cpus.push(cpu);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back if sysfs iteration above fails to find any cpufreq CPUs.
|
// Fall back if sysfs iteration above fails to find any cpufreq CPUs.
|
||||||
if cpus.is_empty() {
|
if cpus.is_empty() {
|
||||||
cpus = (0..num_cpus::get() as u32).collect();
|
for number in 0..num_cpus::get() as u32 {
|
||||||
|
cpus.push(Self::new(number)?);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(cpus)
|
Ok(cpus)
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the governor for a CPU.
|
|
||||||
pub fn set_governor(governor: &str, cpu: u32) -> anyhow::Result<()> {
|
|
||||||
let governors = get_available_governors_for(cpu);
|
|
||||||
|
|
||||||
if !governors
|
|
||||||
.iter()
|
|
||||||
.any(|avail_governor| avail_governor == governor)
|
|
||||||
{
|
|
||||||
bail!(
|
|
||||||
"governor '{governor}' is not available for CPU {cpu}. valid governors: {governors}",
|
|
||||||
governors = governors.join(", "),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
write(
|
/// Rescan CPU, tuning local copy of settings.
|
||||||
format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_governor"),
|
pub fn rescan(&mut self) -> anyhow::Result<()> {
|
||||||
governor,
|
let has_cpufreq = exists(format!(
|
||||||
)
|
"/sys/devices/system/cpu/cpu{number}/cpufreq",
|
||||||
.with_context(|| {
|
number = self.number,
|
||||||
format!(
|
));
|
||||||
"this probably means that CPU {cpu} doesn't exist or doesn't support changing governors"
|
|
||||||
)
|
self.has_cpufreq = has_cpufreq;
|
||||||
})
|
|
||||||
}
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_available_governors(&self) -> Vec<String> {
|
||||||
|
let Self { number, .. } = self;
|
||||||
|
|
||||||
/// Get available CPU governors for a CPU.
|
|
||||||
fn get_available_governors_for(cpu: u32) -> Vec<String> {
|
|
||||||
let Ok(content) = fs::read_to_string(format!(
|
let Ok(content) = fs::read_to_string(format!(
|
||||||
"/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_available_governors"
|
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_available_governors"
|
||||||
)) else {
|
)) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
|
@ -119,23 +115,206 @@ fn get_available_governors_for(cpu: u32) -> Vec<String> {
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
.map(ToString::to_string)
|
.map(ToString::to_string)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, clap::ValueEnum)]
|
pub fn set_governor(&self, governor: &str) -> anyhow::Result<()> {
|
||||||
pub enum Turbo {
|
let Self { number, .. } = self;
|
||||||
Always,
|
|
||||||
Never,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_turbo(setting: Turbo) -> anyhow::Result<()> {
|
let governors = self.get_available_governors();
|
||||||
let value_boost = match setting {
|
|
||||||
Turbo::Always => "1", // boost = 1 means turbo is enabled.
|
if !governors
|
||||||
Turbo::Never => "0", // boost = 0 means turbo is disabled.
|
.iter()
|
||||||
|
.any(|avail_governor| avail_governor == governor)
|
||||||
|
{
|
||||||
|
bail!(
|
||||||
|
"governor '{governor}' is not available for {self}. available governors: {governors}",
|
||||||
|
governors = governors.join(", "),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
write(
|
||||||
|
format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_governor"),
|
||||||
|
governor,
|
||||||
|
)
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"this probably means that {self} doesn't exist or doesn't support changing governors"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_available_epps(&self) -> Vec<String> {
|
||||||
|
let Self { number, .. } = self;
|
||||||
|
|
||||||
|
let Ok(content) = fs::read_to_string(format!(
|
||||||
|
"/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_available_preferences"
|
||||||
|
)) else {
|
||||||
|
return Vec::new();
|
||||||
};
|
};
|
||||||
|
|
||||||
let value_boost_negated = match setting {
|
content
|
||||||
Turbo::Always => "0", // no_turbo = 0 means turbo is enabled.
|
.split_whitespace()
|
||||||
Turbo::Never => "1", // no_turbo = 1 means turbo is disabled.
|
.map(ToString::to_string)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_epp(&self, epp: &str) -> anyhow::Result<()> {
|
||||||
|
let Self { number, .. } = self;
|
||||||
|
|
||||||
|
let epps = self.get_available_epps();
|
||||||
|
|
||||||
|
if !epps.iter().any(|avail_epp| avail_epp == epp) {
|
||||||
|
bail!(
|
||||||
|
"EPP value '{epp}' is not availabile for {self}. available EPP values: {epps}",
|
||||||
|
epps = epps.join(", "),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
write(
|
||||||
|
format!("/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_preference"),
|
||||||
|
epp,
|
||||||
|
)
|
||||||
|
.with_context(|| {
|
||||||
|
format!("this probably means that {self} doesn't exist or doesn't support changing EPP")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_available_epbs(&self) -> &'static [&'static str] {
|
||||||
|
if !self.has_cpufreq {
|
||||||
|
return &[];
|
||||||
|
}
|
||||||
|
|
||||||
|
&[
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
"5",
|
||||||
|
"6",
|
||||||
|
"7",
|
||||||
|
"8",
|
||||||
|
"9",
|
||||||
|
"10",
|
||||||
|
"11",
|
||||||
|
"12",
|
||||||
|
"13",
|
||||||
|
"14",
|
||||||
|
"15",
|
||||||
|
"performance",
|
||||||
|
"balance-performance",
|
||||||
|
"balance_performance", // Alternative form with underscore.
|
||||||
|
"balance-power",
|
||||||
|
"balance_power", // Alternative form with underscore.
|
||||||
|
"power",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_epb(&self, epb: &str) -> anyhow::Result<()> {
|
||||||
|
let Self { number, .. } = self;
|
||||||
|
|
||||||
|
let epbs = self.get_available_epbs();
|
||||||
|
|
||||||
|
if !epbs.contains(&epb) {
|
||||||
|
bail!(
|
||||||
|
"EPB value '{epb}' is not available for {self}. available EPB values: {valid}",
|
||||||
|
valid = epbs.join(", "),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
write(
|
||||||
|
format!("/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias"),
|
||||||
|
epb,
|
||||||
|
)
|
||||||
|
.with_context(|| {
|
||||||
|
format!("this probably means that {self} doesn't exist or doesn't support changing EPB")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_frequency_minimum(&self, frequency_mhz: u64) -> anyhow::Result<()> {
|
||||||
|
let Self { number, .. } = self;
|
||||||
|
|
||||||
|
self.validate_frequency_minimum(frequency_mhz)?;
|
||||||
|
|
||||||
|
// We use u64 for the intermediate calculation to prevent overflow
|
||||||
|
let frequency_khz = u64::from(frequency_mhz) * 1000;
|
||||||
|
let frequency_khz = frequency_khz.to_string();
|
||||||
|
|
||||||
|
write(
|
||||||
|
format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq"),
|
||||||
|
&frequency_khz,
|
||||||
|
)
|
||||||
|
.with_context(|| {
|
||||||
|
format!("this probably means that {self} doesn't exist or doesn't support changing minimum frequency")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_frequency_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> {
|
||||||
|
let Self { number, .. } = self;
|
||||||
|
|
||||||
|
let Ok(minimum_frequency_khz) = read_u64(format!(
|
||||||
|
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq"
|
||||||
|
)) else {
|
||||||
|
// Just let it pass if we can't find anything.
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
if new_frequency_mhz as u64 * 1000 < minimum_frequency_khz {
|
||||||
|
bail!(
|
||||||
|
"new minimum frequency ({new_frequency_mhz} MHz) cannot be lower than the minimum frequency ({} MHz) for {self}",
|
||||||
|
minimum_frequency_khz / 1000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_frequency_maximum(&self, frequency_mhz: u64) -> anyhow::Result<()> {
|
||||||
|
let Self { number, .. } = self;
|
||||||
|
|
||||||
|
self.validate_frequency_maximum(frequency_mhz)?;
|
||||||
|
|
||||||
|
// We use u64 for the intermediate calculation to prevent overflow
|
||||||
|
let frequency_khz = u64::from(frequency_mhz) * 1000;
|
||||||
|
let frequency_khz = frequency_khz.to_string();
|
||||||
|
|
||||||
|
write(
|
||||||
|
format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq"),
|
||||||
|
&frequency_khz,
|
||||||
|
)
|
||||||
|
.with_context(|| {
|
||||||
|
format!("this probably means that {self} doesn't exist or doesn't support changing maximum frequency")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_frequency_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> {
|
||||||
|
let Self { number, .. } = self;
|
||||||
|
|
||||||
|
let Ok(maximum_frequency_khz) = read_u64(format!(
|
||||||
|
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq"
|
||||||
|
)) else {
|
||||||
|
// Just let it pass if we can't find anything.
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
if new_frequency_mhz * 1000 > maximum_frequency_khz {
|
||||||
|
bail!(
|
||||||
|
"new maximum frequency ({new_frequency_mhz} MHz) cannot be higher than the maximum frequency ({} MHz) for {self}",
|
||||||
|
maximum_frequency_khz / 1000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_turbo(on: bool) -> anyhow::Result<()> {
|
||||||
|
let value_boost = match on {
|
||||||
|
true => "1", // boost = 1 means turbo is enabled.
|
||||||
|
false => "0", // boost = 0 means turbo is disabled.
|
||||||
|
};
|
||||||
|
|
||||||
|
let value_boost_negated = match on {
|
||||||
|
true => "0", // no_turbo = 0 means turbo is enabled.
|
||||||
|
false => "1", // no_turbo = 1 means turbo is disabled.
|
||||||
};
|
};
|
||||||
|
|
||||||
// AMD specific paths
|
// AMD specific paths
|
||||||
|
@ -161,9 +340,11 @@ pub fn set_turbo(setting: Turbo) -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also try per-core cpufreq boost for some AMD systems.
|
// Also try per-core cpufreq boost for some AMD systems.
|
||||||
if get_real_cpus()?.iter().any(|cpu| {
|
if Self::all()?.iter().any(|cpu| {
|
||||||
|
let Cpu { number, .. } = cpu;
|
||||||
|
|
||||||
write(
|
write(
|
||||||
&format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/boost"),
|
&format!("/sys/devices/system/cpu/cpu{number}/cpufreq/boost"),
|
||||||
value_boost,
|
value_boost,
|
||||||
)
|
)
|
||||||
.is_ok()
|
.is_ok()
|
||||||
|
@ -172,263 +353,5 @@ pub fn set_turbo(setting: Turbo) -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
bail!("no supported CPU boost control mechanism found");
|
bail!("no supported CPU boost control mechanism found");
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_epp(epp: &str, cpu: u32) -> anyhow::Result<()> {
|
|
||||||
// Validate the EPP value against available options
|
|
||||||
let epps = get_available_epps(cpu);
|
|
||||||
|
|
||||||
if !epps.iter().any(|avail_epp| avail_epp == epp) {
|
|
||||||
bail!(
|
|
||||||
"epp value '{epp}' is not availabile for CPU {cpu}. valid epp values: {epps}",
|
|
||||||
epps = epps.join(", "),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
write(
|
|
||||||
format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/energy_performance_preference"),
|
|
||||||
epp,
|
|
||||||
)
|
|
||||||
.with_context(|| {
|
|
||||||
format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing EPP")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get available EPP values for a CPU.
|
|
||||||
fn get_available_epps(cpu: u32) -> Vec<String> {
|
|
||||||
let Ok(content) = fs::read_to_string(format!(
|
|
||||||
"/sys/devices/system/cpu/cpu{cpu}/cpufreq/energy_performance_available_preferences"
|
|
||||||
)) else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
|
|
||||||
content
|
|
||||||
.split_whitespace()
|
|
||||||
.map(ToString::to_string)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_epb(epb: &str, cpu: u32) -> anyhow::Result<()> {
|
|
||||||
// Validate EPB value - should be a number 0-15 or a recognized string value.
|
|
||||||
validate_epb_value(epb)?;
|
|
||||||
|
|
||||||
write(
|
|
||||||
format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/energy_performance_bias"),
|
|
||||||
epb,
|
|
||||||
)
|
|
||||||
.with_context(|| {
|
|
||||||
format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing EPB")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_epb_value(epb: &str) -> anyhow::Result<()> {
|
|
||||||
// EPB can be a number from 0-15 or a recognized string.
|
|
||||||
|
|
||||||
const VALID_EPB_STRINGS: &[&str] = &[
|
|
||||||
"performance",
|
|
||||||
"balance-performance",
|
|
||||||
"balance_performance", // Alternative form with underscore.
|
|
||||||
"balance-power",
|
|
||||||
"balance_power", // Alternative form with underscore.
|
|
||||||
"power",
|
|
||||||
];
|
|
||||||
|
|
||||||
// Try parsing as a number first.
|
|
||||||
if let Ok(value) = epb.parse::<u8>() {
|
|
||||||
if value <= 15 {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("EPB numeric value must be between 0 and 15, got {value}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not a number, check if it's a recognized string value.
|
|
||||||
if VALID_EPB_STRINGS.contains(&epb) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!(
|
|
||||||
"invalid EPB value: '{epb}'. must be a number between 0-15 inclusive or one of: {valid}",
|
|
||||||
valid = VALID_EPB_STRINGS.join(", "),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_frequency_minimum(frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> {
|
|
||||||
validate_frequency_minimum(frequency_mhz, cpu)?;
|
|
||||||
|
|
||||||
// We use u64 for the intermediate calculation to prevent overflow
|
|
||||||
let frequency_khz = u64::from(frequency_mhz) * 1000;
|
|
||||||
let frequency_khz = frequency_khz.to_string();
|
|
||||||
|
|
||||||
write(
|
|
||||||
format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_min_freq"),
|
|
||||||
&frequency_khz,
|
|
||||||
)
|
|
||||||
.with_context(|| {
|
|
||||||
format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing minimum frequency")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_frequency_maximum(frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> {
|
|
||||||
validate_max_frequency(frequency_mhz, cpu)?;
|
|
||||||
|
|
||||||
// We use u64 for the intermediate calculation to prevent overflow
|
|
||||||
let frequency_khz = u64::from(frequency_mhz) * 1000;
|
|
||||||
let frequency_khz = frequency_khz.to_string();
|
|
||||||
|
|
||||||
write(
|
|
||||||
format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_max_freq"),
|
|
||||||
&frequency_khz,
|
|
||||||
)
|
|
||||||
.with_context(|| {
|
|
||||||
format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing maximum frequency")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_frequency_minimum(new_frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> {
|
|
||||||
let Ok(minimum_frequency_khz) = read_u64(format!(
|
|
||||||
"/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_min_freq"
|
|
||||||
)) else {
|
|
||||||
// Just let it pass if we can't find anything.
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
if new_frequency_mhz as u64 * 1000 < minimum_frequency_khz {
|
|
||||||
bail!(
|
|
||||||
"new minimum frequency ({new_frequency_mhz} MHz) cannot be lower than the minimum frequency ({} MHz) for CPU {cpu}",
|
|
||||||
minimum_frequency_khz / 1000,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_max_frequency(new_frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> {
|
|
||||||
let Ok(maximum_frequency_khz) = read_u64(format!(
|
|
||||||
"/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_min_freq"
|
|
||||||
)) else {
|
|
||||||
// Just let it pass if we can't find anything.
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
if new_frequency_mhz * 1000 > maximum_frequency_khz {
|
|
||||||
bail!(
|
|
||||||
"new maximum frequency ({new_frequency_mhz} MHz) cannot be higher than the maximum frequency ({} MHz) for CPU {cpu}",
|
|
||||||
maximum_frequency_khz / 1000,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 = get_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(", "),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
write("/sys/firmware/acpi/platform_profile", profile)
|
|
||||||
.context("this probably means that your system does not support changing ACPI profiles")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the list of available platform profiles.
|
|
||||||
pub fn get_platform_profiles() -> Vec<String> {
|
|
||||||
let path = "/sys/firmware/acpi/platform_profile_choices";
|
|
||||||
|
|
||||||
let Ok(content) = fs::read_to_string(path) else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
|
|
||||||
content
|
|
||||||
.split_whitespace()
|
|
||||||
.map(ToString::to_string)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Path for storing the governor override state.
|
|
||||||
const GOVERNOR_OVERRIDE_PATH: &str = "/etc/xdg/superfreq/governor_override";
|
|
||||||
|
|
||||||
#[derive(Display, Debug, Clone, Copy, clap::ValueEnum)]
|
|
||||||
pub enum GovernorOverride {
|
|
||||||
#[display("performance")]
|
|
||||||
Performance,
|
|
||||||
#[display("powersave")]
|
|
||||||
Powersave,
|
|
||||||
#[display("reset")]
|
|
||||||
Reset,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_governor_override(mode: GovernorOverride) -> anyhow::Result<()> {
|
|
||||||
let parent = Path::new(GOVERNOR_OVERRIDE_PATH).parent().unwrap();
|
|
||||||
if !parent.exists() {
|
|
||||||
fs::create_dir_all(parent).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to create directory '{path}'",
|
|
||||||
path = parent.display(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
match mode {
|
|
||||||
GovernorOverride::Reset => {
|
|
||||||
// Remove the override file if it exists
|
|
||||||
let result = fs::remove_file(GOVERNOR_OVERRIDE_PATH);
|
|
||||||
|
|
||||||
if let Err(error) = result {
|
|
||||||
if error.kind() != io::ErrorKind::NotFound {
|
|
||||||
return Err(error).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to delete governor override file '{GOVERNOR_OVERRIDE_PATH}'"
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"governor override has been deleted. normal profile-based settings will be used"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
GovernorOverride::Performance | GovernorOverride::Powersave => {
|
|
||||||
let governor = mode.to_string();
|
|
||||||
|
|
||||||
write(GOVERNOR_OVERRIDE_PATH, &governor)
|
|
||||||
.context("failed to write governor override")?;
|
|
||||||
|
|
||||||
// TODO: Apply the setting too.
|
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"governor override set to '{governor}'. this setting will persist across reboots"
|
|
||||||
);
|
|
||||||
log::info!("to reset, run: superfreq set --governor-persist reset");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the current governor override if set.
|
|
||||||
pub fn get_governor_override() -> anyhow::Result<Option<String>> {
|
|
||||||
match fs::read_to_string(GOVERNOR_OVERRIDE_PATH) {
|
|
||||||
Ok(governor_override) => Ok(Some(governor_override)),
|
|
||||||
|
|
||||||
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
|
|
||||||
|
|
||||||
Err(error) => Err(error).with_context(|| {
|
|
||||||
format!("failed to read governor override at '{GOVERNOR_OVERRIDE_PATH}'")
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
use crate::config::{AppConfig, LogLevel};
|
use anyhow::Context;
|
||||||
|
use anyhow::bail;
|
||||||
|
|
||||||
|
use crate::config::AppConfig;
|
||||||
use crate::core::SystemReport;
|
use crate::core::SystemReport;
|
||||||
use crate::engine;
|
use crate::engine;
|
||||||
use crate::monitor;
|
use crate::monitor;
|
||||||
use crate::util::error::{AppError, ControlError};
|
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
@ -60,10 +62,7 @@ fn idle_multiplier(idle_secs: u64) -> f32 {
|
||||||
/// Calculate optimal polling interval based on system conditions and history
|
/// Calculate optimal polling interval based on system conditions and history
|
||||||
///
|
///
|
||||||
/// Returns Ok with the calculated interval, or Err if the configuration is invalid
|
/// Returns Ok with the calculated interval, or Err if the configuration is invalid
|
||||||
fn compute_new(
|
fn compute_new(params: &IntervalParams, system_history: &SystemHistory) -> anyhow::Result<u64> {
|
||||||
params: &IntervalParams,
|
|
||||||
system_history: &SystemHistory,
|
|
||||||
) -> Result<u64, ControlError> {
|
|
||||||
// Use the centralized validation function
|
// Use the centralized validation function
|
||||||
validate_poll_intervals(params.min_interval, params.max_interval)?;
|
validate_poll_intervals(params.min_interval, params.max_interval)?;
|
||||||
|
|
||||||
|
@ -361,7 +360,7 @@ impl SystemHistory {
|
||||||
&self,
|
&self,
|
||||||
config: &AppConfig,
|
config: &AppConfig,
|
||||||
on_battery: bool,
|
on_battery: bool,
|
||||||
) -> Result<u64, ControlError> {
|
) -> anyhow::Result<u64> {
|
||||||
let params = IntervalParams {
|
let params = IntervalParams {
|
||||||
base_interval: config.daemon.poll_interval_sec,
|
base_interval: config.daemon.poll_interval_sec,
|
||||||
min_interval: config.daemon.min_poll_interval_sec,
|
min_interval: config.daemon.min_poll_interval_sec,
|
||||||
|
@ -380,37 +379,31 @@ impl SystemHistory {
|
||||||
|
|
||||||
/// Validates that poll interval configuration is consistent
|
/// Validates that poll interval configuration is consistent
|
||||||
/// Returns Ok if configuration is valid, Err with a descriptive message if invalid
|
/// Returns Ok if configuration is valid, Err with a descriptive message if invalid
|
||||||
fn validate_poll_intervals(min_interval: u64, max_interval: u64) -> Result<(), ControlError> {
|
fn validate_poll_intervals(min_interval: u64, max_interval: u64) -> anyhow::Result<()> {
|
||||||
if min_interval < 1 {
|
if min_interval < 1 {
|
||||||
return Err(ControlError::InvalidValueError(
|
bail!("min_interval must be ≥ 1");
|
||||||
"min_interval must be ≥ 1".to_string(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
if max_interval < 1 {
|
if max_interval < 1 {
|
||||||
return Err(ControlError::InvalidValueError(
|
bail!("max_interval must be ≥ 1");
|
||||||
"max_interval must be ≥ 1".to_string(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
if max_interval >= min_interval {
|
if max_interval >= min_interval {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(ControlError::InvalidValueError(format!(
|
bail!(
|
||||||
"Invalid interval configuration: max_interval ({max_interval}) is less than min_interval ({min_interval})"
|
"Invalid interval configuration: max_interval ({max_interval}) is less than min_interval ({min_interval})"
|
||||||
)))
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the daemon
|
/// Run the daemon
|
||||||
pub fn run_daemon(config: AppConfig) -> Result<(), AppError> {
|
pub fn run_daemon(config: AppConfig) -> anyhow::Result<()> {
|
||||||
log::info!("Starting superfreq daemon...");
|
log::info!("Starting superfreq daemon...");
|
||||||
|
|
||||||
// Validate critical configuration values before proceeding
|
// Validate critical configuration values before proceeding
|
||||||
if let Err(err) = validate_poll_intervals(
|
validate_poll_intervals(
|
||||||
config.daemon.min_poll_interval_sec,
|
config.daemon.min_poll_interval_sec,
|
||||||
config.daemon.max_poll_interval_sec,
|
config.daemon.max_poll_interval_sec,
|
||||||
) {
|
)?;
|
||||||
return Err(AppError::Control(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a flag that will be set to true when a signal is received
|
// Create a flag that will be set to true when a signal is received
|
||||||
let running = Arc::new(AtomicBool::new(true));
|
let running = Arc::new(AtomicBool::new(true));
|
||||||
|
@ -421,7 +414,7 @@ pub fn run_daemon(config: AppConfig) -> Result<(), AppError> {
|
||||||
log::info!("Received shutdown signal, exiting...");
|
log::info!("Received shutdown signal, exiting...");
|
||||||
r.store(false, Ordering::SeqCst);
|
r.store(false, Ordering::SeqCst);
|
||||||
})
|
})
|
||||||
.map_err(|e| AppError::Generic(format!("Error setting Ctrl-C handler: {e}")))?;
|
.context("failed to set Ctrl-C handler")?;
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Daemon initialized with poll interval: {}s",
|
"Daemon initialized with poll interval: {}s",
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
use crate::config::{AppConfig, ProfileConfig, TurboAutoSettings};
|
use crate::config::{AppConfig, ProfileConfig, TurboAutoSettings};
|
||||||
use crate::core::{OperationalMode, SystemReport, TurboSetting};
|
use crate::core::{OperationalMode, SystemReport};
|
||||||
use crate::cpu::{self};
|
use crate::cpu::{self};
|
||||||
use crate::power_supply;
|
use crate::power_supply;
|
||||||
use crate::util::error::{ControlError, EngineError};
|
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
|
@ -119,30 +118,14 @@ impl TurboHysteresis {
|
||||||
/// 1. Try to apply a feature setting
|
/// 1. Try to apply a feature setting
|
||||||
/// 2. If not supported, log a warning and continue
|
/// 2. If not supported, log a warning and continue
|
||||||
/// 3. If other error, propagate the error
|
/// 3. If other error, propagate the error
|
||||||
fn try_apply_feature<F, T>(
|
fn try_apply_feature<F: FnOnce() -> anyhow::Result<()>, T>(
|
||||||
feature_name: &str,
|
feature_name: &str,
|
||||||
value_description: &str,
|
value_description: &str,
|
||||||
apply_fn: F,
|
apply_fn: F,
|
||||||
) -> Result<(), EngineError>
|
) -> anyhow::Result<()> {
|
||||||
where
|
|
||||||
F: FnOnce() -> Result<T, ControlError>,
|
|
||||||
{
|
|
||||||
log::info!("Setting {feature_name} to '{value_description}'");
|
log::info!("Setting {feature_name} to '{value_description}'");
|
||||||
|
|
||||||
match apply_fn() {
|
apply_fn()
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(e) => {
|
|
||||||
if matches!(e, ControlError::NotSupported(_)) {
|
|
||||||
log::warn!(
|
|
||||||
"{feature_name} setting is not supported on this system. Skipping {feature_name} configuration."
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
// Propagate all other errors, including InvalidValueError
|
|
||||||
Err(EngineError::ControlError(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determines the appropriate CPU profile based on power status or forced mode,
|
/// Determines the appropriate CPU profile based on power status or forced mode,
|
||||||
|
@ -151,19 +134,19 @@ pub fn determine_and_apply_settings(
|
||||||
report: &SystemReport,
|
report: &SystemReport,
|
||||||
config: &AppConfig,
|
config: &AppConfig,
|
||||||
force_mode: Option<OperationalMode>,
|
force_mode: Option<OperationalMode>,
|
||||||
) -> Result<(), EngineError> {
|
) -> anyhow::Result<()> {
|
||||||
// First, check if there's a governor override set
|
// // First, check if there's a governor override set
|
||||||
if let Some(override_governor) = cpu::get_governor_override() {
|
// if let Some(override_governor) = cpu::get_governor_override() {
|
||||||
log::info!(
|
// log::info!(
|
||||||
"Governor override is active: '{}'. Setting governor.",
|
// "Governor override is active: '{}'. Setting governor.",
|
||||||
override_governor.trim()
|
// override_governor.trim()
|
||||||
);
|
// );
|
||||||
|
|
||||||
// Apply the override governor setting
|
// // Apply the override governor setting
|
||||||
try_apply_feature("override governor", override_governor.trim(), || {
|
// try_apply_feature("override governor", override_governor.trim(), || {
|
||||||
cpu::set_governor(override_governor.trim(), None)
|
// cpu::set_governor(override_governor.trim(), None)
|
||||||
})?;
|
// })?;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Determine AC/Battery status once, early in the function
|
// Determine AC/Battery status once, early in the function
|
||||||
// For desktops (no batteries), we should always use the AC power profile
|
// For desktops (no batteries), we should always use the AC power profile
|
||||||
|
@ -203,17 +186,11 @@ pub fn determine_and_apply_settings(
|
||||||
// Apply settings from selected_profile_config
|
// Apply settings from selected_profile_config
|
||||||
if let Some(governor) = &selected_profile_config.governor {
|
if let Some(governor) = &selected_profile_config.governor {
|
||||||
log::info!("Setting governor to '{governor}'");
|
log::info!("Setting governor to '{governor}'");
|
||||||
|
for cpu in cpu::Cpu::all()? {
|
||||||
// Let set_governor handle the validation
|
// Let set_governor handle the validation
|
||||||
if let Err(e) = cpu::set_governor(governor, None) {
|
if let Err(error) = cpu.set_governor(governor) {
|
||||||
// If the governor is not available, log a warning
|
// If the governor is not available, log a warning
|
||||||
if matches!(e, ControlError::InvalidGovernor(_))
|
log::warn!("{error}");
|
||||||
|| matches!(e, ControlError::NotSupported(_))
|
|
||||||
{
|
|
||||||
log::warn!(
|
|
||||||
"Configured governor '{governor}' is not available on this system. Skipping."
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Err(e.into());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -297,7 +274,7 @@ fn manage_auto_turbo(
|
||||||
report: &SystemReport,
|
report: &SystemReport,
|
||||||
config: &ProfileConfig,
|
config: &ProfileConfig,
|
||||||
on_ac_power: bool,
|
on_ac_power: bool,
|
||||||
) -> Result<(), EngineError> {
|
) -> anyhow::Result<()> {
|
||||||
// Get the auto turbo settings from the config
|
// Get the auto turbo settings from the config
|
||||||
let turbo_settings = &config.turbo_auto_settings;
|
let turbo_settings = &config.turbo_auto_settings;
|
||||||
|
|
||||||
|
|
359
src/main.rs
359
src/main.rs
|
@ -1,4 +1,3 @@
|
||||||
mod cli;
|
|
||||||
mod config;
|
mod config;
|
||||||
mod core;
|
mod core;
|
||||||
mod cpu;
|
mod cpu;
|
||||||
|
@ -6,7 +5,6 @@ mod daemon;
|
||||||
mod engine;
|
mod engine;
|
||||||
mod monitor;
|
mod monitor;
|
||||||
mod power_supply;
|
mod power_supply;
|
||||||
mod util;
|
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use clap::Parser as _;
|
use clap::Parser as _;
|
||||||
|
@ -33,22 +31,22 @@ enum Command {
|
||||||
/// Start the daemon.
|
/// Start the daemon.
|
||||||
Start,
|
Start,
|
||||||
|
|
||||||
/// Modify attributes.
|
/// Modify CPU attributes.
|
||||||
Set {
|
CpuSet {
|
||||||
/// 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 all CPUs.
|
||||||
#[arg(short = 'c', long = "for")]
|
#[arg(short = 'c', long = "for")]
|
||||||
for_: Option<Vec<u32>>,
|
for_: Option<Vec<u32>>,
|
||||||
|
|
||||||
/// Set the CPU governor.
|
/// Set the CPU governor.
|
||||||
#[arg(long)]
|
#[arg(short = 'g', long)]
|
||||||
governor: Option<String>, // TODO: Validate with clap for available governors.
|
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(long, alias = "epp")]
|
#[arg(short = 'p', long, alias = "epp")]
|
||||||
energy_performance_preference: Option<String>,
|
energy_performance_preference: Option<String>,
|
||||||
|
|
||||||
/// Set CPU Energy Performance Bias (EPB). Short form: --epb.
|
/// Set CPU Energy Performance Bias (EPB). Short form: --epb.
|
||||||
#[arg(long, alias = "epb")]
|
#[arg(short = 'b', long, alias = "epb")]
|
||||||
energy_performance_bias: Option<String>,
|
energy_performance_bias: Option<String>,
|
||||||
|
|
||||||
/// Set minimum CPU frequency in MHz. Short form: --freq-min.
|
/// Set minimum CPU frequency in MHz. Short form: --freq-min.
|
||||||
|
@ -60,20 +58,27 @@ enum Command {
|
||||||
frequency_mhz_maximum: Option<u64>,
|
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(long, conflicts_with = "for_")]
|
#[arg(short = 't', long, conflicts_with = "for_")]
|
||||||
turbo: Option<cpu::Turbo>,
|
turbo: Option<bool>,
|
||||||
|
},
|
||||||
|
|
||||||
/// Set ACPI platform profile. Has to be for all CPUs.
|
/// Modify power supply attributes.
|
||||||
#[arg(long, alias = "profile", conflicts_with = "for_")]
|
PowerSet {
|
||||||
platform_profile: Option<String>,
|
/// The power supplies to apply the changes to. When unspecified, will be applied to all power supplies.
|
||||||
|
#[arg(short = 'p', long = "for")]
|
||||||
|
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 to start. Short form: --charge-start.
|
||||||
#[arg(short = 'p', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100), conflicts_with = "for_")]
|
#[arg(short = 'c', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100))]
|
||||||
charge_threshold_start: Option<u8>,
|
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 = 'P', long, alias = "charge-end", value_parser = clap::value_parser!(u8).range(0..=100), conflicts_with = "for_")]
|
#[arg(short = 'C', long, alias = "charge-end", value_parser = clap::value_parser!(u8).range(0..=100))]
|
||||||
charge_threshold_end: Option<u8>,
|
charge_threshold_end: Option<u8>,
|
||||||
|
|
||||||
|
/// Set ACPI platform profile. Has to be for all power supplies.
|
||||||
|
#[arg(short = 'f', long, alias = "profile", conflicts_with = "for_")]
|
||||||
|
platform_profile: Option<String>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +101,7 @@ fn real_main() -> anyhow::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
Command::Set {
|
Command::CpuSet {
|
||||||
for_,
|
for_,
|
||||||
governor,
|
governor,
|
||||||
energy_performance_preference,
|
energy_performance_preference,
|
||||||
|
@ -104,319 +109,89 @@ fn real_main() -> anyhow::Result<()> {
|
||||||
frequency_mhz_minimum,
|
frequency_mhz_minimum,
|
||||||
frequency_mhz_maximum,
|
frequency_mhz_maximum,
|
||||||
turbo,
|
turbo,
|
||||||
platform_profile,
|
|
||||||
charge_threshold_start,
|
|
||||||
charge_threshold_end,
|
|
||||||
} => {
|
} => {
|
||||||
let cpus = match for_ {
|
let cpus = match for_ {
|
||||||
Some(cpus) => cpus,
|
Some(numbers) => {
|
||||||
None => cpu::get_real_cpus()?,
|
let mut cpus = Vec::with_capacity(numbers.len());
|
||||||
|
|
||||||
|
for number in numbers {
|
||||||
|
cpus.push(cpu::Cpu::new(number)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
cpus
|
||||||
|
}
|
||||||
|
None => cpu::Cpu::all()?,
|
||||||
};
|
};
|
||||||
|
|
||||||
for cpu in cpus {
|
for cpu in cpus {
|
||||||
if let Some(governor) = governor.as_ref() {
|
if let Some(governor) = governor.as_ref() {
|
||||||
cpu::set_governor(governor, cpu)?;
|
cpu.set_governor(governor)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(epp) = energy_performance_preference.as_ref() {
|
if let Some(epp) = energy_performance_preference.as_ref() {
|
||||||
cpu::set_epp(epp, cpu)?;
|
cpu.set_epp(epp)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(epb) = energy_performance_bias.as_ref() {
|
if let Some(epb) = energy_performance_bias.as_ref() {
|
||||||
cpu::set_epb(epb, cpu)?;
|
cpu.set_epb(epb)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(mhz_minimum) = frequency_mhz_minimum {
|
if let Some(mhz_minimum) = frequency_mhz_minimum {
|
||||||
cpu::set_frequency_minimum(mhz_minimum, cpu)?;
|
cpu.set_frequency_minimum(mhz_minimum)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(mhz_maximum) = frequency_mhz_maximum {
|
if let Some(mhz_maximum) = frequency_mhz_maximum {
|
||||||
cpu::set_frequency_maximum(mhz_maximum, cpu)?;
|
cpu.set_frequency_maximum(mhz_maximum)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(turbo) = turbo {
|
if let Some(turbo) = turbo {
|
||||||
cpu::set_turbo(turbo)?;
|
cpu::Cpu::set_turbo(turbo)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(platform_profile) = platform_profile.as_ref() {
|
Ok(())
|
||||||
cpu::set_platform_profile(platform_profile)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for power_supply in power_supply::get_power_supplies()? {
|
Command::PowerSet {
|
||||||
|
for_,
|
||||||
|
charge_threshold_start,
|
||||||
|
charge_threshold_end,
|
||||||
|
platform_profile,
|
||||||
|
} => {
|
||||||
|
let power_supplies = match for_ {
|
||||||
|
Some(names) => {
|
||||||
|
let mut power_supplies = Vec::with_capacity(names.len());
|
||||||
|
|
||||||
|
for name in names {
|
||||||
|
power_supplies.push(power_supply::PowerSupply::from_name(name)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
power_supplies
|
||||||
|
}
|
||||||
|
|
||||||
|
None => power_supply::PowerSupply::all()?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|power_supply| power_supply.threshold_config.is_some())
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
for power_supply in power_supplies {
|
||||||
if let Some(threshold_start) = charge_threshold_start {
|
if let Some(threshold_start) = charge_threshold_start {
|
||||||
power_supply::set_charge_threshold_start(&power_supply, threshold_start)?;
|
power_supply.set_charge_threshold_start(threshold_start)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(threshold_end) = charge_threshold_end {
|
if let Some(threshold_end) = charge_threshold_end {
|
||||||
power_supply::set_charge_threshold_end(&power_supply, threshold_end)?;
|
power_supply.set_charge_threshold_end(threshold_end)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(platform_profile) = platform_profile.as_ref() {
|
||||||
|
power_supply::PowerSupply::set_platform_profile(platform_profile);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This will be moved to a different module in the future.
|
|
||||||
// Some(Command::Info) => match monitor::collect_system_report(&config) {
|
|
||||||
// Ok(report) => {
|
|
||||||
// // Format section headers with proper centering
|
|
||||||
// let format_section = |title: &str| {
|
|
||||||
// let title_len = title.len();
|
|
||||||
// let total_width = title_len + 8; // 8 is for padding (4 on each side)
|
|
||||||
// let separator = "═".repeat(total_width);
|
|
||||||
|
|
||||||
// println!("\n╔{separator}╗");
|
|
||||||
|
|
||||||
// // Calculate centering
|
|
||||||
// println!("║ {title} ║");
|
|
||||||
|
|
||||||
// println!("╚{separator}╝");
|
|
||||||
// };
|
|
||||||
|
|
||||||
// format_section("System Information");
|
|
||||||
// println!("CPU Model: {}", report.system_info.cpu_model);
|
|
||||||
// println!("Architecture: {}", report.system_info.architecture);
|
|
||||||
// println!(
|
|
||||||
// "Linux Distribution: {}",
|
|
||||||
// report.system_info.linux_distribution
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // Format timestamp in a readable way
|
|
||||||
// println!("Current Time: {}", jiff::Timestamp::now());
|
|
||||||
|
|
||||||
// format_section("CPU Global Info");
|
|
||||||
// println!(
|
|
||||||
// "Current Governor: {}",
|
|
||||||
// report
|
|
||||||
// .cpu_global
|
|
||||||
// .current_governor
|
|
||||||
// .as_deref()
|
|
||||||
// .unwrap_or("N/A")
|
|
||||||
// );
|
|
||||||
// println!(
|
|
||||||
// "Available Governors: {}", // 21 length baseline
|
|
||||||
// report.cpu_global.available_governors.join(", ")
|
|
||||||
// );
|
|
||||||
// println!(
|
|
||||||
// "Turbo Status: {}",
|
|
||||||
// match report.cpu_global.turbo_status {
|
|
||||||
// Some(true) => "Enabled",
|
|
||||||
// Some(false) => "Disabled",
|
|
||||||
// None => "Unknown",
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|
||||||
// println!(
|
|
||||||
// "EPP: {}",
|
|
||||||
// report.cpu_global.epp.as_deref().unwrap_or("N/A")
|
|
||||||
// );
|
|
||||||
// println!(
|
|
||||||
// "EPB: {}",
|
|
||||||
// report.cpu_global.epb.as_deref().unwrap_or("N/A")
|
|
||||||
// );
|
|
||||||
// println!(
|
|
||||||
// "Platform Profile: {}",
|
|
||||||
// report
|
|
||||||
// .cpu_global
|
|
||||||
// .platform_profile
|
|
||||||
// .as_deref()
|
|
||||||
// .unwrap_or("N/A")
|
|
||||||
// );
|
|
||||||
// println!(
|
|
||||||
// "CPU Temperature: {}",
|
|
||||||
// report.cpu_global.average_temperature_celsius.map_or_else(
|
|
||||||
// || "N/A (No sensor detected)".to_string(),
|
|
||||||
// |t| format!("{t:.1}°C")
|
|
||||||
// )
|
|
||||||
// );
|
|
||||||
|
|
||||||
// format_section("CPU Core Info");
|
|
||||||
|
|
||||||
// // Get max core ID length for padding
|
|
||||||
// let max_core_id_len = report
|
|
||||||
// .cpu_cores
|
|
||||||
// .last()
|
|
||||||
// .map_or(1, |core| core.core_id.to_string().len());
|
|
||||||
|
|
||||||
// // Table headers
|
|
||||||
// println!(
|
|
||||||
// " {:>width$} │ {:^10} │ {:^10} │ {:^10} │ {:^7} │ {:^9}",
|
|
||||||
// "Core",
|
|
||||||
// "Current",
|
|
||||||
// "Min",
|
|
||||||
// "Max",
|
|
||||||
// "Usage",
|
|
||||||
// "Temp",
|
|
||||||
// width = max_core_id_len + 4
|
|
||||||
// );
|
|
||||||
// println!(
|
|
||||||
// " {:─>width$}──┼─{:─^10}─┼─{:─^10}─┼─{:─^10}─┼─{:─^7}─┼─{:─^9}",
|
|
||||||
// "",
|
|
||||||
// "",
|
|
||||||
// "",
|
|
||||||
// "",
|
|
||||||
// "",
|
|
||||||
// "",
|
|
||||||
// width = max_core_id_len + 4
|
|
||||||
// );
|
|
||||||
|
|
||||||
// for core_info in &report.cpu_cores {
|
|
||||||
// // Format frequencies: if current > max, show in a special way
|
|
||||||
// let current_freq = match core_info.current_frequency_mhz {
|
|
||||||
// Some(freq) => {
|
|
||||||
// let max_freq = core_info.max_frequency_mhz.unwrap_or(0);
|
|
||||||
// if freq > max_freq && max_freq > 0 {
|
|
||||||
// // Special format for boosted frequencies
|
|
||||||
// format!("{freq}*")
|
|
||||||
// } else {
|
|
||||||
// format!("{freq}")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// None => "N/A".to_string(),
|
|
||||||
// };
|
|
||||||
|
|
||||||
// // CPU core display
|
|
||||||
// println!(
|
|
||||||
// " Core {:<width$} │ {:>10} │ {:>10} │ {:>10} │ {:>7} │ {:>9}",
|
|
||||||
// core_info.core_id,
|
|
||||||
// format!("{} MHz", current_freq),
|
|
||||||
// format!(
|
|
||||||
// "{} MHz",
|
|
||||||
// core_info
|
|
||||||
// .min_frequency_mhz
|
|
||||||
// .map_or_else(|| "N/A".to_string(), |f| f.to_string())
|
|
||||||
// ),
|
|
||||||
// format!(
|
|
||||||
// "{} MHz",
|
|
||||||
// core_info
|
|
||||||
// .max_frequency_mhz
|
|
||||||
// .map_or_else(|| "N/A".to_string(), |f| f.to_string())
|
|
||||||
// ),
|
|
||||||
// format!(
|
|
||||||
// "{}%",
|
|
||||||
// core_info
|
|
||||||
// .usage_percent
|
|
||||||
// .map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}"))
|
|
||||||
// ),
|
|
||||||
// format!(
|
|
||||||
// "{}°C",
|
|
||||||
// core_info
|
|
||||||
// .temperature_celsius
|
|
||||||
// .map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}"))
|
|
||||||
// ),
|
|
||||||
// width = max_core_id_len
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Only display battery info for systems that have real batteries
|
|
||||||
// // Skip this section entirely on desktop systems
|
|
||||||
// if !report.batteries.is_empty() {
|
|
||||||
// let has_real_batteries = report.batteries.iter().any(|b| {
|
|
||||||
// // Check if any battery has actual battery data
|
|
||||||
// // (as opposed to peripherals like wireless mice)
|
|
||||||
// b.capacity_percent.is_some() || b.power_rate_watts.is_some()
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if has_real_batteries {
|
|
||||||
// format_section("Battery Info");
|
|
||||||
// for battery_info in &report.batteries {
|
|
||||||
// // Check if this appears to be a real system battery
|
|
||||||
// if battery_info.capacity_percent.is_some()
|
|
||||||
// || battery_info.power_rate_watts.is_some()
|
|
||||||
// {
|
|
||||||
// let power_status = if battery_info.ac_connected {
|
|
||||||
// "Connected to AC"
|
|
||||||
// } else {
|
|
||||||
// "Running on Battery"
|
|
||||||
// };
|
|
||||||
|
|
||||||
// println!("Battery {}:", battery_info.name);
|
|
||||||
// println!(" Power Status: {power_status}");
|
|
||||||
// println!(
|
|
||||||
// " State: {}",
|
|
||||||
// battery_info.charging_state.as_deref().unwrap_or("Unknown")
|
|
||||||
// );
|
|
||||||
|
|
||||||
// if let Some(capacity) = battery_info.capacity_percent {
|
|
||||||
// println!(" Capacity: {capacity}%");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if let Some(power) = battery_info.power_rate_watts {
|
|
||||||
// let direction = if power >= 0.0 {
|
|
||||||
// "charging"
|
|
||||||
// } else {
|
|
||||||
// "discharging"
|
|
||||||
// };
|
|
||||||
// println!(
|
|
||||||
// " Power Rate: {:.2} W ({})",
|
|
||||||
// power.abs(),
|
|
||||||
// direction
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Display charge thresholds if available
|
|
||||||
// if battery_info.charge_start_threshold.is_some()
|
|
||||||
// || battery_info.charge_stop_threshold.is_some()
|
|
||||||
// {
|
|
||||||
// println!(
|
|
||||||
// " Charge Thresholds: {}-{}",
|
|
||||||
// battery_info
|
|
||||||
// .charge_start_threshold
|
|
||||||
// .map_or_else(|| "N/A".to_string(), |t| t.to_string()),
|
|
||||||
// battery_info
|
|
||||||
// .charge_stop_threshold
|
|
||||||
// .map_or_else(|| "N/A".to_string(), |t| t.to_string())
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// format_section("System Load");
|
|
||||||
// println!(
|
|
||||||
// "Load Average (1m): {:.2}",
|
|
||||||
// report.system_load.load_avg_1min
|
|
||||||
// );
|
|
||||||
// println!(
|
|
||||||
// "Load Average (5m): {:.2}",
|
|
||||||
// report.system_load.load_avg_5min
|
|
||||||
// );
|
|
||||||
// println!(
|
|
||||||
// "Load Average (15m): {:.2}",
|
|
||||||
// report.system_load.load_avg_15min
|
|
||||||
// );
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
// Err(e) => Err(AppError::Monitor(e)),
|
|
||||||
// },
|
|
||||||
// Some(CliCommand::SetPlatformProfile { profile }) => {
|
|
||||||
// // Get available platform profiles and validate early if possible
|
|
||||||
// match cpu::get_platform_profiles() {
|
|
||||||
// Ok(available_profiles) => {
|
|
||||||
// if available_profiles.contains(&profile) {
|
|
||||||
// log::info!("Setting platform profile to '{profile}'");
|
|
||||||
// cpu::set_platform_profile(&profile).map_err(AppError::Control)
|
|
||||||
// } else {
|
|
||||||
// log::error!(
|
|
||||||
// "Invalid platform profile: '{}'. Available profiles: {}",
|
|
||||||
// profile,
|
|
||||||
// available_profiles.join(", ")
|
|
||||||
// );
|
|
||||||
// Err(AppError::Generic(format!(
|
|
||||||
// "Invalid platform profile: '{}'. Available profiles: {}",
|
|
||||||
// profile,
|
|
||||||
// available_profiles.join(", ")
|
|
||||||
// )))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// Err(_e) => {
|
|
||||||
// // If we can't get profiles (e.g., feature not supported), pass through to the function
|
|
||||||
// cpu::set_platform_profile(&profile).map_err(AppError::Control)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
use crate::config::AppConfig;
|
use crate::config::AppConfig;
|
||||||
use crate::core::{BatteryInfo, CpuCoreInfo, CpuGlobalInfo, SystemInfo, SystemLoad, SystemReport};
|
use crate::core::{BatteryInfo, CpuCoreInfo, CpuGlobalInfo, SystemInfo, SystemLoad, SystemReport};
|
||||||
use crate::cpu::get_real_cpus;
|
|
||||||
use crate::util::error::SysMonitorError;
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fs,
|
fs,
|
||||||
|
@ -12,10 +10,8 @@ use std::{
|
||||||
time::SystemTime,
|
time::SystemTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type Result<T, E = SysMonitorError> = std::result::Result<T, E>;
|
|
||||||
|
|
||||||
// Read a sysfs file to a string, trimming whitespace
|
// Read a sysfs file to a string, trimming whitespace
|
||||||
fn read_sysfs_file_trimmed(path: impl AsRef<Path>) -> Result<String> {
|
fn read_sysfs_file_trimmed(path: impl AsRef<Path>) -> anyhow::Result<String> {
|
||||||
fs::read_to_string(path.as_ref())
|
fs::read_to_string(path.as_ref())
|
||||||
.map(|s| s.trim().to_string())
|
.map(|s| s.trim().to_string())
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
|
@ -24,7 +20,7 @@ fn read_sysfs_file_trimmed(path: impl AsRef<Path>) -> Result<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read a sysfs file and parse it to a specific type
|
// Read a sysfs file and parse it to a specific type
|
||||||
fn read_sysfs_value<T: FromStr>(path: impl AsRef<Path>) -> Result<T> {
|
fn read_sysfs_value<T: FromStr>(path: impl AsRef<Path>) -> anyhow::Result<T> {
|
||||||
let content = read_sysfs_file_trimmed(path.as_ref())?;
|
let content = read_sysfs_file_trimmed(path.as_ref())?;
|
||||||
content.parse::<T>().map_err(|_| {
|
content.parse::<T>().map_err(|_| {
|
||||||
SysMonitorError::ParseError(format!(
|
SysMonitorError::ParseError(format!(
|
||||||
|
@ -76,7 +72,7 @@ impl CpuTimes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_all_cpu_times() -> Result<HashMap<u32, CpuTimes>> {
|
fn read_all_cpu_times() -> anyhow::Result<HashMap<u32, CpuTimes>> {
|
||||||
let content = fs::read_to_string("/proc/stat").map_err(SysMonitorError::Io)?;
|
let content = fs::read_to_string("/proc/stat").map_err(SysMonitorError::Io)?;
|
||||||
let mut cpu_times_map = HashMap::new();
|
let mut cpu_times_map = HashMap::new();
|
||||||
|
|
||||||
|
@ -156,7 +152,7 @@ pub fn get_cpu_core_info(
|
||||||
core_id: u32,
|
core_id: u32,
|
||||||
prev_times: &CpuTimes,
|
prev_times: &CpuTimes,
|
||||||
current_times: &CpuTimes,
|
current_times: &CpuTimes,
|
||||||
) -> Result<CpuCoreInfo> {
|
) -> anyhow::Result<CpuCoreInfo> {
|
||||||
let cpufreq_path = PathBuf::from(format!("/sys/devices/system/cpu/cpu{core_id}/cpufreq/"));
|
let cpufreq_path = PathBuf::from(format!("/sys/devices/system/cpu/cpu{core_id}/cpufreq/"));
|
||||||
|
|
||||||
let current_frequency_mhz = read_sysfs_value::<u32>(cpufreq_path.join("scaling_cur_freq"))
|
let current_frequency_mhz = read_sysfs_value::<u32>(cpufreq_path.join("scaling_cur_freq"))
|
||||||
|
@ -358,7 +354,7 @@ fn get_fallback_temperature(hw_path: &Path) -> Option<f32> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_all_cpu_core_info() -> Result<Vec<CpuCoreInfo>> {
|
pub fn get_all_cpu_core_info() -> anyhow::Result<Vec<CpuCoreInfo>> {
|
||||||
let initial_cpu_times = read_all_cpu_times()?;
|
let initial_cpu_times = read_all_cpu_times()?;
|
||||||
thread::sleep(Duration::from_millis(250)); // interval for CPU usage calculation
|
thread::sleep(Duration::from_millis(250)); // interval for CPU usage calculation
|
||||||
let final_cpu_times = read_all_cpu_times()?;
|
let final_cpu_times = read_all_cpu_times()?;
|
||||||
|
@ -486,7 +482,7 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_battery_info(config: &AppConfig) -> Result<Vec<BatteryInfo>> {
|
pub fn get_battery_info(config: &AppConfig) -> anyhow::Result<Vec<BatteryInfo>> {
|
||||||
let mut batteries = Vec::new();
|
let mut batteries = Vec::new();
|
||||||
let power_supply_path = Path::new("/sys/class/power_supply");
|
let power_supply_path = Path::new("/sys/class/power_supply");
|
||||||
|
|
||||||
|
@ -682,7 +678,7 @@ fn is_likely_desktop_system() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_system_load() -> Result<SystemLoad> {
|
pub fn get_system_load() -> anyhow::Result<SystemLoad> {
|
||||||
let loadavg_str = read_sysfs_file_trimmed("/proc/loadavg")?;
|
let loadavg_str = read_sysfs_file_trimmed("/proc/loadavg")?;
|
||||||
let parts: Vec<&str> = loadavg_str.split_whitespace().collect();
|
let parts: Vec<&str> = loadavg_str.split_whitespace().collect();
|
||||||
if parts.len() < 3 {
|
if parts.len() < 3 {
|
||||||
|
@ -707,7 +703,7 @@ pub fn get_system_load() -> Result<SystemLoad> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn collect_system_report(config: &AppConfig) -> Result<SystemReport> {
|
pub fn collect_system_report(config: &AppConfig) -> anyhow::Result<SystemReport> {
|
||||||
let system_info = get_system_info();
|
let system_info = get_system_info();
|
||||||
let cpu_cores = get_all_cpu_core_info()?;
|
let cpu_cores = get_all_cpu_core_info()?;
|
||||||
let cpu_global = get_cpu_global_info(&cpu_cores);
|
let cpu_global = get_cpu_global_info(&cpu_cores);
|
||||||
|
@ -724,7 +720,7 @@ pub fn collect_system_report(config: &AppConfig) -> Result<SystemReport> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_cpu_model() -> Result<String> {
|
pub fn get_cpu_model() -> anyhow::Result<String> {
|
||||||
let path = Path::new("/proc/cpuinfo");
|
let path = Path::new("/proc/cpuinfo");
|
||||||
let content = fs::read_to_string(path).map_err(|_| {
|
let content = fs::read_to_string(path).map_err(|_| {
|
||||||
SysMonitorError::ReadError(format!("Cannot read contents of {}.", path.display()))
|
SysMonitorError::ReadError(format!("Cannot read contents of {}.", path.display()))
|
||||||
|
@ -743,7 +739,7 @@ pub fn get_cpu_model() -> Result<String> {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_linux_distribution() -> Result<String> {
|
pub fn get_linux_distribution() -> anyhow::Result<String> {
|
||||||
let os_release_path = Path::new("/etc/os-release");
|
let os_release_path = Path::new("/etc/os-release");
|
||||||
let content = fs::read_to_string(os_release_path).map_err(|_| {
|
let content = fs::read_to_string(os_release_path).map_err(|_| {
|
||||||
SysMonitorError::ReadError(format!(
|
SysMonitorError::ReadError(format!(
|
||||||
|
|
|
@ -1,73 +1,10 @@
|
||||||
use anyhow::Context;
|
use anyhow::{Context, bail};
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fmt, fs,
|
fmt, fs,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Represents a pattern of path suffixes used to control charge thresholds
|
|
||||||
/// for different device vendors.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct PowerSupplyConfig {
|
|
||||||
pub manufacturer: &'static str,
|
|
||||||
pub path_start: &'static str,
|
|
||||||
pub path_end: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Charge threshold configs.
|
|
||||||
const POWER_SUPPLY_CONFIGS: &[PowerSupplyConfig] = &[
|
|
||||||
PowerSupplyConfig {
|
|
||||||
manufacturer: "Standard",
|
|
||||||
path_start: "charge_control_start_threshold",
|
|
||||||
path_end: "charge_control_end_threshold",
|
|
||||||
},
|
|
||||||
PowerSupplyConfig {
|
|
||||||
manufacturer: "ASUS",
|
|
||||||
path_start: "charge_control_start_percentage",
|
|
||||||
path_end: "charge_control_end_percentage",
|
|
||||||
},
|
|
||||||
// Combine Huawei and ThinkPad since they use identical paths.
|
|
||||||
PowerSupplyConfig {
|
|
||||||
manufacturer: "ThinkPad/Huawei",
|
|
||||||
path_start: "charge_start_threshold",
|
|
||||||
path_end: "charge_stop_threshold",
|
|
||||||
},
|
|
||||||
// Framework laptop support.
|
|
||||||
PowerSupplyConfig {
|
|
||||||
manufacturer: "Framework",
|
|
||||||
path_start: "charge_behaviour_start_threshold",
|
|
||||||
path_end: "charge_behaviour_end_threshold",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Represents a power supply that supports charge threshold control.
|
|
||||||
pub struct PowerSupply {
|
|
||||||
pub name: String,
|
|
||||||
pub path: PathBuf,
|
|
||||||
pub config: PowerSupplyConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for PowerSupply {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"power suppply '{name}' from manufacturer '{manufacturer}'",
|
|
||||||
name = &self.name,
|
|
||||||
manufacturer = &self.config.manufacturer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PowerSupply {
|
|
||||||
pub fn charge_threshold_path_start(&self) -> PathBuf {
|
|
||||||
self.path.join(self.config.path_start)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn charge_threshold_path_end(&self) -> PathBuf {
|
|
||||||
self.path.join(self.config.path_end)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Migrate to central utils file. Same exists in cpu.rs.
|
// TODO: Migrate to central utils file. Same exists in cpu.rs.
|
||||||
fn write(path: impl AsRef<Path>, value: &str) -> anyhow::Result<()> {
|
fn write(path: impl AsRef<Path>, value: &str) -> anyhow::Result<()> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
@ -80,22 +17,111 @@ fn write(path: impl AsRef<Path>, value: &str) -> anyhow::Result<()> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_power_supply(path: &Path) -> anyhow::Result<bool> {
|
/// Represents a pattern of path suffixes used to control charge thresholds
|
||||||
let type_path = path.join("type");
|
/// for different device vendors.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
let type_ = fs::read_to_string(&type_path)
|
pub struct PowerSupplyThresholdConfig {
|
||||||
.with_context(|| format!("failed to read '{path}'", path = type_path.display()))?;
|
pub manufacturer: &'static str,
|
||||||
|
pub path_start: &'static str,
|
||||||
Ok(type_ == "Battery")
|
pub path_end: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all batteries in the system that support threshold control.
|
/// Power supply threshold configs.
|
||||||
pub fn get_power_supplies() -> anyhow::Result<Vec<PowerSupply>> {
|
const POWER_SUPPLY_THRESHOLD_CONFIGS: &[PowerSupplyThresholdConfig] = &[
|
||||||
const PATH: &str = "/sys/class/power_supply";
|
PowerSupplyThresholdConfig {
|
||||||
|
manufacturer: "Standard",
|
||||||
|
path_start: "charge_control_start_threshold",
|
||||||
|
path_end: "charge_control_end_threshold",
|
||||||
|
},
|
||||||
|
PowerSupplyThresholdConfig {
|
||||||
|
manufacturer: "ASUS",
|
||||||
|
path_start: "charge_control_start_percentage",
|
||||||
|
path_end: "charge_control_end_percentage",
|
||||||
|
},
|
||||||
|
// Combine Huawei and ThinkPad since they use identical paths.
|
||||||
|
PowerSupplyThresholdConfig {
|
||||||
|
manufacturer: "ThinkPad/Huawei",
|
||||||
|
path_start: "charge_start_threshold",
|
||||||
|
path_end: "charge_stop_threshold",
|
||||||
|
},
|
||||||
|
// Framework laptop support.
|
||||||
|
PowerSupplyThresholdConfig {
|
||||||
|
manufacturer: "Framework",
|
||||||
|
path_start: "charge_behaviour_start_threshold",
|
||||||
|
path_end: "charge_behaviour_end_threshold",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Represents a power supply that supports charge threshold control.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct PowerSupply {
|
||||||
|
pub name: String,
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub threshold_config: Option<PowerSupplyThresholdConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for PowerSupply {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"power supply '{name}' at '{path}'",
|
||||||
|
name = &self.name,
|
||||||
|
path = self.path.display(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if let Some(config) = self.threshold_config.as_ref() {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
" from manufacturer '{manufacturer}'",
|
||||||
|
manufacturer = config.manufacturer,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const POWER_SUPPLY_PATH: &str = "/sys/class/power_supply";
|
||||||
|
|
||||||
|
impl PowerSupply {
|
||||||
|
pub fn from_name(name: String) -> anyhow::Result<Self> {
|
||||||
|
let mut power_supply = Self {
|
||||||
|
path: Path::new(POWER_SUPPLY_PATH).join(&name),
|
||||||
|
name,
|
||||||
|
threshold_config: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
power_supply.rescan()?;
|
||||||
|
|
||||||
|
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,
|
||||||
|
|
||||||
|
threshold_config: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
power_supply.rescan()?;
|
||||||
|
|
||||||
|
Ok(power_supply)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn all() -> anyhow::Result<Vec<PowerSupply>> {
|
||||||
let mut power_supplies = Vec::new();
|
let mut power_supplies = Vec::new();
|
||||||
|
|
||||||
'entries: for entry in fs::read_dir(PATH).with_context(|| format!("failed to read '{PATH}'"))? {
|
for entry in fs::read_dir(POWER_SUPPLY_PATH)
|
||||||
|
.with_context(|| format!("failed to read '{POWER_SUPPLY_PATH}'"))?
|
||||||
|
{
|
||||||
let entry = match entry {
|
let entry = match entry {
|
||||||
Ok(entry) => entry,
|
Ok(entry) => entry,
|
||||||
|
|
||||||
|
@ -105,71 +131,121 @@ pub fn get_power_supplies() -> anyhow::Result<Vec<PowerSupply>> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let entry_path = entry.path();
|
power_supplies.push(PowerSupply::from_path(entry.path())?);
|
||||||
|
|
||||||
if !is_power_supply(&entry_path).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to determine whether if '{path}' is a power supply",
|
|
||||||
path = entry_path.display(),
|
|
||||||
)
|
|
||||||
})? {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for config in POWER_SUPPLY_CONFIGS {
|
|
||||||
if entry_path.join(config.path_start).exists()
|
|
||||||
&& entry_path.join(config.path_end).exists()
|
|
||||||
{
|
|
||||||
power_supplies.push(PowerSupply {
|
|
||||||
name: entry_path
|
|
||||||
.file_name()
|
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to get file name of '{path}'",
|
|
||||||
path = entry_path.display(),
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string(),
|
|
||||||
|
|
||||||
path: entry_path,
|
|
||||||
|
|
||||||
config: *config,
|
|
||||||
});
|
|
||||||
continue 'entries;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(power_supplies)
|
Ok(power_supplies)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_charge_threshold_start(
|
fn get_type(&self) -> anyhow::Result<String> {
|
||||||
power_supply: &PowerSupply,
|
let type_path = self.path.join("type");
|
||||||
charge_threshold_start: u8,
|
|
||||||
) -> anyhow::Result<()> {
|
let type_ = fs::read_to_string(&type_path)
|
||||||
|
.with_context(|| format!("failed to read '{path}'", path = type_path.display()))?;
|
||||||
|
|
||||||
|
Ok(type_)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rescan(&mut self) -> anyhow::Result<()> {
|
||||||
|
let threshold_config = self
|
||||||
|
.get_type()
|
||||||
|
.with_context(|| format!("failed to determine what type of power supply '{self}' is"))?
|
||||||
|
.eq("Battery")
|
||||||
|
.then(|| {
|
||||||
|
for config in POWER_SUPPLY_THRESHOLD_CONFIGS {
|
||||||
|
if self.path.join(config.path_start).exists()
|
||||||
|
&& self.path.join(config.path_end).exists()
|
||||||
|
{
|
||||||
|
return Some(*config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
self.threshold_config = threshold_config;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn charge_threshold_path_start(&self) -> Option<PathBuf> {
|
||||||
|
self.threshold_config
|
||||||
|
.map(|config| self.path.join(config.path_start))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn charge_threshold_path_end(&self) -> Option<PathBuf> {
|
||||||
|
self.threshold_config
|
||||||
|
.map(|config| self.path.join(config.path_end))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_charge_threshold_start(&self, charge_threshold_start: u8) -> anyhow::Result<()> {
|
||||||
write(
|
write(
|
||||||
&power_supply.charge_threshold_path_start(),
|
&self.charge_threshold_path_start().ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"power supply '{name}' does not support changing charge threshold levels",
|
||||||
|
name = self.name,
|
||||||
|
)
|
||||||
|
})?,
|
||||||
&charge_threshold_start.to_string(),
|
&charge_threshold_start.to_string(),
|
||||||
)
|
)
|
||||||
.with_context(|| format!("failed to set charge threshold start for {power_supply}"))?;
|
.with_context(|| format!("failed to set charge threshold start for {self}"))?;
|
||||||
|
|
||||||
log::info!("set battery threshold start for {power_supply} to {charge_threshold_start}%");
|
log::info!("set battery threshold start for {self} to {charge_threshold_start}%");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_charge_threshold_end(
|
pub fn set_charge_threshold_end(&self, charge_threshold_end: u8) -> anyhow::Result<()> {
|
||||||
power_supply: &PowerSupply,
|
|
||||||
charge_threshold_end: u8,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
write(
|
write(
|
||||||
&power_supply.charge_threshold_path_end(),
|
&self.charge_threshold_path_end().ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"power supply '{name}' does not support changing charge threshold levels",
|
||||||
|
name = self.name,
|
||||||
|
)
|
||||||
|
})?,
|
||||||
&charge_threshold_end.to_string(),
|
&charge_threshold_end.to_string(),
|
||||||
)
|
)
|
||||||
.with_context(|| format!("failed to set charge threshold end for {power_supply}"))?;
|
.with_context(|| format!("failed to set charge threshold end for {self}"))?;
|
||||||
|
|
||||||
log::info!("set battery threshold end for {power_supply} to {charge_threshold_end}%");
|
log::info!("set battery threshold end for {self} to {charge_threshold_end}%");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_available_platform_profiles() -> Vec<String> {
|
||||||
|
let path = "/sys/firmware/acpi/platform_profile_choices";
|
||||||
|
|
||||||
|
let Ok(content) = fs::read_to_string(path) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
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(", "),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
write("/sys/firmware/acpi/platform_profile", profile)
|
||||||
|
.context("this probably means that your system does not support changing ACPI profiles")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,80 +0,0 @@
|
||||||
use std::io;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum ControlError {
|
|
||||||
#[error("I/O error: {0}")]
|
|
||||||
Io(#[from] io::Error),
|
|
||||||
|
|
||||||
#[error("Failed to write to sysfs path: {0}")]
|
|
||||||
WriteError(String),
|
|
||||||
|
|
||||||
#[error("Failed to read sysfs path: {0}")]
|
|
||||||
ReadError(String),
|
|
||||||
|
|
||||||
#[error("Invalid value for setting: {0}")]
|
|
||||||
InvalidValueError(String),
|
|
||||||
|
|
||||||
#[error("Control action not supported: {0}")]
|
|
||||||
NotSupported(String),
|
|
||||||
|
|
||||||
#[error("Permission denied: {0}. Try running with sudo.")]
|
|
||||||
PermissionDenied(String),
|
|
||||||
|
|
||||||
#[error("Invalid platform control profile {0} supplied, please provide a valid one.")]
|
|
||||||
InvalidProfile(String),
|
|
||||||
|
|
||||||
#[error("Invalid governor: {0}")]
|
|
||||||
InvalidGovernor(String),
|
|
||||||
|
|
||||||
#[error("Failed to parse value: {0}")]
|
|
||||||
ParseError(String),
|
|
||||||
|
|
||||||
#[error("Path missing: {0}")]
|
|
||||||
PathMissing(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum SysMonitorError {
|
|
||||||
#[error("I/O error: {0}")]
|
|
||||||
Io(#[from] io::Error),
|
|
||||||
|
|
||||||
#[error("Failed to read sysfs path: {0}")]
|
|
||||||
ReadError(String),
|
|
||||||
|
|
||||||
#[error("Failed to parse value: {0}")]
|
|
||||||
ParseError(String),
|
|
||||||
|
|
||||||
#[error("Failed to parse /proc/stat: {0}")]
|
|
||||||
ProcStatParseError(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum EngineError {
|
|
||||||
#[error("CPU control error: {0}")]
|
|
||||||
ControlError(#[from] ControlError),
|
|
||||||
|
|
||||||
#[error("Configuration error: {0}")]
|
|
||||||
ConfigurationError(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
// A unified error type for the entire application
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum AppError {
|
|
||||||
#[error("{0}")]
|
|
||||||
Control(#[from] ControlError),
|
|
||||||
|
|
||||||
#[error("{0}")]
|
|
||||||
Monitor(#[from] SysMonitorError),
|
|
||||||
|
|
||||||
#[error("{0}")]
|
|
||||||
Engine(#[from] EngineError),
|
|
||||||
|
|
||||||
#[error("{0}")]
|
|
||||||
Config(#[from] crate::config::ConfigError),
|
|
||||||
|
|
||||||
#[error("{0}")]
|
|
||||||
Generic(String),
|
|
||||||
|
|
||||||
#[error("I/O error: {0}")]
|
|
||||||
Io(#[from] io::Error),
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub mod sysfs;
|
|
|
@ -1,80 +0,0 @@
|
||||||
use crate::util::error::ControlError;
|
|
||||||
use std::{fs, io, path::Path};
|
|
||||||
|
|
||||||
/// Write a value to a sysfs file with consistent error handling
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `path` - The file path to write to
|
|
||||||
/// * `value` - The string value to write
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns a `ControlError` variant based on the specific error:
|
|
||||||
/// - `ControlError::PermissionDenied` if permission is denied
|
|
||||||
/// - `ControlError::PathMissing` if the path doesn't exist
|
|
||||||
/// - `ControlError::WriteError` for other I/O errors
|
|
||||||
pub fn write_sysfs_value(path: impl AsRef<Path>, value: &str) -> Result<(), ControlError> {
|
|
||||||
let p = path.as_ref();
|
|
||||||
|
|
||||||
fs::write(p, value).map_err(|e| {
|
|
||||||
let error_msg = format!("Path: {:?}, Value: '{}', Error: {}", p.display(), value, e);
|
|
||||||
match e.kind() {
|
|
||||||
io::ErrorKind::PermissionDenied => ControlError::PermissionDenied(error_msg),
|
|
||||||
io::ErrorKind::NotFound => {
|
|
||||||
ControlError::PathMissing(format!("Path '{}' does not exist", p.display()))
|
|
||||||
}
|
|
||||||
_ => ControlError::WriteError(error_msg),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a value from a sysfs file with consistent error handling
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `path` - The file path to read from
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// Returns the trimmed contents of the file as a String
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns a `ControlError` variant based on the specific error:
|
|
||||||
/// - `ControlError::PermissionDenied` if permission is denied
|
|
||||||
/// - `ControlError::PathMissing` if the path doesn't exist
|
|
||||||
/// - `ControlError::ReadError` for other I/O errors
|
|
||||||
pub fn read_sysfs_value(path: impl AsRef<Path>) -> Result<String, ControlError> {
|
|
||||||
let p = path.as_ref();
|
|
||||||
fs::read_to_string(p)
|
|
||||||
.map_err(|e| {
|
|
||||||
let error_msg = format!("Path: {:?}, Error: {}", p.display(), e);
|
|
||||||
match e.kind() {
|
|
||||||
io::ErrorKind::PermissionDenied => ControlError::PermissionDenied(error_msg),
|
|
||||||
io::ErrorKind::NotFound => {
|
|
||||||
ControlError::PathMissing(format!("Path '{}' does not exist", p.display()))
|
|
||||||
}
|
|
||||||
_ => ControlError::ReadError(error_msg),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(|s| s.trim().to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Safely check if a path exists and is writable
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `path` - The file path to check
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// Returns true if the path exists and is writable, false otherwise
|
|
||||||
pub fn path_exists_and_writable(path: &Path) -> bool {
|
|
||||||
if !path.exists() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to open the file with write access to verify write permission
|
|
||||||
fs::OpenOptions::new().write(true).open(path).is_ok()
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue