1#[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}