decdn_node/
systemd_watchdog.rs1use std::ffi::OsString;
15use std::time::Duration;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct Heartbeat {
20 pub socket: OsString,
23 pub interval: Duration,
25}
26
27impl Heartbeat {
28 #[must_use]
34 pub fn from_env_vars(
35 notify_socket: Option<OsString>,
36 watchdog_usec: Option<&str>,
37 watchdog_pid: Option<&str>,
38 own_pid: u32,
39 ) -> Option<Self> {
40 let socket = notify_socket.filter(|s| !s.is_empty())?;
41 let usec: u64 = watchdog_usec?.trim().parse().ok()?;
42 if usec == 0 {
43 return None;
44 }
45 if let Some(pid) = watchdog_pid
46 && pid.trim().parse::<u32>().ok()? != own_pid
47 {
48 return None;
49 }
50 let interval = Duration::from_micros(usec / 2).max(Duration::from_millis(1));
51 Some(Self { socket, interval })
52 }
53
54 #[must_use]
56 pub fn from_env() -> Option<Self> {
57 Self::from_env_vars(
58 std::env::var_os("NOTIFY_SOCKET"),
59 std::env::var("WATCHDOG_USEC").ok().as_deref(),
60 std::env::var("WATCHDOG_PID").ok().as_deref(),
61 std::process::id(),
62 )
63 }
64}
65
66#[cfg(unix)]
69#[derive(Debug)]
70pub struct Notifier {
71 sock: std::os::unix::net::UnixDatagram,
72 addr: std::os::unix::net::SocketAddr,
73}
74
75#[cfg(unix)]
76impl Notifier {
77 pub fn new(socket: &std::ffi::OsStr) -> std::io::Result<Self> {
84 use std::os::unix::ffi::OsStrExt;
85 use std::os::unix::net::{SocketAddr, UnixDatagram};
86
87 let addr = match socket.as_bytes().strip_prefix(b"@") {
88 Some(name) => abstract_addr(name)?,
89 None => SocketAddr::from_pathname(socket)?,
90 };
91 let sock = UnixDatagram::unbound()?;
92 sock.set_nonblocking(true)?;
94 Ok(Self { sock, addr })
95 }
96
97 pub fn notify(&self) -> std::io::Result<()> {
103 self.sock.send_to_addr(b"WATCHDOG=1", &self.addr).map(drop)
104 }
105}
106
107#[cfg(target_os = "linux")]
108fn abstract_addr(name: &[u8]) -> std::io::Result<std::os::unix::net::SocketAddr> {
109 use std::os::linux::net::SocketAddrExt;
110 std::os::unix::net::SocketAddr::from_abstract_name(name)
111}
112
113#[cfg(all(unix, not(target_os = "linux")))]
114fn abstract_addr(_name: &[u8]) -> std::io::Result<std::os::unix::net::SocketAddr> {
115 Err(std::io::Error::new(
116 std::io::ErrorKind::Unsupported,
117 "abstract NOTIFY_SOCKET names exist only on Linux",
118 ))
119}
120
121pub fn spawn() {
126 #[cfg(unix)]
127 if let Some(heartbeat) = Heartbeat::from_env() {
128 tokio::spawn(run(heartbeat));
129 }
130}
131
132#[cfg(unix)]
133async fn run(heartbeat: Heartbeat) {
134 let mut ticker = tokio::time::interval(heartbeat.interval);
135 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
136 let mut notifier = None;
137 let mut warned = false;
138 loop {
139 ticker.tick().await;
140 let sent = match ¬ifier {
142 Some(n) => Notifier::notify(n),
143 None => Notifier::new(&heartbeat.socket).and_then(|n| {
144 let sent = n.notify();
145 notifier = Some(n);
146 sent
147 }),
148 };
149 match sent {
150 Ok(()) => warned = false,
151 Err(error) if !warned => {
152 warned = true;
153 tracing::warn!(%error, "systemd watchdog heartbeat failed");
154 }
155 Err(_) => {}
156 }
157 }
158}
159
160#[cfg(test)]
161mod tests;