You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

206 lines
5.0 KiB
Rust

mod config;
mod fs_watcher;
mod render;
mod service;
mod template;
use std::{
env,
error,
fmt,
fs,
io,
net::SocketAddr,
path::PathBuf,
process,
sync::Arc,
thread,
};
use hyper::Server;
use tokio::signal;
use tracing::{error, info};
use blog::{
codeblock::CodeBlockRenderer,
db::ConcurrentPostsStore,
};
use config::Config;
use render::Renderer;
fn main() {
if let Err(err) = run() {
eprintln!("***** Fatal error *****");
eprintln!("{}", err);
process::exit(1);
}
}
fn run() -> Result<(), Error> {
tracing_subscriber::fmt::init();
// Load the configuration from the TOML config file specified by the first command-line
// argument.
let config = Arc::new({
let config_path = env::args().nth(1)
.ok_or(Error::NoConfig)?;
info!(path = %config_path, "Loading config");
let contents = fs::read_to_string(&config_path)
.map_err(Error::ReadConfig)?;
contents.parse::<Config>()
.map_err(Error::BadConfig)?
});
// Create the data structure used to store the rendered posts. This uses an `Arc` internally,
// so clones will point to the same underlying data.
let posts_store = ConcurrentPostsStore::new();
let code_renderer = CodeBlockRenderer::new();
// Create the post renderer and the mpsc channel that will be used to communicate with it.
let (renderer, tx) = Renderer::new(
config.clone(),
posts_store.clone(),
code_renderer,
config.posts_dir.clone()
);
// Dropping the watcher stops its thread, so keep it alive until the server has stopped.
let watcher = fs_watcher::start_watching(
tx,
&config.posts_dir,
config.fs_event_delay
)?;
let renderer_handle = thread::spawn(move || {
renderer.handle_events();
});
info!("Started renderer thread");
// To run the web server, we need to be in an async context, so create a new Tokio runtime and
// pass control to it.
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(Error::TokioRuntime)?
.block_on(run_server(config, posts_store))?;
info!("Stopped server");
info!("Stopping filesystem watcher");
drop(watcher);
info!("Waiting for renderer thread to exit");
if renderer_handle.join().is_err() {
error!("Renderer thread panicked!");
}
info!("Goodbye!");
Ok(())
}
async fn run_server(
config: Arc<Config>,
posts_store: ConcurrentPostsStore,
) -> Result<(), Error>
{
let service = service::site_service(config.clone(), posts_store);
info!(address = %config.bind, "Starting server");
Server::try_bind(&config.bind)
.map_err(|err| Error::Bind(config.bind, err))?
.serve(service.into_make_service())
.with_graceful_shutdown(handle_interrupt())
.await
.map_err(Error::Server)
}
async fn handle_interrupt() {
info!("Installing interrupt handler");
let sigint = async {
signal::ctrl_c()
.await
.unwrap()
};
#[cfg(unix)] {
use signal::unix::{signal, SignalKind};
let sigterm = async {
signal(SignalKind::terminate())
.unwrap()
.recv()
.await;
};
tokio::select! {
biased;
_ = sigterm => {
info!("Received SIGTERM");
},
_ = sigint => {
info!("Received SIGINT");
},
};
}
#[cfg(not(unix))] {
sigint.await;
}
info!("Shutdown signal received, stopping");
}
#[derive(Debug)]
enum Error {
NoConfig,
ReadConfig(io::Error),
BadConfig(toml::de::Error),
CreateWatcher(notify::Error),
WatchDir(PathBuf, notify::Error),
TokioRuntime(io::Error),
Bind(SocketAddr, hyper::Error),
Server(hyper::Error),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NoConfig => {
write!(f, "no config file specified")
},
Self::ReadConfig(err) => {
write!(f, "failed to read config file: {}", err)
},
Self::BadConfig(err) => {
write!(f, "error in config: {}", err)
},
Self::CreateWatcher(err) => {
write!(f, "failed to create filesystem watcher: {}", err)
},
Self::WatchDir(path, err) => {
write!(f, "failed to watch directory {}: {}", path.to_string_lossy(), err)
},
Self::TokioRuntime(err) => {
write!(f, "failed to create async runtime: {}", err)
},
Self::Bind(addr, err) => {
write!(f, "failed to bind {}: {}", addr, err)
},
Self::Server(err) => {
write!(f, "error while running server: {}", err)
},
}
}
}
impl error::Error for Error {}