use std::{
process::Child,
sync::atomic::{AtomicBool, Ordering},
time::{Duration, Instant},
};
pub(crate) fn kill_and_reap(child: &mut Child) {
let _ = child.kill();
let _ = child.wait();
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum RequestError {
Cancelled,
TimedOut,
#[cfg(target_os = "macos")]
MemoryUnavailable,
#[cfg(target_os = "macos")]
MemoryExceeded,
}
pub(crate) struct RequestGuard<'a> {
cancel: &'a AtomicBool,
deadline: Instant,
#[cfg(target_os = "macos")]
next_memory_check: Instant,
#[cfg(target_os = "macos")]
memory_limit: u64,
}
impl<'a> RequestGuard<'a> {
pub(crate) fn new(cancel: &'a AtomicBool, deadline: Instant, _memory_limit: u64) -> Self {
Self {
cancel,
deadline,
#[cfg(target_os = "macos")]
next_memory_check: Instant::now(),
#[cfg(target_os = "macos")]
memory_limit: _memory_limit,
}
}
pub(crate) fn check_completion(&self) -> Result<(), RequestError> {
if self.cancel.load(Ordering::Relaxed) {
return Err(RequestError::Cancelled);
}
if Instant::now() >= self.deadline {
return Err(RequestError::TimedOut);
}
Ok(())
}
pub(crate) fn check(&mut self, _pid: u32, _force_memory: bool) -> Result<(), RequestError> {
self.check_completion()?;
#[cfg(target_os = "macos")]
{
let now = Instant::now();
if _force_memory || now >= self.next_memory_check {
self.next_memory_check = now + Duration::from_millis(20);
let bytes = resident_bytes(_pid).ok_or(RequestError::MemoryUnavailable)?;
if bytes > self.memory_limit {
return Err(RequestError::MemoryExceeded);
}
}
}
Ok(())
}
pub(crate) fn remaining(&self) -> Duration {
self.deadline.saturating_duration_since(Instant::now())
}
}
#[cfg(target_os = "macos")]
pub(crate) fn resident_bytes(pid: u32) -> Option<u64> {
let mut info = std::mem::MaybeUninit::<libc::proc_taskinfo>::uninit();
let size = std::mem::size_of::<libc::proc_taskinfo>() as i32;
let count = unsafe {
libc::proc_pidinfo(
pid as i32,
libc::PROC_PIDTASKINFO,
0,
info.as_mut_ptr().cast(),
size,
)
};
if count == size {
Some(unsafe { info.assume_init() }.pti_resident_size)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cancellation_and_expiry_apply_even_to_completed_requests() {
let cancel = AtomicBool::new(false);
let mut guard =
RequestGuard::new(&cancel, Instant::now() + Duration::from_secs(60), u64::MAX);
assert!(guard.check_completion().is_ok());
cancel.store(true, Ordering::Relaxed);
assert_eq!(guard.check_completion(), Err(RequestError::Cancelled));
guard.deadline = Instant::now();
assert_eq!(guard.check_completion(), Err(RequestError::Cancelled));
cancel.store(false, Ordering::Relaxed);
assert_eq!(guard.check_completion(), Err(RequestError::TimedOut));
assert_eq!(
guard.check(std::process::id(), true),
Err(RequestError::TimedOut)
);
assert_eq!(guard.remaining(), Duration::ZERO);
}
#[cfg(target_os = "macos")]
#[test]
fn memory_sampling_is_bounded_and_completion_can_force_a_sample() {
let cancel = AtomicBool::new(false);
let mut guard =
RequestGuard::new(&cancel, Instant::now() + Duration::from_secs(60), u64::MAX);
let pid = std::process::id();
assert_eq!(guard.check(pid, true), Ok(()));
guard.memory_limit = 1;
guard.next_memory_check = Instant::now() + Duration::from_secs(60);
assert_eq!(guard.check(pid, false), Ok(()));
assert_eq!(guard.check(pid, true), Err(RequestError::MemoryExceeded));
assert_eq!(
guard.check(u32::MAX, true),
Err(RequestError::MemoryUnavailable)
);
}
}