From 97658811682a7e3ecf498a022e59f711a9dc93cd Mon Sep 17 00:00:00 2001 From: Torben Schweren Date: Sat, 30 Dec 2023 18:30:27 +0100 Subject: [PATCH 1/4] Implement service framework - Make main async - Implement Status enum - Implement Priority enum - Implement ServiceInfo struct - Implement ServiceInternals trait - Implement Service trait --- src/main.rs | 4 +- src/service.rs | 131 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 src/service.rs diff --git a/src/main.rs b/src/main.rs index 992058b..60c62d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,10 @@ mod config; +mod service; pub const BOT_NAME: &str = "Lum"; -fn main() { +#[tokio::main] +async fn main() { let config_handler = config::ConfigHandler::new(BOT_NAME.to_lowercase().as_str()); let config = match config_handler.get_config() { Ok(config) => config, diff --git a/src/service.rs b/src/service.rs new file mode 100644 index 0000000..2e4efce --- /dev/null +++ b/src/service.rs @@ -0,0 +1,131 @@ +use std::{ + error::Error, + fmt::Display, + io, + sync::{self, Arc, PoisonError}, +}; + +use tokio::sync::{Mutex, MutexGuard}; + +#[derive(Debug)] +pub enum Status { + Started, + Stopped, + Starting, + Stopping, + FailedStarting(Box), + FailedStopping(Box), + RuntimeError(Box), +} + +impl Display for Status { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Status::Started => write!(f, "Started"), + Status::Stopped => write!(f, "Stopped"), + Status::Starting => write!(f, "Starting"), + Status::Stopping => write!(f, "Stopping"), + Status::FailedStarting(error) => write!(f, "Failed to start: {}", error), + Status::FailedStopping(error) => write!(f, "Failed to stop: {}", error), + Status::RuntimeError(error) => write!(f, "Runtime error: {}", error), + } + } +} + +#[derive(Debug)] +pub enum Priority { + Essential, + Optional, +} + +impl Display for Priority { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Priority::Essential => write!(f, "Essential"), + Priority::Optional => write!(f, "Optional"), + } + } +} + +pub struct ServiceInfo { + pub name: String, + pub priority: Priority, + + pub status: Arc>, +} + +impl ServiceInfo { + pub fn new(name: &str, priority: Priority) -> Self { + Self { + name: name.to_string(), + priority, + status: Arc::new(Mutex::new(Status::Stopped)), + } + } +} + +#[derive(Debug)] +struct IoError(io::Error); + +impl From>> for IoError { + fn from(error: PoisonError>) -> Self { + Self(io::Error::new(io::ErrorKind::Other, format!("{}", error))) + } +} + +impl Display for IoError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +unsafe impl Send for IoError {} +unsafe impl Sync for IoError {} +impl Error for IoError {} + +pub trait ServiceInternals { + async fn start(&mut self) -> Result<(), Box>; + async fn stop(&mut self) -> Result<(), Box>; +} + +pub trait Service: ServiceInternals { + fn info(&self) -> &ServiceInfo; + + async fn start(&mut self) { + let mut lock = self.info().status.lock().await; + *lock = Status::Starting; + drop(lock); + + match ServiceInternals::start(self).await { + Ok(()) => { + let mut lock = self.info().status.lock().await; + *lock = Status::Started; + } + Err(error) => { + let mut lock = self.info().status.lock().await; + *lock = Status::FailedStarting(error); + } + } + } + async fn stop(&mut self) { + let mut lock = self.info().status.lock().await; + *lock = Status::Stopping; + drop(lock); + + match ServiceInternals::stop(self).await { + Ok(()) => { + let mut lock = self.info().status.lock().await; + *lock = Status::Stopped; + } + Err(error) => { + let mut lock = self.info().status.lock().await; + *lock = Status::FailedStopping(error); + } + } + } + + async fn is_available(&self) -> bool { + let lock = self.info().status.lock().await; + matches!(&*lock, Status::Started) + } +} From 91a8675e33e69feb6a4f5a7745acfc584dc5085c Mon Sep 17 00:00:00 2001 From: Torben Schweren Date: Sun, 31 Dec 2023 02:50:08 +0100 Subject: [PATCH 2/4] Bot library - Add fern crate - Add humantime crate - Add log crate - Implement Bot - Implement BotBuilder - Refactor config Display trait implementation - Implement library is_debug() function - Implement library run(Bot) function - Implement log module (log::setup(), log::is_set_up() and log::get_min_log_level()) - Adapt main to new changes --- Cargo.toml | 3 + src/bot.rs | 56 ++++++++++++++++ src/config.rs | 9 ++- src/lib.rs | 54 +++++++++++++++ src/log.rs | 51 ++++++++++++++ src/main.rs | 39 +++++++++-- src/service.rs | 178 +++++++++++++++++++++++++++++++++++++++++-------- 7 files changed, 356 insertions(+), 34 deletions(-) create mode 100644 src/bot.rs create mode 100644 src/lib.rs create mode 100644 src/log.rs diff --git a/Cargo.toml b/Cargo.toml index ee0b2a2..b32aaf3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,9 @@ repository = "https://github.com/Kitt3120/lum" [dependencies] dirs = "5.0.1" +fern = { version = "0.6.2", features = ["chrono", "colored", "date-based"] } +humantime = "2.1.0" +log = { version = "0.4.20", features = ["serde"] } serde = { version = "1.0.193", features = ["derive"] } serde_json = "1.0.108" sqlx = { version = "0.7.3", features = ["runtime-tokio", "any", "postgres", "mysql", "sqlite", "tls-native-tls", "migrate", "macros", "uuid", "chrono", "json"] } diff --git a/src/bot.rs b/src/bot.rs new file mode 100644 index 0000000..e52cc64 --- /dev/null +++ b/src/bot.rs @@ -0,0 +1,56 @@ +use crate::service::{Service, ServiceManager, ServiceManagerBuilder}; + +pub struct BotBuilder { + name: String, + services: ServiceManagerBuilder, +} + +impl BotBuilder { + pub fn new(name: &str) -> Self { + Self { + name: name.to_string(), + services: ServiceManager::builder(), + } + } + + pub fn with_service(mut self, service: Box) -> Self { + self.services.with_service(service); // The ServiceManagerBuilder itself will warn when adding a service multiple times + self + } + + pub fn with_services(mut self, services: Vec>) -> Self { + for service in services { + self.services.with_service(service); + } + + self + } + + pub fn build(self) -> Bot { + Bot::from(self) + } +} + +pub struct Bot { + pub name: String, + pub service_manager: ServiceManager, +} + +impl Bot { + pub fn builder(name: &str) -> BotBuilder { + BotBuilder::new(name) + } + + pub async fn init(&mut self) { + self.service_manager.start_services().await; + } +} + +impl From for Bot { + fn from(builder: BotBuilder) -> Self { + Self { + name: builder.name, + service_manager: builder.services.build(), + } + } +} diff --git a/src/config.rs b/src/config.rs index 7a17995..550fe23 100644 --- a/src/config.rs +++ b/src/config.rs @@ -54,7 +54,14 @@ impl Default for Config { impl Display for Config { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "discord_token: {}", self.discord_token) + let content = match serde_json::to_string(self) { + Ok(content) => content, + Err(error) => { + return write!(f, "Unable to serialize config: {}", error); + } + }; + + write!(f, "{}", content) } } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..09aeaa0 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,54 @@ +use std::time::SystemTime; + +use ::log::info; +use bot::Bot; + +pub mod bot; +pub mod config; +pub mod log; +pub mod service; + +pub fn is_debug() -> bool { + cfg!(debug_assertions) +} + +pub async fn run(mut bot: Bot) { + let now = SystemTime::now(); + + if !log::is_set_up() { + eprintln!( + "Logger has not been set up! {} will not initialize.", + bot.name + ); + return; + } + + bot.init().await; + + let elapsed = match now.elapsed() { + Ok(elapsed) => elapsed, + Err(error) => { + panic!( + "Error getting elapsed startup time: {}\n{} will exit.", + error, bot.name + ); + } + }; + + info!( + "{} is alive! Startup took {}ms", + bot.name, + elapsed.as_millis() + ); + + //TODO: Add CLI commands + while match tokio::signal::ctrl_c().await { + Ok(_) => { + info!("Received SIGINT, shutting down..."); + false + } + Err(error) => { + panic!("Error receiving SIGINT: {}\n{} will exit.", error, bot.name); + } + } {} +} diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..6aa9664 --- /dev/null +++ b/src/log.rs @@ -0,0 +1,51 @@ +use std::{ + io, + sync::atomic::{AtomicBool, Ordering}, + time::SystemTime, +}; + +use fern::colors::{Color, ColoredLevelConfig}; +use log::{LevelFilter, SetLoggerError}; + +use crate::is_debug; + +static IS_LOGGER_SET_UP: AtomicBool = AtomicBool::new(false); + +pub fn is_set_up() -> bool { + IS_LOGGER_SET_UP.load(Ordering::Relaxed) +} + +pub fn setup() -> Result<(), SetLoggerError> { + let colors = ColoredLevelConfig::new() + .info(Color::Green) + .debug(Color::Magenta) + .warn(Color::Yellow) + .error(Color::Red) + .trace(Color::Cyan); + + fern::Dispatch::new() + .format(move |out, message, record| { + out.finish(format_args!( + "[{} {: >25} {: <5}] {}", + humantime::format_rfc3339_seconds(SystemTime::now()), + record.target(), + colors.color(record.level()), + message + )) + }) + .level(get_min_log_level()) + .chain(io::stdout()) + .apply()?; + + IS_LOGGER_SET_UP.store(true, Ordering::Relaxed); + + Ok(()) +} + +fn get_min_log_level() -> LevelFilter { + if is_debug() { + LevelFilter::Debug + } else { + LevelFilter::Info + } +} diff --git a/src/main.rs b/src/main.rs index 60c62d9..7292f7d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,44 @@ -mod config; -mod service; +use ::log::{debug, warn}; +use lum::{bot::Bot, config::ConfigHandler, log, service::ServiceManager}; -pub const BOT_NAME: &str = "Lum"; +const BOT_NAME: &str = "Lum"; #[tokio::main] async fn main() { - let config_handler = config::ConfigHandler::new(BOT_NAME.to_lowercase().as_str()); + if lum::is_debug() { + warn!("THIS IS A DEBUG RELEASE!"); + } + + if let Err(error) = log::setup() { + panic!( + "Error setting up the Logger: {}\n{} will exit.", + error, BOT_NAME + ); + } + + let config_handler = ConfigHandler::new(BOT_NAME.to_lowercase().as_str()); let config = match config_handler.get_config() { Ok(config) => config, Err(err) => { - panic!("Error reading config file: {}", err); + panic!( + "Error reading config file: {}\n{} will exit.", + err, BOT_NAME + ); } }; + debug!("Using config: {}", config); + + let service_manager = initialize_services(); + + let bot = Bot::builder(BOT_NAME) + .with_services(service_manager.services) + .build(); + lum::run(bot).await; +} + +fn initialize_services() -> ServiceManager { + //TODO: Add services + //... - println!("Config: {}", config); + ServiceManager::builder().build() } diff --git a/src/service.rs b/src/service.rs index 2e4efce..300fa74 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,10 +1,13 @@ use std::{ error::Error, fmt::Display, + future::Future, io, + pin::Pin, sync::{self, Arc, PoisonError}, }; +use log::{info, warn}; use tokio::sync::{Mutex, MutexGuard}; #[derive(Debug)] @@ -83,49 +86,170 @@ unsafe impl Send for IoError {} unsafe impl Sync for IoError {} impl Error for IoError {} +//TODO: When Rust allows async trait methods to be object-safe, refactor this to use async instead of returning a future pub trait ServiceInternals { - async fn start(&mut self) -> Result<(), Box>; - async fn stop(&mut self) -> Result<(), Box>; + fn start( + &mut self, + ) -> Pin>> + '_>>; + fn stop( + &mut self, + ) -> Pin>> + '_>>; } +//TODO: When Rust allows async trait methods to be object-safe, refactor this to use async instead of returning a future pub trait Service: ServiceInternals { fn info(&self) -> &ServiceInfo; - async fn start(&mut self) { - let mut lock = self.info().status.lock().await; - *lock = Status::Starting; - drop(lock); + fn wrapped_start(&mut self) -> Pin + '_>> { + Box::pin(async move { + let mut lock = self.info().status.lock().await; + + if !matches!(&*lock, Status::Started) { + warn!( + "Tried to start service {} while it was in state {}. Ignoring start request.", + self.info().name, + lock + ); + return; + } - match ServiceInternals::start(self).await { - Ok(()) => { - let mut lock = self.info().status.lock().await; - *lock = Status::Started; + *lock = Status::Starting; + drop(lock); + + match ServiceInternals::start(self).await { + Ok(()) => { + let mut lock = self.info().status.lock().await; + *lock = Status::Started; + } + Err(error) => { + let mut lock = self.info().status.lock().await; + *lock = Status::FailedStarting(error); + } } - Err(error) => { - let mut lock = self.info().status.lock().await; - *lock = Status::FailedStarting(error); + }) + } + + fn wrapped_stop(&mut self) -> Pin + '_>> { + Box::pin(async move { + let mut lock = self.info().status.lock().await; + + if matches!(&*lock, Status::Started) { + warn!( + "Tried to stop service {} while it was in state {}. Ignoring stop request.", + self.info().name, + lock + ); + return; + } + + *lock = Status::Stopping; + drop(lock); + + match ServiceInternals::stop(self).await { + Ok(()) => { + let mut lock = self.info().status.lock().await; + *lock = Status::Stopped; + } + Err(error) => { + let mut lock = self.info().status.lock().await; + *lock = Status::FailedStopping(error); + } } + }) + } + + fn is_available(&self) -> Pin + '_>> { + Box::pin(async move { + let lock = self.info().status.lock().await; + matches!(&*lock, Status::Started) + }) + } +} + +#[derive(Default)] +pub struct ServiceManagerBuilder { + services: Vec>, +} + +impl ServiceManagerBuilder { + pub fn new() -> Self { + Self { services: vec![] } + } + + pub fn with_service(&mut self, service: Box) { + let service_exists = self + .services + .iter() + .any(|s| s.info().name == service.info().name); + + if service_exists { + warn!( + "Tried to add service {} multiple times. Ignoring.", + service.info().name + ); + return; } + + self.services.push(service); + } + + pub fn build(self) -> ServiceManager { + ServiceManager::from(self) } - async fn stop(&mut self) { - let mut lock = self.info().status.lock().await; - *lock = Status::Stopping; - drop(lock); +} - match ServiceInternals::stop(self).await { - Ok(()) => { - let mut lock = self.info().status.lock().await; - *lock = Status::Stopped; +pub struct ServiceManager { + pub services: Vec>, +} + +impl ServiceManager { + pub fn builder() -> ServiceManagerBuilder { + ServiceManagerBuilder::new() + } + + pub fn start_services(&mut self) -> Pin + '_>> { + Box::pin(async move { + for service in &mut self.services { + info!("Starting service: {}", service.info().name); + service.wrapped_start().await; } - Err(error) => { - let mut lock = self.info().status.lock().await; - *lock = Status::FailedStopping(error); + }) + } + + pub fn stop_services(&mut self) -> Pin + '_>> { + Box::pin(async move { + for service in &mut self.services { + info!("Stopping service: {}", service.info().name); + service.wrapped_stop().await; + } + }) + } +} + +impl Display for ServiceManager { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Services: ")?; + + if self.services.is_empty() { + write!(f, "None")?; + return Ok(()); + } + + let mut services = self.services.iter().peekable(); + while let Some(service) = services.next() { + write!(f, "{}", service.info().name)?; + if services.peek().is_some() { + write!(f, ", ")?; } } + Ok(()) } +} - async fn is_available(&self) -> bool { - let lock = self.info().status.lock().await; - matches!(&*lock, Status::Started) +impl From for ServiceManager { + fn from(builder: ServiceManagerBuilder) -> Self { + Self { + services: builder.services, + } } } From 88e296ede49d4e6bd4a0920a2a7ad4bd834fb884 Mon Sep 17 00:00:00 2001 From: Torben Schweren Date: Sun, 31 Dec 2023 17:06:44 +0100 Subject: [PATCH 3/4] WIP: Finish services framework Just a lot of refactoring and fixing. No time to describe all this now. Happy new year! :) --- src/bot.rs | 23 ++++++--- src/config.rs | 2 +- src/lib.rs | 35 ++++++++------ src/log.rs | 2 +- src/main.rs | 79 +++++++++++++++++++++++------- src/service.rs | 129 +++++++++++++++++++++++++++++++++++-------------- 6 files changed, 193 insertions(+), 77 deletions(-) diff --git a/src/bot.rs b/src/bot.rs index e52cc64..fa8d953 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,26 +1,27 @@ -use crate::service::{Service, ServiceManager, ServiceManagerBuilder}; +use crate::service::{OverallStatus, Service, ServiceManager, ServiceManagerBuilder}; pub struct BotBuilder { name: String, - services: ServiceManagerBuilder, + service_manager: ServiceManagerBuilder, } impl BotBuilder { pub fn new(name: &str) -> Self { Self { name: name.to_string(), - services: ServiceManager::builder(), + service_manager: ServiceManager::builder(), } } pub fn with_service(mut self, service: Box) -> Self { - self.services.with_service(service); // The ServiceManagerBuilder itself will warn when adding a service multiple times + self.service_manager = self.service_manager.with_service(service); // The ServiceManagerBuilder itself will warn when adding a service multiple times + self } pub fn with_services(mut self, services: Vec>) -> Self { for service in services { - self.services.with_service(service); + self.service_manager = self.service_manager.with_service(service); } self @@ -41,16 +42,24 @@ impl Bot { BotBuilder::new(name) } - pub async fn init(&mut self) { + pub async fn start(&mut self) { self.service_manager.start_services().await; } + + pub async fn stop(&mut self) { + self.service_manager.stop_services().await; + } + + pub async fn overall_status(&self) -> OverallStatus { + self.service_manager.overall_status().await + } } impl From for Bot { fn from(builder: BotBuilder) -> Self { Self { name: builder.name, - service_manager: builder.services.build(), + service_manager: builder.service_manager.build(), } } } diff --git a/src/config.rs b/src/config.rs index 550fe23..8cf1e07 100644 --- a/src/config.rs +++ b/src/config.rs @@ -38,7 +38,7 @@ fn discord_token_default() -> String { String::from("Please provide a token") } -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[derive(Debug, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct Config { #[serde(rename = "discordToken", default = "discord_token_default")] pub discord_token: String, diff --git a/src/lib.rs b/src/lib.rs index 09aeaa0..d7be7a7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,10 @@ use std::time::SystemTime; -use ::log::info; +use ::log::{error, info}; use bot::Bot; +use crate::service::OverallStatus; + pub mod bot; pub mod config; pub mod log; @@ -13,42 +15,45 @@ pub fn is_debug() -> bool { } pub async fn run(mut bot: Bot) { - let now = SystemTime::now(); - if !log::is_set_up() { eprintln!( "Logger has not been set up! {} will not initialize.", bot.name ); + return; } - bot.init().await; + let now = SystemTime::now(); + + bot.start().await; - let elapsed = match now.elapsed() { - Ok(elapsed) => elapsed, + match now.elapsed() { + Ok(elapsed) => info!("Startup took {}ms", elapsed.as_millis()), Err(error) => { - panic!( + error!( "Error getting elapsed startup time: {}\n{} will exit.", error, bot.name ); + + return; } }; - info!( - "{} is alive! Startup took {}ms", - bot.name, - elapsed.as_millis() - ); + if bot.overall_status().await != OverallStatus::Healthy { + error!("{} is not healthy! Some essential services did not start up successfully. Please check the logs.\n{} will exit.", bot.name, bot.name); + return; + } + + info!("{} is alive!", bot.name,); //TODO: Add CLI commands - while match tokio::signal::ctrl_c().await { + match tokio::signal::ctrl_c().await { Ok(_) => { info!("Received SIGINT, shutting down..."); - false } Err(error) => { panic!("Error receiving SIGINT: {}\n{} will exit.", error, bot.name); } - } {} + } } diff --git a/src/log.rs b/src/log.rs index 6aa9664..da02dbb 100644 --- a/src/log.rs +++ b/src/log.rs @@ -26,7 +26,7 @@ pub fn setup() -> Result<(), SetLoggerError> { fern::Dispatch::new() .format(move |out, message, record| { out.finish(format_args!( - "[{} {: >25} {: <5}] {}", + "[{} {: <25} {: <5}] {}", humantime::format_rfc3339_seconds(SystemTime::now()), record.target(), colors.color(record.level()), diff --git a/src/main.rs b/src/main.rs index 7292f7d..c671bdf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,44 +1,89 @@ -use ::log::{debug, warn}; -use lum::{bot::Bot, config::ConfigHandler, log, service::ServiceManager}; +use std::{error::Error, future::Future, pin::Pin}; + +use ::log::{debug, error, warn}; +use lum::{ + bot::Bot, + config::{Config, ConfigHandler, ConfigParseError}, + log, + service::{Priority, Service, ServiceInfo, ServiceInternals}, +}; const BOT_NAME: &str = "Lum"; #[tokio::main] async fn main() { + setup_logger(); + if lum::is_debug() { warn!("THIS IS A DEBUG RELEASE!"); } - if let Err(error) = log::setup() { - panic!( - "Error setting up the Logger: {}\n{} will exit.", - error, BOT_NAME - ); - } - - let config_handler = ConfigHandler::new(BOT_NAME.to_lowercase().as_str()); - let config = match config_handler.get_config() { + let config = match get_config() { Ok(config) => config, Err(err) => { - panic!( + error!( "Error reading config file: {}\n{} will exit.", err, BOT_NAME ); + + return; } }; debug!("Using config: {}", config); - let service_manager = initialize_services(); - let bot = Bot::builder(BOT_NAME) - .with_services(service_manager.services) + .with_services(initialize_services()) .build(); + lum::run(bot).await; } -fn initialize_services() -> ServiceManager { +fn setup_logger() { + if let Err(error) = log::setup() { + panic!( + "Error setting up the Logger: {}\n{} will exit.", + error, BOT_NAME + ); + } +} + +fn get_config() -> Result { + let config_handler = ConfigHandler::new(BOT_NAME.to_lowercase().as_str()); + + config_handler.get_config() +} + +fn initialize_services() -> Vec> { //TODO: Add services //... - ServiceManager::builder().build() + let crash_service = CrashService { + info: ServiceInfo::new("CrashService", Priority::Essential), + }; + + vec![Box::new(crash_service)] +} + +struct CrashService { + info: ServiceInfo, +} + +impl ServiceInternals for CrashService { + fn start( + &mut self, + ) -> Pin>> + '_>> { + Box::pin(async move { Ok(()) }) + } + + fn stop( + &mut self, + ) -> Pin>> + '_>> { + Box::pin(async move { Ok(()) }) + } +} + +impl Service for CrashService { + fn info(&self) -> &ServiceInfo { + &self.info + } } diff --git a/src/service.rs b/src/service.rs index 300fa74..5b82ee9 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,14 +1,7 @@ -use std::{ - error::Error, - fmt::Display, - future::Future, - io, - pin::Pin, - sync::{self, Arc, PoisonError}, -}; - -use log::{info, warn}; -use tokio::sync::{Mutex, MutexGuard}; +use std::{collections::HashMap, error::Error, fmt::Display, future::Future, pin::Pin, sync::Arc}; + +use log::{error, info, warn}; +use tokio::sync::Mutex; #[derive(Debug)] pub enum Status { @@ -35,7 +28,39 @@ impl Display for Status { } } -#[derive(Debug)] +impl PartialEq for Status { + fn eq(&self, other: &Self) -> bool { + matches!( + (self, other), + (Status::Started, Status::Started) + | (Status::Stopped, Status::Stopped) + | (Status::Starting, Status::Starting) + | (Status::Stopping, Status::Stopping) + | (Status::FailedStarting(_), Status::FailedStarting(_)) + | (Status::FailedStopping(_), Status::FailedStopping(_)) + | (Status::RuntimeError(_), Status::RuntimeError(_)) + ) + } +} + +impl Eq for Status {} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum OverallStatus { + Healthy, + Unhealthy, +} + +impl Display for OverallStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OverallStatus::Healthy => write!(f, "Healthy"), + OverallStatus::Unhealthy => write!(f, "Unhealthy"), + } + } +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Priority { Essential, Optional, @@ -50,6 +75,7 @@ impl Display for Priority { } } +#[derive(Debug)] pub struct ServiceInfo { pub name: String, pub priority: Priority, @@ -67,25 +93,6 @@ impl ServiceInfo { } } -#[derive(Debug)] -struct IoError(io::Error); - -impl From>> for IoError { - fn from(error: PoisonError>) -> Self { - Self(io::Error::new(io::ErrorKind::Other, format!("{}", error))) - } -} - -impl Display for IoError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -unsafe impl Send for IoError {} -unsafe impl Sync for IoError {} -impl Error for IoError {} - //TODO: When Rust allows async trait methods to be object-safe, refactor this to use async instead of returning a future pub trait ServiceInternals { fn start( @@ -116,14 +123,16 @@ pub trait Service: ServiceInternals { *lock = Status::Starting; drop(lock); - match ServiceInternals::start(self).await { + match self.start().await { Ok(()) => { let mut lock = self.info().status.lock().await; *lock = Status::Started; + info!("Started service: {}", self.info().name); } Err(error) => { let mut lock = self.info().status.lock().await; *lock = Status::FailedStarting(error); + error!("Failed to start service: {}", self.info().name); } } }) @@ -166,6 +175,20 @@ pub trait Service: ServiceInternals { } } +impl PartialEq for dyn Service { + fn eq(&self, other: &Self) -> bool { + self.info().name == other.info().name + } +} + +impl Eq for dyn Service {} + +impl std::hash::Hash for dyn Service { + fn hash(&self, state: &mut H) { + self.info().name.hash(state); + } +} + #[derive(Default)] pub struct ServiceManagerBuilder { services: Vec>, @@ -176,21 +199,24 @@ impl ServiceManagerBuilder { Self { services: vec![] } } - pub fn with_service(&mut self, service: Box) { + pub fn with_service(mut self, service: Box) -> Self { let service_exists = self .services .iter() - .any(|s| s.info().name == service.info().name); + .any(|s| s.info().name == service.info().name); // Can't use *s == service here because value would be moved if service_exists { warn!( "Tried to add service {} multiple times. Ignoring.", service.info().name ); - return; + + return self; } self.services.push(service); + + self } pub fn build(self) -> ServiceManager { @@ -207,7 +233,7 @@ impl ServiceManager { ServiceManagerBuilder::new() } - pub fn start_services(&mut self) -> Pin + '_>> { + pub async fn start_services(&mut self) -> Pin + '_>> { Box::pin(async move { for service in &mut self.services { info!("Starting service: {}", service.info().name); @@ -224,6 +250,37 @@ impl ServiceManager { } }) } + + pub fn status_map( + &self, + ) -> Pin, String>> + '_>> { + Box::pin(async move { + let mut map = HashMap::new(); + + for service in &self.services { + let status = service.info().status.lock().await; + + let status = status.to_string(); + map.insert(service, status.to_string()); + } + + map + }) + } + + pub fn overall_status(&self) -> Pin + '_>> { + Box::pin(async move { + for service in self.services.iter() { + let status = service.info().status.lock().await; + + if !matches!(&*status, Status::Started) { + return OverallStatus::Unhealthy; + } + } + + OverallStatus::Healthy + }) + } } impl Display for ServiceManager { From e682c81dfd6a984f50a2b7beae9b530bf9ad7c82 Mon Sep 17 00:00:00 2001 From: Torben Schweren Date: Mon, 1 Jan 2024 19:19:16 +0100 Subject: [PATCH 4/4] Finish services framework Too much to describe. It's done, that's it. This was one hell of a ride. --- src/bot.rs | 22 +++-- src/config.rs | 3 +- src/lib.rs | 28 ++++--- src/log.rs | 5 +- src/main.rs | 40 +-------- src/service.rs | 223 ++++++++++++++++++++++++++++++++++++++----------- 6 files changed, 212 insertions(+), 109 deletions(-) diff --git a/src/bot.rs b/src/bot.rs index fa8d953..e9c8946 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,4 +1,4 @@ -use crate::service::{OverallStatus, Service, ServiceManager, ServiceManagerBuilder}; +use crate::service::{PinnedBoxedFuture, Service, ServiceManager, ServiceManagerBuilder}; pub struct BotBuilder { name: String, @@ -42,16 +42,20 @@ impl Bot { BotBuilder::new(name) } - pub async fn start(&mut self) { - self.service_manager.start_services().await; + //TODO: When Rust allows async trait methods to be object-safe, refactor this to use async instead of returning a future + pub fn start(&mut self) -> PinnedBoxedFuture<'_, ()> { + Box::pin(async move { + self.service_manager.start_services().await; + //TODO: Potential for further initialization here, like modules + }) } - pub async fn stop(&mut self) { - self.service_manager.stop_services().await; - } - - pub async fn overall_status(&self) -> OverallStatus { - self.service_manager.overall_status().await + //TODO: When Rust allows async trait methods to be object-safe, refactor this to use async instead of returning a future + pub fn stop(&mut self) -> PinnedBoxedFuture<'_, ()> { + Box::pin(async move { + self.service_manager.stop_services().await; + //TODO: Potential for further deinitialization here, like modules + }) } } diff --git a/src/config.rs b/src/config.rs index 8cf1e07..ef69e4e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,11 +1,10 @@ use core::fmt; +use serde::{Deserialize, Serialize}; use std::{ fmt::{Display, Formatter}, fs, io, path::PathBuf, }; - -use serde::{Deserialize, Serialize}; use thiserror::Error; #[derive(Debug, Error)] diff --git a/src/lib.rs b/src/lib.rs index d7be7a7..1296293 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,7 @@ -use std::time::SystemTime; - +use crate::service::OverallStatus; use ::log::{error, info}; use bot::Bot; - -use crate::service::OverallStatus; +use std::time::SystemTime; pub mod bot; pub mod config; @@ -16,10 +14,7 @@ pub fn is_debug() -> bool { pub async fn run(mut bot: Bot) { if !log::is_set_up() { - eprintln!( - "Logger has not been set up! {} will not initialize.", - bot.name - ); + eprintln!("Logger has not been set up!\n{} will exit.", bot.name); return; } @@ -40,20 +35,29 @@ pub async fn run(mut bot: Bot) { } }; - if bot.overall_status().await != OverallStatus::Healthy { - error!("{} is not healthy! Some essential services did not start up successfully. Please check the logs.\n{} will exit.", bot.name, bot.name); + if bot.service_manager.overall_status().await != OverallStatus::Healthy { + let status_tree = bot.service_manager.status_tree().await; + + error!("{} is not healthy! Some essential services did not start up successfully. Please check the logs.\nService status tree:\n{}\n{} will exit.", + bot.name, + status_tree, + bot.name); return; } - info!("{} is alive!", bot.name,); + info!("{} is alive", bot.name,); //TODO: Add CLI commands match tokio::signal::ctrl_c().await { Ok(_) => { - info!("Received SIGINT, shutting down..."); + info!("Received SIGINT, {} will now shut down", bot.name); } Err(error) => { panic!("Error receiving SIGINT: {}\n{} will exit.", error, bot.name); } } + + bot.stop().await; + + info!("{} has shut down", bot.name); } diff --git a/src/log.rs b/src/log.rs index da02dbb..4898578 100644 --- a/src/log.rs +++ b/src/log.rs @@ -1,12 +1,11 @@ +use fern::colors::{Color, ColoredLevelConfig}; +use log::{LevelFilter, SetLoggerError}; use std::{ io, sync::atomic::{AtomicBool, Ordering}, time::SystemTime, }; -use fern::colors::{Color, ColoredLevelConfig}; -use log::{LevelFilter, SetLoggerError}; - use crate::is_debug; static IS_LOGGER_SET_UP: AtomicBool = AtomicBool::new(false); diff --git a/src/main.rs b/src/main.rs index c671bdf..77f7094 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,9 @@ -use std::{error::Error, future::Future, pin::Pin}; - -use ::log::{debug, error, warn}; +use ::log::{error, warn}; use lum::{ bot::Bot, config::{Config, ConfigHandler, ConfigParseError}, log, - service::{Priority, Service, ServiceInfo, ServiceInternals}, + service::Service, }; const BOT_NAME: &str = "Lum"; @@ -18,7 +16,7 @@ async fn main() { warn!("THIS IS A DEBUG RELEASE!"); } - let config = match get_config() { + let _config = match get_config() { Ok(config) => config, Err(err) => { error!( @@ -29,7 +27,6 @@ async fn main() { return; } }; - debug!("Using config: {}", config); let bot = Bot::builder(BOT_NAME) .with_services(initialize_services()) @@ -49,7 +46,6 @@ fn setup_logger() { fn get_config() -> Result { let config_handler = ConfigHandler::new(BOT_NAME.to_lowercase().as_str()); - config_handler.get_config() } @@ -57,33 +53,5 @@ fn initialize_services() -> Vec> { //TODO: Add services //... - let crash_service = CrashService { - info: ServiceInfo::new("CrashService", Priority::Essential), - }; - - vec![Box::new(crash_service)] -} - -struct CrashService { - info: ServiceInfo, -} - -impl ServiceInternals for CrashService { - fn start( - &mut self, - ) -> Pin>> + '_>> { - Box::pin(async move { Ok(()) }) - } - - fn stop( - &mut self, - ) -> Pin>> + '_>> { - Box::pin(async move { Ok(()) }) - } -} - -impl Service for CrashService { - fn info(&self) -> &ServiceInfo { - &self.info - } + vec![] } diff --git a/src/service.rs b/src/service.rs index 5b82ee9..a3340cd 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,6 +1,14 @@ -use std::{collections::HashMap, error::Error, fmt::Display, future::Future, pin::Pin, sync::Arc}; - use log::{error, info, warn}; +use std::{ + cmp::Ordering, + collections::HashMap, + error::Error, + fmt::Display, + future::Future, + hash::{Hash, Hasher}, + pin::Pin, + sync::Arc, +}; use tokio::sync::Mutex; #[derive(Debug)] @@ -45,7 +53,7 @@ impl PartialEq for Status { impl Eq for Status {} -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] pub enum OverallStatus { Healthy, Unhealthy, @@ -60,7 +68,7 @@ impl Display for OverallStatus { } } -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] pub enum Priority { Essential, Optional, @@ -77,6 +85,7 @@ impl Display for Priority { #[derive(Debug)] pub struct ServiceInfo { + pub id: String, pub name: String, pub priority: Priority, @@ -84,84 +93,87 @@ pub struct ServiceInfo { } impl ServiceInfo { - pub fn new(name: &str, priority: Priority) -> Self { + pub fn new(id: &str, name: &str, priority: Priority) -> Self { Self { + id: id.to_string(), name: name.to_string(), priority, status: Arc::new(Mutex::new(Status::Stopped)), } } + + pub async fn set_status(&self, status: Status) { + let mut lock = self.status.lock().await; + *lock = status; + } } +pub type PinnedBoxedFuture<'a, T> = Pin + 'a>>; + +pub type PinnedBoxedFutureResult<'a, T> = + PinnedBoxedFuture<'a, Result>>; + //TODO: When Rust allows async trait methods to be object-safe, refactor this to use async instead of returning a future pub trait ServiceInternals { - fn start( - &mut self, - ) -> Pin>> + '_>>; - fn stop( - &mut self, - ) -> Pin>> + '_>>; + fn start(&mut self) -> PinnedBoxedFutureResult<'_, ()>; + fn stop(&mut self) -> PinnedBoxedFutureResult<'_, ()>; } //TODO: When Rust allows async trait methods to be object-safe, refactor this to use async instead of returning a future pub trait Service: ServiceInternals { fn info(&self) -> &ServiceInfo; - fn wrapped_start(&mut self) -> Pin + '_>> { + fn wrapped_start(&mut self) -> PinnedBoxedFuture<'_, ()> { Box::pin(async move { - let mut lock = self.info().status.lock().await; + let mut status = self.info().status.lock().await; - if !matches!(&*lock, Status::Started) { + if !matches!(&*status, Status::Stopped) { warn!( "Tried to start service {} while it was in state {}. Ignoring start request.", self.info().name, - lock + status ); return; } - *lock = Status::Starting; - drop(lock); + *status = Status::Starting; + drop(status); match self.start().await { Ok(()) => { - let mut lock = self.info().status.lock().await; - *lock = Status::Started; + self.info().set_status(Status::Started).await; info!("Started service: {}", self.info().name); } Err(error) => { - let mut lock = self.info().status.lock().await; - *lock = Status::FailedStarting(error); + self.info().set_status(Status::FailedStarting(error)).await; error!("Failed to start service: {}", self.info().name); } } }) } - fn wrapped_stop(&mut self) -> Pin + '_>> { + fn wrapped_stop(&mut self) -> PinnedBoxedFuture<'_, ()> { Box::pin(async move { - let mut lock = self.info().status.lock().await; + let mut status = self.info().status.lock().await; - if matches!(&*lock, Status::Started) { + if matches!(&*status, Status::Started) { warn!( "Tried to stop service {} while it was in state {}. Ignoring stop request.", self.info().name, - lock + status ); return; } - *lock = Status::Stopping; - drop(lock); + *status = Status::Stopping; + drop(status); match ServiceInternals::stop(self).await { Ok(()) => { - let mut lock = self.info().status.lock().await; - *lock = Status::Stopped; + self.info().set_status(Status::Stopped).await; } Err(error) => { - let mut lock = self.info().status.lock().await; - *lock = Status::FailedStopping(error); + self.info().set_status(Status::FailedStopping(error)).await; } } }) @@ -175,16 +187,28 @@ pub trait Service: ServiceInternals { } } +impl Eq for dyn Service {} + impl PartialEq for dyn Service { fn eq(&self, other: &Self) -> bool { self.info().name == other.info().name } } -impl Eq for dyn Service {} +impl Ord for dyn Service { + fn cmp(&self, other: &Self) -> Ordering { + self.info().name.cmp(&other.info().name) + } +} + +impl PartialOrd for dyn Service { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} -impl std::hash::Hash for dyn Service { - fn hash(&self, state: &mut H) { +impl Hash for dyn Service { + fn hash(&self, state: &mut H) { self.info().name.hash(state); } } @@ -233,7 +257,7 @@ impl ServiceManager { ServiceManagerBuilder::new() } - pub async fn start_services(&mut self) -> Pin + '_>> { + pub fn start_services(&mut self) -> PinnedBoxedFuture<'_, ()> { Box::pin(async move { for service in &mut self.services { info!("Starting service: {}", service.info().name); @@ -242,7 +266,7 @@ impl ServiceManager { }) } - pub fn stop_services(&mut self) -> Pin + '_>> { + pub fn stop_services(&mut self) -> PinnedBoxedFuture<'_, ()> { Box::pin(async move { for service in &mut self.services { info!("Stopping service: {}", service.info().name); @@ -251,24 +275,30 @@ impl ServiceManager { }) } - pub fn status_map( - &self, - ) -> Pin, String>> + '_>> { - Box::pin(async move { - let mut map = HashMap::new(); + pub fn get_service(&self, id: &str) -> Option<&dyn Service> { + self.services + .iter() + .find(|s| s.info().id == id) + .map(|s| &**s) + } - for service in &self.services { - let status = service.info().status.lock().await; + pub fn status_map(&self) -> PinnedBoxedFuture<'_, HashMap>>> { + Box::pin(async move { + let mut status_map = HashMap::new(); - let status = status.to_string(); - map.insert(service, status.to_string()); + for service in self.services.iter() { + status_map.insert( + service.info().id.clone(), + Arc::clone(&service.info().status), + ); } - map + status_map }) } - pub fn overall_status(&self) -> Pin + '_>> { + //TODO: When Rust allows async closures, refactor this to use iterator methods instead of for loop + pub fn overall_status(&self) -> PinnedBoxedFuture<'_, OverallStatus> { Box::pin(async move { for service in self.services.iter() { let status = service.info().status.lock().await; @@ -281,6 +311,105 @@ impl ServiceManager { OverallStatus::Healthy }) } + + //TODO: When Rust allows async closures, refactor this to use iterator methods instead of for loop + pub fn status_tree(&self) -> PinnedBoxedFuture<'_, String> { + Box::pin(async move { + let status_map = self.status_map().await; + + let mut text_buffer = String::new(); + + let mut failed_essentials = HashMap::new(); + let mut failed_optionals = HashMap::new(); + let mut non_failed_essentials = HashMap::new(); + let mut non_failed_optionals = HashMap::new(); + let mut others = HashMap::new(); + + for (service, status) in status_map.into_iter() { + let priority = match self.get_service(service.as_str()) { + Some(service) => service.info().priority, + None => unreachable!( + "Service with ID {} not found in ServiceManager. This should never happen!", + service, + ), + }; + + let status = status.lock().await; + + match &*status { + Status::Started | Status::Stopped => { + if priority == Priority::Essential { + non_failed_essentials.insert(service, status.to_string()); + } else { + non_failed_optionals.insert(service, status.to_string()); + } + } + Status::FailedStarting(_) + | Status::FailedStopping(_) + | Status::RuntimeError(_) => { + if priority == Priority::Essential { + failed_essentials.insert(service, status.to_string()); + } else { + failed_optionals.insert(service, status.to_string()); + } + } + _ => { + others.insert(service, status.to_string()); + } + } + } + + let section_generator = |services: &HashMap, title: &str| -> String { + let mut text_buffer = String::new(); + + text_buffer.push_str(&format!("- {}:\n", title)); + + for (service, status) in services.iter() { + let service = match self.get_service(service) { + Some(service) => service, + None => unreachable!( + "Service with ID {} not found in ServiceManager. This should never happen!", + service + ), + }; + + text_buffer.push_str(&format!(" - {}: {}\n", service.info().name, status)); + } + + text_buffer + }; + + if !failed_essentials.is_empty() { + text_buffer.push_str( + section_generator(&failed_essentials, "Failed essential services").as_str(), + ); + } + + if !failed_optionals.is_empty() { + text_buffer.push_str( + section_generator(&failed_optionals, "Failed optional services").as_str(), + ); + } + + if !non_failed_essentials.is_empty() { + text_buffer.push_str( + section_generator(&non_failed_essentials, "Essential services").as_str(), + ); + } + + if !non_failed_optionals.is_empty() { + text_buffer.push_str( + section_generator(&non_failed_optionals, "Optional services").as_str(), + ); + } + + if !others.is_empty() { + text_buffer.push_str(section_generator(&others, "Other services").as_str()); + } + + text_buffer + }) + } } impl Display for ServiceManager {