diff --git a/Cargo.lock b/Cargo.lock index 814b813..74a0671 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -715,7 +715,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" dependencies = [ - "toml_edit", + "toml_edit 0.21.1", ] [[package]] @@ -826,6 +826,15 @@ dependencies = [ "syn 2.0.63", ] +[[package]] +name = "serde_spanned" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +dependencies = [ + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -890,12 +899,15 @@ dependencies = [ "itertools", "libc", "nix", + "serde", "tempfile", "tokio", "tokio-stream", "tokio-util", + "toml", "tracing", "tracing-subscriber", + "xdg", "zbus", "zbus_xml", ] @@ -998,11 +1010,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.12", +] + [[package]] name = "toml_datetime" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -1012,7 +1039,20 @@ checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ "indexmap", "toml_datetime", - "winnow", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.8", ] [[package]] @@ -1262,6 +1302,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" +dependencies = [ + "memchr", +] + +[[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + [[package]] name = "xdg-home" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9c2635e..b9d25ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,12 @@ inotify = { version = "0.10", default-features = false, features = ["stream"] } libc = "0.2" itertools = "0.12" nix = { version = "0.28", default-features = false, features = ["fs", "signal"] } +serde = { version = "1.0", default-features = false, features = ["derive"] } tokio = { version = "1", default-features = false, features = ["fs", "io-std", "io-util", "macros", "process", "rt-multi-thread", "signal", "sync"] } tokio-stream = { version = "0.1", default-features = false } tokio-util = { version = "0.7", default-features = false } +toml = "0.8" tracing = { version = "0.1", default-features = false } tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } +xdg = "2.5" zbus = { version = "4", default-features = false, features = ["tokio"] } diff --git a/src/daemon/config.rs b/src/daemon/config.rs index 0a3fb73..bc87820 100644 --- a/src/daemon/config.rs +++ b/src/daemon/config.rs @@ -5,8 +5,40 @@ * SPDX-License-Identifier: MIT */ -use anyhow::Result; +use anyhow::{anyhow, Result}; +use std::io::ErrorKind; +use tokio::fs::{create_dir_all, read_to_string, write}; +use tracing::{error, info}; -pub(in crate::daemon) async fn read_config() -> Result<()> { +use crate::daemon::DaemonContext; + +pub(in crate::daemon) async fn read_state(context: &C) -> Result { + let path = context.state_path()?; + let state = match read_to_string(path).await { + Ok(state) => state, + Err(e) => { + if e.kind() == ErrorKind::NotFound { + info!("No state file found, reloading default state"); + return Ok(C::State::default()); + } + error!("Error loading state: {e}"); + return Err(e.into()); + } + }; + Ok(toml::from_str(state.as_str())?) +} + +pub(in crate::daemon) async fn write_state(context: &C) -> Result<()> { + let path = context.state_path()?; + create_dir_all(path.parent().ok_or(anyhow!( + "Context path {} has no parent dir", + path.to_string_lossy() + ))?) + .await?; + let state = toml::to_string_pretty(&context.state())?; + Ok(write(path, state.as_bytes()).await?) +} + +pub(in crate::daemon) async fn read_config(_context: &C) -> Result { todo!(); } diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index dcabd90..a826e2c 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -6,6 +6,10 @@ */ use anyhow::{anyhow, ensure, Result}; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; +use std::marker::PhantomData; +use std::path::PathBuf; use tokio::signal::unix::{signal, SignalKind}; use tokio::task::JoinSet; use tokio_util::sync::CancellationToken; @@ -14,7 +18,7 @@ use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::registry::LookupSpan; use zbus::connection::Connection; -use crate::daemon::config::read_config; +use crate::daemon::config::{read_config, read_state}; use crate::sls::{LogLayer, LogReceiver}; use crate::Service; @@ -25,16 +29,40 @@ mod user; pub use root::daemon as root; pub use user::daemon as user; -pub(crate) struct Daemon { - services: JoinSet>, - token: CancellationToken, +pub(crate) trait DaemonContext: Sized { + type State: for<'a> Deserialize<'a> + Serialize + Default + Debug; + type Config: for<'a> Deserialize<'a> + Default + Debug; + + fn state_path(&self) -> Result { + let config_path = self.user_config_path()?; + Ok(config_path.join("state.toml")) + } + + fn user_config_path(&self) -> Result; + fn system_config_path(&self) -> Result; + fn state(&self) -> Self::State; + + async fn start( + &mut self, + state: Self::State, + config: Self::Config, + daemon: &mut Daemon, + ) -> Result<()>; + + async fn reload(&mut self, config: Self::Config, daemon: &mut Daemon) -> Result<()>; } -impl Daemon { +pub(crate) struct Daemon { + services: JoinSet>, + token: CancellationToken, + _context: PhantomData, +} + +impl Daemon { pub(crate) async fn new LookupSpan<'a>>( subscriber: S, connection: Connection, - ) -> Result { + ) -> Result> { let services = JoinSet::new(); let token = CancellationToken::new(); @@ -43,7 +71,11 @@ impl Daemon { let subscriber = subscriber.with(remote_logger); tracing::subscriber::set_global_default(subscriber)?; - let mut daemon = Daemon { services, token }; + let mut daemon = Daemon { + services, + token, + _context: PhantomData::default(), + }; daemon.add_service(log_receiver); Ok(daemon) @@ -57,12 +89,16 @@ impl Daemon { token } - pub(crate) async fn run(&mut self) -> Result<()> { + pub(crate) async fn run(&mut self, mut context: C) -> Result<()> { ensure!( !self.services.is_empty(), "Can't run a daemon with no services attached." ); + let state = read_state(&context).await?; + let config = read_config(&context).await?; + context.start(state, config, self).await?; + let mut res = loop { let mut sigterm = signal(SignalKind::terminate())?; let mut sigquit = signal(SignalKind::quit())?; @@ -81,10 +117,14 @@ impl Daemon { }, e = sighup.recv() => match e { Some(_) => { - if let Err(error) = read_config().await { - error!("Failed to reload configuration: {error}"); + match read_config(&context).await { + Ok(config) => + context.reload(config, self).await, + Err(error) => { + error!("Failed to load configuration: {error}"); + Ok(()) + } } - Ok(()) } None => Err(anyhow!("SIGHUP machine broke")), }, diff --git a/src/daemon/root.rs b/src/daemon/root.rs index 2b065c7..1a8f621 100644 --- a/src/daemon/root.rs +++ b/src/daemon/root.rs @@ -6,17 +6,74 @@ */ use anyhow::{bail, Result}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; use tracing::error; use tracing_subscriber::prelude::*; use tracing_subscriber::{fmt, Registry}; use zbus::connection::Connection; use zbus::ConnectionBuilder; -use crate::daemon::Daemon; +use crate::daemon::{Daemon, DaemonContext}; use crate::ds_inhibit::Inhibitor; use crate::manager::root::SteamOSManager; +use crate::path; use crate::sls::ftrace::Ftrace; +#[derive(Copy, Clone, Default, Deserialize, Serialize, Debug)] +pub(crate) struct RootConfig { + pub services: RootServicesConfig, +} + +#[derive(Copy, Clone, Default, Deserialize, Serialize, Debug)] +pub(crate) struct RootServicesConfig {} + +#[derive(Copy, Clone, Default, Deserialize, Serialize, Debug)] +pub(crate) struct RootState { + pub services: RootServicesState, +} + +#[derive(Copy, Clone, Default, Deserialize, Serialize, Debug)] +pub(crate) struct RootServicesState {} + +struct RootContext {} + +impl DaemonContext for RootContext { + type State = RootState; + type Config = RootConfig; + + fn user_config_path(&self) -> Result { + Ok(path("/etc/steamos-manager")) + } + + fn system_config_path(&self) -> Result { + Ok(path("/usr/lib/steamos-manager/system.d")) + } + + fn state(&self) -> RootState { + RootState::default() + } + + async fn start( + &mut self, + _state: RootState, + _config: RootConfig, + _daemon: &mut Daemon, + ) -> Result<()> { + // Nothing to do yet + Ok(()) + } + + async fn reload( + &mut self, + _config: RootConfig, + _daemon: &mut Daemon, + ) -> Result<()> { + // Nothing to do yet + Ok(()) + } +} + async fn create_connection() -> Result { let connection = ConnectionBuilder::system()? .name("com.steampowered.SteamOSManager1")? @@ -45,6 +102,8 @@ pub async fn daemon() -> Result<()> { bail!(e); } }; + + let context = RootContext {}; let mut daemon = Daemon::new(subscriber, connection.clone()).await?; let ftrace = Ftrace::init(connection.clone()).await?; @@ -53,5 +112,5 @@ pub async fn daemon() -> Result<()> { let inhibitor = Inhibitor::init().await?; daemon.add_service(inhibitor); - daemon.run().await + daemon.run(context).await } diff --git a/src/daemon/user.rs b/src/daemon/user.rs index 96e7c0d..18eb64f 100644 --- a/src/daemon/user.rs +++ b/src/daemon/user.rs @@ -6,14 +6,80 @@ */ use anyhow::{bail, Result}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; use tracing::error; use tracing_subscriber::prelude::*; use tracing_subscriber::{fmt, Registry}; +#[cfg(not(test))] +use xdg::BaseDirectories; use zbus::connection::Connection; use zbus::ConnectionBuilder; -use crate::daemon::Daemon; +use crate::daemon::{Daemon, DaemonContext}; use crate::manager::user::SteamOSManager; +use crate::path; + +#[derive(Copy, Clone, Default, Deserialize, Serialize, Debug)] +struct UserConfig { + pub services: UserServicesConfig, +} + +#[derive(Copy, Clone, Default, Deserialize, Serialize, Debug)] +pub(crate) struct UserServicesConfig {} + +#[derive(Copy, Clone, Default, Deserialize, Serialize, Debug)] +struct UserState { + pub services: UserServicesState, +} + +#[derive(Copy, Clone, Default, Deserialize, Serialize, Debug)] +pub(crate) struct UserServicesState {} + +struct UserContext {} + +impl DaemonContext for UserContext { + type State = UserState; + type Config = UserConfig; + + #[cfg(not(test))] + fn user_config_path(&self) -> Result { + let xdg_base = BaseDirectories::new()?; + Ok(xdg_base.get_config_file("steamos-manager")) + } + + #[cfg(test)] + fn user_config_path(&self) -> Result { + Ok(path("steamos-manager")) + } + + fn system_config_path(&self) -> Result { + Ok(path("/usr/lib/steamos-manager/user.d")) + } + + fn state(&self) -> UserState { + UserState::default() + } + + async fn start( + &mut self, + _state: UserState, + _config: UserConfig, + _daemon: &mut Daemon, + ) -> Result<()> { + // Nothing to do yet + Ok(()) + } + + async fn reload( + &mut self, + _config: UserConfig, + _daemon: &mut Daemon, + ) -> Result<()> { + // Nothing to do yet + Ok(()) + } +} async fn create_connections() -> Result<(Connection, Connection)> { let system = Connection::system().await?; @@ -47,7 +113,8 @@ pub async fn daemon() -> Result<()> { } }; + let context = UserContext {}; let mut daemon = Daemon::new(subscriber, system).await?; - daemon.run().await + daemon.run(context).await }