use std::net::SocketAddr;
use std::os::fd::{FromRawFd, RawFd};
use std::sync::{Mutex, OnceLock};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Kind {
Tcp,
Udp,
}
#[derive(Debug)]
struct Inherited {
fd: RawFd,
kind: Kind,
addr: SocketAddr,
name: Option<String>,
}
fn inherited() -> &'static Mutex<Vec<Inherited>> {
static SOCKETS: OnceLock<Mutex<Vec<Inherited>>> = OnceLock::new();
SOCKETS.get_or_init(|| {
let found = read_inherited();
if !found.is_empty() {
let list: Vec<String> = found.iter().map(|s| format!("{} {:?}{}", s.addr, s.kind, s.name.as_deref().map(|n| format!(" ({n})")).unwrap_or_default())).collect();
println!("systemd passed {} socket(s): {}", found.len(), list.join(", "));
}
Mutex::new(found)
})
}
fn read_inherited() -> Vec<Inherited> {
let pid_matches = std::env::var("LISTEN_PID").ok().and_then(|p| p.parse::<u32>().ok()) == Some(std::process::id());
let count = std::env::var("LISTEN_FDS").ok().and_then(|n| n.parse::<i32>().ok()).unwrap_or(0);
let names: Vec<String> = std::env::var("LISTEN_FDNAMES").map(|n| n.split(':').map(str::to_string).collect()).unwrap_or_default();
for var in ["LISTEN_PID", "LISTEN_FDS", "LISTEN_FDNAMES"] {
unsafe { std::env::remove_var(var) };
}
if !pid_matches || count <= 0 {
return Vec::new();
}
const FIRST: RawFd = 3; let mut out = Vec::new();
for (i, fd) in (FIRST..FIRST + count.min(64)).enumerate() {
unsafe { libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC) };
let mut kind: libc::c_int = 0;
let mut len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
if unsafe { libc::getsockopt(fd, libc::SOL_SOCKET, libc::SO_TYPE, &mut kind as *mut _ as *mut libc::c_void, &mut len) } != 0 {
continue;
}
let kind = match kind {
libc::SOCK_STREAM => Kind::Tcp,
libc::SOCK_DGRAM => Kind::Udp,
_ => continue,
};
let addr = match kind {
Kind::Tcp => {
let socket = unsafe { std::net::TcpListener::from_raw_fd(fd) };
let addr = socket.local_addr();
std::mem::forget(socket);
addr
}
Kind::Udp => {
let socket = unsafe { std::net::UdpSocket::from_raw_fd(fd) };
let addr = socket.local_addr();
std::mem::forget(socket);
addr
}
};
let Ok(addr) = addr else { continue };
out.push(Inherited { fd, kind, addr, name: names.get(i).cloned().filter(|n| !n.is_empty() && n != "unknown") });
}
out
}
fn find(kind: Kind, addr: SocketAddr, name: Option<&str>) -> Option<RawFd> {
let sockets = inherited().lock().unwrap_or_else(|e| e.into_inner());
let candidates = sockets.iter().filter(|s| s.kind == kind);
let by_name = name.and_then(|n| candidates.clone().find(|s| s.name.as_deref() == Some(n)));
by_name
.or_else(|| candidates.clone().find(|s| s.addr == addr))
.or_else(|| candidates.clone().find(|s| s.addr.port() == addr.port()))
.map(|s| s.fd)
}
pub fn tcp(addr: SocketAddr, name: Option<&str>) -> Option<std::net::TcpListener> {
let fd = find(Kind::Tcp, addr, name)?;
let copy = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 3) };
(copy >= 0).then(|| unsafe { std::net::TcpListener::from_raw_fd(copy) })
}
pub fn udp(addr: SocketAddr, name: Option<&str>) -> Option<std::net::UdpSocket> {
let fd = find(Kind::Udp, addr, name)?;
let copy = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 3) };
(copy >= 0).then(|| unsafe { std::net::UdpSocket::from_raw_fd(copy) })
}
pub fn activated() -> bool {
!inherited().lock().unwrap_or_else(|e| e.into_inner()).is_empty()
}
pub fn notify(message: &str) -> bool {
let Ok(target) = std::env::var("NOTIFY_SOCKET") else { return false };
let Ok(socket) = std::os::unix::net::UnixDatagram::unbound() else { return false };
let sent = match target.strip_prefix('@') {
#[cfg(target_os = "linux")]
Some(name) => {
use std::os::linux::net::SocketAddrExt;
std::os::unix::net::SocketAddr::from_abstract_name(name.as_bytes()).and_then(|addr| socket.send_to_addr(message.as_bytes(), &addr))
}
#[cfg(not(target_os = "linux"))]
Some(_) => return false,
None => socket.send_to(message.as_bytes(), &target),
};
sent.is_ok()
}
pub fn ready(status_line: &str) {
if notify(&format!("READY=1\nSTATUS={status_line}")) {
start_watchdog();
}
}
pub fn reloading() {
let mut now = libc::timespec { tv_sec: 0, tv_nsec: 0 };
unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut now) };
let usec = now.tv_sec as u64 * 1_000_000 + now.tv_nsec as u64 / 1_000;
notify(&format!("RELOADING=1\nMONOTONIC_USEC={usec}"));
}
pub fn reloaded() {
notify("READY=1");
}
pub fn stopping() {
notify("STOPPING=1");
}
pub fn status(line: &str) {
notify(&format!("STATUS={line}"));
}
fn start_watchdog() {
static STARTED: OnceLock<()> = OnceLock::new();
let Some(usec) = std::env::var("WATCHDOG_USEC").ok().and_then(|u| u.parse::<u64>().ok()).filter(|u| *u > 0) else { return };
if std::env::var("WATCHDOG_PID").ok().and_then(|p| p.parse::<u32>().ok()).is_some_and(|p| p != std::process::id()) {
return;
}
if STARTED.set(()).is_err() {
return;
}
let every = std::time::Duration::from_micros(usec / 2);
#[cfg(feature = "http2")]
if let Ok(handle) = tokio::runtime::Handle::try_current() {
handle.spawn(async move {
loop {
notify("WATCHDOG=1");
tokio::time::sleep(every).await;
}
});
return;
}
let _ = std::thread::Builder::new().name("rws-watchdog".into()).spawn(move || loop {
notify("WATCHDOG=1");
std::thread::sleep(every);
});
}
#[cfg(test)]
mod tests;