use std::ffi::OsStr;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::os::unix::ffi::OsStrExt;
use std::time::Duration;
use anyhow::{Context, Result, bail};
#[derive(Debug)]
pub(crate) struct SdNotify {
fd: OwnedFd,
addr: libc::sockaddr_un,
addr_len: libc::socklen_t,
watchdog: Option<Duration>,
}
impl SdNotify {
pub(crate) fn from_env() -> Result<Option<Self>> {
let Some(socket) = std::env::var_os("NOTIFY_SOCKET") else {
return Ok(None);
};
if socket.is_empty() {
return Ok(None);
}
let mut notify = Self::connect(&socket)?;
notify.watchdog = watchdog_ping_interval_from_env();
Ok(Some(notify))
}
fn connect(address: &OsStr) -> Result<Self> {
let bytes = address.as_bytes();
let mut addr: libc::sockaddr_un = unsafe { std::mem::zeroed() };
addr.sun_family = libc::AF_UNIX as libc::sa_family_t;
let offset = std::mem::offset_of!(libc::sockaddr_un, sun_path);
let capacity = std::mem::size_of_val(&addr.sun_path);
let (name, abstract_namespace) = match bytes.split_first() {
Some((b'@', rest)) => (rest, true),
_ => (bytes, false),
};
let needed = name.len() + 1;
if needed > capacity {
bail!(
"NOTIFY_SOCKET address is {needed} bytes but the platform supports at most {capacity}"
);
}
let start = usize::from(abstract_namespace);
for (slot, byte) in addr.sun_path[start..start + name.len()]
.iter_mut()
.zip(name)
{
*slot = *byte as libc::c_char;
}
let addr_len = (offset + needed) as libc::socklen_t;
let raw = unsafe { libc::socket(libc::AF_UNIX, libc::SOCK_DGRAM, 0) };
if raw < 0 {
return Err(std::io::Error::last_os_error()).context("create sd_notify socket");
}
let fd = unsafe { OwnedFd::from_raw_fd(raw) };
set_cloexec(&fd).context("set FD_CLOEXEC on the sd_notify socket")?;
Ok(Self {
fd,
addr,
addr_len,
watchdog: None,
})
}
pub(crate) fn watchdog_interval(&self) -> Option<Duration> {
self.watchdog
}
pub(crate) fn notify_ready(&self) -> Result<()> {
self.send(b"READY=1\n")
}
pub(crate) fn notify_watchdog(&self) -> Result<()> {
self.send(b"WATCHDOG=1\n")
}
fn send(&self, message: &[u8]) -> Result<()> {
let sent = unsafe {
libc::sendto(
self.fd.as_raw_fd(),
message.as_ptr().cast::<libc::c_void>(),
message.len(),
0,
std::ptr::addr_of!(self.addr).cast::<libc::sockaddr>(),
self.addr_len,
)
};
if sent < 0 {
return Err(std::io::Error::last_os_error()).context("send sd_notify datagram");
}
Ok(())
}
}
fn set_cloexec(fd: &OwnedFd) -> Result<()> {
let raw = fd.as_raw_fd();
let flags = unsafe { libc::fcntl(raw, libc::F_GETFD) };
if flags < 0 {
return Err(std::io::Error::last_os_error()).context("read fd flags");
}
if flags & libc::FD_CLOEXEC == libc::FD_CLOEXEC {
return Ok(());
}
if unsafe { libc::fcntl(raw, libc::F_SETFD, flags | libc::FD_CLOEXEC) } < 0 {
return Err(std::io::Error::last_os_error()).context("set fd flags");
}
Ok(())
}
fn watchdog_ping_interval_from_env() -> Option<Duration> {
if let Some(pid) = std::env::var("WATCHDOG_PID")
.ok()
.and_then(|value| value.parse::<i32>().ok())
&& pid != std::process::id() as i32
{
return None;
}
let usec = std::env::var("WATCHDOG_USEC")
.ok()
.and_then(|value| value.parse::<u64>().ok())?;
watchdog_ping_interval_from_usec(usec)
}
fn watchdog_ping_interval_from_usec(usec: u64) -> Option<Duration> {
(usec > 0).then(|| Duration::from_micros(usec / 2))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn watchdog_interval_is_half_the_timeout_and_zero_disables_it() {
assert_eq!(
watchdog_ping_interval_from_usec(10_000_000),
Some(Duration::from_micros(5_000_000))
);
assert_eq!(
watchdog_ping_interval_from_usec(3),
Some(Duration::from_micros(1))
);
assert_eq!(watchdog_ping_interval_from_usec(0), None);
}
}