rust_web_server/systemd/
mod.rs1use std::net::SocketAddr;
16use std::os::fd::{FromRawFd, RawFd};
17use std::sync::{Mutex, OnceLock};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20enum Kind {
21 Tcp,
22 Udp,
23}
24
25#[derive(Debug)]
26struct Inherited {
27 fd: RawFd,
28 kind: Kind,
29 addr: SocketAddr,
30 name: Option<String>,
31}
32
33fn inherited() -> &'static Mutex<Vec<Inherited>> {
36 static SOCKETS: OnceLock<Mutex<Vec<Inherited>>> = OnceLock::new();
37 SOCKETS.get_or_init(|| {
38 let found = read_inherited();
39 if !found.is_empty() {
40 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();
41 println!("systemd passed {} socket(s): {}", found.len(), list.join(", "));
42 }
43 Mutex::new(found)
44 })
45}
46
47fn read_inherited() -> Vec<Inherited> {
48 let pid_matches = std::env::var("LISTEN_PID").ok().and_then(|p| p.parse::<u32>().ok()) == Some(std::process::id());
49 let count = std::env::var("LISTEN_FDS").ok().and_then(|n| n.parse::<i32>().ok()).unwrap_or(0);
50 let names: Vec<String> = std::env::var("LISTEN_FDNAMES").map(|n| n.split(':').map(str::to_string).collect()).unwrap_or_default();
51 for var in ["LISTEN_PID", "LISTEN_FDS", "LISTEN_FDNAMES"] {
52 unsafe { std::env::remove_var(var) };
55 }
56 if !pid_matches || count <= 0 {
57 return Vec::new();
58 }
59 const FIRST: RawFd = 3; let mut out = Vec::new();
61 for (i, fd) in (FIRST..FIRST + count.min(64)).enumerate() {
62 unsafe { libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC) };
64 let mut kind: libc::c_int = 0;
65 let mut len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
66 if unsafe { libc::getsockopt(fd, libc::SOL_SOCKET, libc::SO_TYPE, &mut kind as *mut _ as *mut libc::c_void, &mut len) } != 0 {
68 continue;
69 }
70 let kind = match kind {
71 libc::SOCK_STREAM => Kind::Tcp,
72 libc::SOCK_DGRAM => Kind::Udp,
73 _ => continue,
74 };
75 let addr = match kind {
78 Kind::Tcp => {
79 let socket = unsafe { std::net::TcpListener::from_raw_fd(fd) };
81 let addr = socket.local_addr();
82 std::mem::forget(socket);
83 addr
84 }
85 Kind::Udp => {
86 let socket = unsafe { std::net::UdpSocket::from_raw_fd(fd) };
88 let addr = socket.local_addr();
89 std::mem::forget(socket);
90 addr
91 }
92 };
93 let Ok(addr) = addr else { continue };
94 out.push(Inherited { fd, kind, addr, name: names.get(i).cloned().filter(|n| !n.is_empty() && n != "unknown") });
95 }
96 out
97}
98
99fn find(kind: Kind, addr: SocketAddr, name: Option<&str>) -> Option<RawFd> {
102 let sockets = inherited().lock().unwrap_or_else(|e| e.into_inner());
103 let candidates = sockets.iter().filter(|s| s.kind == kind);
104 let by_name = name.and_then(|n| candidates.clone().find(|s| s.name.as_deref() == Some(n)));
105 by_name
106 .or_else(|| candidates.clone().find(|s| s.addr == addr))
107 .or_else(|| candidates.clone().find(|s| s.addr.port() == addr.port()))
108 .map(|s| s.fd)
109}
110
111pub fn tcp(addr: SocketAddr, name: Option<&str>) -> Option<std::net::TcpListener> {
115 let fd = find(Kind::Tcp, addr, name)?;
116 let copy = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 3) };
118 (copy >= 0).then(|| unsafe { std::net::TcpListener::from_raw_fd(copy) })
119}
120
121pub fn udp(addr: SocketAddr, name: Option<&str>) -> Option<std::net::UdpSocket> {
123 let fd = find(Kind::Udp, addr, name)?;
124 let copy = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 3) };
126 (copy >= 0).then(|| unsafe { std::net::UdpSocket::from_raw_fd(copy) })
127}
128
129pub fn activated() -> bool {
131 !inherited().lock().unwrap_or_else(|e| e.into_inner()).is_empty()
132}
133
134pub fn notify(message: &str) -> bool {
136 let Ok(target) = std::env::var("NOTIFY_SOCKET") else { return false };
137 let Ok(socket) = std::os::unix::net::UnixDatagram::unbound() else { return false };
138 let sent = match target.strip_prefix('@') {
139 #[cfg(target_os = "linux")]
140 Some(name) => {
141 use std::os::linux::net::SocketAddrExt;
142 std::os::unix::net::SocketAddr::from_abstract_name(name.as_bytes()).and_then(|addr| socket.send_to_addr(message.as_bytes(), &addr))
143 }
144 #[cfg(not(target_os = "linux"))]
145 Some(_) => return false,
146 None => socket.send_to(message.as_bytes(), &target),
147 };
148 sent.is_ok()
149}
150
151pub fn ready(status_line: &str) {
153 if notify(&format!("READY=1\nSTATUS={status_line}")) {
154 start_watchdog();
155 }
156}
157
158pub fn reloading() {
160 let mut now = libc::timespec { tv_sec: 0, tv_nsec: 0 };
161 unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut now) };
163 let usec = now.tv_sec as u64 * 1_000_000 + now.tv_nsec as u64 / 1_000;
164 notify(&format!("RELOADING=1\nMONOTONIC_USEC={usec}"));
165}
166
167pub fn reloaded() {
169 notify("READY=1");
170}
171
172pub fn stopping() {
174 notify("STOPPING=1");
175}
176
177pub fn status(line: &str) {
179 notify(&format!("STATUS={line}"));
180}
181
182fn start_watchdog() {
186 static STARTED: OnceLock<()> = OnceLock::new();
187 let Some(usec) = std::env::var("WATCHDOG_USEC").ok().and_then(|u| u.parse::<u64>().ok()).filter(|u| *u > 0) else { return };
188 if std::env::var("WATCHDOG_PID").ok().and_then(|p| p.parse::<u32>().ok()).is_some_and(|p| p != std::process::id()) {
189 return;
190 }
191 if STARTED.set(()).is_err() {
192 return;
193 }
194 let every = std::time::Duration::from_micros(usec / 2);
195 #[cfg(feature = "http2")]
196 if let Ok(handle) = tokio::runtime::Handle::try_current() {
197 handle.spawn(async move {
198 loop {
199 notify("WATCHDOG=1");
200 tokio::time::sleep(every).await;
201 }
202 });
203 return;
204 }
205 let _ = std::thread::Builder::new().name("rws-watchdog".into()).spawn(move || loop {
206 notify("WATCHDOG=1");
207 std::thread::sleep(every);
208 });
209}
210
211#[cfg(test)]
212mod tests;