use super::*;
use std::io::{BufRead, BufReader, Write};
use std::os::fd::IntoRawFd;
use std::process::{Child, Command, Stdio};
use std::sync::atomic::AtomicI32;
static MARKER_FD: AtomicI32 = AtomicI32::new(-1);
#[test]
fn reaped_root_cleanup_cannot_signal_an_unrelated_reused_pid() {
let mut unrelated = start_process_probe("immune", true, None);
let registration =
register_active_process_cleanup(Some(unrelated.id()), &new_process_cleanup_token(), None)
.terminate_on_drop();
registration.update_root_pid(None);
drop(registration);
assert!(
unrelated.try_wait().unwrap().is_none(),
"an obsolete root must not signal the current process at that PID"
);
unrelated.kill().unwrap();
unrelated.wait().unwrap();
let mut owned = start_process_probe("immune", true, None);
let registration =
register_active_process_cleanup(Some(owned.id()), &new_process_cleanup_token(), None)
.terminate_on_drop();
drop(registration);
assert!(
!owned.wait().unwrap().success(),
"a known live root must actually be terminated by the same guard"
);
}
extern "C" fn handle_term(_: libc::c_int) {
if !record_term_receipt(&MARKER_FD) {
return;
}
unsafe {
libc::_exit(0);
}
}
fn record_term_receipt(marker_fd: &AtomicI32) -> bool {
let fd = marker_fd.swap(-1, Ordering::Relaxed);
if fd < 0 {
return false;
}
unsafe {
libc::write(fd, b"TERM".as_ptr().cast(), 4);
}
true
}
#[test]
fn term_receipt_has_one_writer_under_concurrent_delivery() {
use std::os::fd::AsRawFd;
use std::sync::Barrier;
let marker = tempfile::NamedTempFile::new().expect("term receipt");
let fd = Arc::new(AtomicI32::new(marker.as_raw_fd()));
let ready = Arc::new(Barrier::new(8));
let writers = (0..8)
.map(|_| {
let fd = fd.clone();
let ready = ready.clone();
crate::runtime_stack::spawn(move || {
ready.wait();
record_term_receipt(&fd)
})
})
.collect::<Vec<_>>();
let owners = writers
.into_iter()
.map(|writer| writer.join().expect("receipt writer"))
.filter(|owned| *owned)
.count();
assert_eq!(owners, 1, "only the receipt owner may exit the child");
assert_eq!(std::fs::read(marker.path()).unwrap(), b"TERM");
}
#[test]
fn process_probe_child() {
let Ok(mode) = std::env::var("HARN_TEST_INTERRUPT_PROBE_MODE") else {
return;
};
let mut leaf = None;
match mode.as_str() {
"immune" => unsafe {
assert_ne!(libc::signal(libc::SIGTERM, libc::SIG_IGN), libc::SIG_ERR);
},
"polite" => {
let marker = std::fs::File::create(
std::env::var_os("HARN_TEST_INTERRUPT_PROBE_MARKER").unwrap(),
)
.expect("open SIGTERM marker");
MARKER_FD.store(marker.into_raw_fd(), Ordering::Relaxed);
unsafe {
assert_ne!(
libc::signal(
libc::SIGTERM,
handle_term as *const () as libc::sighandler_t
),
libc::SIG_ERR
);
}
}
"tree" => {
leaf = Some(start_process_probe("leaf", false, None));
}
"leaf" => {}
other => panic!("unknown process probe mode: {other}"),
}
println!("\nprobe-ready");
std::io::stdout().flush().expect("flush probe readiness");
let _leaf = leaf;
loop {
std::thread::park();
}
}
pub(super) struct ProbeChild {
child: Child,
group: bool,
}
impl std::ops::Deref for ProbeChild {
type Target = Child;
fn deref(&self) -> &Child {
&self.child
}
}
impl std::ops::DerefMut for ProbeChild {
fn deref_mut(&mut self) -> &mut Child {
&mut self.child
}
}
impl Drop for ProbeChild {
fn drop(&mut self) {
if matches!(self.child.try_wait(), Ok(Some(_))) {
return;
}
if self.group {
terminate_child_group(&mut self.child);
} else {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
}
pub(super) fn start_process_probe(
mode: &str,
group: bool,
marker: Option<&std::path::Path>,
) -> ProbeChild {
let mut command = Command::new(std::env::current_exe().expect("test binary"));
command
.args([
"--exact",
"op_interrupt::probe_tests::process_probe_child",
"--nocapture",
])
.env("HARN_TEST_INTERRUPT_PROBE_MODE", mode)
.env_remove("HARN_TEST_INTERRUPT_PROBE_MARKER")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null());
if let Some(marker) = marker {
command.env("HARN_TEST_INTERRUPT_PROBE_MARKER", marker);
}
if group {
configure_kill_group(&mut command);
}
let mut child = ProbeChild {
child: command.spawn().expect("spawn process probe"),
group,
};
let stdout = child.stdout.take().expect("probe stdout");
let (tx, rx) = std::sync::mpsc::channel();
crate::runtime_stack::spawn(move || {
let ready = BufReader::new(stdout)
.lines()
.any(|line| line.is_ok_and(|line| line == "probe-ready"));
let _ = tx.send(ready);
});
assert!(
harn_clock::test_support::recv_within("process probe readiness", &rx),
"process probe exited before readiness"
);
child
}