/* * tmtd - Suckless To Do list * Copyright (C) 2022 lemon.sh & karx * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ use crate::web::App; use crate::{config::Config, database::Database}; use async_sqlx_session::PostgresSessionStore; use std::str::FromStr; use std::time::Duration; use std::{env, sync::Arc}; use tokio::sync::broadcast; use tokio::task::JoinHandle; use tokio::time::sleep; use tracing::{debug, error, info, Level}; mod config; mod database; mod web; #[cfg(unix)] async fn terminate_signal() { use tokio::signal::unix::{signal, SignalKind}; let mut sigterm = signal(SignalKind::terminate()).unwrap(); let mut sigint = signal(SignalKind::interrupt()).unwrap(); debug!("Installed ctrl+c handler"); tokio::select! { _ = sigterm.recv() => {}, _ = sigint.recv() => {} } } #[cfg(windows)] async fn terminate_signal() { use tokio::signal::windows::ctrl_c; let mut ctrlc = ctrl_c().unwrap(); debug!("Installed ctrl+c handler"); let _ = ctrlc.recv().await; } #[tokio::main(flavor = "current_thread")] async fn main() -> anyhow::Result<()> { let config_var = env::var("TMTD_CONFIG"); let config_path = config_var.as_deref().unwrap_or("tmtd.toml"); println!("Loading config from '{}'...", config_path); let cfg = Arc::new(Config::load_from_file(config_path).await?); tracing_subscriber::fmt::fmt() .with_max_level({ if let Some(o) = cfg.log_level.as_deref() { Level::from_str(o)? } else { Level::INFO } }) .init(); info!(concat!("Initializing - tmtd ", env!("CARGO_PKG_VERSION"))); let (ctx, _) = broadcast::channel(1); let database = Arc::new(Database::connect(&cfg.connection_string).await?); let session_store = PostgresSessionStore::from_client(database.pool()).with_table_name("sessions"); session_store.migrate().await?; let cleanup_task = spawn_session_cleanup_task(&session_store, Duration::from_secs(600), ctx.subscribe()); info!("Started session cleanup task"); let web_app = App::new(cfg.clone(), database.clone(), session_store)?; let web_task = tokio::spawn(web_app.run(ctx.subscribe())); info!("Started the web app at http://{}", cfg.listen_addr); terminate_signal().await; ctx.send(()).unwrap(); cleanup_task .await .unwrap_or_else(|e| error!("Couldn't join cleanup task: {}", e)); web_task .await .unwrap_or_else(|e| error!("Couldn't join web task: {}", e)); database.close().await; Ok(()) } fn spawn_session_cleanup_task( store: &PostgresSessionStore, period: Duration, mut cancel: broadcast::Receiver<()>, ) -> JoinHandle<()> { let store = store.clone(); tokio::spawn(async move { loop { tokio::select! { _ = sleep(period) => { if let Err(error) = store.cleanup().await { error!("Error in cleanup task: {}", error); } } _ = cancel.recv() => break } } info!("Cleanup task has been shut down"); }) }