use boxlite_shared::errors::{BoxliteError, BoxliteResult};
use std::os::fd::{FromRawFd, OwnedFd, RawFd};
pub const PIPE_FD: i32 = 3;
pub struct Keepalive {
_pipe_write: OwnedFd,
}
pub struct ChildSetup {
pipe_read: RawFd,
}
impl ChildSetup {
pub fn raw_fd(&self) -> RawFd {
self.pipe_read
}
}
impl Drop for ChildSetup {
fn drop(&mut self) {
unsafe {
libc::close(self.pipe_read);
}
}
}
pub fn create() -> BoxliteResult<(Keepalive, ChildSetup)> {
let fds = create_pipe_cloexec()?;
Ok((
Keepalive {
_pipe_write: unsafe { OwnedFd::from_raw_fd(fds[1]) },
},
ChildSetup { pipe_read: fds[0] },
))
}
fn create_pipe_cloexec() -> BoxliteResult<[i32; 2]> {
let mut fds = [0i32; 2];
#[cfg(target_os = "linux")]
{
if unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC) } != 0 {
return Err(BoxliteError::Engine(format!(
"Failed to create watchdog pipe: {}",
std::io::Error::last_os_error()
)));
}
}
#[cfg(not(target_os = "linux"))]
{
if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
return Err(BoxliteError::Engine(format!(
"Failed to create watchdog pipe: {}",
std::io::Error::last_os_error()
)));
}
for &fd in &fds {
if unsafe { libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC) } < 0 {
let err = std::io::Error::last_os_error();
unsafe {
libc::close(fds[0]);
libc::close(fds[1]);
}
return Err(BoxliteError::Engine(format!(
"Failed to set CLOEXEC on watchdog pipe: {err}"
)));
}
}
}
Ok(fds)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pipe_has_cloexec_set() {
let fds = create_pipe_cloexec().expect("pipe creation should succeed");
for &fd in &fds {
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
assert!(flags >= 0, "fcntl F_GETFD should succeed");
assert_ne!(
flags & libc::FD_CLOEXEC,
0,
"fd {fd} must have FD_CLOEXEC set"
);
}
unsafe {
libc::close(fds[0]);
libc::close(fds[1]);
}
}
#[test]
fn test_create_returns_valid_fds() {
let (keepalive, child_setup) = create().expect("pipe creation should succeed");
let read_fd = child_setup.raw_fd();
assert!(read_fd >= 0, "read fd should be valid");
let result = unsafe { libc::fcntl(read_fd, libc::F_GETFD) };
assert!(result >= 0, "read fd should be open");
drop(child_setup);
drop(keepalive);
}
#[test]
fn test_child_setup_raw_fd() {
let (_keepalive, child_setup) = create().expect("pipe creation should succeed");
let fd = child_setup.raw_fd();
assert!(fd >= 3, "pipe fd should be >= 3 (not stdin/stdout/stderr)");
drop(child_setup);
}
#[test]
fn test_child_setup_drop_closes_read_end() {
let (_keepalive, child_setup) = create().expect("pipe creation should succeed");
let read_fd = child_setup.raw_fd();
assert!(unsafe { libc::fcntl(read_fd, libc::F_GETFD) } >= 0);
drop(child_setup);
assert_eq!(unsafe { libc::fcntl(read_fd, libc::F_GETFD) }, -1);
}
#[test]
fn test_keepalive_drop_closes_write_end_triggers_pollhup() {
let (keepalive, child_setup) = create().expect("pipe creation should succeed");
let read_fd = child_setup.raw_fd();
drop(keepalive);
let mut pollfd = libc::pollfd {
fd: read_fd,
events: libc::POLLIN,
revents: 0,
};
let ret = unsafe { libc::poll(&mut pollfd, 1, 100) }; assert_eq!(ret, 1, "poll should return 1 (one fd ready)");
assert_ne!(
pollfd.revents & libc::POLLHUP,
0,
"should get POLLHUP when write end is closed"
);
drop(child_setup);
}
#[test]
fn test_spawned_child_does_not_block_pollhup() {
let (keepalive, child_setup) = create().expect("pipe creation should succeed");
let read_fd = child_setup.raw_fd();
let mut child = std::process::Command::new("/bin/sleep")
.arg("10")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("failed to spawn sleep");
drop(keepalive);
let mut pollfd = libc::pollfd {
fd: read_fd,
events: libc::POLLIN,
revents: 0,
};
let ret = unsafe { libc::poll(&mut pollfd, 1, 100) };
let _ = child.kill();
let _ = child.wait();
drop(child_setup);
assert_eq!(
ret, 1,
"poll should return 1 (POLLHUP), not 0 (timeout). \
The spawned child inherited the pipe write-end because FD_CLOEXEC \
is missing — this is the orphan shim bug."
);
assert_ne!(
pollfd.revents & libc::POLLHUP,
0,
"should get POLLHUP — child must not hold the write-end"
);
}
}