systemd_connector/
notify.rs

1//! Notify systemd of service status changes
2
3use std::{fmt, io, sync::Arc};
4
5use camino::Utf8PathBuf;
6use thiserror::Error;
7use tokio::net::UnixDatagram;
8
9use crate::socket::SocketError;
10
11/// The environment variable that systemd uses to set the unix socket path
12/// for notifications.
13const NOTIFY_SOCKET: &str = "NOTIFY_SOCKET";
14
15/// Error returned when sending a notification didn't work
16#[derive(Debug, Error)]
17pub enum NotifyError {
18    /// An IO error occurred while sending the notification
19    #[error("{}", .0)]
20    IO(#[from] io::Error),
21
22    /// A required environment variable was missing
23    #[error("Missing ${0} variable")]
24    MissingVar(&'static str),
25
26    /// An environment variable had an invalid value
27    #[error("Invalid ${0}={1}")]
28    InvalidVar(&'static str, String),
29}
30
31impl From<SocketError> for NotifyError {
32    fn from(value: SocketError) -> Self {
33        match value {
34            SocketError::IO(err) => NotifyError::IO(err),
35            SocketError::MissingVar(var) => NotifyError::MissingVar(var),
36            SocketError::InvalidVar(var, value) => NotifyError::InvalidVar(var, value),
37            err => panic!("Unexpected error for Notify: {err}"),
38        }
39    }
40}
41
42/// Custom variable to send to SystemD
43#[derive(Debug, Clone)]
44pub struct CustomVariable {
45    key: String,
46    value: String,
47}
48
49impl fmt::Display for CustomVariable {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        write!(f, "X-{}={}", self.key, self.value)
52    }
53}
54
55/// Notification kinds to send to systemd
56#[derive(Debug, Clone)]
57pub enum Notification {
58    /// Notify systemd that the service is ready
59    Ready,
60
61    /// Notify systemd that the service is reloading
62    Reloading,
63
64    /// Notify systemd that the service is stopping
65    Stopping,
66
67    /// Notify systemd of the service status
68    Status(String),
69
70    /// Notify systemd of an error number
71    Errno(i32),
72
73    /// Notify systemd that the service is ok (heartbeat)
74    WatchdogOk,
75
76    /// Notify systemd to trigger the watchdog
77    WatchdogTrigger,
78
79    /// Send a custom notification
80    Custom(CustomVariable),
81}
82
83impl fmt::Display for Notification {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        match self {
86            Notification::Ready => f.write_str("READY=1"),
87            Notification::Reloading => f.write_str("RELOADING=1"),
88            Notification::Stopping => f.write_str("STOPPING=1"),
89            Notification::Status(status) => write!(f, "STATUS={status}"),
90            Notification::Errno(errno) => write!(f, "ERRNO={errno}"),
91            Notification::WatchdogOk => f.write_str("WATCHDOG=1"),
92            Notification::WatchdogTrigger => f.write_str("WATCHDOG=trigger"),
93            Notification::Custom(variable) => write!(f, "{variable}"),
94        }
95    }
96}
97
98/// A systemd notification message, which
99/// can consist of a series of known or custom systemd variables.
100#[derive(Debug, Clone, Default)]
101pub struct Message {
102    variables: Vec<Notification>,
103}
104
105impl Message {
106    /// Create a new message
107    pub fn new() -> Self {
108        Self {
109            variables: Vec::new(),
110        }
111    }
112
113    /// Add a notification to the message
114    pub fn push(&mut self, notification: Notification) {
115        self.variables.push(notification)
116    }
117}
118
119impl From<Notification> for Message {
120    fn from(value: Notification) -> Self {
121        Message {
122            variables: vec![value],
123        }
124    }
125}
126
127impl FromIterator<Notification> for Message {
128    fn from_iter<I: IntoIterator<Item = Notification>>(iter: I) -> Self {
129        let variables = iter.into_iter().collect();
130        Message { variables }
131    }
132}
133
134impl fmt::Display for Message {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        for variable in &self.variables {
137            writeln!(f, "{variable}")?;
138        }
139        Ok(())
140    }
141}
142
143/// Notification socket for sending messages to Systemd
144///
145/// The default construction is to build this from the environment via [SystemDNotify::from_environment].
146#[derive(Debug, Clone)]
147pub struct SystemDNotify {
148    socket: Arc<UnixDatagram>,
149    address: Utf8PathBuf,
150}
151
152impl SystemDNotify {
153    /// Create a new SystemDNotify client from the environment
154    pub fn from_environment() -> Result<Self, NotifyError> {
155        let address = crate::socket::var(NOTIFY_SOCKET)?.into();
156        let socket = UnixDatagram::unbound()?;
157
158        Ok(SystemDNotify {
159            socket: Arc::new(socket),
160            address,
161        })
162    }
163
164    /// Send a message to systemd
165    pub async fn send<M: Into<Message>>(&self, message: M) -> Result<(), NotifyError> {
166        let message = message.into().to_string();
167        self.socket
168            .send_to(message.as_bytes(), &self.address)
169            .await?;
170        Ok(())
171    }
172}
173
174/// Notify systemd that this service is ready.
175///
176/// This is implemented as sending a single message to systemd with the appropriate
177/// ready command.
178pub async fn ready() {
179    if let Ok(notify) = SystemDNotify::from_environment() {
180        if let Err(err) = notify.send(Notification::Ready).await {
181            tracing::warn!("Failed to notify systemd: {err}");
182        }
183    }
184}