graceful shutdown

This commit is contained in:
jude 2022-03-21 23:11:52 +00:00
parent 93da746bdc
commit 878ea11502
9 changed files with 189 additions and 74 deletions

View File

@ -2,14 +2,31 @@ mod sender;
use std::env; use std::env;
use log::info; use log::{info, warn};
use serenity::client::Context; use serenity::client::Context;
use sqlx::{Executor, MySql}; use sqlx::{Executor, MySql};
use tokio::time::{sleep_until, Duration, Instant}; use tokio::{
sync::broadcast::Receiver,
time::{sleep_until, Duration, Instant},
};
type Database = MySql; type Database = MySql;
pub async fn initialize(ctx: Context, pool: impl Executor<'_, Database = Database> + Copy) { pub async fn initialize(
mut kill: Receiver<()>,
ctx: Context,
pool: impl Executor<'_, Database = Database> + Copy,
) -> Result<(), &'static str> {
tokio::select! {
output = _initialize(ctx, pool) => Ok(output),
_ = kill.recv() => {
warn!("Received terminate signal. Goodbye");
Err("Received terminate signal. Goodbye")
}
}
}
async fn _initialize(ctx: Context, pool: impl Executor<'_, Database = Database> + Copy) {
let remind_interval = env::var("REMIND_INTERVAL") let remind_interval = env::var("REMIND_INTERVAL")
.map(|inner| inner.parse::<u64>().ok()) .map(|inner| inner.parse::<u64>().ok())
.ok() .ok()

View File

@ -1,6 +1,6 @@
use std::{collections::HashMap, env, sync::atomic::Ordering}; use std::{collections::HashMap, env, sync::atomic::Ordering};
use log::{info, warn}; use log::{error, info, warn};
use poise::{ use poise::{
serenity::{model::interactions::Interaction, utils::shard_id}, serenity::{model::interactions::Interaction, utils::shard_id},
serenity_prelude as serenity, serenity_prelude as serenity,
@ -15,10 +15,12 @@ pub async fn listener(
) -> Result<(), Error> { ) -> Result<(), Error> {
match event { match event {
poise::Event::CacheReady { .. } => { poise::Event::CacheReady { .. } => {
info!("Cache Ready!"); info!("Cache Ready! Preparing extra processes");
info!("Preparing to send reminders");
if !data.is_loop_running.load(Ordering::Relaxed) { if !data.is_loop_running.load(Ordering::Relaxed) {
let kill_tx = data.broadcast.clone();
let kill_recv = data.broadcast.subscribe();
let ctx1 = ctx.clone(); let ctx1 = ctx.clone();
let ctx2 = ctx.clone(); let ctx2 = ctx.clone();
@ -29,7 +31,12 @@ pub async fn listener(
if !run_settings.contains("postman") { if !run_settings.contains("postman") {
tokio::spawn(async move { tokio::spawn(async move {
postman::initialize(ctx1, &pool1).await; match postman::initialize(kill_recv, ctx1, &pool1).await {
Ok(_) => {}
Err(e) => {
error!("postman exiting: {}", e);
}
};
}); });
} else { } else {
warn!("Not running postman") warn!("Not running postman")
@ -37,7 +44,7 @@ pub async fn listener(
if !run_settings.contains("web") { if !run_settings.contains("web") {
tokio::spawn(async move { tokio::spawn(async move {
reminder_web::initialize(ctx2, pool2).await.unwrap(); reminder_web::initialize(kill_tx, ctx2, pool2).await.unwrap();
}); });
} else { } else {
warn!("Not running web") warn!("Not running web")

View File

@ -12,7 +12,13 @@ mod models;
mod time_parser; mod time_parser;
mod utils; mod utils;
use std::{collections::HashMap, env, fmt::Formatter, sync::atomic::AtomicBool}; use std::{
collections::HashMap,
env,
error::Error as StdError,
fmt::{Debug, Display, Formatter},
sync::atomic::AtomicBool,
};
use chrono_tz::Tz; use chrono_tz::Tz;
use dotenv::dotenv; use dotenv::dotenv;
@ -21,7 +27,7 @@ use poise::serenity::model::{
id::{GuildId, UserId}, id::{GuildId, UserId},
}; };
use sqlx::{MySql, Pool}; use sqlx::{MySql, Pool};
use tokio::sync::RwLock; use tokio::sync::{broadcast, broadcast::Sender, RwLock};
use crate::{ use crate::{
commands::{info_cmds, moderation_cmds, reminder_cmds, todo_cmds}, commands::{info_cmds, moderation_cmds, reminder_cmds, todo_cmds},
@ -43,6 +49,7 @@ pub struct Data {
recording_macros: RwLock<HashMap<(GuildId, UserId), CommandMacro<Data, Error>>>, recording_macros: RwLock<HashMap<(GuildId, UserId), CommandMacro<Data, Error>>>,
popular_timezones: Vec<Tz>, popular_timezones: Vec<Tz>,
is_loop_running: AtomicBool, is_loop_running: AtomicBool,
broadcast: Sender<()>,
} }
impl std::fmt::Debug for Data { impl std::fmt::Debug for Data {
@ -51,8 +58,33 @@ impl std::fmt::Debug for Data {
} }
} }
struct Ended;
impl Debug for Ended {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str("Process ended.")
}
}
impl Display for Ended {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str("Process ended.")
}
}
impl StdError for Ended {}
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn main() -> Result<(), Box<dyn StdError + Send + Sync>> {
let (tx, mut rx) = broadcast::channel(16);
tokio::select! {
output = _main(tx) => output,
_ = rx.recv() => Err(Box::new(Ended) as Box<dyn StdError + Send + Sync>)
}
}
async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
env_logger::init(); env_logger::init();
dotenv()?; dotenv()?;
@ -157,6 +189,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
popular_timezones, popular_timezones,
recording_macros: Default::default(), recording_macros: Default::default(),
is_loop_running: AtomicBool::new(false), is_loop_running: AtomicBool::new(false),
broadcast: tx,
}) })
}) })
}) })

View File

@ -9,7 +9,7 @@ mod routes;
use std::{collections::HashMap, env}; use std::{collections::HashMap, env};
use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl}; use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
use rocket::fs::FileServer; use rocket::{fs::FileServer, tokio::sync::broadcast::Sender};
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use serenity::{ use serenity::{
client::Context, client::Context,
@ -53,6 +53,7 @@ async fn internal_server_error() -> Template {
} }
pub async fn initialize( pub async fn initialize(
kill_channel: Sender<()>,
serenity_context: Context, serenity_context: Context,
db_pool: Pool<Database>, db_pool: Pool<Database>,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
@ -119,6 +120,10 @@ pub async fn initialize(
.launch() .launch()
.await?; .await?;
warn!("Exiting rocket runtime");
// distribute kill signal
kill_channel.send(());
Ok(()) Ok(())
} }

View File

@ -57,6 +57,14 @@ button.change-color {
left: calc(-1rem - 40px); left: calc(-1rem - 40px);
} }
button.disable-enable[data-action="enable"]:after {
content: "Enable";
}
button.disable-enable[data-action="disable"]:after {
content: "Disable";
}
.media-content { .media-content {
overflow-x: visible; overflow-x: visible;
} }

View File

@ -3,10 +3,7 @@ let $discordFrame;
const $loader = document.querySelector("#loader"); const $loader = document.querySelector("#loader");
const $colorPickerModal = document.querySelector("div#pickColorModal"); const $colorPickerModal = document.querySelector("div#pickColorModal");
const $colorPickerInput = $colorPickerModal.querySelector("input"); const $colorPickerInput = $colorPickerModal.querySelector("input");
const $deleteReminderBtn = document.querySelector("#delete-reminder-confirm");
let timezone = luxon.DateTime.now().zone.name;
const browserTimezone = luxon.DateTime.now().zone.name;
let botTimezone = "UTC";
let channels; let channels;
let roles; let roles;
@ -75,6 +72,8 @@ function fetch_roles(guild_id) {
} }
async function fetch_reminders(guild_id) { async function fetch_reminders(guild_id) {
document.dispatchEvent(new Event("remindersLoading"));
const $reminderBox = document.querySelector("div#guildReminders"); const $reminderBox = document.querySelector("div#guildReminders");
// reset div contents // reset div contents
@ -113,7 +112,6 @@ async function fetch_reminders(guild_id) {
} }
let $enableBtn = newFrame.querySelector(".disable-enable"); let $enableBtn = newFrame.querySelector(".disable-enable");
$enableBtn.textContent = reminder["enabled"] ? "Disable" : "Enable";
$enableBtn.dataset.action = reminder["enabled"] $enableBtn.dataset.action = reminder["enabled"]
? "disable" ? "disable"
: "enable"; : "enable";
@ -164,14 +162,36 @@ document.addEventListener("remindersLoaded", (event) => {
if (data.error) { if (data.error) {
show_error(data.error); show_error(data.error);
} else { } else {
enableBtn.textContent = data["enabled"] ? "Disable" : "Enable";
enableBtn.dataset.action = data["enabled"] ? "enable" : "disable"; enableBtn.dataset.action = data["enabled"] ? "enable" : "disable";
} }
}); });
}); });
reminder.node
.querySelector("button.delete-reminder")
.addEventListener("click", () => {
let uid = reminder.node.closest(".reminderContent").dataset.uid;
$deleteReminderBtn.dataset["uid"] = uid;
$deleteReminderBtn.closest(".modal").classList.toggle("is-active");
});
} }
}); });
$deleteReminderBtn.addEventListener("click", () => {
let guild = document.querySelector(".guildList a.is-active").dataset["guild"];
fetch(`/dashboard/api/guild/${guild}/reminders`, {
method: "DELETE",
body: JSON.stringify({
uid: $deleteReminderBtn.dataset["uid"],
}),
}).then(() => {
document.querySelector("#deleteReminderModal").classList.remove("is-active");
fetch_reminders(guild);
});
});
function show_error(error) { function show_error(error) {
document.getElementById("errors").querySelector("span.error-message").textContent = document.getElementById("errors").querySelector("span.error-message").textContent =
error; error;
@ -182,60 +202,6 @@ function show_error(error) {
}, 5000); }, 5000);
} }
function update_times() {
document.querySelectorAll("span.set-timezone").forEach((element) => {
element.textContent = timezone;
});
document.querySelectorAll("span.set-time").forEach((element) => {
element.textContent = luxon.DateTime.now().setZone(timezone).toFormat("HH:mm");
});
document.querySelectorAll("span.browser-timezone").forEach((element) => {
element.textContent = browserTimezone;
});
document.querySelectorAll("span.browser-time").forEach((element) => {
element.textContent = luxon.DateTime.now().toFormat("HH:mm");
});
document.querySelectorAll("span.bot-timezone").forEach((element) => {
element.textContent = botTimezone;
});
document.querySelectorAll("span.bot-time").forEach((element) => {
element.textContent = luxon.DateTime.now().setZone(botTimezone).toFormat("HH:mm");
});
}
window.setInterval(() => {
update_times();
}, 30000);
document.getElementById("set-bot-timezone").addEventListener("click", () => {
timezone = botTimezone;
update_times();
});
document.getElementById("set-browser-timezone").addEventListener("click", () => {
timezone = browserTimezone;
update_times();
});
document.getElementById("update-bot-timezone").addEventListener("click", () => {
timezone = browserTimezone;
fetch("/dashboard/api/user", {
method: "PATCH",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({ timezone: timezone }),
})
.then((response) => response.json())
.then((data) => {
if (data.error) {
show_error(data.error);
} else {
botTimezone = browserTimezone;
update_times();
}
});
});
$colorPickerInput.value = colorPicker.color.hexString; $colorPickerInput.value = colorPicker.color.hexString;
$colorPickerInput.addEventListener("input", () => { $colorPickerInput.addEventListener("input", () => {
@ -482,6 +448,7 @@ $createReminder.querySelector("button#createReminder").addEventListener("click",
.then((data) => console.log(data)); .then((data) => console.log(data));
// process response // process response
fetch_reminders(guild);
// reset inputs // reset inputs
}); });

57
web/static/js/timezone.js Normal file
View File

@ -0,0 +1,57 @@
let timezone = luxon.DateTime.now().zone.name;
const browserTimezone = luxon.DateTime.now().zone.name;
let botTimezone = "UTC";
function update_times() {
document.querySelectorAll("span.set-timezone").forEach((element) => {
element.textContent = timezone;
});
document.querySelectorAll("span.set-time").forEach((element) => {
element.textContent = luxon.DateTime.now().setZone(timezone).toFormat("HH:mm");
});
document.querySelectorAll("span.browser-timezone").forEach((element) => {
element.textContent = browserTimezone;
});
document.querySelectorAll("span.browser-time").forEach((element) => {
element.textContent = luxon.DateTime.now().toFormat("HH:mm");
});
document.querySelectorAll("span.bot-timezone").forEach((element) => {
element.textContent = botTimezone;
});
document.querySelectorAll("span.bot-time").forEach((element) => {
element.textContent = luxon.DateTime.now().setZone(botTimezone).toFormat("HH:mm");
});
}
window.setInterval(() => {
update_times();
}, 30000);
document.getElementById("set-bot-timezone").addEventListener("click", () => {
timezone = botTimezone;
update_times();
});
document.getElementById("set-browser-timezone").addEventListener("click", () => {
timezone = browserTimezone;
update_times();
});
document.getElementById("update-bot-timezone").addEventListener("click", () => {
timezone = browserTimezone;
fetch("/dashboard/api/user", {
method: "PATCH",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({ timezone: timezone }),
})
.then((response) => response.json())
.then((data) => {
if (data.error) {
show_error(data.error);
} else {
botTimezone = browserTimezone;
update_times();
}
});
});

View File

@ -140,6 +140,27 @@
<button class="modal-close is-large close-modal" aria-label="close"></button> <button class="modal-close is-large close-modal" aria-label="close"></button>
</div> </div>
<div class="modal" id="deleteReminderModal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<label class="modal-card-title" for="urlInput">Delete Reminder</label>
<button class="delete close-modal" aria-label="close"></button>
</header>
<section class="modal-card-body">
<p>
This reminder will be permenantly deleted. Are you sure?
</p>
<br>
<div class="has-text-centered">
<button class="button is-danger" id="delete-reminder-confirm">Delete</button>
<button class="button is-light close-modal">Cancel</button>
</div>
</section>
</div>
<button class="modal-close is-large close-modal" aria-label="close"></button>
</div>
<div class="columns is-gapless dashboard-frame"> <div class="columns is-gapless dashboard-frame">
<div class="column is-2 is-sidebar-menu dashboard-sidebar is-hidden-touch" style="display: flex; flex-direction: column;"> <div class="column is-2 is-sidebar-menu dashboard-sidebar is-hidden-touch" style="display: flex; flex-direction: column;">
<a href="/"> <a href="/">
@ -300,6 +321,7 @@
{% include "reminder_dashboard/personal_reminder" %} {% include "reminder_dashboard/personal_reminder" %}
</template> </template>
<script src="/static/js/timezone.js"></script>
<script src="/static/js/main.js"></script> <script src="/static/js/main.js"></script>
</body> </body>

View File

@ -215,9 +215,8 @@
Saved! Saved!
</button> </button>
<button class="button is-warning disable-enable"> <button class="button is-warning disable-enable">
Disable
</button> </button>
<button class="button is-danger"> <button class="button is-danger delete-reminder">
Delete Delete
</button> </button>
{% endif %} {% endif %}