diff --git a/Cargo.lock b/Cargo.lock index dc24e57..82611dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1241,6 +1241,26 @@ dependencies = [ "serde", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "itoa" version = "1.0.10" @@ -1256,6 +1276,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kqueue" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "lact" version = "0.5.4" @@ -1302,6 +1342,7 @@ dependencies = [ "libdrm_amdgpu_sys", "libflate", "nix", + "notify", "os-release", "pciid-parser", "serde", @@ -1514,6 +1555,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.48.0", ] @@ -1531,6 +1573,23 @@ dependencies = [ "memoffset", ] +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.4.2", + "filetime", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "walkdir", + "windows-sys 0.48.0", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1927,6 +1986,15 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2472,6 +2540,16 @@ dependencies = [ "vk-parse", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2548,6 +2626,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/lact-daemon/Cargo.toml b/lact-daemon/Cargo.toml index 2a31532..9692f4a 100644 --- a/lact-daemon/Cargo.toml +++ b/lact-daemon/Cargo.toml @@ -38,3 +38,4 @@ tar = "0.4.40" libflate = "2.0.0" chrono = "0.4.31" os-release = "0.1.0" +notify = { version = "6.1.1", default-features = false } diff --git a/lact-daemon/src/config.rs b/lact-daemon/src/config.rs index 3be1d81..a4630db 100644 --- a/lact-daemon/src/config.rs +++ b/lact-daemon/src/config.rs @@ -3,10 +3,12 @@ use amdgpu_sysfs::gpu_handle::{PerformanceLevel, PowerLevelKind}; use anyhow::Context; use lact_schema::{default_fan_curve, request::SetClocksCommand, FanControlMode, PmfwOptions}; use nix::unistd::getuid; +use notify::{RecommendedWatcher, Watcher}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use std::{collections::HashMap, env, fs, path::PathBuf}; -use tracing::debug; +use tokio::sync::mpsc; +use tracing::{debug, error}; const FILE_NAME: &str = "config.yaml"; const DEFAULT_ADMIN_GROUPS: [&str; 2] = ["wheel", "sudo"]; @@ -158,6 +160,55 @@ impl Config { } } +pub fn start_watcher() -> mpsc::UnboundedReceiver { + let (config_tx, config_rx) = mpsc::unbounded_channel(); + let (event_tx, event_rx) = std::sync::mpsc::channel(); + + tokio::task::spawn_blocking(move || { + let mut watcher = RecommendedWatcher::new(event_tx, notify::Config::default()) + .expect("Could not create config file watcher"); + + let config_path = get_path(); + let watch_path = config_path + .parent() + .expect("Config path always has a parent"); + watcher + .watch(watch_path, notify::RecursiveMode::Recursive) + .expect("Could not subscribe to config file changes"); + + for res in event_rx { + debug!("got config file event {res:?}"); + match res { + Ok(event) => { + use notify::EventKind; + + if !event.paths.contains(&config_path) { + continue; + } + + match event.kind { + EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_) => { + match Config::load() { + Ok(Some(new_config)) => config_tx.send(new_config).unwrap(), + Ok(None) => error!("config was removed!"), + Err(err) => { + error!("could not read config after it was changed: {err:#}"); + } + } + } + _ => (), + } + } + Err(err) => error!("filesystem event error: {err}"), + } + } + + debug!("registered config file event listener at path {watch_path:?}"); + }); + + config_rx +} + fn get_path() -> PathBuf { let uid = getuid(); if uid.is_root() { diff --git a/lact-daemon/src/lib.rs b/lact-daemon/src/lib.rs index 96b673f..9f2cb29 100644 --- a/lact-daemon/src/lib.rs +++ b/lact-daemon/src/lib.rs @@ -54,6 +54,7 @@ pub fn run() -> anyhow::Result<()> { let server = Server::new(config).await?; let handler = server.handler.clone(); + tokio::task::spawn_local(listen_config_changes(handler.clone())); tokio::task::spawn_local(listen_exit_signals(handler.clone())); tokio::task::spawn_local(suspend::listen_events(handler)); server.run().await; @@ -102,6 +103,16 @@ async fn listen_exit_signals(handler: Handler) { std::process::exit(0); } +async fn listen_config_changes(handler: Handler) { + let mut rx = config::start_watcher(); + while let Some(new_config) = rx.recv().await { + info!("config file was changed, reloading"); + handler.config.replace(new_config); + handler.apply_current_config().await; + info!("configuration reloaded"); + } +} + async fn ensure_sufficient_uptime() { match get_uptime() { Ok(current_uptime) => { diff --git a/lact-daemon/src/server/handler.rs b/lact-daemon/src/server/handler.rs index 51c8f0c..3e9ccd7 100644 --- a/lact-daemon/src/server/handler.rs +++ b/lact-daemon/src/server/handler.rs @@ -102,7 +102,7 @@ impl<'a> Handler { config: Rc::new(RefCell::new(config)), confirm_config_tx: Rc::new(RefCell::new(None)), }; - handler.load_config().await; + handler.apply_current_config().await; // Eagerly release memory // `load_controllers` allocates and deallocates the entire PCI ID database, @@ -114,7 +114,7 @@ impl<'a> Handler { Ok(handler) } - pub async fn load_config(&self) { + pub async fn apply_current_config(&self) { let config = self.config.borrow().clone(); // Clone to avoid locking the RwLock on an await point for (id, gpu_config) in &config.gpus { diff --git a/lact-daemon/src/suspend.rs b/lact-daemon/src/suspend.rs index d91b969..208aabd 100644 --- a/lact-daemon/src/suspend.rs +++ b/lact-daemon/src/suspend.rs @@ -10,7 +10,7 @@ pub async fn listen_events(handler: Handler) { Ok(mut stream) => { while stream.next().await.is_some() { info!("suspend/resume event detected, reloading config"); - handler.load_config().await; + handler.apply_current_config().await; } } Err(err) => error!("could not subscribe to suspend events: {err:#}"),