feat(server): add basic cli error handling to avoid unwraps
parent
be04d0c7fe
commit
82e52bc8f9
|
@ -1,5 +1,7 @@
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
|
|
||||||
|
use super::CliError;
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
/// Add devices of a specific user to the same sync group
|
/// Add devices of a specific user to the same sync group
|
||||||
|
@ -12,26 +14,24 @@ pub enum Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command {
|
impl Command {
|
||||||
pub fn run(&self, config: &crate::config::Config) -> u8 {
|
pub fn run(&self, config: &crate::config::Config) -> Result<(), CliError> {
|
||||||
let store =
|
let store =
|
||||||
gpodder_sqlite::SqliteRepository::from_path(config.data_dir.join(crate::DB_FILENAME))
|
gpodder_sqlite::SqliteRepository::from_path(config.data_dir.join(crate::DB_FILENAME))
|
||||||
.unwrap();
|
.map_err(|err| CliError::from(err).msg("failed to initialize Sqlite repository"))?;
|
||||||
let store = gpodder::GpodderRepository::new(store);
|
let store = gpodder::GpodderRepository::new(store);
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
Self::Sync { username, devices } => {
|
Self::Sync { username, devices } => {
|
||||||
let user = store.get_user(username).unwrap();
|
let user = store.get_user(username)?;
|
||||||
store
|
store.update_device_sync_status(
|
||||||
.update_device_sync_status(
|
|
||||||
&user,
|
&user,
|
||||||
vec![devices.iter().map(|s| s.as_ref()).collect()],
|
vec![devices.iter().map(|s| s.as_ref()).collect()],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
)
|
)?;
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
Self::Devices { username } => {
|
Self::Devices { username } => {
|
||||||
let user = store.get_user(username).unwrap();
|
let user = store.get_user(username)?;
|
||||||
let devices = store.devices_for_user(&user).unwrap();
|
let devices = store.devices_for_user(&user)?;
|
||||||
|
|
||||||
for device in devices {
|
for device in devices {
|
||||||
println!("{} ({} subscriptions)", device.id, device.subscriptions);
|
println!("{} ({} subscriptions)", device.id, device.subscriptions);
|
||||||
|
@ -39,6 +39,6 @@ impl Command {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
0
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,8 +85,40 @@ pub enum Command {
|
||||||
Gpo(gpo::Command),
|
Gpo(gpo::Command),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct CliError {
|
||||||
|
msg: Option<String>,
|
||||||
|
err: Box<dyn std::error::Error>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: std::error::Error + 'static> From<E> for CliError {
|
||||||
|
fn from(error: E) -> Self {
|
||||||
|
Self {
|
||||||
|
msg: None,
|
||||||
|
err: Box::new(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for CliError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let Some(msg) = &self.msg {
|
||||||
|
write!(f, "{msg}: ")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(f, "{}", self.err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CliError {
|
||||||
|
pub fn msg(mut self, msg: impl AsRef<str>) -> Self {
|
||||||
|
self.msg = Some(String::from(msg.as_ref()));
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Cli {
|
impl Cli {
|
||||||
pub fn run(&self) -> u8 {
|
pub fn run(&self) -> Result<(), CliError> {
|
||||||
let mut figment =
|
let mut figment =
|
||||||
Figment::new().merge(Serialized::defaults(crate::config::Config::default()));
|
Figment::new().merge(Serialized::defaults(crate::config::Config::default()));
|
||||||
|
|
||||||
|
@ -94,18 +126,11 @@ impl Cli {
|
||||||
figment = figment.merge(Toml::file(config_path));
|
figment = figment.merge(Toml::file(config_path));
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = figment
|
let config: crate::config::Config = figment
|
||||||
.merge(Env::prefixed("OTTER_"))
|
.merge(Env::prefixed("OTTER_"))
|
||||||
.merge(Serialized::defaults(self.config.clone()));
|
.merge(Serialized::defaults(self.config.clone()))
|
||||||
|
.extract()
|
||||||
let config = match config.extract::<crate::config::Config>() {
|
.map_err(|err| CliError::from(err).msg("failed to read config"))?;
|
||||||
Ok(config) => config,
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("{}", err);
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match &self.cmd {
|
match &self.cmd {
|
||||||
Command::Serve => serve::serve(&config),
|
Command::Serve => serve::serve(&config),
|
||||||
|
|
|
@ -2,18 +2,22 @@ use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
use crate::{config::NetConfig, server};
|
use crate::{config::NetConfig, server};
|
||||||
|
|
||||||
pub fn serve(config: &crate::config::Config) -> u8 {
|
use super::CliError;
|
||||||
|
|
||||||
|
pub fn serve(config: &crate::config::Config) -> Result<(), CliError> {
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_max_level(tracing::Level::from(config.log_level))
|
.with_max_level(tracing::Level::from(config.log_level))
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
tracing::info!("Initializing database and running migrations");
|
tracing::info!("Initializing database and running migrations");
|
||||||
|
|
||||||
// TODO remove unwraps
|
|
||||||
let store =
|
let store =
|
||||||
gpodder_sqlite::SqliteRepository::from_path(config.data_dir.join(crate::DB_FILENAME))
|
gpodder_sqlite::SqliteRepository::from_path(config.data_dir.join(crate::DB_FILENAME))
|
||||||
.unwrap();
|
.map_err(|err| CliError::from(err).msg("failed to initialize Sqlite repository"))?;
|
||||||
|
|
||||||
|
// SAFETY this runs on statically embedded templates, and should therefore never fail
|
||||||
let tera = crate::web::initialize_tera().unwrap();
|
let tera = crate::web::initialize_tera().unwrap();
|
||||||
|
|
||||||
let store = gpodder::GpodderRepository::new(store);
|
let store = gpodder::GpodderRepository::new(store);
|
||||||
|
|
||||||
let ctx = server::Context {
|
let ctx = server::Context {
|
||||||
|
@ -25,7 +29,7 @@ pub fn serve(config: &crate::config::Config) -> u8 {
|
||||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.map_err(|err| CliError::from(err).msg("failed to initialize Tokio runtime"))?;
|
||||||
|
|
||||||
// Spawn the session cleanup background task
|
// Spawn the session cleanup background task
|
||||||
let session_removal_duration = Duration::from_secs(config.session_cleanup_interval);
|
let session_removal_duration = Duration::from_secs(config.session_cleanup_interval);
|
||||||
|
@ -59,11 +63,16 @@ pub fn serve(config: &crate::config::Config) -> u8 {
|
||||||
tracing::info!("Listening on TCP address {address}");
|
tracing::info!("Listening on TCP address {address}");
|
||||||
|
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
let listener = tokio::net::TcpListener::bind(address).await.unwrap();
|
let listener = tokio::net::TcpListener::bind(address)
|
||||||
|
.await
|
||||||
|
.map_err(|err| CliError::from(err).msg("failed to bind TCP listener"))?;
|
||||||
|
|
||||||
axum::serve(listener, app.into_make_service())
|
axum::serve(listener, app.into_make_service())
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.map_err(|err| {
|
||||||
});
|
CliError::from(err).msg("error occured while running Axum server")
|
||||||
|
})
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
NetConfig::Unix { path } => {
|
NetConfig::Unix { path } => {
|
||||||
// Try to remove the socket file first if it exists
|
// Try to remove the socket file first if it exists
|
||||||
|
@ -76,14 +85,17 @@ pub fn serve(config: &crate::config::Config) -> u8 {
|
||||||
tracing::info!("Listening on Unix socket {:?}", path);
|
tracing::info!("Listening on Unix socket {:?}", path);
|
||||||
|
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
let listener = tokio::net::UnixListener::bind(path).unwrap();
|
let listener = tokio::net::UnixListener::bind(path)
|
||||||
|
.map_err(|err| CliError::from(err).msg("failed to bind Unix listener"))?;
|
||||||
|
|
||||||
axum::serve(listener, app.into_make_service())
|
axum::serve(listener, app.into_make_service())
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.map_err(|err| {
|
||||||
});
|
CliError::from(err).msg("error occured while running Axum server")
|
||||||
|
})
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
0
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,5 +30,12 @@ impl<E: std::error::Error> ErrorExt for E {}
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
let args = cli::Cli::parse();
|
let args = cli::Cli::parse();
|
||||||
|
|
||||||
ExitCode::from(args.run())
|
match args.run() {
|
||||||
|
Ok(()) => ExitCode::SUCCESS,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("{}", err);
|
||||||
|
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue