use std::{
env::args_os,
error::Error,
io::{IsTerminal as _, stdin},
process::{ExitCode, Termination},
};
use libc::{SIGALRM, SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGUSR1, SIGUSR2};
use serde::de::DeserializeOwned;
#[cfg(target_os = "linux")]
use sd_notify::NotifyState;
use signal_hook::iterator::Signals;
use crate::{
app::{
config::{ParseTomlFileError, parse_toml_file},
log::ProvidesEnvFilter,
},
providers::ProvidesExitCode,
};
pub struct MainResult<E: ProvidesExitCode + Error> {
error: Option<E>,
}
impl<E: ProvidesExitCode + Error> From<Result<(), E>> for MainResult<E> {
fn from(value: Result<(), E>) -> Self {
Self { error: value.err() }
}
}
impl<E: ProvidesExitCode + Error> Termination for MainResult<E> {
fn report(self) -> ExitCode {
self.error
.map(|error| {
eprintln!("{error:-}");
error.exit_code()
})
.unwrap_or(ExitCode::SUCCESS)
}
}
#[cfg(feature = "http")]
pub mod axum;
pub mod config;
pub mod consts;
pub mod log;
pub trait ReloadableService: Sized {
const INTERVAL_SECONDS: libc::c_uint = 5;
type Error: core::error::Error + ProvidesExitCode + From<ParseTomlFileError>;
type Config: DeserializeOwned + ProvidesEnvFilter;
fn start(config: Self::Config) -> Result<Self, Self::Error>;
fn reload(&mut self, config: Self::Config) -> Result<Self, Self::Error>;
fn sigusr1(&mut self) {}
fn sigusr2(&mut self) {}
fn interval(&mut self) -> Result<(), Self::Error> {
Ok(())
}
fn stop(self) -> Result<(), Self::Error>;
}
pub fn service_main<T: ReloadableService>() -> Result<(), T::Error> {
let mut signals = Signals::new([SIGALRM, SIGHUP, SIGINT, SIGTERM, SIGQUIT]).expect("set up signal hooks");
let config_file_path = args_os().nth(1);
let first_config = parse_toml_file(config_file_path.clone())?;
#[cfg(target_os = "linux")]
let running_systemd = sd_notify::notify(
false,
&[
NotifyState::MainPid(std::process::id()),
NotifyState::Status("starting..."),
],
)
.is_ok();
#[cfg(not(target_os = "linux"))]
let running_systemd = false;
let log_reload_handle = log::setup_local_logging(ProvidesEnvFilter::log_filter(&first_config), running_systemd);
let mut service = T::start(first_config).inspect_err(|err| {
#[cfg(target_os = "linux")]
let _ = sd_notify::notify(false, &[NotifyState::Status(&format!("failed to start: {err:-}"))]);
})?;
#[cfg(target_os = "linux")]
let _ = sd_notify::notify(false, &[NotifyState::Status("started!"), NotifyState::Ready]);
unsafe { libc::alarm(T::INTERVAL_SECONDS) };
for signal in signals.forever() {
let is_terminal = stdin().is_terminal();
match signal {
SIGHUP if !is_terminal => {
#[cfg(target_os = "linux")]
let _ = NotifyState::monotonic_usec_now().and_then(|notify_curtime| {
sd_notify::notify(
false,
&[
NotifyState::Status("reloading..."),
NotifyState::Reloading,
notify_curtime,
],
)
});
if let Err(reload_err) = {
|| -> Result<(), T::Error> {
let new_config = parse_toml_file(config_file_path.clone())?;
log_reload_handle.replace_filter(ProvidesEnvFilter::log_filter(&new_config));
service.reload(new_config)?;
Ok(())
}
}() {
tracing::error!(
error_details = format!("{reload_err:-#}"),
"service failed to reload; but we're continuing anyway: {reload_err:-.3}"
);
#[cfg(target_os = "linux")]
let _ = sd_notify::notify(
false,
&[
NotifyState::Status(&format!("reload error: {reload_err:-}")),
NotifyState::Ready,
],
);
} else {
#[cfg(target_os = "linux")]
let _ = sd_notify::notify(false, &[NotifyState::Status("reloaded!"), NotifyState::Ready]);
}
},
SIGINT | SIGTERM | SIGHUP | SIGQUIT => {
#[cfg(target_os = "linux")]
let _ = sd_notify::notify(false, &[NotifyState::Status("stopping..."), NotifyState::Stopping]);
service.stop().inspect_err(|stop_err| {
tracing::error!(
error_details = format!("{stop_err:-#}"),
"service returned on stop request: {stop_err:-.3}"
);
#[cfg(target_os = "linux")]
let _ = sd_notify::notify(
false,
&[NotifyState::Status(&format!("error while stopping: {stop_err:-}"))],
);
})?;
#[cfg(target_os = "linux")]
let _ = sd_notify::notify(false, &[NotifyState::Status("stopped")]);
break;
},
SIGALRM => {
tracing::trace!("received SIGALRM");
service.interval().inspect_err(|interval_err| {
tracing::error!(
error_details = format!("{interval_err:-#}"),
"service returned error on interval: {interval_err:-.3}"
);
#[cfg(target_os = "linux")]
let _ = sd_notify::notify(
false,
&[NotifyState::Status(&format!("error during interval: {interval_err:-}"))],
);
})?;
unsafe { libc::alarm(T::INTERVAL_SECONDS) };
},
SIGUSR1 => {
tracing::trace!("received SIGUSR1");
service.sigusr1();
},
SIGUSR2 => {
tracing::trace!("received SIGUSR2");
service.sigusr2();
},
_ => unreachable!("signal {signal} should have been handled"),
}
}
tracing::info!("service stopped");
Ok(())
}
#[cfg(test)]
#[path = "tests/app.rs"]
mod tests;