diff --git a/Cargo.lock b/Cargo.lock index ac60184105728b4360734684a19465dc70ff0406..9710fe5a6efc1d02ac7a5e181ca9065480e44c6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1992,6 +1992,7 @@ dependencies = [ "anyhow", "async-trait", "axum", + "clap", "futures", "hyper", "lektor_mpris", diff --git a/lektor_utils/src/config/base.rs b/lektor_utils/src/config/base.rs index 8e95d67ec8c9f31c3ed3cc9636f561d075d374f4..0207bef0a8a5075823f885b3d206c5b20f005411 100644 --- a/lektor_utils/src/config/base.rs +++ b/lektor_utils/src/config/base.rs @@ -35,6 +35,8 @@ pub struct LektorPlayerConfig { pub font_size: u64, pub font_name: String, pub msg_duration: u64, + + #[cfg(unix)] pub force_x11: bool, } diff --git a/lektor_utils/src/config/mod.rs b/lektor_utils/src/config/mod.rs index 7a0a22adeb33b03c738b977fb38fd4482ba83fbb..78ed93498f59506a6715ae81a4a17f96673cb962 100644 --- a/lektor_utils/src/config/mod.rs +++ b/lektor_utils/src/config/mod.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; pub fn get_or_write_default_config<Config: Default + Serialize + for<'de> Deserialize<'de>>( appli: &'static str, ) -> Result<Config> { - let path = crate::user_config_directory("lektor").join(format!("{appli}.toml")); + let path = crate::user_config_directory_sync("lektor").join(format!("{appli}.toml")); match std::fs::read_to_string(&path) { Ok(config) => toml::from_str::<Config>(&config) .with_context(|| format!("invalid config file `{}`", path.to_string_lossy())), @@ -47,3 +47,19 @@ pub async fn write_config_async<Config: Default + Serialize + for<'de> Deseriali ) }) } + +/// Write the config to its file +pub fn write_config_sync<Config: Default + Serialize + for<'de> Deserialize<'de>>( + appli: &'static str, + config: Config, +) -> Result<()> { + let path = crate::user_config_directory_sync("lektor").join(format!("{appli}.toml")); + let pretty_config = + toml::to_string_pretty(&config).expect("failed to prettify the default config..."); + std::fs::write(&path, pretty_config).with_context(|| { + format!( + "failed to write default config to file `{}`", + path.to_string_lossy() + ) + }) +} diff --git a/lektor_utils/src/lib.rs b/lektor_utils/src/lib.rs index ce8d14b283d64a1acf86c29f7188bc19e8145948..abfd8fbb223c7e7986bbd2eae9a459d89fd9679e 100644 --- a/lektor_utils/src/lib.rs +++ b/lektor_utils/src/lib.rs @@ -40,7 +40,7 @@ pub fn user_home_directory() -> std::path::PathBuf { /// USERPROFILE are defined this function will panic. The config directory is /// named `.config` and is placed in the home folder. This is the most common /// behaviour on unix systems. -pub fn user_config_directory(app: impl AsRef<str>) -> std::path::PathBuf { +pub fn user_config_directory_sync(app: impl AsRef<str>) -> std::path::PathBuf { let folder = user_home_directory().join(".config").join(app.as_ref()); std::fs::create_dir_all(&folder).unwrap_or_else(|err| { panic!( @@ -51,7 +51,7 @@ pub fn user_config_directory(app: impl AsRef<str>) -> std::path::PathBuf { folder } -/// Same as [user_config_directory] but with async using tokio. +/// Same as [user_config_directory_sync] but with async using tokio. pub async fn user_config_directory_async(app: impl AsRef<str>) -> std::path::PathBuf { let folder = user_home_directory().join(".config").join(app.as_ref()); tokio::fs::create_dir_all(&folder) diff --git a/lektord/Cargo.toml b/lektord/Cargo.toml index b22fcff071228ca8474f7092991c16b7c736a4b6..79a6ffe14872327d36831da1bf5359b7123569da 100644 --- a/lektord/Cargo.toml +++ b/lektord/Cargo.toml @@ -20,6 +20,8 @@ tokio.workspace = true hyper.workspace = true async-trait.workspace = true +clap.workspace = true + lektor_nkdb = { path = "../lektor_nkdb" } lektor_repo = { path = "../lektor_repo" } lektor_utils = { path = "../lektor_utils" } diff --git a/lektord/src/app.rs b/lektord/src/app.rs index a1f76f1717db5ab439ce389e81923a084a1ee1d7..a1bbe7788f3d2aa58e4a6b958f2c1c414f37e0b2 100644 --- a/lektord/src/app.rs +++ b/lektord/src/app.rs @@ -162,7 +162,6 @@ pub(crate) type LektorStatePtr = Arc<LektorState>; impl LektorState { /// Create a new server state from the configuration file. pub async fn new(config: LektorConfig, shutdown: Sender<()>) -> Result<LektorStatePtr> { - lektor_utils::logger::level(config.log); let LektorConfig { database: LektorDatabaseConfig { folder, .. }, player, diff --git a/lektord/src/cmd.rs b/lektord/src/cmd.rs new file mode 100644 index 0000000000000000000000000000000000000000..144e395852cf3c2710d298d226bece4f63894bf4 --- /dev/null +++ b/lektord/src/cmd.rs @@ -0,0 +1,54 @@ +use clap::{Parser, Subcommand}; +use lektor_utils::*; + +#[derive(Parser, Debug, Default)] +#[command( author + , version = version() + , about + , long_about = None + , disable_help_subcommand = false + , args_conflicts_with_subcommands = true +)] +pub struct Args { + #[command(subcommand)] + pub action: Option<SubCommand>, + + /// Make lkt more verbose, repeat to make it even more verbose. + #[arg( long + , short = 'v' + , action = clap::ArgAction::Count + )] + pub verbose: u8, +} + +#[derive(Subcommand, Debug, Default)] +#[command(long_about = None, about = None)] +pub enum SubCommand { + /// Launch the daemon, this is the default command, if nothing is passed then the daemon will + /// be unleashed ;) + #[command(short_flag = 'S')] + #[default] + Start, + + /// Config manipulation commands, to not edit it by hand! + #[command(short_flag = 'C', arg_required_else_help = true)] + Config { + /// Show the current config. + #[arg( action = clap::ArgAction::SetTrue + , exclusive = true + , short = 's' + , long = "show" + )] + show: bool, + + /// Edit the config. + #[arg( action = clap::ArgAction::Set + , exclusive = true + , num_args = 2 + , short = 'e' + , long = "edit" + , value_name = "<OPT.NAME> <VALUE>" + )] + edit: Option<Vec<String>>, + }, +} diff --git a/lektord/src/config.rs b/lektord/src/config.rs index b211e3ebe3421609de6f4f4ad72caea767f0ffc7..09406eb0dea99c49afc69e431342eb76690e6c76 100644 --- a/lektord/src/config.rs +++ b/lektord/src/config.rs @@ -1,6 +1,7 @@ +use anyhow::{bail, Context, Result}; use lektor_utils::{config::*, log::Level as LogLevel}; use serde::{Deserialize, Serialize}; -use std::net::SocketAddr; +use std::{net::SocketAddr, path::PathBuf}; /// Lektord configuration. #[derive(Debug, Serialize, Deserialize, Clone)] @@ -41,3 +42,111 @@ impl Default for LektorConfig { } } } + +impl LektorConfig { + pub fn edit(mut self, name: impl AsRef<str>, value: impl AsRef<str>) -> Result<()> { + match &name.as_ref().split('.').collect::<Vec<_>>()[..] { + // Flat + ["log"] => todo!(), + ["workers"] => { + self.workers = value + .as_ref() + .parse::<usize>() + .with_context(|| "invalid value for 'workers'")? + } + ["mpris"] => { + self.mpris = value + .as_ref() + .parse::<bool>() + .with_context(|| "invalid value for 'mpris'")? + } + + // Database + ["database", "folder"] => self.database.folder = PathBuf::from(value.as_ref()), + ["database", "autoclear"] => { + self.database.autoclear = value + .as_ref() + .parse::<bool>() + .with_context(|| "invalid value for 'database.autoclear'")? + } + ["database", "save_history"] => { + self.database.save_history = value + .as_ref() + .parse::<bool>() + .with_context(|| "invalid value for 'database.save_history'")? + } + + // Player + ["player", "font_name"] => self.player.font_name = value.as_ref().to_string(), + ["player", "font_size"] => { + self.player.font_size = value + .as_ref() + .parse::<u64>() + .with_context(|| "invalid value for 'player.font_size'")? + } + ["player", "msg_duration"] => { + self.player.msg_duration = value + .as_ref() + .parse::<u64>() + .with_context(|| "invalid value for 'player.msg_duration'")? + } + #[cfg(unix)] + ["player", "force_x11"] => { + self.player.force_x11 = value + .as_ref() + .parse::<bool>() + .with_context(|| "invalid value for 'player.force_x11'")? + } + + // + _ => bail!("invalid option name {}", name.as_ref()), + } + write_config_sync("lektord", self) + } +} + +impl std::fmt::Display for LektorConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "log: {}", self.log)?; + writeln!(f, "mpris: {}", self.mpris)?; + writeln!(f, "workers: {}", self.workers)?; + + writeln!(f, "\n[users]")?; + for UserConfig { user, token, admin } in &self.users { + let title = match admin { + true => "admin:", + false => "user: ", + }; + writeln!(f, "{title} {user} -> {token}")?; + } + + writeln!(f, "\n[listen]")?; + for (idx, addr) in self.listen.iter().enumerate() { + writeln!(f, "{idx}: {addr}")?; + } + + writeln!(f, "\n[database]")?; + let folder = self.database.folder.to_string_lossy(); + writeln!(f, "folder: {}", folder)?; + writeln!(f, "autoclear: {}", self.database.autoclear)?; + writeln!(f, "save_history: {}", self.database.save_history)?; + + writeln!(f, "\n[player]")?; + writeln!(f, "font_name: {}", self.player.font_name)?; + writeln!(f, "font_size: {}", self.player.font_size)?; + writeln!(f, "msg_duration: {}", self.player.msg_duration)?; + #[cfg(unix)] + writeln!(f, "force_x11: {}", self.player.force_x11)?; + + for repo in &self.repo { + writeln!(f, "\n[repo.{}]", repo.name)?; + writeln!(f, "api: {:?}", repo.api)?; + writeln!(f, "token: {}", repo.token)?; + for (idx, url) in repo.urls.iter().enumerate() { + writeln!(f, "url.{idx}: {url}")?; + } + } + + Ok(()) + } +} diff --git a/lektord/src/main.rs b/lektord/src/main.rs index 27e362c8bf7d24b5d1c0d8926c9dad74190fd0a2..91bcc2d607f7f31ee181b73a8e72446c6c1178f7 100644 --- a/lektord/src/main.rs +++ b/lektord/src/main.rs @@ -1,5 +1,6 @@ mod app; pub mod c_wrapper; +mod cmd; mod config; mod error; mod listen; @@ -12,6 +13,7 @@ pub use error::*; pub use listen::*; use anyhow::{Context, Result}; +use cmd::SubCommand; use lektor_utils::*; use std::sync::atomic::{AtomicU64, Ordering}; use tokio::{signal, sync::oneshot::Receiver}; @@ -19,28 +21,52 @@ use tokio::{signal, sync::oneshot::Receiver}; fn main() -> Result<()> { logger::init(Some(log::Level::Debug)).expect("failed to install logger"); let config = lektor_utils::config::get_or_write_default_config::<LektorConfig>("lektord")?; - tokio::runtime::Builder::new_multi_thread() - .worker_threads(config.workers) // Thread count from the config file. - .thread_name_fn(|| { - static COUNT: AtomicU64 = AtomicU64::new(0); - format!("lektord-{}", COUNT.fetch_add(1, Ordering::SeqCst)) - }) // Custom names, to see utilization... and because it's fancy! - .enable_all() // Don't care, enable everything. - .thread_stack_size(3 * 1024 * 1024) // 3Mio for each thread, should be enaugh - .max_blocking_threads(1024) - .build()? - .block_on(async move { - lektor_utils::config::write_config_async("lektord", config.clone()).await?; // Write to apply changes... - let addrs = AddrIncomingCombined::from_iter(&config.listen).err()?; - let (app, shutdown) = app(config) - .await - .with_context(|| "failed to build service")?; - hyper::Server::builder(addrs) - .serve(app.into_make_service()) - .with_graceful_shutdown(shutdown_signal(shutdown)) - .await?; + let args = <cmd::Args as clap::Parser>::parse(); + lektor_utils::logger::level(config.log); + if args.verbose != 0 { + lektor_utils::logger::level_int(args.verbose); + } + + match args.action.unwrap_or_default() { + SubCommand::Config { show: true, .. } => { + println!("{config}"); Ok(()) - }) + } + + SubCommand::Config { + edit: Some(args), .. + } => match &args[..] { + [name, value] => config.edit(name, value), + args => unreachable!("{args:#?}"), + }, + + SubCommand::Start => { + log::info!("starting the lektord daemon"); + tokio::runtime::Builder::new_multi_thread() + .worker_threads(config.workers) // Thread count from the config file. + .thread_name_fn(|| { + static COUNT: AtomicU64 = AtomicU64::new(0); + format!("lektord-{}", COUNT.fetch_add(1, Ordering::SeqCst)) + }) + .enable_all() + .thread_stack_size(3 * 1024 * 1024) // 3Mio for each thread, should be enaugh + .build()? + .block_on(async move { + lektor_utils::config::write_config_async("lektord", config.clone()).await?; // Write to apply changes... + let addrs = AddrIncomingCombined::from_iter(&config.listen).err()?; + let (app, shutdown) = app(config) + .await + .with_context(|| "failed to build service")?; + hyper::Server::builder(addrs) + .serve(app.into_make_service()) + .with_graceful_shutdown(shutdown_signal(shutdown)) + .await?; + Ok(()) + }) + } + + args => unreachable!("{args:?}"), + } } /// Gracefull ctrl+c handling.