Keep isolated document extraction working on macOS

This commit is contained in:
karl committed 2026-10-23 09:47:08 +02:00
1 parent 5b93dd67a7
commit dd589e88ae
3 files changed
+60

No files matched your search

@@ -48,6 +48,33 @@ enum Event {
Stderr,
}
#[cfg(target_os = "macos")]
fn check_resident_memory(child: &mut Child, maximum: u64) -> Result<(), ProcessError> {
let mut info = std::mem::MaybeUninit::<libc::proc_taskinfo>::uninit();
let size = std::mem::size_of::<libc::proc_taskinfo>();
// The child stays owned and unreaped until I/O completes. A failed query is
// accepted only when it has already exited; otherwise fail closed.
let read = unsafe {
libc::proc_pidinfo(
child.id() as i32,
libc::PROC_PIDTASKINFO,
0,
info.as_mut_ptr().cast(),
size as i32,
)
};
if read != size as i32 {
if child.try_wait().map_err(failed)?.is_some() {
return Ok(());
}
return Err(failed("Cannot inspect extraction memory usage"));
}
if unsafe { info.assume_init() }.pti_resident_size > maximum {
return Err(failed("Extraction worker resident memory exceeds budget"));
}
Ok(())
}
/// Includes pipe writes, response EOF and process exit in the same deadline.
/// No parser fallback occurs when limits, transport or worker execution fail.
pub fn run(
@@ -161,6 +188,8 @@ pub fn run(
if started.elapsed() >= config.timeout {
return Err(ProcessError::TimedOut);
}
#[cfg(target_os = "macos")]
check_resident_memory(&mut owned.child, 512 * 1024 * 1024)?;
match rx.recv_timeout(
Duration::from_millis(5).min(config.timeout.saturating_sub(started.elapsed())),
) {
@@ -288,6 +317,20 @@ mod tests {
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[cfg(target_os = "macos")]
#[test]
fn mac_memory_budget_rejects_a_live_owned_child() {
let mut child = Command::new("/bin/sleep").arg("30").spawn().unwrap();
// Wait for exec before measuring, then impose a deliberately tiny budget.
thread::sleep(Duration::from_millis(50));
let result = check_resident_memory(&mut child, 1);
let _ = child.kill();
child.wait().unwrap();
assert!(
matches!(result, Err(ProcessError::Failed(message)) if message.contains("exceeds budget"))
);
}
fn fixtures() -> tempfile::TempDir {
let directory = tempfile::tempdir().unwrap();
let source = directory.path().join("fake.rs");
@@ -1,5 +1,6 @@
use anyhow::{ensure, Result};
#[cfg(not(target_os = "macos"))]
const MEMORY_BYTES: usize = 512 * 1024 * 1024;
#[cfg(windows)]
@@ -37,7 +38,10 @@ pub fn apply() -> Result<()> {
#[cfg(unix)]
pub fn apply() -> Result<()> {
// Darwin rejects finite AS/DATA limits. The parent enforces a sampled RSS
// budget there; retain the CPU limit and bounded path-free transport here.
for (resource, maximum) in [
#[cfg(not(target_os = "macos"))]
(libc::RLIMIT_AS, MEMORY_BYTES as libc::rlim_t),
(libc::RLIMIT_CPU, 30 as libc::rlim_t),
] {