Skip to content

Commit

Permalink
Add Service Framework (#21)
Browse files Browse the repository at this point in the history
* Implement service framework

 - Make main async
 - Implement Status enum
 - Implement Priority enum
 - Implement ServiceInfo struct
 - Implement ServiceInternals trait
 - Implement Service trait

* 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

* WIP: Finish services framework

Just a lot of refactoring and fixing. No time to describe all this now. Happy new year! :)

* Finish services framework

Too much to describe. It's done, that's it. This was one hell of a ride.
  • Loading branch information
Kitt3120 committed Jan 1, 2024
1 parent e1f2040 commit 98707d2
Show file tree
Hide file tree
Showing 7 changed files with 685 additions and 11 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
69 changes: 69 additions & 0 deletions src/bot.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use crate::service::{PinnedBoxedFuture, Service, ServiceManager, ServiceManagerBuilder};

pub struct BotBuilder {
name: String,
service_manager: ServiceManagerBuilder,
}

impl BotBuilder {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
service_manager: ServiceManager::builder(),
}
}

pub fn with_service(mut self, service: Box<dyn Service>) -> Self {
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<Box<dyn Service>>) -> Self {
for service in services {
self.service_manager = self.service_manager.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)
}

//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
})
}

//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
})
}
}

impl From<BotBuilder> for Bot {
fn from(builder: BotBuilder) -> Self {
Self {
name: builder.name,
service_manager: builder.service_manager.build(),
}
}
}
14 changes: 10 additions & 4 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -38,7 +37,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,
Expand All @@ -54,7 +53,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)
}
}

Expand Down
63 changes: 63 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use crate::service::OverallStatus;
use ::log::{error, info};
use bot::Bot;
use std::time::SystemTime;

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) {
if !log::is_set_up() {
eprintln!("Logger has not been set up!\n{} will exit.", bot.name);

return;
}

let now = SystemTime::now();

bot.start().await;

match now.elapsed() {
Ok(elapsed) => info!("Startup took {}ms", elapsed.as_millis()),
Err(error) => {
error!(
"Error getting elapsed startup time: {}\n{} will exit.",
error, bot.name
);

return;
}
};

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,);

//TODO: Add CLI commands
match tokio::signal::ctrl_c().await {
Ok(_) => {
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);
}
50 changes: 50 additions & 0 deletions src/log.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use fern::colors::{Color, ColoredLevelConfig};
use log::{LevelFilter, SetLoggerError};
use std::{
io,
sync::atomic::{AtomicBool, Ordering},
time::SystemTime,
};

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
}
}
56 changes: 49 additions & 7 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,57 @@
mod config;
use ::log::{error, warn};
use lum::{
bot::Bot,
config::{Config, ConfigHandler, ConfigParseError},
log,
service::Service,
};

pub const BOT_NAME: &str = "Lum";
const BOT_NAME: &str = "Lum";

fn main() {
let config_handler = config::ConfigHandler::new(BOT_NAME.to_lowercase().as_str());
let config = match config_handler.get_config() {
#[tokio::main]
async fn main() {
setup_logger();

if lum::is_debug() {
warn!("THIS IS A DEBUG RELEASE!");
}

let _config = match get_config() {
Ok(config) => config,
Err(err) => {
panic!("Error reading config file: {}", err);
error!(
"Error reading config file: {}\n{} will exit.",
err, BOT_NAME
);

return;
}
};

println!("Config: {}", config);
let bot = Bot::builder(BOT_NAME)
.with_services(initialize_services())
.build();

lum::run(bot).await;
}

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<Config, ConfigParseError> {
let config_handler = ConfigHandler::new(BOT_NAME.to_lowercase().as_str());
config_handler.get_config()
}

fn initialize_services() -> Vec<Box<dyn Service>> {
//TODO: Add services
//...

vec![]
}
Loading

0 comments on commit 98707d2

Please sign in to comment.