use std::time::{Duration, Instant};
use super::fork_payload::{Dec, ForkPayload};
pub(super) const KILL_GRACE: Duration = Duration::from_secs(2);
const POLL_SLICE_MS: std::os::raw::c_int = 50;
const CHILD_EXIT_PANIC: std::os::raw::c_int = 91;
const CHILD_EXIT_WRITE_FAILED: std::os::raw::c_int = 92;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum ForkOutcome<T> {
Completed(T),
Killed {
pid: i32,
},
Failed(String),
}
#[cfg(unix)]
pub(super) fn run_forked_with_deadline<T: ForkPayload>(
deadline: Instant,
f: impl FnOnce() -> Option<T>,
) -> ForkOutcome<Option<T>> {
if !forking_is_sound() {
return ForkOutcome::Completed(f());
}
fork_with_kill_deadline(deadline + KILL_GRACE, f)
}
#[cfg(unix)]
pub(super) fn forking_is_sound() -> bool {
threads_in_this_process().unwrap_or(1) == 1
}
#[cfg(unix)]
pub(super) fn threads_in_this_process() -> Option<usize> {
std::fs::read_to_string("/proc/self/status")
.ok()?
.lines()
.find_map(|l| l.strip_prefix("Threads:"))?
.trim()
.parse()
.ok()
}
#[cfg(not(unix))]
pub(super) fn run_forked_with_deadline<T: ForkPayload>(
_deadline: Instant,
f: impl FnOnce() -> Option<T>,
) -> ForkOutcome<Option<T>> {
ForkOutcome::Completed(f())
}
#[cfg(unix)]
pub(super) fn fork_with_kill_deadline<T: ForkPayload>(
kill_deadline: Instant,
f: impl FnOnce() -> Option<T>,
) -> ForkOutcome<Option<T>> {
flush_std_buffers();
let mut fds: [std::os::raw::c_int; 2] = [0; 2];
if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
return ForkOutcome::Completed(f());
}
let (rd, wr) = (fds[0], fds[1]);
let pid = unsafe { libc::fork() };
if pid < 0 {
unsafe {
libc::close(rd);
libc::close(wr);
}
return ForkOutcome::Completed(f());
}
if pid == 0 {
unsafe { libc::close(rd) };
tie_lifetime_to_parent();
let code = child_body(wr, f);
unsafe { libc::_exit(code) };
#[allow(unreachable_code)]
unsafe {
libc::abort()
};
}
unsafe { libc::close(wr) };
parent_wait(pid, rd, kill_deadline)
}
#[cfg(all(unix, target_os = "linux"))]
fn tie_lifetime_to_parent() {
unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) };
if unsafe { libc::getppid() } == 1 {
unsafe { libc::_exit(0) };
}
}
#[cfg(all(unix, not(target_os = "linux")))]
fn tie_lifetime_to_parent() {}
#[cfg(unix)]
fn child_body<T: ForkPayload>(
wr: std::os::raw::c_int,
f: impl FnOnce() -> Option<T>,
) -> std::os::raw::c_int {
let encoded = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let out = f();
let mut buf = Vec::new();
match out {
Some(v) => {
buf.push(1u8);
v.encode(&mut buf);
}
None => buf.push(0u8),
}
buf
}));
let buf = match encoded {
Ok(b) => b,
Err(_) => return CHILD_EXIT_PANIC,
};
flush_std_buffers();
if !write_all_fd(wr, &buf) {
return CHILD_EXIT_WRITE_FAILED;
}
unsafe { libc::close(wr) };
0
}
#[cfg(unix)]
fn parent_wait<T: ForkPayload>(
pid: libc::pid_t,
rd: std::os::raw::c_int,
kill_deadline: Instant,
) -> ForkOutcome<Option<T>> {
let mut buf: Vec<u8> = Vec::new();
let mut chunk = [0u8; 64 * 1024];
loop {
let now = Instant::now();
if now >= kill_deadline {
unsafe { libc::kill(pid, libc::SIGKILL) };
let _ = reap(pid);
unsafe { libc::close(rd) };
return ForkOutcome::Killed { pid };
}
let slice = kill_deadline
.duration_since(now)
.as_millis()
.min(POLL_SLICE_MS as u128)
.max(1) as std::os::raw::c_int;
let mut pfd = libc::pollfd {
fd: rd,
events: libc::POLLIN,
revents: 0,
};
let r = unsafe { libc::poll(&mut pfd, 1, slice) };
if r < 0 {
if last_errno() == Some(libc::EINTR) {
continue;
}
let err = std::io::Error::last_os_error();
unsafe { libc::close(rd) };
unsafe { libc::kill(pid, libc::SIGKILL) };
let _ = reap(pid);
return ForkOutcome::Failed(format!("poll failed: {err}"));
}
if r == 0 {
continue;
}
let n = unsafe { libc::read(rd, chunk.as_mut_ptr() as *mut libc::c_void, chunk.len()) };
if n < 0 {
if last_errno() == Some(libc::EINTR) {
continue;
}
let err = std::io::Error::last_os_error();
unsafe { libc::close(rd) };
unsafe { libc::kill(pid, libc::SIGKILL) };
let _ = reap(pid);
return ForkOutcome::Failed(format!("read failed: {err}"));
}
if n == 0 {
break; }
buf.extend_from_slice(&chunk[..n as usize]);
}
unsafe { libc::close(rd) };
let status = match reap(pid) {
Some(s) => s,
None => return ForkOutcome::Failed("waitpid failed".to_string()),
};
if !libc::WIFEXITED(status) {
let sig = if libc::WIFSIGNALED(status) {
libc::WTERMSIG(status)
} else {
-1
};
return ForkOutcome::Failed(format!("child died on signal {sig}"));
}
let code = libc::WEXITSTATUS(status);
if code != 0 {
let why = match code {
CHILD_EXIT_PANIC => "panicked",
CHILD_EXIT_WRITE_FAILED => "could not write its result",
_ => "exited nonzero",
};
return ForkOutcome::Failed(format!("child {why} (exit {code})"));
}
let mut dec = Dec::new(&buf);
match dec.get_u8() {
Some(0) => ForkOutcome::Completed(None),
Some(1) => match T::decode(&mut dec) {
Some(v) => ForkOutcome::Completed(Some(v)),
None => ForkOutcome::Failed("could not decode child result".to_string()),
},
_ => ForkOutcome::Failed("child result missing or corrupt".to_string()),
}
}
#[cfg(unix)]
fn reap(pid: libc::pid_t) -> Option<std::os::raw::c_int> {
loop {
let mut status: std::os::raw::c_int = 0;
let r = unsafe { libc::waitpid(pid, &mut status, 0) };
if r == pid {
return Some(status);
}
if r < 0 && last_errno() == Some(libc::EINTR) {
continue;
}
return None;
}
}
#[cfg(unix)]
fn write_all_fd(fd: std::os::raw::c_int, mut buf: &[u8]) -> bool {
while !buf.is_empty() {
let n = unsafe { libc::write(fd, buf.as_ptr() as *const libc::c_void, buf.len()) };
if n < 0 {
if last_errno() == Some(libc::EINTR) {
continue;
}
return false;
}
if n == 0 {
return false;
}
buf = &buf[n as usize..];
}
true
}
#[cfg(unix)]
fn last_errno() -> Option<std::os::raw::c_int> {
std::io::Error::last_os_error().raw_os_error()
}
#[cfg(unix)]
fn flush_std_buffers() {
use std::io::Write;
let _ = std::io::stdout().flush();
let _ = std::io::stderr().flush();
unsafe { libc::fflush(std::ptr::null_mut()) };
}