From 4bf4ab5673b87d6f7b00aa083f88f2891381707b Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 13 May 2025 22:49:16 +0300 Subject: [PATCH] daemon: less verbosity by default; fine-tune via config --- Cargo.lock | 10 -- src/config.rs | 124 ++++++++++++++++++++++- src/daemon.rs | 259 ++++++++++++++++++++++++++++++++++++++++++++++--- src/engine.rs | 2 +- src/main.rs | 7 +- src/monitor.rs | 2 +- 6 files changed, 371 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 14dcaa3..4ac1b6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,15 +126,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "daemonize" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab8bfdaacb3c887a54d41bdf48d3af8873b3f5566469f8ba21b92057509f116e" -dependencies = [ - "libc", -] - [[package]] name = "dirs" version = "6.0.0" @@ -333,7 +324,6 @@ version = "0.1.0" dependencies = [ "clap", "ctrlc", - "daemonize", "dirs", "num_cpus", "serde", diff --git a/src/config.rs b/src/config.rs index 962e801..1fbbc08 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ -use crate::core::{OperationalMode, TurboSetting}; +use crate::core::TurboSetting; use serde::Deserialize; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; // Structs for configuration using serde::Deserialize #[derive(Deserialize, Debug, Clone)] @@ -41,6 +41,8 @@ pub struct AppConfig { pub ignored_power_supplies: Option>, #[serde(default = "default_poll_interval_sec")] pub poll_interval_sec: u64, + #[serde(default)] + pub daemon: DaemonConfig, } fn default_poll_interval_sec() -> u64 { @@ -116,6 +118,19 @@ pub fn load_config() -> Result { .battery_charge_thresholds, ignored_power_supplies: toml_app_config.ignored_power_supplies, poll_interval_sec: toml_app_config.poll_interval_sec, + daemon: DaemonConfig { + poll_interval_sec: toml_app_config.daemon.poll_interval_sec, + adaptive_interval: toml_app_config.daemon.adaptive_interval, + min_poll_interval_sec: toml_app_config + .daemon + .min_poll_interval_sec, + max_poll_interval_sec: toml_app_config + .daemon + .max_poll_interval_sec, + throttle_on_battery: toml_app_config.daemon.throttle_on_battery, + log_level: toml_app_config.daemon.log_level.clone(), + stats_file_path: toml_app_config.daemon.stats_file_path.clone(), + }, }; return Ok(app_config); } @@ -140,6 +155,7 @@ pub fn load_config() -> Result { battery_charge_thresholds: default_toml_config.battery_charge_thresholds, ignored_power_supplies: default_toml_config.ignored_power_supplies, poll_interval_sec: default_toml_config.poll_interval_sec, + daemon: DaemonConfig::default(), }) } @@ -165,6 +181,8 @@ pub struct AppConfigToml { pub ignored_power_supplies: Option>, #[serde(default = "default_poll_interval_sec")] pub poll_interval_sec: u64, + #[serde(default)] + pub daemon: DaemonConfigToml, } impl Default for ProfileConfigToml { @@ -192,9 +210,9 @@ pub struct TurboAutoSettings { } // Default thresholds for Auto turbo mode -pub const DEFAULT_LOAD_THRESHOLD_HIGH: f32 = 70.0; // Enable turbo if load is above this -pub const DEFAULT_LOAD_THRESHOLD_LOW: f32 = 30.0; // Disable turbo if load is below this -pub const DEFAULT_TEMP_THRESHOLD_HIGH: f32 = 75.0; // Disable turbo if temperature is above this +pub const DEFAULT_LOAD_THRESHOLD_HIGH: f32 = 70.0; // enable turbo if load is above this +pub const DEFAULT_LOAD_THRESHOLD_LOW: f32 = 30.0; // disable turbo if load is below this +pub const DEFAULT_TEMP_THRESHOLD_HIGH: f32 = 75.0; // disable turbo if temperature is above this fn default_load_threshold_high() -> f32 { DEFAULT_LOAD_THRESHOLD_HIGH @@ -237,3 +255,99 @@ impl From for ProfileConfig { } } } + +#[derive(Deserialize, Debug, Clone)] +pub struct DaemonConfig { + #[serde(default = "default_poll_interval_sec")] + pub poll_interval_sec: u64, + #[serde(default = "default_adaptive_interval")] + pub adaptive_interval: bool, + #[serde(default = "default_min_poll_interval_sec")] + pub min_poll_interval_sec: u64, + #[serde(default = "default_max_poll_interval_sec")] + pub max_poll_interval_sec: u64, + #[serde(default = "default_throttle_on_battery")] + pub throttle_on_battery: bool, + #[serde(default = "default_log_level")] + pub log_level: LogLevel, + #[serde(default = "default_stats_file_path")] + pub stats_file_path: Option, +} + +#[derive(Deserialize, Debug, Clone, PartialEq)] +pub enum LogLevel { + Error, + Warning, + Info, + Debug, +} + +impl Default for DaemonConfig { + fn default() -> Self { + Self { + poll_interval_sec: default_poll_interval_sec(), + adaptive_interval: default_adaptive_interval(), + min_poll_interval_sec: default_min_poll_interval_sec(), + max_poll_interval_sec: default_max_poll_interval_sec(), + throttle_on_battery: default_throttle_on_battery(), + log_level: default_log_level(), + stats_file_path: default_stats_file_path(), + } + } +} + +fn default_adaptive_interval() -> bool { + false +} + +fn default_min_poll_interval_sec() -> u64 { + 1 +} + +fn default_max_poll_interval_sec() -> u64 { + 30 +} + +fn default_throttle_on_battery() -> bool { + true +} + +fn default_log_level() -> LogLevel { + LogLevel::Info +} + +fn default_stats_file_path() -> Option { + None +} + +#[derive(Deserialize, Debug, Clone)] +pub struct DaemonConfigToml { + #[serde(default = "default_poll_interval_sec")] + pub poll_interval_sec: u64, + #[serde(default = "default_adaptive_interval")] + pub adaptive_interval: bool, + #[serde(default = "default_min_poll_interval_sec")] + pub min_poll_interval_sec: u64, + #[serde(default = "default_max_poll_interval_sec")] + pub max_poll_interval_sec: u64, + #[serde(default = "default_throttle_on_battery")] + pub throttle_on_battery: bool, + #[serde(default = "default_log_level")] + pub log_level: LogLevel, + #[serde(default = "default_stats_file_path")] + pub stats_file_path: Option, +} + +impl Default for DaemonConfigToml { + fn default() -> Self { + Self { + poll_interval_sec: default_poll_interval_sec(), + adaptive_interval: default_adaptive_interval(), + min_poll_interval_sec: default_min_poll_interval_sec(), + max_poll_interval_sec: default_max_poll_interval_sec(), + throttle_on_battery: default_throttle_on_battery(), + log_level: default_log_level(), + stats_file_path: default_stats_file_path(), + } + } +} diff --git a/src/daemon.rs b/src/daemon.rs index d302d07..967978b 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,13 +1,27 @@ -use crate::config::AppConfig; +use crate::config::{AppConfig, LogLevel}; +use crate::core::SystemReport; use crate::engine; use crate::monitor; +use std::fs::File; +use std::io::Write; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; -/// Run the daemon in foreground mode -pub fn run_background(config: AppConfig) -> Result<(), Box> { - println!("Starting superfreq daemon in foreground mode..."); +/// Run the daemon +pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), Box> { + // Set effective log level based on config and verbose flag + let effective_log_level = if verbose { + LogLevel::Debug + } else { + config.daemon.log_level.clone() + }; + + log_message( + &effective_log_level, + LogLevel::Info, + "Starting superfreq daemon...", + ); // Create a flag that will be set to true when a signal is received let running = Arc::new(AtomicBool::new(true)); @@ -20,42 +34,259 @@ pub fn run_background(config: AppConfig) -> Result<(), Box { - println!("Collected system report, applying settings..."); + log_message( + &effective_log_level, + LogLevel::Debug, + "Collected system report, applying settings...", + ); + + // Determine current system state + let current_state = determine_system_state(&report); + + // Update the stats file if configured + if let Some(stats_path) = &config.daemon.stats_file_path { + if let Err(e) = write_stats_file(stats_path, &report) { + log_message( + &effective_log_level, + LogLevel::Error, + &format!("Failed to write stats file: {}", e), + ); + } + } + match engine::determine_and_apply_settings(&report, &config, None) { Ok(()) => { - println!("Successfully applied system settings"); + log_message( + &effective_log_level, + LogLevel::Debug, + "Successfully applied system settings", + ); + + // If system state changed or settings were applied differently, record the time + if current_state != last_system_state { + last_settings_change = Instant::now(); + last_system_state = current_state.clone(); + + log_message( + &effective_log_level, + LogLevel::Info, + &format!("System state changed to: {:?}", current_state), + ); + } } Err(e) => { - eprintln!("Error applying system settings: {}", e); + log_message( + &effective_log_level, + LogLevel::Error, + &format!("Error applying system settings: {}", e), + ); } } + + // Adjust poll interval if adaptive polling is enabled + if config.daemon.adaptive_interval { + let time_since_change = last_settings_change.elapsed().as_secs(); + + // If we've been stable for a while, increase the interval (up to max) + if time_since_change > 60 { + current_poll_interval = + (current_poll_interval * 2).min(config.daemon.max_poll_interval_sec); + + log_message( + &effective_log_level, + LogLevel::Debug, + &format!( + "Adaptive polling: increasing interval to {}s", + current_poll_interval + ), + ); + } else if time_since_change < 10 { + // If we've had recent changes, decrease the interval (down to min) + current_poll_interval = + (current_poll_interval / 2).max(config.daemon.min_poll_interval_sec); + + log_message( + &effective_log_level, + LogLevel::Debug, + &format!( + "Adaptive polling: decreasing interval to {}s", + current_poll_interval + ), + ); + } + } else { + // If not adaptive, use the configured poll interval + current_poll_interval = config.daemon.poll_interval_sec; + } + + // If on battery and throttling is enabled, lengthen the poll interval to save power + if config.daemon.throttle_on_battery + && !report.batteries.is_empty() + && report.batteries.first().map_or(false, |b| !b.ac_connected) + { + let battery_multiplier = 2; // Poll half as often on battery + current_poll_interval = (current_poll_interval * battery_multiplier) + .min(config.daemon.max_poll_interval_sec); + + log_message( + &effective_log_level, + LogLevel::Debug, + "On battery power, increasing poll interval to save energy", + ); + } } Err(e) => { - eprintln!("Error collecting system report: {}", e); + log_message( + &effective_log_level, + LogLevel::Error, + &format!("Error collecting system report: {}", e), + ); } } // Sleep for the remaining time in the poll interval let elapsed = start_time.elapsed(); - let poll_duration = Duration::from_secs(config.poll_interval_sec); + let poll_duration = Duration::from_secs(current_poll_interval); if elapsed < poll_duration { let sleep_time = poll_duration - elapsed; - println!("Sleeping for {}s until next cycle", sleep_time.as_secs()); + log_message( + &effective_log_level, + LogLevel::Debug, + &format!("Sleeping for {}s until next cycle", sleep_time.as_secs()), + ); std::thread::sleep(sleep_time); } } - println!("Daemon stopped"); + log_message(&effective_log_level, LogLevel::Info, "Daemon stopped"); Ok(()) } + +/// Log a message based on the current log level +fn log_message(effective_level: &LogLevel, msg_level: LogLevel, message: &str) { + // Only log messages at or above the effective log level + let should_log = match effective_level { + LogLevel::Error => matches!(msg_level, LogLevel::Error), + LogLevel::Warning => matches!(msg_level, LogLevel::Error | LogLevel::Warning), + LogLevel::Info => matches!( + msg_level, + LogLevel::Error | LogLevel::Warning | LogLevel::Info + ), + LogLevel::Debug => true, + }; + + if should_log { + match msg_level { + LogLevel::Error => eprintln!("ERROR: {}", message), + LogLevel::Warning => eprintln!("WARNING: {}", message), + LogLevel::Info => println!("INFO: {}", message), + LogLevel::Debug => println!("DEBUG: {}", message), + } + } +} + +/// Write current system stats to a file for --stats to read +fn write_stats_file(path: &str, report: &SystemReport) -> Result<(), std::io::Error> { + let mut file = File::create(path)?; + + writeln!(file, "timestamp={:?}", report.timestamp)?; + + // CPU info + writeln!(file, "governor={:?}", report.cpu_global.current_governor)?; + writeln!(file, "turbo={:?}", report.cpu_global.turbo_status)?; + + if let Some(temp) = report.cpu_global.average_temperature_celsius { + writeln!(file, "cpu_temp={:.1}", temp)?; + } + + // Battery info + if !report.batteries.is_empty() { + let battery = &report.batteries[0]; + writeln!(file, "ac_power={}", battery.ac_connected)?; + if let Some(cap) = battery.capacity_percent { + writeln!(file, "battery_percent={}", cap)?; + } + } + + // System load + writeln!(file, "load_1m={:.2}", report.system_load.load_avg_1min)?; + writeln!(file, "load_5m={:.2}", report.system_load.load_avg_5min)?; + writeln!(file, "load_15m={:.2}", report.system_load.load_avg_15min)?; + + Ok(()) +} + +/// Simplified system state used for determining when to adjust polling interval +#[derive(Debug, PartialEq, Eq, Clone)] +enum SystemState { + Unknown, + OnAC, + OnBattery, + HighLoad, + LowLoad, + HighTemp, +} + +/// Determine the current system state for adaptive polling +fn determine_system_state(report: &SystemReport) -> SystemState { + // Check power state first + if !report.batteries.is_empty() { + if let Some(battery) = report.batteries.first() { + if battery.ac_connected { + return SystemState::OnAC; + } else { + return SystemState::OnBattery; + } + } + } else { + // No batteries means desktop, so always AC + return SystemState::OnAC; + } + + // Check temperature + if let Some(temp) = report.cpu_global.average_temperature_celsius { + if temp > 80.0 { + return SystemState::HighTemp; + } + } + + // Check load + let avg_load = report.system_load.load_avg_1min; + if avg_load > 3.0 { + return SystemState::HighLoad; + } else if avg_load < 0.5 { + return SystemState::LowLoad; + } + + // Default case + SystemState::Unknown +} diff --git a/src/engine.rs b/src/engine.rs index 3b16af7..d4f3af2 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,4 +1,4 @@ -use crate::config::{AppConfig, ProfileConfig, TurboAutoSettings}; +use crate::config::{AppConfig, ProfileConfig}; use crate::core::{OperationalMode, SystemReport, TurboSetting}; use crate::cpu::{self, ControlError}; diff --git a/src/main.rs b/src/main.rs index 4c5cb11..3114b79 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,7 +21,10 @@ enum Commands { /// Display current system information Info, /// Run as a daemon in the background - Daemon, + Daemon { + #[clap(long)] + verbose: bool, + }, /// Set CPU governor SetGovernor { governor: String, @@ -186,7 +189,7 @@ fn main() { } Some(Commands::SetPlatformProfile { profile }) => cpu::set_platform_profile(&profile) .map_err(|e| Box::new(e) as Box), - Some(Commands::Daemon) => daemon::run_background(config), + Some(Commands::Daemon { verbose }) => daemon::run_daemon(config, verbose), None => { println!("Welcome to superfreq! Use --help for commands."); println!("Current effective configuration: {:?}", config); diff --git a/src/monitor.rs b/src/monitor.rs index 96e3aa3..6271010 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -578,7 +578,7 @@ pub fn get_battery_info(config: &AppConfig) -> Result> { let mut batteries = Vec::new(); let power_supply_path = Path::new("/sys/class/power_supply"); - if (!power_supply_path.exists()) { + if !power_supply_path.exists() { return Ok(batteries); // no power supply directory }