diff --git a/README.md b/README.md index 4ec74ae..edb5649 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A lightweight crate for sending `systemd` service state notifications. ## Quick start ```rust -let _ = sd_notify::notify(true, &[NotifyState::Ready]); +let _ = sd_notify::notify(&[NotifyState::Ready]); ``` ## Releases diff --git a/src/lib.rs b/src/lib.rs index 38e9698..4815f37 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,3 @@ -#![doc(html_root_url = "https://docs.rs/sd-notify/0.1.0")] #![deny(missing_docs)] //! Lightweight crate for interacting with `systemd`. @@ -19,13 +18,14 @@ //! ```no_run //! # use sd_notify::NotifyState; //! # -//! let _ = sd_notify::notify(true, &[NotifyState::Ready]); +//! let _ = sd_notify::notify(&[NotifyState::Ready]); //! ``` use std::convert::TryFrom; use std::env; use std::fmt::{self, Display, Formatter, Write}; use std::fs; +use std::ffi::OsStr; use std::io::{self, ErrorKind}; use std::mem::MaybeUninit; #[cfg(feature = "fdstore")] @@ -137,13 +137,16 @@ pub fn booted() -> io::Result { Ok(m.is_dir()) } +// Constants for env variable names so that we don't typo them. +const LISTEN_FDNAMES: &str = "LISTEN_FDNAMES"; +const LISTEN_FDS: &str = "LISTEN_FDS"; +const LISTEN_PID: &str = "LISTEN_PID"; +const NOTIFY_SOCKET: &str = "NOTIFY_SOCKET"; +const WATCHDOG_PID: &str = "WATCHDOG_PID"; +const WATCHDOG_USEC: &str = "WATCHDOG_USEC"; + /// Sends the service manager a list of state changes. /// -/// If the `unset_env` parameter is set, the `NOTIFY_SOCKET` environment variable -/// will be unset before returning. Further calls to `sd_notify` will fail, but -/// child processes will no longer inherit the variable. -/// -/// The notification mechanism involves sending a datagram to a Unix domain socket. /// See [`sd_notify(3)`][sd_notify] for details. /// /// [sd_notify]: https://www.freedesktop.org/software/systemd/man/sd_notify.html @@ -162,16 +165,17 @@ pub fn booted() -> io::Result { /// ```no_run /// # use sd_notify::NotifyState; /// # -/// let _ = sd_notify::notify(true, &[NotifyState::Ready]); +/// let _ = sd_notify::notify(&[NotifyState::Ready]); /// ``` -pub fn notify(unset_env: bool, state: &[NotifyState]) -> io::Result<()> { - let mut msg = String::new(); - let Some(sock) = connect_notify_socket(unset_env)? else { +pub fn notify(state: &[NotifyState]) -> io::Result<()> { + let Some(socket_path) = env::var_os(NOTIFY_SOCKET) else { return Ok(()); }; + let mut msg = String::new(); for s in state { - let _ = writeln!(msg, "{}", s); + writeln!(msg, "{}", s).unwrap(); } + let sock = connected_unix_datagram(&socket_path)?; let len = sock.send(msg.as_bytes())?; if len != msg.len() { Err(io::Error::new(ErrorKind::WriteZero, "incomplete write")) @@ -180,11 +184,27 @@ pub fn notify(unset_env: bool, state: &[NotifyState]) -> io::Result<()> { } } -/// Sends the service manager a list of state changes with file descriptors. +/// Sends the service manager a list of state changes. /// -/// If the `unset_env` parameter is set, the `NOTIFY_SOCKET` environment variable -/// will be unset before returning. Further calls to `sd_notify` will fail, but -/// child processes will no longer inherit the variable. +/// This does the same as the [`notify`] and unsets the `NOTIFY_SOCKET` +/// environment variable afterwards, so that child processes no longer inherit +/// the variable. +/// +/// # Safety +/// +/// Since this function calls [`std::env::remove_var`], it has the same safety +/// preconditions. See its safety documentation for more details. It can only +/// be safely called before threads are spawned, in particular before any +/// `tokio` runtime initialization or `#[tokio::main]`. +pub unsafe fn notify_and_unset_env(state: &[NotifyState]) -> io::Result<()> { + let result = notify(state); + unsafe { + env::remove_var(NOTIFY_SOCKET); + } + result +} + +/// Sends the service manager a list of state changes with file descriptors. /// /// The notification mechanism involves sending a datagram to a Unix domain socket. /// See [`sd_pid_notify_with_fds(3)`][sd_pid_notify_with_fds] for details. @@ -206,23 +226,23 @@ pub fn notify(unset_env: bool, state: &[NotifyState]) -> io::Result<()> { /// # /// # let fd = unsafe { BorrowedFd::borrow_raw(0) }; /// # -/// let _ = sd_notify::notify_with_fds(false, &[NotifyState::FdStore], &[fd]); +/// let _ = sd_notify::notify_with_fds(&[NotifyState::FdStore], &[fd]); /// ``` #[cfg(feature = "fdstore")] pub fn notify_with_fds( - unset_env: bool, state: &[NotifyState], fds: &[BorrowedFd<'_>], ) -> io::Result<()> { use sendfd::SendWithFd; - let mut msg = String::new(); - let Some(sock) = connect_notify_socket(unset_env)? else { + let Some(socket_path) = env::var_os(NOTIFY_SOCKET) else { return Ok(()); }; + let mut msg = String::new(); for s in state { - let _ = writeln!(msg, "{}", s); + writeln!(msg, "{}", s).unwrap(); } + let sock = connected_unix_datagram(&socket_path)?; let len = sock.send_with_fd(msg.as_bytes(), borrowed_fd_slice(fds))?; if len != msg.len() { Err(io::Error::new(ErrorKind::WriteZero, "incomplete write")) @@ -231,6 +251,30 @@ pub fn notify_with_fds( } } +/// Sends the service manager a list of state changes with file descriptors. +/// +/// This does the same as the [`notify_with_fds`] and unsets the +/// `NOTIFY_SOCKET` environment variable afterwards, so that child processes no +/// longer inherit the variable. +/// +/// # Safety +/// +/// Since this function calls [`std::env::remove_var`], it has the same safety +/// preconditions. See its safety documentation for more details. It can only +/// be safely called before threads are spawned, in particular before any +/// `tokio` runtime initialization or `#[tokio::main]`. +#[cfg(feature = "fdstore")] +pub unsafe fn notify_with_fds_and_unset_env( + state: &[NotifyState], + fds: &[BorrowedFd<'_>], +) -> io::Result<()> { + let result = notify_with_fds(state, fds); + unsafe { + env::remove_var(NOTIFY_SOCKET); + } + result +} + #[cfg(feature = "fdstore")] fn borrowed_fd_slice<'a>(s: &'a [BorrowedFd<'_>]) -> &'a [RawFd] { // SAFETY: BorrowedFd is #[repr(transparent)] over RawFd (memory safety) @@ -239,18 +283,10 @@ fn borrowed_fd_slice<'a>(s: &'a [BorrowedFd<'_>]) -> &'a [RawFd] { unsafe { std::mem::transmute(s) } } -fn connect_notify_socket(unset_env: bool) -> io::Result> { - let Some(socket_path) = env::var_os("NOTIFY_SOCKET") else { - return Ok(None); - }; - - if unset_env { - env::remove_var("NOTIFY_SOCKET"); - } - - let sock = UnixDatagram::unbound()?; - sock.connect(socket_path)?; - Ok(Some(sock)) +fn connected_unix_datagram(path: &OsStr) -> io::Result { + let result = UnixDatagram::unbound()?; + result.connect(path)?; + Ok(result) } /// Checks for file descriptors passed by the service manager for socket @@ -271,21 +307,8 @@ fn connect_notify_socket(unset_env: bool) -> io::Result> { /// ```no_run /// let socket = sd_notify::listen_fds().map(|mut fds| fds.next().expect("missing fd")); /// ``` -pub fn listen_fds() -> io::Result> { - listen_fds_internal(true) -} - -fn listen_fds_internal(unset_env: bool) -> io::Result> { - let _guard1 = UnsetEnvGuard { - name: "LISTEN_PID", - unset_env, - }; - let _guard2 = UnsetEnvGuard { - name: "LISTEN_FDS", - unset_env, - }; - - let listen_pid = if let Ok(pid) = env::var("LISTEN_PID") { +pub fn listen_fds() -> io::Result> { + let listen_pid = if let Ok(pid) = env::var(LISTEN_PID) { pid } else { return Ok(0..0); @@ -297,7 +320,7 @@ fn listen_fds_internal(unset_env: bool) -> io::Result io::Result io::Result> { + let result = listen_fds(); + unsafe { + env::remove_var(LISTEN_PID); + env::remove_var(LISTEN_FDS); + } + result +} + /// Checks for file descriptors passed by the service manager for socket /// activation. /// @@ -328,12 +373,6 @@ fn listen_fds_internal(unset_env: bool) -> io::Result io::Result io::Result> { - let listen_fds = listen_fds_internal(unset_env)?; - let _guard = UnsetEnvGuard { - name: "LISTEN_FDNAMES", - unset_env, - }; - zip_fds_with_names(listen_fds, env::var("LISTEN_FDNAMES").ok()) +pub fn listen_fds_with_names() -> io::Result> { + let listen_fds = listen_fds()?; + zip_fds_with_names(listen_fds, env::var(LISTEN_FDNAMES).ok()) +} + +/// Checks for file descriptors passed by the service manager for socket +/// activation. +/// +/// This does the same as the [`listen_fds_with_names`] and unsets the +/// `LISTEN_FDNAMES`, `LISTEN_FDS` and `LISTEN_PID` environment variables +/// afterwards, so that child processes no longer inherit the variable. +/// +/// # Safety +/// +/// Since this function calls [`std::env::remove_var`], it has the same safety +/// preconditions. See its safety documentation for more details. It can only +/// be safely called before threads are spawned, in particular before any +/// `tokio` runtime initialization or `#[tokio::main]`. +pub unsafe fn listen_fds_with_names_and_unset_env() -> io::Result> { + let result = listen_fds_with_names(); + unsafe { + env::remove_var(LISTEN_PID); + env::remove_var(LISTEN_FDS); + env::remove_var(LISTEN_FDNAMES); + } + result } /// Internal helper that is independent of listen_fds function, for testing purposes. @@ -389,19 +445,6 @@ fn zip_fds_with_names( } } -struct UnsetEnvGuard { - name: &'static str, - unset_env: bool, -} - -impl Drop for UnsetEnvGuard { - fn drop(&mut self) { - if self.unset_env { - env::remove_var(self.name); - } - } -} - fn fd_cloexec(fd: u32) -> io::Result<()> { let fd = RawFd::try_from(fd).map_err(|_| io::Error::from_raw_os_error(libc::EBADF))?; let flags = unsafe { libc::fcntl(fd, libc::F_GETFD, 0) }; @@ -420,43 +463,23 @@ fn fd_cloexec(fd: u32) -> io::Result<()> { /// Asks the service manager for enabled watchdog. /// -/// If the `unset_env` parameter is set, the `WATCHDOG_USEC` and `WATCHDOG_PID` environment variables -/// will be unset before returning. Further calls to `watchdog_enabled` will fail, but -/// child processes will no longer inherit the variable. -/// /// See [`sd_watchdog_enabled(3)`][sd_watchdog_enabled] for details. /// /// [sd_watchdog_enabled]: https://www.freedesktop.org/software/systemd/man/sd_watchdog_enabled.html /// -/// /// # Example /// /// ```no_run /// # use sd_notify; /// # /// let mut usec = 0; -/// let enabled = sd_notify::watchdog_enabled(true, &mut usec); +/// let enabled = sd_notify::watchdog_enabled(&mut usec); /// ``` -pub fn watchdog_enabled(unset_env: bool, usec: &mut u64) -> bool { - struct Guard { - unset_env: bool, - } - - impl Drop for Guard { - fn drop(&mut self) { - if self.unset_env { - env::remove_var("WATCHDOG_USEC"); - env::remove_var("WATCHDOG_PID"); - } - } - } - - let _guard = Guard { unset_env }; - - let s = env::var("WATCHDOG_USEC") +pub fn watchdog_enabled(usec: &mut u64) -> bool { + let s = env::var(WATCHDOG_USEC) .ok() .and_then(|s| u64::from_str(&s).ok()); - let p = env::var("WATCHDOG_PID") + let p = env::var(WATCHDOG_PID) .ok() .and_then(|s| u32::from_str(&s).ok()); @@ -469,6 +492,27 @@ pub fn watchdog_enabled(unset_env: bool, usec: &mut u64) -> bool { } } +/// Asks the service manager for enabled watchdog. +/// +/// This does the same as the [`watchdog_enabled`] and unsets the +/// `WATCHDOG_PID` and `WATCHDOG_USEC` environment variables afterwards, so +/// that child processes no longer inherit the variable. +/// +/// # Safety +/// +/// Since this function calls [`std::env::remove_var`], it has the same safety +/// preconditions. See its safety documentation for more details. It can only +/// be safely called before threads are spawned, in particular before any +/// `tokio` runtime initialization or `#[tokio::main]`. +pub unsafe fn watchdog_enabled_and_unset_env(usec: &mut u64) -> bool { + let result = watchdog_enabled(usec); + unsafe { + env::remove_var(WATCHDOG_USEC); + env::remove_var(WATCHDOG_PID); + } + result +} + fn monotonic_time_usec() -> io::Result { let mut timespec = MaybeUninit::uninit(); let rv = unsafe { libc::clock_gettime(CLOCK_MONOTONIC, timespec.as_mut_ptr()) }; @@ -486,81 +530,7 @@ fn monotonic_time_usec() -> io::Result { #[cfg(test)] mod tests { - use super::NotifyState; - use std::env; - use std::fs; use std::os::fd::RawFd; - use std::os::unix::net::UnixDatagram; - use std::path::PathBuf; - use std::process; - - struct SocketHelper(PathBuf, UnixDatagram); - - impl SocketHelper { - pub fn recv_string(&self) -> String { - let mut buf = [0; 1024]; - let len = self.1.recv(&mut buf).unwrap(); - String::from_utf8(Vec::from(&buf[0..len])).unwrap() - } - } - - impl Drop for SocketHelper { - fn drop(&mut self) { - let _ = fs::remove_file(&self.0); - } - } - - fn bind_socket() -> SocketHelper { - let path = env::temp_dir().join("sd-notify-test-sock"); - let _ = fs::remove_file(&path); - - env::set_var("NOTIFY_SOCKET", &path); - let sock = UnixDatagram::bind(&path).unwrap(); - SocketHelper(path, sock) - } - - #[test] - fn notify() { - let s = bind_socket(); - - super::notify(false, &[NotifyState::Ready]).unwrap(); - assert_eq!(s.recv_string(), "READY=1\n"); - assert!(env::var_os("NOTIFY_SOCKET").is_some()); - - super::notify( - true, - &[ - NotifyState::Status("Reticulating splines"), - NotifyState::Watchdog, - NotifyState::Custom("X_WORKS=1"), - ], - ) - .unwrap(); - assert_eq!( - s.recv_string(), - "STATUS=Reticulating splines\nWATCHDOG=1\nX_WORKS=1\n" - ); - assert!(env::var_os("NOTIFY_SOCKET").is_none()); - } - - #[test] - fn listen_fds() { - // We are not testing the success case because `fd_cloexec` would fail. - - assert!(super::listen_fds().unwrap().next().is_none()); - - env::set_var("LISTEN_PID", "1"); - env::set_var("LISTEN_FDS", "1"); - assert!(super::listen_fds().unwrap().next().is_none()); - assert!(env::var_os("LISTEN_PID").is_none()); - assert!(env::var_os("LISTEN_FDS").is_none()); - - env::set_var("LISTEN_PID", "no way"); - env::set_var("LISTEN_FDS", "1"); - assert!(super::listen_fds().is_err()); - assert!(env::var_os("LISTEN_PID").is_none()); - assert!(env::var_os("LISTEN_FDS").is_none()); - } #[test] fn listen_fds_with_names() { @@ -610,49 +580,4 @@ mod tests { ) .is_err()); } - - #[test] - fn watchdog_enabled() { - // test original logic: https://github.com/systemd/systemd/blob/f3376ee8fa28aab3f7edfad1ddfbcceca5bc841c/src/libsystemd/sd-daemon/sd-daemon.c#L632 - - // invalid pid and unset env - env::set_var("WATCHDOG_USEC", "5"); - env::set_var("WATCHDOG_PID", "1"); - - let mut usec = 0; - assert!(!super::watchdog_enabled(true, &mut usec)); - assert_eq!(usec, 0); - - assert!(env::var_os("WATCHDOG_USEC").is_none()); - assert!(env::var_os("WATCHDOG_PID").is_none()); - - // invalid usec and no unset env - env::set_var("WATCHDOG_USEC", "invalid-usec"); - env::set_var("WATCHDOG_PID", process::id().to_string()); - - let mut usec = 0; - assert!(!super::watchdog_enabled(true, &mut usec)); - assert_eq!(usec, 0); - - assert!(env::var_os("WATCHDOG_USEC").is_none()); - assert!(env::var_os("WATCHDOG_PID").is_none()); - - // no usec, no pip no unset env - let mut usec = 0; - assert!(!super::watchdog_enabled(false, &mut usec)); - assert_eq!(usec, 0); - - assert!(env::var_os("WATCHDOG_USEC").is_none()); - assert!(env::var_os("WATCHDOG_PID").is_none()); - - // valid pip - env::set_var("WATCHDOG_USEC", "5"); - env::set_var("WATCHDOG_PID", process::id().to_string()); - - let mut usec = 0; - assert!(super::watchdog_enabled(false, &mut usec)); - assert_eq!(usec, 5); - assert!(env::var_os("WATCHDOG_USEC").is_some()); - assert!(env::var_os("WATCHDOG_PID").is_some()); - } } diff --git a/tests/listen_fds.rs b/tests/listen_fds.rs new file mode 100644 index 0000000..dcca717 --- /dev/null +++ b/tests/listen_fds.rs @@ -0,0 +1,27 @@ +// There must only be one test in this file because of the thread-unsafety of +// environment variable acceses. + +use std::env; + +#[test] +fn listen_fds() { + // We are not testing the success case because `fd_cloexec` would fail. + + unsafe { + assert!(sd_notify::listen_fds_and_unset_env().unwrap().next().is_none()); + + env::set_var("LISTEN_PID", "1"); + env::set_var("LISTEN_FDS", "1"); + assert!(sd_notify::listen_fds_and_unset_env().unwrap().next().is_none()); + } + assert!(env::var_os("LISTEN_PID").is_none()); + assert!(env::var_os("LISTEN_FDS").is_none()); + + unsafe { + env::set_var("LISTEN_PID", "no way"); + env::set_var("LISTEN_FDS", "1"); + assert!(sd_notify::listen_fds_and_unset_env().is_err()); + } + assert!(env::var_os("LISTEN_PID").is_none()); + assert!(env::var_os("LISTEN_FDS").is_none()); +} diff --git a/tests/notify.rs b/tests/notify.rs new file mode 100644 index 0000000..563d4f6 --- /dev/null +++ b/tests/notify.rs @@ -0,0 +1,58 @@ +// There must only be one test in this file because of the thread-unsafety of +// environment variable acceses. + +use sd_notify::NotifyState; +use std::env; +use std::fs; +use std::os::unix::net::UnixDatagram; +use std::path::PathBuf; + +struct SocketHelper(PathBuf, UnixDatagram); + +impl SocketHelper { + pub fn recv_string(&self) -> String { + let mut buf = [0; 1024]; + let len = self.1.recv(&mut buf).unwrap(); + String::from_utf8(Vec::from(&buf[0..len])).unwrap() + } +} + +impl Drop for SocketHelper { + fn drop(&mut self) { + let _ = fs::remove_file(&self.0); + } +} + +unsafe fn bind_socket() -> SocketHelper { + let path = env::temp_dir().join("sd-notify-test-sock"); + let _ = fs::remove_file(&path); + + env::set_var("NOTIFY_SOCKET", &path); + let sock = UnixDatagram::bind(&path).unwrap(); + SocketHelper(path, sock) +} + +#[test] +fn notify() { + let s = unsafe { bind_socket() }; + + sd_notify::notify(&[NotifyState::Ready]).unwrap(); + assert_eq!(s.recv_string(), "READY=1\n"); + assert!(env::var_os("NOTIFY_SOCKET").is_some()); + + unsafe { + sd_notify::notify_and_unset_env( + &[ + NotifyState::Status("Reticulating splines"), + NotifyState::Watchdog, + NotifyState::Custom("X_WORKS=1"), + ], + ) + .unwrap(); + } + assert_eq!( + s.recv_string(), + "STATUS=Reticulating splines\nWATCHDOG=1\nX_WORKS=1\n" + ); + assert!(env::var_os("NOTIFY_SOCKET").is_none()); +} diff --git a/tests/watchdog_enabled.rs b/tests/watchdog_enabled.rs new file mode 100644 index 0000000..02bcfcf --- /dev/null +++ b/tests/watchdog_enabled.rs @@ -0,0 +1,60 @@ +// There must only be one test in this file because of the thread-unsafety of +// environment variable acceses. + +use std::env; +use std::process; + +#[test] +fn watchdog_enabled() { + // test original logic: https://github.com/systemd/systemd/blob/f3376ee8fa28aab3f7edfad1ddfbcceca5bc841c/src/libsystemd/sd-daemon/sd-daemon.c#L632 + + // invalid pid and unset env + unsafe { + env::set_var("WATCHDOG_USEC", "5"); + env::set_var("WATCHDOG_PID", "1"); + } + + let mut usec = 0; + unsafe { + assert!(!sd_notify::watchdog_enabled_and_unset_env(&mut usec)); + } + assert_eq!(usec, 0); + + assert!(env::var_os("WATCHDOG_USEC").is_none()); + assert!(env::var_os("WATCHDOG_PID").is_none()); + + // invalid usec and no unset env + unsafe { + env::set_var("WATCHDOG_USEC", "invalid-usec"); + env::set_var("WATCHDOG_PID", process::id().to_string()); + } + + let mut usec = 0; + unsafe { + assert!(!sd_notify::watchdog_enabled_and_unset_env(&mut usec)); + } + assert_eq!(usec, 0); + + assert!(env::var_os("WATCHDOG_USEC").is_none()); + assert!(env::var_os("WATCHDOG_PID").is_none()); + + // no usec, no pip no unset env + let mut usec = 0; + assert!(!sd_notify::watchdog_enabled(&mut usec)); + assert_eq!(usec, 0); + + assert!(env::var_os("WATCHDOG_USEC").is_none()); + assert!(env::var_os("WATCHDOG_PID").is_none()); + + // valid pip + unsafe { + env::set_var("WATCHDOG_USEC", "5"); + env::set_var("WATCHDOG_PID", process::id().to_string()); + } + + let mut usec = 0; + assert!(sd_notify::watchdog_enabled(&mut usec)); + assert_eq!(usec, 5); + assert!(env::var_os("WATCHDOG_USEC").is_some()); + assert!(env::var_os("WATCHDOG_PID").is_some()); +}