Proper NOTIFY_SOCKET
support
Still need to implement watchdog detection.
This commit is contained in:
parent
c142afbc2c
commit
776be0c96a
|
@ -1,16 +1,49 @@
|
|||
//! Contains the error types used by the [`notify`](super) module.
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
|
||||
use core::fmt::Display;
|
||||
use std::error::Error;
|
||||
|
||||
use rustix::io::Errno;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum NotifyError {
|
||||
SanityCheck(SanityCheckError),
|
||||
pub enum NewNotifierError {
|
||||
InvalidAbstractSocket(Errno),
|
||||
InvalidSocketPath(Errno),
|
||||
CouldntOpenSocket(std::io::Error),
|
||||
}
|
||||
|
||||
impl Display for NewNotifierError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
NewNotifierError::InvalidAbstractSocket(error) => {
|
||||
write!(f, "couldn't open notify abstract socket: {error}")
|
||||
}
|
||||
NewNotifierError::InvalidSocketPath(error) => {
|
||||
write!(f, "couldn't open notify socket from path: {error}")
|
||||
}
|
||||
NewNotifierError::CouldntOpenSocket(error) => {
|
||||
write!(f, "couldn't open unix socket: {error}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for NewNotifierError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match self {
|
||||
NewNotifierError::InvalidAbstractSocket(error)
|
||||
| NewNotifierError::InvalidSocketPath(error) => Some(error),
|
||||
NewNotifierError::CouldntOpenSocket(error) => Some(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum NotifyError {
|
||||
SanityCheck(SanityCheckError),
|
||||
SendMsg(Errno),
|
||||
AncillarySize,
|
||||
PushAncillaryMessage,
|
||||
PartialSend,
|
||||
}
|
||||
|
||||
|
@ -20,6 +53,31 @@ impl From<SanityCheckError> for NotifyError {
|
|||
}
|
||||
}
|
||||
|
||||
impl Display for NotifyError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
NotifyError::SanityCheck(error) => {
|
||||
write!(f, "NotifyState sanity check failed: {error}")
|
||||
}
|
||||
NotifyError::SendMsg(error) => write!(f, "couldn't send the message: {error}"),
|
||||
NotifyError::PushAncillaryMessage => {
|
||||
f.write_str("couldn't push necessary ancillary message for fd passing")
|
||||
}
|
||||
NotifyError::PartialSend => f.write_str("only some of the message could be sent"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for NotifyError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match self {
|
||||
NotifyError::SanityCheck(error) => Some(error),
|
||||
NotifyError::SendMsg(error) => Some(error),
|
||||
NotifyError::PushAncillaryMessage | NotifyError::PartialSend => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SanityCheckError {
|
||||
InvalidFdName(FdNameError),
|
||||
|
@ -29,13 +87,19 @@ impl Display for SanityCheckError {
|
|||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SanityCheckError::InvalidFdName(error) => {
|
||||
write!(f, "The value of FDNAME was invalid : {error}.")
|
||||
write!(f, "the value of FDNAME was invalid : {error}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for SanityCheckError {}
|
||||
impl Error for SanityCheckError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match self {
|
||||
SanityCheckError::InvalidFdName(error) => Some(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FdNameError {
|
||||
|
@ -47,9 +111,11 @@ pub enum FdNameError {
|
|||
impl Display for FdNameError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
FdNameError::TooLong { length, name } => write!(f, "The file descriptor name {name:?} is too long (is {length} characters and should be less than 255)."),
|
||||
FdNameError::NotAsciiNonControl { disallowed_char, name } => write!(f, "The file descriptor name {name:?} contains invalid character '{disallowed_char}' (only ASCII allowed)."),
|
||||
FdNameError::ContainColon(name) => write!(f, "The file descriptor name {name:?} contains a colon (':') which isn't allowed."),
|
||||
FdNameError::TooLong { length, name } => write!(f, "the file descriptor name {name:?} is too long (is {length} characters and should be less than 255)"),
|
||||
FdNameError::NotAsciiNonControl { disallowed_char, name } => write!(f, "the file descriptor name {name:?} contains invalid character '{disallowed_char}' (only ASCII allowed)"),
|
||||
FdNameError::ContainColon(name) => write!(f, "the file descriptor name {name:?} contains a colon (':') which isn't allowed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for FdNameError {}
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
//! Interface for the `NOTIFY_SOCKET` readiness protocol
|
||||
//!
|
||||
//! The entry point of this module is the [`Notifier`] struct.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! # main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! // Do whatever you need for your service to be ready.
|
||||
//! if let Some(notifier) = Notifier::new(true)? {
|
||||
//! notifier.notify(&[NotifyState::Ready])?
|
||||
//! }
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use core::fmt::Display;
|
||||
use std::env;
|
||||
use std::ffi::OsString;
|
||||
|
@ -6,79 +22,121 @@ use std::os::unix::net::UnixDatagram;
|
|||
|
||||
use rustix::fd::{AsFd, BorrowedFd};
|
||||
|
||||
use self::error::{FdNameError, NotifyError, SanityCheckError};
|
||||
use self::error::{FdNameError, NewNotifierError, NotifyError, SanityCheckError};
|
||||
|
||||
pub mod error;
|
||||
|
||||
pub struct Sent(bool);
|
||||
|
||||
pub fn notify_with_fds(
|
||||
unset_env: bool,
|
||||
state: &[NotifyState<'_>],
|
||||
fds: &[BorrowedFd],
|
||||
) -> Result<Sent, NotifyError> {
|
||||
let env_sock = match env::var_os("NOTIFY_SOCKET") {
|
||||
None => return Ok(Sent(false)),
|
||||
Some(v) => v,
|
||||
};
|
||||
|
||||
if unset_env {
|
||||
env::remove_var("NOTIFY_SOCKET");
|
||||
}
|
||||
|
||||
state.iter().try_for_each(NotifyState::sanity_check)?;
|
||||
|
||||
// If the first character of `$NOTIFY_SOCKET` is '@', the string
|
||||
// is understood as Linux abstract namespace socket.
|
||||
let socket_addr = match env_sock.as_encoded_bytes().strip_prefix(b"@").map(|v| {
|
||||
// SAFETY:
|
||||
// - Only strip ASCII '@' which is a non-empty UTF-8 substring
|
||||
unsafe { OsString::from_encoded_bytes_unchecked(v.to_vec()) }
|
||||
}) {
|
||||
Some(stripped_addr) => {
|
||||
rustix::net::SocketAddrUnix::new_abstract_name(stripped_addr.as_encoded_bytes())
|
||||
.map_err(NotifyError::InvalidAbstractSocket)?
|
||||
}
|
||||
None => {
|
||||
rustix::net::SocketAddrUnix::new(env_sock).map_err(NotifyError::InvalidSocketPath)?
|
||||
}
|
||||
};
|
||||
|
||||
let socket = UnixDatagram::unbound().map_err(NotifyError::CouldntOpenSocket)?;
|
||||
let msg = state
|
||||
.iter()
|
||||
.fold(String::new(), |acc, state| format!("{acc}{state}\n"))
|
||||
.into_bytes();
|
||||
let msg_len = msg.len();
|
||||
let msg_iov = IoSlice::new(&msg);
|
||||
|
||||
let mut ancillary = if !fds.is_empty() {
|
||||
let mut ancillary = rustix::net::SendAncillaryBuffer::default();
|
||||
let tmp = rustix::net::SendAncillaryMessage::ScmRights(fds);
|
||||
if !ancillary.push(tmp) {
|
||||
return Err(NotifyError::AncillarySize);
|
||||
}
|
||||
ancillary
|
||||
} else {
|
||||
rustix::net::SendAncillaryBuffer::default()
|
||||
};
|
||||
|
||||
let sent_len = rustix::net::sendmsg_unix(
|
||||
socket.as_fd(),
|
||||
&socket_addr,
|
||||
&[msg_iov],
|
||||
&mut ancillary,
|
||||
rustix::net::SendFlags::empty(),
|
||||
)
|
||||
.map_err(NotifyError::SendMsg)?;
|
||||
|
||||
if sent_len != msg_len {
|
||||
return Err(NotifyError::PartialSend);
|
||||
}
|
||||
|
||||
Ok(Sent(true))
|
||||
/// A wrapper around the socket specified by `$NOTIFY_SOCKET`
|
||||
#[derive(Debug)]
|
||||
pub struct Notifier {
|
||||
sock_addr: rustix::net::SocketAddrUnix,
|
||||
socket: UnixDatagram,
|
||||
}
|
||||
|
||||
impl Notifier {
|
||||
/// Create a new [`Notifier`].
|
||||
///
|
||||
/// If `unset_env` is set to `true` this will unset `NOTIFY_SOCKET` resulting in further call to this function to return `Ok(None)`.
|
||||
///
|
||||
/// Returns `Ok(None)` if notification isn't supported (`NOTIFY_SOCKET` isn't defined).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function error out if the socket couldn't be opened.
|
||||
pub fn new(unset_env: bool) -> Result<Option<Self>, NewNotifierError> {
|
||||
let env_sock = match env::var_os("NOTIFY_SOCKET") {
|
||||
None => return Ok(None),
|
||||
Some(v) => v,
|
||||
};
|
||||
|
||||
if unset_env {
|
||||
env::remove_var("NOTIFY_SOCKET");
|
||||
}
|
||||
|
||||
// If the first character of `$NOTIFY_SOCKET` is '@', the string
|
||||
// is understood as Linux abstract namespace socket.
|
||||
let socket_addr = match env_sock.as_encoded_bytes().strip_prefix(b"@").map(|v| {
|
||||
// SAFETY:
|
||||
// - Only strip ASCII '@' which is a non-empty UTF-8 substring
|
||||
unsafe { OsString::from_encoded_bytes_unchecked(v.to_vec()) }
|
||||
}) {
|
||||
Some(stripped_addr) => {
|
||||
rustix::net::SocketAddrUnix::new_abstract_name(stripped_addr.as_encoded_bytes())
|
||||
.map_err(NewNotifierError::InvalidAbstractSocket)?
|
||||
}
|
||||
None => rustix::net::SocketAddrUnix::new(env_sock)
|
||||
.map_err(NewNotifierError::InvalidSocketPath)?,
|
||||
};
|
||||
|
||||
let socket = UnixDatagram::unbound().map_err(NewNotifierError::CouldntOpenSocket)?;
|
||||
let ret = Self {
|
||||
sock_addr: socket_addr,
|
||||
socket,
|
||||
};
|
||||
Ok(Some(ret))
|
||||
}
|
||||
|
||||
/// Notify service manager about status change and send file descriptors.
|
||||
///
|
||||
/// Use this together with [`NotifyState::FdStore`]. Otherwise works like [`Notifier::notify`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will error out if the passed [`NotifyState`] do not follow the rules set by systemd or if they couldn't be fully sent.
|
||||
pub fn notify_with_fds(
|
||||
&self,
|
||||
state: &[NotifyState<'_>],
|
||||
fds: &[BorrowedFd],
|
||||
) -> Result<(), NotifyError> {
|
||||
state.iter().try_for_each(NotifyState::sanity_check)?;
|
||||
|
||||
let msg = state
|
||||
.iter()
|
||||
.fold(String::new(), |acc, state| format!("{acc}{state}\n"))
|
||||
.into_bytes();
|
||||
let msg_len = msg.len();
|
||||
let msg_iov = IoSlice::new(&msg);
|
||||
|
||||
let mut ancillary = if !fds.is_empty() {
|
||||
let mut ancillary = rustix::net::SendAncillaryBuffer::default();
|
||||
let tmp = rustix::net::SendAncillaryMessage::ScmRights(fds);
|
||||
if !ancillary.push(tmp) {
|
||||
return Err(NotifyError::PushAncillaryMessage);
|
||||
}
|
||||
ancillary
|
||||
} else {
|
||||
rustix::net::SendAncillaryBuffer::default()
|
||||
};
|
||||
|
||||
let sent_len = rustix::net::sendmsg_unix(
|
||||
self.socket.as_fd(),
|
||||
&self.sock_addr,
|
||||
&[msg_iov],
|
||||
&mut ancillary,
|
||||
rustix::net::SendFlags::empty(),
|
||||
)
|
||||
.map_err(NotifyError::SendMsg)?;
|
||||
|
||||
if sent_len != msg_len {
|
||||
return Err(NotifyError::PartialSend);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Notify service manager about status changes.
|
||||
///
|
||||
/// Send a notification to the manager about service status changes. Also see [`Notifier::notify_with_fds`] to send file descriptors.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will error out if the passed [`NotifyState`] do not follow the rules set by systemd or if they couldn't be fully sent.
|
||||
pub fn notify(&self, state: &[NotifyState<'_>]) -> Result<(), NotifyError> {
|
||||
self.notify_with_fds(state, &[])
|
||||
}
|
||||
}
|
||||
|
||||
/// Status changes, see sd_notify(3).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum NotifyState<'a> {
|
||||
/// D-Bus error-style error code.
|
||||
|
@ -87,13 +145,13 @@ pub enum NotifyState<'a> {
|
|||
Errno(u8),
|
||||
/// A name for the submitted file descriptors.
|
||||
FdName(&'a str),
|
||||
/// Stores additional file descriptors in the service manager. Use [`notify_with_fds`] with this.
|
||||
Fdstore,
|
||||
/// Remove stored file descriptors. Must be used together with [`NotifyState::Fdname`].
|
||||
FdstoreRemove,
|
||||
/// Stores additional file descriptors in the service manager. Use [`Notifier::notify_with_fds`] with this.
|
||||
FdStore,
|
||||
/// Remove stored file descriptors. Must be used together with [`NotifyState::FdName`].
|
||||
FdStoreRemove,
|
||||
/// Tell the service manager to not poll the filedescriptors for errors. This causes
|
||||
/// systemd to hold on to broken file descriptors which must be removed manually.
|
||||
/// Must be used together with [`NotifyState::Fdstore`].
|
||||
/// Must be used together with [`NotifyState::FdStore`].
|
||||
FdpollDisable,
|
||||
/// The main process ID of the service, in case of forking applications.
|
||||
Mainpid(libc::pid_t),
|
||||
|
@ -125,8 +183,8 @@ impl<'a> Display for NotifyState<'a> {
|
|||
NotifyState::BusError(s) => write!(f, "BUSERROR={s}"),
|
||||
NotifyState::Errno(e) => write!(f, "ERRNO={e}"),
|
||||
NotifyState::FdName(name) => write!(f, "FDNAME={name}"),
|
||||
NotifyState::Fdstore => f.write_str("FDSTORE=1"),
|
||||
NotifyState::FdstoreRemove => f.write_str("FDSTOREREMOVE=1"),
|
||||
NotifyState::FdStore => f.write_str("FDSTORE=1"),
|
||||
NotifyState::FdStoreRemove => f.write_str("FDSTOREREMOVE=1"),
|
||||
NotifyState::FdpollDisable => f.write_str("FDPOLL=0"),
|
||||
NotifyState::Mainpid(pid) => write!(f, "MAINPID={pid}"),
|
||||
NotifyState::Other(message) => Display::fmt(message, f),
|
||||
|
|
Loading…
Reference in a new issue