systemdaemon 1.0.1

System daemon building blocks. Designed for but not limited to systemd.
Documentation
// SPDX-FileCopyrightText: 2025 Simon Brummer
//
// SPDX-License-Identifier: MPL-2.0

// SystemD implementations
#[cfg(all(feature = "systemd", target_os = "linux"))]
pub mod systemd {
    use sd_notify::{NotifyState, notify, watchdog_enabled};
    use std::time::Duration;
    use tracing::level_filters::LevelFilter;
    use tracing::{Level, debug, error, info, instrument, warn};
    use tracing_subscriber::Layer;
    use tracing_subscriber::layer::SubscriberExt;
    use tracing_subscriber::util::SubscriberInitExt;
    use tracing_systemd::SystemdLayer;

    use crate::{Logger, Notify, Reload, Shutdown, Watchdog};

    /// A systemd integration implementing the following traits:
    /// [`Logger`], [`Watchdog`], [`Shutdown`], [`Reload`], [`Notify`].
    #[derive(Copy, Clone)]
    pub struct SystemdIntegration;

    impl Logger for SystemdIntegration {
        #[instrument(skip_all)]
        fn setup_logger(&self, level: Level) {
            let systemd_logger = SystemdLayer::stdout()
                .with_target(true)
                .with_thread_ids(true)
                .with_filter(LevelFilter::from_level(level));

            tracing_subscriber::registry().with(systemd_logger).init();

            info!("Initialized logger, level is level {level}");
        }
    }

    impl Watchdog for SystemdIntegration {
        #[instrument(skip_all)]
        fn notify_interval(&self) -> Option<Duration> {
            match watchdog_enabled() {
                Some(interval) => {
                    debug!(
                        "Systemd watchdog is enabled. Trigger interval is {} µs.",
                        interval.as_micros()
                    );
                    Some(interval)
                }
                None => {
                    debug!("Systemd watchdog is disabled.");
                    None
                }
            }
        }

        #[instrument(skip_all)]
        fn notify(&self) {
            if let Err(err) = notify(&[NotifyState::Watchdog]) {
                error!("Failed to trigger systemd watchdog. Error was: {err}");
            }
        }
    }

    impl Shutdown for SystemdIntegration {
        #[instrument(skip_all)]
        async fn wait_for_shutdown(&self) {
            #[cfg(not(feature = "tokio"))]
            {
                use async_signal::{Signal, Signals};
                use futures::StreamExt;

                let mut signals =
                    Signals::new([Signal::Int, Signal::Term, Signal::Quit, Signal::Abort]).unwrap();

                match signals.next().await {
                    Some(Ok(Signal::Int)) => debug!("Received OS signal SIGINT."),
                    Some(Ok(Signal::Term)) => debug!("Received OS signal SIGTERM."),
                    Some(Ok(Signal::Quit)) => debug!("Received OS signal SIGQUIT."),
                    Some(Ok(Signal::Abort)) => debug!("Received OS signal SIGABRT."),
                    _ => panic!(
                        "Something unexpected happened while waiting for signals. This is a bug."
                    ),
                }
            }
            #[cfg(feature = "tokio")]
            {
                use libc::{SIGABRT, SIGINT, SIGQUIT, SIGTERM};
                use tokio::signal::unix::{SignalKind, signal};

                let mut sigint = signal(SignalKind::from_raw(SIGINT)).unwrap();
                let mut sigterm = signal(SignalKind::from_raw(SIGTERM)).unwrap();
                let mut sigquit = signal(SignalKind::from_raw(SIGQUIT)).unwrap();
                let mut sigabrt = signal(SignalKind::from_raw(SIGABRT)).unwrap();
                tokio::select! {
                    _ = sigint.recv() => debug!("Received OS signal SIGINT."),
                    _ = sigterm.recv() => debug!("Received OS signal SIGTERM."),
                    _ = sigquit.recv() => debug!("Received OS signal SIGQUIT."),
                    _ = sigabrt.recv() => debug!("Received OS signal SIGABRT."),
                }
            }
        }
    }

    impl Reload for SystemdIntegration {
        #[instrument(skip_all)]
        async fn wait_for_reload(&self) {
            // Note: systemd triggers daemon reloading by sending SIGHUP to the
            // managed process. This task unblocks after it is received.
            // See the [systemd.service manpage](https://www.man7.org/linux/man-pages/man5/systemd.service.5.html) for details.
            #[cfg(not(feature = "tokio"))]
            {
                use async_signal::{Signal, Signals};
                use futures::StreamExt;

                let mut signals = Signals::new([Signal::Hup]).unwrap();
                match signals.next().await {
                    Some(Ok(Signal::Hup)) => debug!("Received OS signal SIGHUP."),
                    _ => panic!(
                        "Something unexpected happened while waiting for signals. This is a bug."
                    ),
                }
            }

            #[cfg(feature = "tokio")]
            {
                use libc::SIGHUP;
                use tokio::signal::unix::{SignalKind, signal};

                signal(SignalKind::from_raw(SIGHUP)).unwrap().recv().await;
                debug!("Received OS signal SIGHUP.")
            }
        }
    }

    impl Notify for SystemdIntegration {
        #[instrument(skip_all)]
        fn notify_ready(&self) {
            match NotifyState::monotonic_usec_now()
                .and_then(|now| notify(&[NotifyState::Ready, now]))
            {
                Ok(()) => debug!("Daemon is ready"),
                Err(err) => error!("Daemon is ready notification failed. Error was {err}"),
            }
        }

        #[instrument(skip_all)]
        fn notify_reloading(&self) {
            // Note: systemd requires a monotonic usec event after reloading.
            // See the [systemd.service manpage](https://www.man7.org/linux/man-pages/man5/systemd.service.5.html) for details.
            match NotifyState::monotonic_usec_now()
                .and_then(|now| notify(&[NotifyState::Reloading, now]))
            {
                Ok(()) => debug!("Daemon is reloading"),
                Err(err) => error!("Daemon is reloading notification failed. Error was {err}"),
            }
        }

        #[instrument(skip_all)]
        fn notify_stopping(&self) {
            match notify(&[NotifyState::Stopping]) {
                Ok(()) => debug!("Daemon is stopping"),
                Err(err) => error!("Daemon is stopping notification failed. Error was {err}"),
            }
        }
    }
}

#[cfg(test)]
pub mod mocks {
    use mockall::mock;
    use std::time::Duration;
    use tracing::Level;

    use crate::{Logger, Notify, Reload, Shutdown, Watchdog};

    mock! {
        pub Integration {}

        impl Logger for Integration {
            fn setup_logger(&self, max_level: Level);
        }

        impl Watchdog for Integration {
            fn notify_interval(&self) -> Option<Duration>;
            fn notify(&self);
        }

        impl Reload for Integration {
            fn wait_for_reload(&self)-> impl Future<Output = ()> + Send;
        }

        impl Shutdown for Integration {
            fn wait_for_shutdown(&self) -> impl Future<Output = ()> + Send;
        }

        impl Notify for Integration {
            fn notify_ready(&self);
            fn notify_reloading(&self);
            fn notify_stopping(&self);
        }
    }
}