Skip to main content

blit_sd_notify/
lib.rs

1//! `sd_notify(3)` over `libc` for signalling readiness to systemd.
2
3#[cfg(target_os = "linux")]
4pub fn notify_ready(verbose: bool) {
5    notify(b"READY=1\n", verbose);
6}
7
8#[cfg(not(target_os = "linux"))]
9pub fn notify_ready(_verbose: bool) {}
10
11#[cfg(target_os = "linux")]
12fn notify(payload: &[u8], verbose: bool) {
13    let path = match std::env::var_os("NOTIFY_SOCKET") {
14        Some(p) => p,
15        None => return,
16    };
17    let path_bytes = std::os::unix::ffi::OsStrExt::as_bytes(path.as_os_str());
18    if path_bytes.is_empty() {
19        return;
20    }
21
22    let mut addr: libc::sockaddr_un = unsafe { std::mem::zeroed() };
23    if path_bytes.len() > addr.sun_path.len() {
24        if verbose {
25            eprintln!(
26                "sd_notify: NOTIFY_SOCKET path too long ({} bytes)",
27                path_bytes.len()
28            );
29        }
30        return;
31    }
32    addr.sun_family = libc::AF_UNIX as libc::sa_family_t;
33
34    let addr_len = if path_bytes[0] == b'@' {
35        addr.sun_path[0] = 0;
36        for (i, b) in path_bytes[1..].iter().enumerate() {
37            addr.sun_path[i + 1] = *b as libc::c_char;
38        }
39        std::mem::size_of::<libc::sa_family_t>() + path_bytes.len()
40    } else if path_bytes[0] == b'/' {
41        for (i, b) in path_bytes.iter().enumerate() {
42            addr.sun_path[i] = *b as libc::c_char;
43        }
44        std::mem::size_of::<libc::sa_family_t>() + path_bytes.len() + 1
45    } else {
46        if verbose {
47            eprintln!("sd_notify: NOTIFY_SOCKET must start with '/' or '@'");
48        }
49        return;
50    };
51
52    let fd = unsafe { libc::socket(libc::AF_UNIX, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0) };
53    if fd < 0 {
54        if verbose {
55            eprintln!(
56                "sd_notify: socket() failed: {}",
57                std::io::Error::last_os_error()
58            );
59        }
60        return;
61    }
62
63    let sent = unsafe {
64        libc::sendto(
65            fd,
66            payload.as_ptr() as *const libc::c_void,
67            payload.len(),
68            libc::MSG_NOSIGNAL,
69            &addr as *const libc::sockaddr_un as *const libc::sockaddr,
70            addr_len as libc::socklen_t,
71        )
72    };
73    if sent < 0 && verbose {
74        eprintln!(
75            "sd_notify: sendto() failed: {}",
76            std::io::Error::last_os_error()
77        );
78    }
79
80    unsafe { libc::close(fd) };
81}
82
83#[cfg(all(test, target_os = "linux"))]
84mod tests {
85    use super::*;
86    use std::os::unix::net::UnixDatagram;
87
88    #[test]
89    fn noop_when_env_unset() {
90        unsafe { std::env::remove_var("NOTIFY_SOCKET") };
91        notify_ready(true);
92    }
93
94    #[test]
95    fn sends_ready_to_filesystem_socket() {
96        let dir = tempdir();
97        let path = format!("{dir}/notify.sock");
98        let listener = UnixDatagram::bind(&path).expect("bind");
99        listener
100            .set_read_timeout(Some(std::time::Duration::from_secs(2)))
101            .unwrap();
102
103        unsafe { std::env::set_var("NOTIFY_SOCKET", &path) };
104        notify_ready(true);
105        unsafe { std::env::remove_var("NOTIFY_SOCKET") };
106
107        let mut buf = [0u8; 64];
108        let n = listener.recv(&mut buf).expect("recv");
109        assert_eq!(&buf[..n], b"READY=1\n");
110    }
111
112    #[test]
113    fn malformed_address_is_ignored() {
114        unsafe { std::env::set_var("NOTIFY_SOCKET", "not-a-valid-prefix") };
115        notify_ready(true);
116        unsafe { std::env::remove_var("NOTIFY_SOCKET") };
117    }
118
119    fn tempdir() -> String {
120        let base = std::env::temp_dir();
121        let dir = base.join(format!("blit-sd-notify-{}", std::process::id()));
122        let _ = std::fs::remove_dir_all(&dir);
123        std::fs::create_dir_all(&dir).unwrap();
124        dir.to_string_lossy().into_owned()
125    }
126}