Skip to content

Commit 7c6a290

Browse files
authored
Merge pull request #8 from cbranch/cbranch/notify-with-fds2
Support file descriptor store messages
2 parents dc4d4d0 + b80e6eb commit 7c6a290

File tree

2 files changed

+244
-19
lines changed

2 files changed

+244
-19
lines changed

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,9 @@ repository = "https://github.com/lnicola/sd-notify"
1111

1212
[package.metadata.docs.rs]
1313
targets = ["x86_64-unknown-linux-gnu"]
14+
15+
[features]
16+
fdstore = ["dep:sendfd"]
17+
18+
[dependencies]
19+
sendfd = { version = "0.4", optional = true }

src/lib.rs

Lines changed: 238 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ use std::env;
2727
use std::fmt::{self, Display, Formatter, Write};
2828
use std::fs;
2929
use std::io::{self, ErrorKind};
30+
#[cfg(feature = "fdstore")]
31+
use std::os::fd::BorrowedFd;
3032
use std::os::unix::io::RawFd;
3133
use std::os::unix::net::UnixDatagram;
3234
use std::process;
@@ -59,6 +61,15 @@ pub enum NotifyState<'a> {
5961
WatchdogUsec(u32),
6062
/// Tells the service manager to extend the service timeout.
6163
ExtendTimeoutUsec(u32),
64+
/// Tells the service manager to store attached file descriptors.
65+
#[cfg(feature = "fdstore")]
66+
FdStore,
67+
/// Tells the service manager to remove stored file descriptors.
68+
#[cfg(feature = "fdstore")]
69+
FdStoreRemove,
70+
/// Tells the service manager to use this name for the attached file descriptor.
71+
#[cfg(feature = "fdstore")]
72+
FdName(&'a str),
6273
/// Custom state.
6374
Custom(&'a str),
6475
}
@@ -77,6 +88,12 @@ impl Display for NotifyState<'_> {
7788
NotifyState::WatchdogTrigger => write!(f, "WATCHDOG=trigger"),
7889
NotifyState::WatchdogUsec(usec) => write!(f, "WATCHDOG_USEC={}", usec),
7990
NotifyState::ExtendTimeoutUsec(usec) => write!(f, "EXTEND_TIMEOUT_USEC={}", usec),
91+
#[cfg(feature = "fdstore")]
92+
NotifyState::FdStore => write!(f, "FDSTORE=1"),
93+
#[cfg(feature = "fdstore")]
94+
NotifyState::FdStoreRemove => write!(f, "FDSTOREREMOVE=1"),
95+
#[cfg(feature = "fdstore")]
96+
NotifyState::FdName(name) => write!(f, "FDNAME={}", name),
8097
NotifyState::Custom(state) => write!(f, "{}", state),
8198
}
8299
}
@@ -110,9 +127,11 @@ pub fn booted() -> io::Result<bool> {
110127
/// # Limitations
111128
///
112129
/// The implementation of this function is somewhat naive: it doesn't support
113-
/// sending notifications on behalf of other processes, doesn't pass file
114-
/// descriptors, doesn't send credentials, and does not increase the send
115-
/// buffer size. It's still useful, though, in usual situations.
130+
/// sending notifications on behalf of other processes, doesn't send credentials,
131+
/// and does not increase the send buffer size. It's still useful, though, in
132+
/// usual situations.
133+
///
134+
/// If you wish to send file descriptors, use the `notify_with_fds` function.
116135
///
117136
/// # Example
118137
///
@@ -122,27 +141,94 @@ pub fn booted() -> io::Result<bool> {
122141
/// let _ = sd_notify::notify(true, &[NotifyState::Ready]);
123142
/// ```
124143
pub fn notify(unset_env: bool, state: &[NotifyState]) -> io::Result<()> {
125-
let socket_path = match env::var_os("NOTIFY_SOCKET") {
126-
Some(path) => path,
127-
None => return Ok(()),
144+
let mut msg = String::new();
145+
let Some(sock) = connect_notify_socket(unset_env)? else {
146+
return Ok(());
128147
};
129-
if unset_env {
130-
env::remove_var("NOTIFY_SOCKET");
148+
for s in state {
149+
let _ = writeln!(msg, "{}", s);
131150
}
151+
let len = sock.send(msg.as_bytes())?;
152+
if len != msg.len() {
153+
Err(io::Error::new(ErrorKind::WriteZero, "incomplete write"))
154+
} else {
155+
Ok(())
156+
}
157+
}
158+
159+
/// Sends the service manager a list of state changes with file descriptors.
160+
///
161+
/// If the `unset_env` parameter is set, the `NOTIFY_SOCKET` environment variable
162+
/// will be unset before returning. Further calls to `sd_notify` will fail, but
163+
/// child processes will no longer inherit the variable.
164+
///
165+
/// The notification mechanism involves sending a datagram to a Unix domain socket.
166+
/// See [`sd_pid_notify_with_fds(3)`][sd_pid_notify_with_fds] for details.
167+
///
168+
/// [sd_pid_notify_with_fds]: https://www.freedesktop.org/software/systemd/man/sd_notify.html
169+
///
170+
/// # Limitations
171+
///
172+
/// The implementation of this function is somewhat naive: it doesn't support
173+
/// sending notifications on behalf of other processes, doesn't send credentials,
174+
/// and does not increase the send buffer size. It's still useful, though, in
175+
/// usual situations.
176+
///
177+
/// # Example
178+
///
179+
/// ```no_run
180+
/// # use sd_notify::NotifyState;
181+
/// # use std::os::fd::BorrowedFd;
182+
/// #
183+
/// # let fd = unsafe { BorrowedFd::borrow_raw(0) };
184+
/// #
185+
/// let _ = sd_notify::notify_with_fds(false, &[NotifyState::FdStore], &[fd]);
186+
/// ```
187+
#[cfg(feature = "fdstore")]
188+
pub fn notify_with_fds(
189+
unset_env: bool,
190+
state: &[NotifyState],
191+
fds: &[BorrowedFd<'_>],
192+
) -> io::Result<()> {
193+
use sendfd::SendWithFd;
132194

133195
let mut msg = String::new();
134-
let sock = UnixDatagram::unbound()?;
196+
let Some(sock) = connect_notify_socket(unset_env)? else {
197+
return Ok(());
198+
};
135199
for s in state {
136200
let _ = writeln!(msg, "{}", s);
137201
}
138-
let len = sock.send_to(msg.as_bytes(), socket_path)?;
202+
let len = sock.send_with_fd(msg.as_bytes(), borrowed_fd_slice(fds))?;
139203
if len != msg.len() {
140204
Err(io::Error::new(ErrorKind::WriteZero, "incomplete write"))
141205
} else {
142206
Ok(())
143207
}
144208
}
145209

210+
#[cfg(feature = "fdstore")]
211+
fn borrowed_fd_slice<'a>(s: &'a [BorrowedFd<'_>]) -> &'a [RawFd] {
212+
// SAFETY: BorrowedFd is #[repr(transparent)] over RawFd (memory safety)
213+
// and implements AsRawFd (lifetime safety).
214+
// Required only because sendfd does not have i/o safety traits.
215+
unsafe { std::mem::transmute(s) }
216+
}
217+
218+
fn connect_notify_socket(unset_env: bool) -> io::Result<Option<UnixDatagram>> {
219+
let Some(socket_path) = env::var_os("NOTIFY_SOCKET") else {
220+
return Ok(None);
221+
};
222+
223+
if unset_env {
224+
env::remove_var("NOTIFY_SOCKET");
225+
}
226+
227+
let sock = UnixDatagram::unbound()?;
228+
sock.connect(socket_path)?;
229+
Ok(Some(sock))
230+
}
231+
146232
/// Checks for file descriptors passed by the service manager for socket
147233
/// activation.
148234
///
@@ -162,16 +248,18 @@ pub fn notify(unset_env: bool, state: &[NotifyState]) -> io::Result<()> {
162248
/// let socket = sd_notify::listen_fds().map(|mut fds| fds.next().expect("missing fd"));
163249
/// ```
164250
pub fn listen_fds() -> io::Result<impl Iterator<Item = RawFd>> {
165-
struct Guard;
166-
167-
impl Drop for Guard {
168-
fn drop(&mut self) {
169-
env::remove_var("LISTEN_PID");
170-
env::remove_var("LISTEN_FDS");
171-
}
172-
}
251+
listen_fds_internal(true)
252+
}
173253

174-
let _guard = Guard;
254+
fn listen_fds_internal(unset_env: bool) -> io::Result<impl ExactSizeIterator<Item = RawFd>> {
255+
let _guard1 = UnsetEnvGuard {
256+
name: "LISTEN_PID",
257+
unset_env,
258+
};
259+
let _guard2 = UnsetEnvGuard {
260+
name: "LISTEN_FDS",
261+
unset_env,
262+
};
175263

176264
let listen_pid = if let Ok(pid) = env::var("LISTEN_PID") {
177265
pid
@@ -209,6 +297,87 @@ pub fn listen_fds() -> io::Result<impl Iterator<Item = RawFd>> {
209297
Ok(listen_fds)
210298
}
211299

300+
/// Checks for file descriptors passed by the service manager for socket
301+
/// activation.
302+
///
303+
/// The function returns an iterator over file descriptors, starting from
304+
/// `SD_LISTEN_FDS_START`. The number of descriptors is obtained from the
305+
/// `LISTEN_FDS` environment variable.
306+
///
307+
/// If the `unset_env` parameter is set, the `LISTEN_PID`, `LISTEN_FDS` and
308+
/// `LISTEN_FDNAMES` environment variable will be unset before returning.
309+
/// Child processes will not see the fdnames passed to this process. This is
310+
/// usually not necessary, as a process should only use the `LISTEN_FDS`
311+
/// variable if it is the PID given in `LISTEN_PID`.
312+
///
313+
/// Before returning, the file descriptors are set as `O_CLOEXEC`.
314+
///
315+
/// See [`sd_listen_fds_with_names(3)`][sd_listen_fds_with_names] for details.
316+
///
317+
/// [sd_listen_fds_with_names]: https://www.freedesktop.org/software/systemd/man/sd_listen_fds.html
318+
///
319+
/// # Example
320+
///
321+
/// ```no_run
322+
/// let socket = sd_notify::listen_fds().map(|mut fds| fds.next().expect("missing fd"));
323+
/// ```
324+
pub fn listen_fds_with_names(
325+
unset_env: bool,
326+
) -> io::Result<impl ExactSizeIterator<Item = (RawFd, String)>> {
327+
let listen_fds = listen_fds_internal(unset_env)?;
328+
let _guard = UnsetEnvGuard {
329+
name: "LISTEN_FDNAMES",
330+
unset_env,
331+
};
332+
zip_fds_with_names(listen_fds, env::var("LISTEN_FDNAMES").ok())
333+
}
334+
335+
/// Internal helper that is independent of listen_fds function, for testing purposes.
336+
fn zip_fds_with_names(
337+
listen_fds: impl ExactSizeIterator<Item = RawFd>,
338+
listen_fdnames: Option<String>,
339+
) -> io::Result<impl ExactSizeIterator<Item = (RawFd, String)>> {
340+
let listen_fdnames = if let Some(names) = listen_fdnames {
341+
// systemd shouldn't provide an empty fdname element. However if it does, the
342+
// sd_listen_fds_with_names function will return an empty string for that fd,
343+
// as in the following C example:
344+
//
345+
// void main() {
346+
// char **names;
347+
// setenv("LISTEN_FDNAMES", "x::z", 1);
348+
// int n = sd_listen_fds_with_names(0, &names);
349+
// assert(*names[1] == 0);
350+
// }
351+
names.split(':').map(|x| x.to_owned()).collect::<Vec<_>>()
352+
} else {
353+
let mut names = vec![];
354+
names.resize(listen_fds.len(), "unknown".to_string());
355+
names
356+
};
357+
358+
if listen_fdnames.len() == listen_fds.len() {
359+
Ok(listen_fds.zip(listen_fdnames))
360+
} else {
361+
Err(io::Error::new(
362+
ErrorKind::InvalidInput,
363+
"invalid LISTEN_FDNAMES",
364+
))
365+
}
366+
}
367+
368+
struct UnsetEnvGuard {
369+
name: &'static str,
370+
unset_env: bool,
371+
}
372+
373+
impl Drop for UnsetEnvGuard {
374+
fn drop(&mut self) {
375+
if self.unset_env {
376+
env::remove_var(self.name);
377+
}
378+
}
379+
}
380+
212381
fn fd_cloexec(fd: u32) -> io::Result<()> {
213382
let fd = RawFd::try_from(fd).map_err(|_| io::Error::from_raw_os_error(ffi::EBADF))?;
214383
let flags = unsafe { ffi::fcntl(fd, ffi::F_GETFD, 0) };
@@ -281,6 +450,7 @@ mod tests {
281450
use super::NotifyState;
282451
use std::env;
283452
use std::fs;
453+
use std::os::fd::RawFd;
284454
use std::os::unix::net::UnixDatagram;
285455
use std::path::PathBuf;
286456
use std::process;
@@ -353,6 +523,55 @@ mod tests {
353523
assert!(env::var_os("LISTEN_FDS").is_none());
354524
}
355525

526+
#[test]
527+
fn listen_fds_with_names() {
528+
assert_eq!(
529+
super::zip_fds_with_names(3 as RawFd..4 as RawFd, Some("omelette".to_string()))
530+
.unwrap()
531+
.collect::<Vec<_>>(),
532+
vec![(3 as RawFd, "omelette".to_string())]
533+
);
534+
535+
assert_eq!(
536+
super::zip_fds_with_names(
537+
3 as RawFd..5 as RawFd,
538+
Some("omelette:baguette".to_string())
539+
)
540+
.unwrap()
541+
.collect::<Vec<_>>(),
542+
vec![
543+
(3 as RawFd, "omelette".to_string()),
544+
(4 as RawFd, "baguette".to_string())
545+
]
546+
);
547+
548+
// LISTEN_FDNAMES is cleared
549+
assert_eq!(
550+
super::zip_fds_with_names(3 as RawFd..4 as RawFd, None)
551+
.unwrap()
552+
.next(),
553+
Some((3 as RawFd, "unknown".to_string()))
554+
);
555+
556+
// LISTEN_FDNAMES is cleared, every fd should have the name "unknown"
557+
assert_eq!(
558+
super::zip_fds_with_names(3 as RawFd..5 as RawFd, None)
559+
.unwrap()
560+
.collect::<Vec<_>>(),
561+
vec![
562+
(3 as RawFd, "unknown".to_string()),
563+
(4 as RawFd, "unknown".to_string())
564+
],
565+
);
566+
567+
// Raise an error if LISTEN_FDNAMES has a different number of entries as fds
568+
assert!(super::zip_fds_with_names(
569+
3 as RawFd..6 as RawFd,
570+
Some("omelette:baguette".to_string())
571+
)
572+
.is_err());
573+
}
574+
356575
#[test]
357576
fn watchdog_enabled() {
358577
// test original logic: https://github.com/systemd/systemd/blob/f3376ee8fa28aab3f7edfad1ddfbcceca5bc841c/src/libsystemd/sd-daemon/sd-daemon.c#L632

0 commit comments

Comments
 (0)