1use std::io;
43use std::os::fd::AsRawFd;
44use std::os::fd::FromRawFd;
45use std::os::fd::OwnedFd;
46use std::sync::Arc;
47use std::sync::Condvar;
48use std::sync::Mutex;
49use std::thread::JoinHandle;
50use std::time::Duration;
51use std::time::Instant;
52
53use tokio::io::Interest;
54use tokio::io::unix::AsyncFd;
55
56#[derive(Debug)]
58pub enum DrainError {
59 TimedOut(Vec<i32>),
61 Watcher(io::Error),
63}
64
65struct Entry {
66 pidfd: Arc<OwnedFd>,
68 raw_pid: i32,
69 identity: Option<u64>,
72}
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub enum Watched {
77 New,
79 Joined,
82 AlreadyReported,
85}
86
87const PIDFS_MAGIC: i64 = 0x5049_4446;
89
90pub fn birth_identity(pidfd: &OwnedFd) -> Option<u64> {
97 let mut filesystem: libc::statfs = unsafe { std::mem::zeroed() };
98 if unsafe { libc::fstatfs(pidfd.as_raw_fd(), &mut filesystem) } != 0 {
99 return None;
100 }
101 #[allow(clippy::unnecessary_cast)]
103 if filesystem.f_type as i64 != PIDFS_MAGIC {
104 return None;
105 }
106 let mut metadata: libc::stat = unsafe { std::mem::zeroed() };
107 (unsafe { libc::fstat(pidfd.as_raw_fd(), &mut metadata) } == 0).then_some(metadata.st_ino)
108}
109
110fn await_publication(entry: &Entry) -> io::Result<()> {
113 let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
114 loop {
115 let rc = unsafe {
116 libc::waitid(
117 libc::P_PIDFD,
118 entry.pidfd.as_raw_fd() as libc::id_t,
119 &mut info,
120 libc::WEXITED | libc::WNOHANG | libc::WNOWAIT,
121 )
122 };
123 if rc == 0 {
124 return Ok(());
125 }
126 let error = io::Error::last_os_error();
127 match error.raw_os_error() {
128 Some(libc::ECHILD) => return Ok(()),
130 Some(libc::EINTR) => continue,
131 _ => {
132 return Err(io::Error::new(
133 error.kind(),
134 format!(
135 "waitid on the pidfd of exited process {}: {error}",
136 entry.raw_pid
137 ),
138 ));
139 }
140 }
141 }
142}
143
144#[derive(Default)]
145struct State {
146 live: Vec<Entry>,
148 delivering: Vec<i32>,
150 shutdown: bool,
152 failure: Option<io::Error>,
154 killing: bool,
157 exited: std::collections::HashSet<u64>,
160}
161
162impl State {
163 fn drain_outcome(&self) -> Option<Result<(), DrainError>> {
165 if let Some(failure) = &self.failure {
166 return Some(Err(DrainError::Watcher(io::Error::new(
167 failure.kind(),
168 failure.to_string(),
169 ))));
170 }
171 (self.live.is_empty() && self.delivering.is_empty()).then_some(Ok(()))
172 }
173
174 fn pending(&self) -> Vec<i32> {
175 let mut pending: Vec<i32> = self.live.iter().map(|entry| entry.raw_pid).collect();
176 pending.extend(&self.delivering);
177 pending
178 }
179
180 fn fail(&mut self, error: io::Error) {
181 if self.failure.is_none() {
182 self.failure = Some(error);
183 }
184 }
185}
186
187struct Shared {
188 state: Mutex<State>,
189 changed: Condvar,
191 changed_async: tokio::sync::Notify,
193 wake: OwnedFd,
195}
196
197impl Shared {
198 fn notify_changed(&self) {
199 self.changed.notify_all();
200 self.changed_async.notify_waiters();
201 }
202
203 fn record_failure(&self, error: io::Error) {
204 self.state.lock().unwrap().fail(error);
205 self.notify_changed();
206 }
207
208 fn delivered(&self, raw_pid: i32) {
210 let mut state = self.state.lock().unwrap();
211 if let Some(index) = state.delivering.iter().position(|&pid| pid == raw_pid) {
212 state.delivering.remove(index);
213 }
214 drop(state);
215 self.notify_changed();
216 }
217
218 fn wake_thread(&self) {
219 let one: u64 = 1;
220 let _ = unsafe {
223 libc::write(
224 self.wake.as_raw_fd(),
225 &one as *const u64 as *const libc::c_void,
226 std::mem::size_of::<u64>(),
227 )
228 };
229 }
230}
231
232type ExitCallback = Arc<dyn Fn(i32) + Send + Sync>;
233
234enum Driver {
236 Thread(Option<JoinHandle<()>>),
238 Runtime {
240 handle: tokio::runtime::Handle,
241 on_exit: ExitCallback,
242 tasks: Mutex<Vec<tokio::task::JoinHandle<()>>>,
243 },
244}
245
246pub struct PhysicalExitWatch {
248 shared: Arc<Shared>,
249 driver: Driver,
250}
251
252impl PhysicalExitWatch {
253 fn shared() -> io::Result<Arc<Shared>> {
254 let wake = unsafe { libc::eventfd(0, libc::EFD_CLOEXEC | libc::EFD_NONBLOCK) };
255 if wake < 0 {
256 return Err(io::Error::last_os_error());
257 }
258 Ok(Arc::new(Shared {
259 state: Mutex::new(State::default()),
260 changed: Condvar::new(),
261 changed_async: tokio::sync::Notify::new(),
262 wake: unsafe { OwnedFd::from_raw_fd(wake) },
263 }))
264 }
265
266 pub fn on_runtime(
273 handle: tokio::runtime::Handle,
274 on_exit: impl Fn(i32) + Send + Sync + 'static,
275 ) -> io::Result<Self> {
276 Ok(Self {
277 shared: Self::shared()?,
278 driver: Driver::Runtime {
279 handle,
280 on_exit: Arc::new(on_exit),
281 tasks: Mutex::new(Vec::new()),
282 },
283 })
284 }
285
286 pub fn new(on_exit: impl Fn(i32) + Send + 'static) -> io::Result<Self> {
289 let shared = Self::shared()?;
290 let thread_shared = Arc::clone(&shared);
291 let thread = std::thread::Builder::new()
292 .name("physical-exit-watch".to_string())
293 .spawn(move || {
294 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
298 watch_loop(&thread_shared, &on_exit)
299 }));
300 let failure = match outcome {
301 Ok(Ok(())) => None,
302 Ok(Err(error)) => Some(error),
303 Err(_) => Some(io::Error::other("the physical-exit callback panicked")),
304 };
305 if let Some(error) = failure {
306 thread_shared.record_failure(error);
307 }
308 })?;
309 Ok(Self {
310 shared,
311 driver: Driver::Thread(Some(thread)),
312 })
313 }
314
315 pub fn watch(&self, pidfd: OwnedFd, raw_pid: i32) -> io::Result<Watched> {
327 let identity = birth_identity(&pidfd);
328 let mut state = self.shared.state.lock().unwrap();
329 if let Some(failure) = &state.failure {
330 return Err(io::Error::new(failure.kind(), failure.to_string()));
331 }
332 if let Some(identity) = identity {
333 if state.exited.contains(&identity) {
334 return Ok(Watched::AlreadyReported);
335 }
336 if state
337 .live
338 .iter()
339 .any(|entry| entry.identity == Some(identity))
340 {
341 return Ok(Watched::Joined);
342 }
343 }
344 let pidfd = Arc::new(pidfd);
345 state.live.push(Entry {
346 pidfd: Arc::clone(&pidfd),
347 raw_pid,
348 identity,
349 });
350 let killing = state.killing;
351 drop(state);
352 if killing {
353 if let Err(error) = send_sigkill(&pidfd, raw_pid) {
355 self.shared.record_failure(error);
356 }
357 }
358 match &self.driver {
359 Driver::Thread(_) => self.shared.wake_thread(),
360 Driver::Runtime {
361 handle,
362 on_exit,
363 tasks,
364 } => {
365 let shared = Arc::clone(&self.shared);
366 let on_exit = Arc::clone(on_exit);
367 let task = handle.spawn(async move {
368 if let Err(error) = watch_one(&shared, pidfd, raw_pid, &on_exit).await {
369 shared.record_failure(error);
370 }
371 });
372 let mut tasks = tasks.lock().unwrap();
373 tasks.retain(|task| !task.is_finished());
374 tasks.push(task);
375 }
376 }
377 Ok(Watched::New)
378 }
379
380 pub fn failure(&self) -> Option<io::Error> {
382 let state = self.shared.state.lock().unwrap();
383 state
384 .failure
385 .as_ref()
386 .map(|failure| io::Error::new(failure.kind(), failure.to_string()))
387 }
388
389 pub fn fail_and_kill_all(&self) -> io::Result<()> {
397 let mut state = self.shared.state.lock().unwrap();
398 state.killing = true;
399 let mut first_error = None;
400 for entry in &state.live {
401 if let Err(error) = send_sigkill(&entry.pidfd, entry.raw_pid) {
402 first_error.get_or_insert(error);
403 }
404 }
405 first_error.map_or(Ok(()), Err)
406 }
407
408 pub fn drain(&self, timeout: Duration) -> Result<(), DrainError> {
412 let deadline = Instant::now().checked_add(timeout);
414 let mut state = self.shared.state.lock().unwrap();
415 loop {
416 if let Some(outcome) = state.drain_outcome() {
417 return outcome;
418 }
419 let Some(deadline) = deadline else {
420 state = self.shared.changed.wait(state).unwrap();
421 continue;
422 };
423 let now = Instant::now();
424 if now >= deadline {
425 return Err(DrainError::TimedOut(state.pending()));
426 }
427 state = self
428 .shared
429 .changed
430 .wait_timeout(state, deadline - now)
431 .unwrap()
432 .0;
433 }
434 }
435
436 pub async fn drain_async(&self, timeout: Duration) -> Result<(), DrainError> {
440 let deadline = tokio::time::Instant::now().checked_add(timeout);
441 loop {
442 let changed = self.shared.changed_async.notified();
445 tokio::pin!(changed);
446 changed.as_mut().enable();
447 {
448 let state = self.shared.state.lock().unwrap();
449 if let Some(outcome) = state.drain_outcome() {
450 return outcome;
451 }
452 if deadline.is_some_and(|deadline| tokio::time::Instant::now() >= deadline) {
453 return Err(DrainError::TimedOut(state.pending()));
454 }
455 }
456 match deadline {
457 Some(deadline) => {
458 let _ = tokio::time::timeout_at(deadline, changed).await;
459 }
460 None => changed.await,
461 }
462 }
463 }
464}
465
466impl Drop for PhysicalExitWatch {
467 fn drop(&mut self) {
468 self.shared.state.lock().unwrap().shutdown = true;
469 match &mut self.driver {
470 Driver::Thread(thread) => {
471 self.shared.wake_thread();
472 if let Some(thread) = thread.take() {
473 let _ = thread.join();
474 }
475 }
476 Driver::Runtime { tasks, .. } => {
477 for task in tasks.get_mut().unwrap().drain(..) {
478 task.abort();
479 }
480 }
481 }
482 }
483}
484
485fn send_sigkill(pidfd: &OwnedFd, raw_pid: i32) -> io::Result<()> {
489 let rc = unsafe {
490 libc::syscall(
491 libc::SYS_pidfd_send_signal,
492 pidfd.as_raw_fd(),
493 libc::SIGKILL,
494 std::ptr::null::<libc::siginfo_t>(),
495 0,
496 )
497 };
498 if rc < 0 {
499 let error = io::Error::last_os_error();
500 if error.raw_os_error() != Some(libc::ESRCH) {
501 return Err(io::Error::new(
502 error.kind(),
503 format!("SIGKILL to watched process {raw_pid}: {error}"),
504 ));
505 }
506 }
507 Ok(())
508}
509
510async fn watch_one(
511 shared: &Shared,
512 pidfd: Arc<OwnedFd>,
513 raw_pid: i32,
514 on_exit: &ExitCallback,
515) -> io::Result<()> {
516 {
520 let registration = AsyncFd::with_interest(Arc::clone(&pidfd), Interest::READABLE)?;
521 let _ready = registration.readable().await?;
522 }
523 let entry = {
524 let mut state = shared.state.lock().unwrap();
525 let Some(index) = state
526 .live
527 .iter()
528 .position(|entry| Arc::ptr_eq(&entry.pidfd, &pidfd))
529 else {
530 return Ok(());
531 };
532 let entry = state.live.remove(index);
533 state.exited.extend(entry.identity);
534 state.delivering.push(raw_pid);
535 entry
536 };
537 await_publication(&entry)?;
538 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| on_exit(raw_pid)))
540 .map_err(|_| io::Error::other("the physical-exit callback panicked"))?;
541 shared.delivered(raw_pid);
542 Ok(())
543}
544
545fn watch_loop(shared: &Shared, on_exit: &impl Fn(i32)) -> io::Result<()> {
546 loop {
547 let mut fds = vec![libc::pollfd {
550 fd: shared.wake.as_raw_fd(),
551 events: libc::POLLIN,
552 revents: 0,
553 }];
554 {
555 let state = shared.state.lock().unwrap();
556 if state.shutdown {
557 return Ok(());
558 }
559 fds.extend(state.live.iter().map(|entry| libc::pollfd {
560 fd: entry.pidfd.as_raw_fd(),
561 events: libc::POLLIN,
562 revents: 0,
563 }));
564 }
565 let ready = unsafe { libc::poll(fds.as_mut_ptr(), fds.len() as libc::nfds_t, -1) };
566 if ready < 0 {
567 let error = io::Error::last_os_error();
568 if error.kind() == io::ErrorKind::Interrupted {
569 continue;
570 }
571 return Err(error);
572 }
573 if fds[0].revents != 0 {
574 let mut counter: u64 = 0;
575 let _ = unsafe {
576 libc::read(
577 shared.wake.as_raw_fd(),
578 &mut counter as *mut u64 as *mut libc::c_void,
579 std::mem::size_of::<u64>(),
580 )
581 };
582 }
583 let exited: Vec<libc::c_int> = fds[1..]
584 .iter()
585 .filter(|fd| fd.revents != 0)
586 .map(|fd| fd.fd)
587 .collect();
588 if exited.is_empty() {
589 continue;
590 }
591 let reported: Vec<Entry> = {
594 let mut state = shared.state.lock().unwrap();
595 let (reported, live): (Vec<Entry>, Vec<Entry>) = std::mem::take(&mut state.live)
596 .into_iter()
597 .partition(|entry| exited.contains(&entry.pidfd.as_raw_fd()));
598 state.live = live;
599 state
600 .exited
601 .extend(reported.iter().filter_map(|entry| entry.identity));
602 state
603 .delivering
604 .extend(reported.iter().map(|entry| entry.raw_pid));
605 reported
606 };
607 for entry in reported {
611 await_publication(&entry)?;
612 on_exit(entry.raw_pid);
613 shared.delivered(entry.raw_pid);
614 }
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use std::os::unix::process::ExitStatusExt;
621 use std::process::Child;
622 use std::process::Command;
623 use std::sync::mpsc;
624
625 use super::*;
626
627 fn pidfd_open(pid: u32) -> OwnedFd {
628 let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid as libc::pid_t, 0) };
629 assert!(fd >= 0, "pidfd_open: {}", io::Error::last_os_error());
630 unsafe { OwnedFd::from_raw_fd(fd as i32) }
631 }
632
633 struct Reaped(Child);
636
637 impl Reaped {
638 fn spawn(command: &mut Command) -> Self {
639 Self(command.spawn().unwrap())
640 }
641 fn pid(&self) -> u32 {
642 self.0.id()
643 }
644 }
645
646 impl Drop for Reaped {
647 fn drop(&mut self) {
648 let _ = self.0.kill();
649 let _ = self.0.wait();
650 }
651 }
652
653 struct ForkedChild(Option<libc::pid_t>);
655
656 impl ForkedChild {
657 fn reap(&mut self) -> libc::c_int {
658 let pid = self.0.take().unwrap();
659 let mut status = 0;
660 assert_eq!(unsafe { libc::waitpid(pid, &mut status, 0) }, pid);
661 status
662 }
663 }
664
665 impl Drop for ForkedChild {
666 fn drop(&mut self) {
667 if let Some(pid) = self.0 {
668 let mut status = 0;
669 unsafe {
670 libc::kill(pid, libc::SIGKILL);
671 libc::waitpid(pid, &mut status, 0);
672 }
673 }
674 }
675 }
676
677 #[test]
678 fn exiting_child_is_reported_once_with_its_pid() {
679 let (sender, receiver) = mpsc::channel();
680 let watch = PhysicalExitWatch::new(move |pid| sender.send(pid).unwrap()).unwrap();
681 let mut child = Reaped::spawn(Command::new("sh").args(["-c", "exit 3"]));
682 let pid = child.pid();
683 watch.watch(pidfd_open(pid), pid as i32).unwrap();
684 assert_eq!(
685 receiver.recv_timeout(Duration::from_secs(30)).unwrap(),
686 pid as i32
687 );
688 watch.drain(Duration::from_secs(30)).unwrap();
689 assert_eq!(child.0.wait().unwrap().code(), Some(3));
691 drop(watch);
692 assert!(
693 receiver.try_recv().is_err(),
694 "the exit was reported more than once"
695 );
696 }
697
698 fn pidfs_by_metadata() -> bool {
702 let pidfd = pidfd_open(std::process::id());
703 let mut filesystem: libc::statfs = unsafe { std::mem::zeroed() };
704 assert_eq!(
705 unsafe { libc::fstatfs(pidfd.as_raw_fd(), &mut filesystem) },
706 0
707 );
708 #[allow(clippy::unnecessary_cast)]
709 let magic = filesystem.f_type as i64;
710 magic == 0x5049_4446
711 }
712
713 #[test]
717 fn a_birth_identity_names_one_process() {
718 let pidfs = pidfs_by_metadata();
719 let first = Reaped::spawn(Command::new("sleep").arg("60"));
720 let second = Reaped::spawn(Command::new("sleep").arg("60"));
721 let (a, b) = (pidfd_open(first.pid()), pidfd_open(first.pid()));
722 let other = pidfd_open(second.pid());
723 let anonymous = unsafe { libc::eventfd(0, libc::EFD_CLOEXEC) };
724 assert!(anonymous >= 0);
725 let anonymous = unsafe { OwnedFd::from_raw_fd(anonymous) };
726 assert_eq!(birth_identity(&anonymous), None);
727 if !pidfs {
728 assert_eq!(birth_identity(&a), None);
729 assert_eq!(birth_identity(&other), None);
730 return;
731 }
732 let mut metadata: libc::stat = unsafe { std::mem::zeroed() };
733 assert_eq!(unsafe { libc::fstat(a.as_raw_fd(), &mut metadata) }, 0);
734 let identity = metadata.st_ino;
735 assert_eq!(birth_identity(&a), Some(identity));
736 assert_eq!(birth_identity(&b), Some(identity));
737 assert_ne!(birth_identity(&other), Some(identity));
738 assert!(birth_identity(&other).is_some());
739 }
740
741 #[test]
744 fn a_second_registration_of_a_process_joins_the_first() {
745 let (sender, receiver) = mpsc::channel();
746 let watch = PhysicalExitWatch::new(move |pid| sender.send(pid).unwrap()).unwrap();
747 let mut child = Reaped::spawn(Command::new("sleep").arg("60"));
748 let pid = child.pid();
749 let keyed = pidfs_by_metadata();
750 assert_eq!(
751 watch.watch(pidfd_open(pid), pid as i32).unwrap(),
752 Watched::New
753 );
754 let second = watch.watch(pidfd_open(pid), pid as i32).unwrap();
755 assert_eq!(second, if keyed { Watched::Joined } else { Watched::New });
756 child.0.kill().unwrap();
757 watch.drain(Duration::from_secs(30)).unwrap();
758 let reports = receiver.try_iter().collect::<Vec<i32>>();
759 let expected = if keyed {
760 vec![pid as i32]
761 } else {
762 vec![pid as i32; 2]
763 };
764 assert_eq!(reports, expected);
765 }
766
767 #[test]
770 fn a_registration_after_the_exit_was_observed_is_not_reported_again() {
771 let (sender, receiver) = mpsc::channel();
772 let watch = PhysicalExitWatch::new(move |pid| sender.send(pid).unwrap()).unwrap();
773 let mut child = Reaped::spawn(Command::new("sh").args(["-c", "exit 0"]));
774 let pid = child.pid();
775 let late = pidfd_open(pid);
776 if !pidfs_by_metadata() {
777 return;
778 }
779 assert_eq!(
780 watch.watch(pidfd_open(pid), pid as i32).unwrap(),
781 Watched::New
782 );
783 assert_eq!(
784 receiver.recv_timeout(Duration::from_secs(30)).unwrap(),
785 pid as i32
786 );
787 watch.drain(Duration::from_secs(30)).unwrap();
788 child.0.wait().unwrap();
789 assert_eq!(
790 watch.watch(late, pid as i32).unwrap(),
791 Watched::AlreadyReported
792 );
793 watch.drain(Duration::from_secs(30)).unwrap();
794 drop(watch);
795 assert!(receiver.try_recv().is_err(), "the exit was reported again");
796 }
797
798 #[test]
799 fn drain_honours_its_timeout() {
800 let watch = PhysicalExitWatch::new(|_| {}).unwrap();
801 let mut child = Reaped::spawn(Command::new("sleep").arg("60"));
802 let pid = child.pid();
803 watch.watch(pidfd_open(pid), pid as i32).unwrap();
804 let start = Instant::now();
805 match watch.drain(Duration::from_millis(200)) {
806 Err(DrainError::TimedOut(live)) => assert_eq!(live, [pid as i32]),
807 other => panic!("expected a timeout, got {other:?}"),
808 }
809 let waited = start.elapsed();
810 assert!(
811 waited >= Duration::from_millis(200),
812 "returned early: {waited:?}"
813 );
814 assert!(waited < Duration::from_secs(20), "overran: {waited:?}");
815 child.0.kill().unwrap();
816 assert_eq!(child.0.wait().unwrap().signal(), Some(libc::SIGKILL));
817 }
818
819 #[test]
820 fn drain_without_processes_accepts_an_unbounded_timeout() {
821 let watch = PhysicalExitWatch::new(|_| {}).unwrap();
822 watch.drain(Duration::MAX).unwrap();
823 }
824
825 #[test]
826 fn drain_waits_for_the_callback_to_return() {
827 let (started_sender, started) = mpsc::channel();
830 let (release, release_receiver) = mpsc::channel::<()>();
831 let watch = PhysicalExitWatch::new(move |pid| {
832 started_sender.send(pid).unwrap();
833 let _ = release_receiver.recv();
834 })
835 .unwrap();
836 let release = release;
839 let mut child = Reaped::spawn(&mut Command::new("true"));
840 let pid = child.pid();
841 watch.watch(pidfd_open(pid), pid as i32).unwrap();
842 assert_eq!(
843 started.recv_timeout(Duration::from_secs(30)).unwrap(),
844 pid as i32
845 );
846 match watch.drain(Duration::from_millis(200)) {
847 Err(DrainError::TimedOut(pending)) => assert_eq!(pending, [pid as i32]),
848 other => panic!("drain returned before the callback did: {other:?}"),
849 }
850 release.send(()).unwrap();
851 watch.drain(Duration::from_secs(30)).unwrap();
852 child.0.wait().unwrap();
853 }
854
855 #[test]
856 fn a_panicking_callback_fails_the_watcher() {
857 let watch = PhysicalExitWatch::new(|_| panic!("callback failure under test")).unwrap();
858 let mut child = Reaped::spawn(&mut Command::new("true"));
859 let pid = child.pid();
860 watch.watch(pidfd_open(pid), pid as i32).unwrap();
861 match watch.drain(Duration::from_secs(30)) {
862 Err(DrainError::Watcher(_)) => {}
863 other => panic!("expected the watcher's failure, got {other:?}"),
864 }
865 child.0.wait().unwrap();
866 let other = Reaped::spawn(Command::new("sleep").arg("60"));
867 assert!(
868 watch
869 .watch(pidfd_open(other.pid()), other.pid() as i32)
870 .is_err(),
871 "a failed watcher accepted a new process"
872 );
873 }
874
875 #[test]
876 fn kill_all_ends_every_watched_process() {
877 let (sender, receiver) = mpsc::channel();
878 let watch = PhysicalExitWatch::new(move |pid| sender.send(pid).unwrap()).unwrap();
879 let mut children: Vec<Reaped> = (0..3)
880 .map(|_| Reaped::spawn(Command::new("sleep").arg("60")))
881 .collect();
882 let stopped = children[0].pid();
884 assert_eq!(
885 unsafe { libc::kill(stopped as libc::pid_t, libc::SIGSTOP) },
886 0
887 );
888 let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
889 let rc = unsafe {
890 libc::waitid(
891 libc::P_PID,
892 stopped,
893 &mut info,
894 libc::WSTOPPED | libc::WNOWAIT,
895 )
896 };
897 assert_eq!(rc, 0, "waitid: {}", io::Error::last_os_error());
898 assert_eq!(info.si_code, libc::CLD_STOPPED);
899 for child in &children {
900 watch
901 .watch(pidfd_open(child.pid()), child.pid() as i32)
902 .unwrap();
903 }
904 watch.fail_and_kill_all().unwrap();
905 watch.drain(Duration::from_secs(30)).unwrap();
906 let mut reported: Vec<i32> = receiver.try_iter().collect();
907 reported.sort_unstable();
908 let mut expected: Vec<i32> = children.iter().map(|child| child.pid() as i32).collect();
909 expected.sort_unstable();
910 assert_eq!(reported, expected);
911 for child in &mut children {
912 assert_eq!(child.0.wait().unwrap().signal(), Some(libc::SIGKILL));
913 }
914 }
915
916 #[test]
917 fn a_process_watched_after_kill_all_is_killed_and_reported() {
918 let (sender, receiver) = mpsc::channel();
919 let watch = PhysicalExitWatch::new(move |pid| sender.send(pid).unwrap()).unwrap();
920 watch.fail_and_kill_all().unwrap();
921 let mut late = Reaped::spawn(Command::new("sleep").arg("60"));
923 watch
924 .watch(pidfd_open(late.pid()), late.pid() as i32)
925 .unwrap();
926 watch.drain(Duration::from_secs(30)).unwrap();
927 assert_eq!(
928 receiver.try_iter().collect::<Vec<i32>>(),
929 [late.pid() as i32]
930 );
931 assert_eq!(late.0.wait().unwrap().signal(), Some(libc::SIGKILL));
932 }
933
934 #[test]
935 fn already_exited_process_is_reported_at_once() {
936 let (sender, receiver) = mpsc::channel();
937 let watch = PhysicalExitWatch::new(move |pid| sender.send(pid).unwrap()).unwrap();
938 let mut child = Reaped::spawn(&mut Command::new("true"));
939 let pid = child.pid();
940 let pidfd = pidfd_open(pid);
941 let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
943 let rc =
944 unsafe { libc::waitid(libc::P_PID, pid, &mut info, libc::WEXITED | libc::WNOWAIT) };
945 assert_eq!(rc, 0, "waitid: {}", io::Error::last_os_error());
946 watch.watch(pidfd, pid as i32).unwrap();
947 assert_eq!(
948 receiver.recv_timeout(Duration::from_secs(30)).unwrap(),
949 pid as i32
950 );
951 child.0.wait().unwrap();
952 }
953
954 fn thread_count() -> usize {
955 std::fs::read_dir("/proc/self/task").unwrap().count()
956 }
957
958 #[test]
959 fn runtime_mode_reports_each_exit_once_and_creates_no_thread() {
960 if std::env::var_os(INNER_RUN).is_none() {
963 return run_in_fresh_process(
964 "physical_exit_watch::tests::runtime_mode_reports_each_exit_once_and_creates_no_thread",
965 false,
966 );
967 }
968 let runtime = tokio::runtime::Builder::new_current_thread()
969 .enable_all()
970 .build()
971 .unwrap();
972 let threads = thread_count();
973 runtime.block_on(async {
974 let (sender, receiver) = mpsc::channel();
975 let watch =
976 PhysicalExitWatch::on_runtime(tokio::runtime::Handle::current(), move |pid| {
977 sender.send(pid).unwrap()
978 })
979 .unwrap();
980 let mut children: Vec<Reaped> = (0..3)
981 .map(|code| Reaped::spawn(Command::new("sh").args(["-c", &format!("exit {code}")])))
982 .collect();
983 for child in &children {
984 watch
985 .watch(pidfd_open(child.pid()), child.pid() as i32)
986 .unwrap();
987 }
988 watch.drain_async(Duration::from_secs(30)).await.unwrap();
989 assert_eq!(
990 thread_count(),
991 threads,
992 "the runtime-mode watcher started a thread"
993 );
994 let mut reported: Vec<i32> = receiver.try_iter().collect();
995 reported.sort_unstable();
996 let mut expected: Vec<i32> = children.iter().map(|child| child.pid() as i32).collect();
997 expected.sort_unstable();
998 assert_eq!(reported, expected);
999 for (code, child) in children.iter_mut().enumerate() {
1001 assert_eq!(child.0.wait().unwrap().code(), Some(code as i32));
1002 }
1003 drop(watch);
1004 assert!(
1005 receiver.try_recv().is_err(),
1006 "an exit was reported more than once"
1007 );
1008 });
1009 }
1010
1011 #[tokio::test]
1012 async fn runtime_mode_drain_honours_its_timeout() {
1013 let (sender, receiver) = mpsc::channel();
1014 let watch = PhysicalExitWatch::on_runtime(tokio::runtime::Handle::current(), move |pid| {
1015 sender.send(pid).unwrap()
1016 })
1017 .unwrap();
1018 let mut sleeper = Reaped::spawn(Command::new("sleep").arg("60"));
1019 let pid = sleeper.pid();
1020 watch.watch(pidfd_open(pid), pid as i32).unwrap();
1021 let start = Instant::now();
1022 match watch.drain_async(Duration::from_millis(200)).await {
1023 Err(DrainError::TimedOut(live)) => assert_eq!(live, [pid as i32]),
1024 other => panic!("expected a timeout, got {other:?}"),
1025 }
1026 let waited = start.elapsed();
1027 assert!(
1028 waited >= Duration::from_millis(200),
1029 "returned early: {waited:?}"
1030 );
1031 assert!(waited < Duration::from_secs(20), "overran: {waited:?}");
1032 assert!(receiver.try_recv().is_err(), "reported a live process");
1033 sleeper.0.kill().unwrap();
1034 watch.drain_async(Duration::from_secs(30)).await.unwrap();
1035 assert_eq!(receiver.try_recv().unwrap(), pid as i32);
1036 assert_eq!(sleeper.0.wait().unwrap().signal(), Some(libc::SIGKILL));
1037 }
1038
1039 #[tokio::test]
1040 async fn runtime_mode_reports_an_already_exited_process() {
1041 let (sender, receiver) = mpsc::channel();
1042 let watch = PhysicalExitWatch::on_runtime(tokio::runtime::Handle::current(), move |pid| {
1043 sender.send(pid).unwrap()
1044 })
1045 .unwrap();
1046 let mut child = Reaped::spawn(&mut Command::new("true"));
1047 let pid = child.pid();
1048 let pidfd = pidfd_open(pid);
1049 let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
1051 let rc =
1052 unsafe { libc::waitid(libc::P_PID, pid, &mut info, libc::WEXITED | libc::WNOWAIT) };
1053 assert_eq!(rc, 0, "waitid: {}", io::Error::last_os_error());
1054 watch.watch(pidfd, pid as i32).unwrap();
1055 watch.drain_async(Duration::from_secs(30)).await.unwrap();
1056 assert_eq!(receiver.try_recv().unwrap(), pid as i32);
1057 child.0.wait().unwrap();
1058 }
1059
1060 #[tokio::test]
1061 async fn runtime_mode_panicking_callback_fails_the_watcher() {
1062 let watch = PhysicalExitWatch::on_runtime(tokio::runtime::Handle::current(), |_| {
1063 panic!("callback failure under test")
1064 })
1065 .unwrap();
1066 let mut child = Reaped::spawn(&mut Command::new("true"));
1067 let pid = child.pid();
1068 watch.watch(pidfd_open(pid), pid as i32).unwrap();
1069 match watch.drain_async(Duration::from_secs(30)).await {
1070 Err(DrainError::Watcher(_)) => {}
1071 other => panic!("expected the watcher's failure, got {other:?}"),
1072 }
1073 child.0.wait().unwrap();
1074 let other = Reaped::spawn(Command::new("sleep").arg("60"));
1075 assert!(
1076 watch
1077 .watch(pidfd_open(other.pid()), other.pid() as i32)
1078 .is_err(),
1079 "a failed watcher accepted a new process"
1080 );
1081 }
1082
1083 const INNER_RUN: &str = "HERMIT_PHYSICAL_EXIT_WATCH_INNER_RUN";
1085
1086 fn run_in_fresh_process(name: &str, block_sigchld: bool) {
1094 use std::os::unix::process::CommandExt;
1095 let mut command = Command::new(std::env::current_exe().unwrap());
1096 command
1097 .args(["--exact", name, "--nocapture", "--test-threads=1"])
1098 .env(INNER_RUN, "1");
1099 if block_sigchld {
1100 unsafe {
1101 command.pre_exec(|| {
1102 let set = sigchld_set();
1103 if libc::sigprocmask(libc::SIG_BLOCK, &set, std::ptr::null_mut()) != 0 {
1104 return Err(io::Error::last_os_error());
1105 }
1106 Ok(())
1107 });
1108 }
1109 }
1110 let output = command.output().unwrap();
1111 let stdout = String::from_utf8_lossy(&output.stdout);
1112 let stderr = String::from_utf8_lossy(&output.stderr);
1113 assert!(
1114 output.status.success(),
1115 "inner run failed:\n{stdout}\n{stderr}"
1116 );
1117 assert!(
1118 stdout.contains("1 passed"),
1119 "inner run did not execute the test:\n{stdout}\n{stderr}"
1120 );
1121 }
1122
1123 fn sigchld_set() -> libc::sigset_t {
1124 let mut set: libc::sigset_t = unsafe { std::mem::zeroed() };
1125 unsafe {
1126 libc::sigemptyset(&mut set);
1127 libc::sigaddset(&mut set, libc::SIGCHLD);
1128 }
1129 set
1130 }
1131
1132 #[test]
1133 fn sigchld_is_queued_before_the_exit_is_reported() {
1134 if std::env::var_os(INNER_RUN).is_some() {
1143 return sigchld_inner();
1144 }
1145 run_in_fresh_process(
1146 "physical_exit_watch::tests::sigchld_is_queued_before_the_exit_is_reported",
1147 true,
1148 );
1149 }
1150
1151 fn sigchld_inner() {
1152 let chld = sigchld_set();
1153 let mut mask: libc::sigset_t = unsafe { std::mem::zeroed() };
1154 unsafe { libc::pthread_sigmask(libc::SIG_BLOCK, std::ptr::null(), &mut mask) };
1155 assert_eq!(
1156 unsafe { libc::sigismember(&mask, libc::SIGCHLD) },
1157 1,
1158 "the inner process must start with SIGCHLD blocked"
1159 );
1160 let (sender, receiver) = mpsc::channel();
1161 let watch = PhysicalExitWatch::new(move |pid| {
1162 let mut pending: libc::sigset_t = unsafe { std::mem::zeroed() };
1163 unsafe { libc::sigpending(&mut pending) };
1164 let queued = unsafe { libc::sigismember(&pending, libc::SIGCHLD) } == 1;
1165 sender.send((pid, queued)).unwrap();
1166 })
1167 .unwrap();
1168 let zero = libc::timespec {
1169 tv_sec: 0,
1170 tv_nsec: 0,
1171 };
1172 for _ in 0..50 {
1173 let mut child = Reaped::spawn(&mut Command::new("true"));
1174 let pid = child.pid();
1175 watch.watch(pidfd_open(pid), pid as i32).unwrap();
1176 let (reported, queued) = receiver.recv_timeout(Duration::from_secs(30)).unwrap();
1177 assert_eq!(reported, pid as i32);
1178 assert!(queued, "exit of {pid} reported before SIGCHLD was queued");
1179 child.0.wait().unwrap();
1180 let signal = unsafe { libc::sigtimedwait(&chld, std::ptr::null_mut(), &zero) };
1182 assert_eq!(signal, libc::SIGCHLD);
1183 }
1184 }
1185
1186 #[test]
1187 fn descriptors_are_closed_before_the_pidfd_is_readable() {
1188 if std::env::var_os(INNER_RUN).is_none() {
1192 return run_in_fresh_process(
1193 "physical_exit_watch::tests::descriptors_are_closed_before_the_pidfd_is_readable",
1194 false,
1195 );
1196 }
1197 let mut pipe = [0; 2];
1198 assert_eq!(
1199 unsafe { libc::pipe2(pipe.as_mut_ptr(), libc::O_CLOEXEC) },
1200 0
1201 );
1202 let read_end = unsafe { OwnedFd::from_raw_fd(pipe[0]) };
1203 let write_end = unsafe { OwnedFd::from_raw_fd(pipe[1]) };
1204 let pid = unsafe { libc::fork() };
1205 assert!(pid >= 0);
1206 if pid == 0 {
1207 unsafe {
1209 libc::close(read_end.as_raw_fd());
1210 libc::_exit(0)
1211 };
1212 }
1213 let mut child = ForkedChild(Some(pid));
1214 drop(write_end);
1215 let (sender, receiver) = mpsc::channel();
1216 let watch = PhysicalExitWatch::new(move |pid| sender.send(pid).unwrap()).unwrap();
1217 watch.watch(pidfd_open(pid as u32), pid).unwrap();
1218 assert_eq!(receiver.recv_timeout(Duration::from_secs(30)).unwrap(), pid);
1219 let mut byte = 0u8;
1220 let flags = unsafe { libc::fcntl(read_end.as_raw_fd(), libc::F_GETFL) };
1221 unsafe {
1222 libc::fcntl(
1223 read_end.as_raw_fd(),
1224 libc::F_SETFL,
1225 flags | libc::O_NONBLOCK,
1226 )
1227 };
1228 let n = unsafe {
1229 libc::read(
1230 read_end.as_raw_fd(),
1231 &mut byte as *mut u8 as *mut libc::c_void,
1232 1,
1233 )
1234 };
1235 assert_eq!(
1236 n,
1237 0,
1238 "expected EOF, got {n} ({})",
1239 io::Error::last_os_error()
1240 );
1241 child.reap();
1242 }
1243
1244 #[test]
1250 fn a_child_of_a_parent_ignoring_sigchld_is_reported_once() {
1251 if std::env::var_os(INNER_RUN).is_none() {
1252 return run_in_fresh_process(
1253 "physical_exit_watch::tests::a_child_of_a_parent_ignoring_sigchld_is_reported_once",
1254 false,
1255 );
1256 }
1257 auto_reaped_inner(libc::SIG_IGN, 0);
1258 }
1259
1260 #[test]
1261 fn a_child_of_a_parent_with_sa_nocldwait_is_reported_once() {
1262 if std::env::var_os(INNER_RUN).is_none() {
1263 return run_in_fresh_process(
1264 "physical_exit_watch::tests::a_child_of_a_parent_with_sa_nocldwait_is_reported_once",
1265 false,
1266 );
1267 }
1268 auto_reaped_inner(libc::SIG_DFL, libc::SA_NOCLDWAIT);
1269 }
1270
1271 fn auto_reaped_inner(handler: libc::sighandler_t, flags: libc::c_int) {
1272 let mut action: libc::sigaction = unsafe { std::mem::zeroed() };
1273 action.sa_sigaction = handler;
1274 action.sa_flags = flags;
1275 assert_eq!(
1276 unsafe { libc::sigaction(libc::SIGCHLD, &action, std::ptr::null_mut()) },
1277 0
1278 );
1279 let (sender, receiver) = mpsc::channel();
1280 let watch = PhysicalExitWatch::new(move |pid| sender.send(pid).unwrap()).unwrap();
1281 for _ in 0..20 {
1282 let mut pipe = [0; 2];
1285 assert_eq!(
1286 unsafe { libc::pipe2(pipe.as_mut_ptr(), libc::O_CLOEXEC) },
1287 0
1288 );
1289 let pid = unsafe { libc::fork() };
1290 assert!(pid >= 0);
1291 if pid == 0 {
1292 let mut byte = 0u8;
1293 unsafe {
1294 libc::close(pipe[1]);
1295 libc::read(pipe[0], (&raw mut byte).cast(), 1);
1296 libc::_exit(0)
1297 };
1298 }
1299 unsafe { libc::close(pipe[0]) };
1300 watch.watch(pidfd_open(pid as u32), pid).unwrap();
1301 unsafe { libc::close(pipe[1]) };
1302 assert_eq!(receiver.recv_timeout(Duration::from_secs(30)).unwrap(), pid);
1303 let mut status = 0;
1305 assert_eq!(unsafe { libc::waitpid(pid, &mut status, 0) }, -1);
1306 assert_eq!(
1307 io::Error::last_os_error().raw_os_error(),
1308 Some(libc::ECHILD)
1309 );
1310 }
1311 watch.drain(Duration::from_secs(30)).unwrap();
1312 drop(watch);
1313 assert!(
1314 receiver.try_recv().is_err(),
1315 "an exit was reported more than once"
1316 );
1317 }
1318}