use std::path::Path;
use std::sync::mpsc;
use std::sync::OnceLock;
use std::thread;
use std::time::{Duration, Instant};
pub const DEFAULT_IO_TIMEOUT: Duration = Duration::from_secs(20);
pub const MIN_READ_RATE_MB_S_DEFAULT: u64 = 20;
pub const STAT_TIMEOUT: Duration = Duration::from_secs(5);
static MIN_READ_RATE_OVERRIDE: OnceLock<u64> = OnceLock::new();
pub fn set_min_read_rate_mb_s(rate: u64) {
let _ = MIN_READ_RATE_OVERRIDE.set(rate);
}
fn resolve_min_read_rate(override_val: Option<u64>) -> u64 {
match override_val {
Some(0) | None => MIN_READ_RATE_MB_S_DEFAULT,
Some(n) => n,
}
}
pub fn min_read_rate_mb_s() -> u64 {
resolve_min_read_rate(MIN_READ_RATE_OVERRIDE.get().copied())
}
pub fn timeout_for_size(size_bytes: u64, rate_mb_s: u64) -> Duration {
let rate = if rate_mb_s == 0 {
MIN_READ_RATE_MB_S_DEFAULT
} else {
rate_mb_s
};
let bytes_per_sec = rate.saturating_mul(1_000_000);
let secs = size_bytes / bytes_per_sec.max(1);
Duration::from_secs(secs).max(DEFAULT_IO_TIMEOUT)
}
pub struct TimedOut;
pub fn run_with_timeout<T, F>(timeout: Duration, f: F) -> Result<T, TimedOut>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let _ = tx.send(f());
});
rx.recv_timeout(timeout).map_err(|_| TimedOut)
}
pub fn run_with_timeout_for_path<T, F>(path: &Path, f: F) -> Result<T, TimedOut>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
let owned = path.to_path_buf();
let size = run_with_timeout(STAT_TIMEOUT, move || {
std::fs::metadata(&owned).map(|m| m.len()).ok()
})
.map_err(|_| TimedOut)?
.ok_or(TimedOut)?;
run_with_timeout(timeout_for_size(size, min_read_rate_mb_s()), f)
}
#[derive(Debug, PartialEq, Eq)]
pub enum WaitOutcome {
Success,
Failed,
TimedOut,
}
pub fn wait_with_timeout(child: &mut std::process::Child, timeout: Duration) -> WaitOutcome {
let start = Instant::now();
loop {
match child.try_wait() {
Ok(Some(status)) => {
return if status.success() {
WaitOutcome::Success
} else {
WaitOutcome::Failed
};
}
Ok(None) => {
if start.elapsed() >= timeout {
let _ = child.kill();
let _ = child.wait();
return WaitOutcome::TimedOut;
}
thread::sleep(Duration::from_millis(50));
}
Err(_) => return WaitOutcome::Failed,
}
}
}
pub fn absence_is_trustworthy(path: &Path) -> bool {
let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) else {
return false;
};
let parent = parent.to_path_buf();
run_with_timeout(DEFAULT_IO_TIMEOUT, move || parent.is_dir()).unwrap_or(false)
}
#[cfg(test)]
mod absence_tests {
use super::*;
fn tmp(name: &str) -> std::path::PathBuf {
let d = std::env::temp_dir().join(format!("videre-absence-{}-{name}", std::process::id()));
std::fs::create_dir_all(&d).unwrap();
d
}
#[test]
fn a_missing_file_in_an_existing_directory_is_trustworthy() {
let dir = tmp("present");
assert!(absence_is_trustworthy(&dir.join("gone.jpg")));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_missing_file_in_a_missing_directory_is_not() {
let dir = tmp("absent");
let nested = dir.join("subdir");
assert!(!absence_is_trustworthy(&nested.join("gone.jpg")));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_real_present_file_is_trustworthy_too() {
let dir = tmp("realfile");
let f = dir.join("here.jpg");
std::fs::write(&f, b"x").unwrap();
assert!(absence_is_trustworthy(&f));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_path_without_a_usable_parent_is_not_trustworthy() {
assert!(!absence_is_trustworthy(Path::new("/")));
assert!(!absence_is_trustworthy(Path::new("bare-name.jpg")));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn returns_ok_when_operation_finishes_before_timeout() {
let result = run_with_timeout(Duration::from_secs(1), || 42);
assert!(result.is_ok());
assert_eq!(result.ok(), Some(42));
}
#[test]
fn returns_timed_out_when_operation_exceeds_timeout() {
let result = run_with_timeout(Duration::from_millis(50), || {
thread::sleep(Duration::from_secs(5));
42
});
assert!(result.is_err());
}
#[test]
fn wait_with_timeout_returns_success_for_fast_process() {
let mut child = std::process::Command::new("true").spawn().unwrap();
assert_eq!(
wait_with_timeout(&mut child, Duration::from_secs(5)),
WaitOutcome::Success
);
}
#[test]
fn wait_with_timeout_kills_and_returns_timed_out_for_slow_process() {
let mut child = std::process::Command::new("sleep")
.arg("5")
.spawn()
.unwrap();
let start = Instant::now();
assert_eq!(
wait_with_timeout(&mut child, Duration::from_millis(200)),
WaitOutcome::TimedOut
);
assert!(start.elapsed() < Duration::from_secs(2));
}
}
#[cfg(test)]
mod size_timeout_tests {
use super::*;
#[test]
fn a_small_file_gets_exactly_the_old_constant() {
assert_eq!(timeout_for_size(0, 20), DEFAULT_IO_TIMEOUT);
assert_eq!(timeout_for_size(1_000_000, 20), DEFAULT_IO_TIMEOUT);
assert_eq!(timeout_for_size(399_000_000, 20), DEFAULT_IO_TIMEOUT);
}
#[test]
fn the_file_that_produced_this_bug_now_gets_enough_time() {
let t = timeout_for_size(3_700_000_000, 20);
assert_eq!(t.as_secs(), 185);
assert!(t.as_secs() > 23, "must exceed the real read time");
}
#[test]
fn the_largest_file_in_the_measured_library_is_bounded_and_finite() {
assert_eq!(timeout_for_size(5_720_000_000, 20).as_secs(), 286);
}
#[test]
fn a_zero_rate_falls_back_rather_than_dividing_by_zero() {
assert_eq!(timeout_for_size(3_700_000_000, 0).as_secs(), 185);
}
#[test]
fn absurd_sizes_neither_panic_nor_overflow() {
let t = timeout_for_size(u64::MAX, 1);
assert!(t >= DEFAULT_IO_TIMEOUT);
assert_eq!(timeout_for_size(u64::MAX, u64::MAX), DEFAULT_IO_TIMEOUT);
}
#[test]
fn resolve_uses_the_override_but_refuses_zero() {
assert_eq!(resolve_min_read_rate(Some(50)), 50);
assert_eq!(resolve_min_read_rate(None), MIN_READ_RATE_MB_S_DEFAULT);
assert_eq!(resolve_min_read_rate(Some(0)), MIN_READ_RATE_MB_S_DEFAULT);
}
#[test]
fn a_dead_path_fails_at_the_stat_rather_than_running_the_body() {
let r = run_with_timeout_for_path(
std::path::Path::new("/nonexistent/videre/definitely-not-here"),
|| 42,
);
assert!(r.is_err());
}
#[test]
fn a_real_file_runs_the_body() {
let d = std::env::temp_dir().join(format!("videre-sz-{}", std::process::id()));
std::fs::create_dir_all(&d).unwrap();
let f = d.join("x.bin");
std::fs::write(&f, b"hello").unwrap();
assert_eq!(run_with_timeout_for_path(&f, || 42).ok(), Some(42));
let _ = std::fs::remove_dir_all(&d);
}
}