use std::io::Write;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::time::Duration;
pub const DEFAULT_MAX_RUNTIME_SECS: u64 = 60;
pub const DEFAULT_MAX_OUTPUT_BYTES: u64 = 100 * 1024 * 1024;
static MAX_RUNTIME_MS: AtomicU64 = AtomicU64::new(0);
static MAX_OUTPUT_BYTES: AtomicU64 = AtomicU64::new(0);
static OUTPUT_BYTES_USED: AtomicU64 = AtomicU64::new(0);
static OUTPUT_MODE: AtomicUsize = AtomicUsize::new(0); static ABORTED: AtomicBool = AtomicBool::new(false);
#[derive(Copy, Clone)]
pub enum AbortMode {
Text,
Json,
}
pub fn install(max_runtime: Duration, max_output_bytes: u64, mode: AbortMode) {
let ms = max_runtime.as_millis().min(u64::MAX as u128) as u64;
MAX_RUNTIME_MS.store(ms, Ordering::Relaxed);
MAX_OUTPUT_BYTES.store(max_output_bytes, Ordering::Relaxed);
OUTPUT_BYTES_USED.store(0, Ordering::Relaxed);
OUTPUT_MODE.store(
match mode {
AbortMode::Text => 0,
AbortMode::Json => 1,
},
Ordering::Relaxed,
);
ABORTED.store(false, Ordering::Relaxed);
if ms > 0 {
std::thread::Builder::new()
.name("ilo-runtime-watchdog".to_string())
.spawn(move || {
let start = std::time::Instant::now();
loop {
if ABORTED.load(Ordering::Relaxed) {
return;
}
let elapsed_ms = start.elapsed().as_millis() as u64;
if elapsed_ms >= ms {
abort_with(
"ILO-R016",
&format!(
"wall-clock runtime exceeded {} ms (--max-runtime {})",
ms,
ms / 1000
),
"infinite loop is the most common cause - check loop variables increment, recursion has a base case, or pass `--max-runtime N` if a legitimate program needs longer.",
);
}
std::thread::sleep(Duration::from_millis(100));
}
})
.expect("spawn watchdog thread");
}
}
pub fn record_output(n: usize) {
let cap = MAX_OUTPUT_BYTES.load(Ordering::Relaxed);
if cap == 0 {
return;
}
let total = OUTPUT_BYTES_USED.fetch_add(n as u64, Ordering::Relaxed) + n as u64;
if total > cap {
abort_with(
"ILO-R017",
&format!("stdout output exceeded {cap} bytes (--max-output-bytes)"),
"a loop printing without a break or increment is the most common cause - check `prnt` calls inside `wh`/`fa` bodies. raise the cap with `--max-output-bytes N` if a legitimate program needs more.",
);
}
}
pub fn abort_with(code: &str, message: &str, hint: &str) -> ! {
if ABORTED.swap(true, Ordering::SeqCst) {
loop {
std::thread::sleep(Duration::from_secs(60));
}
}
let stderr = std::io::stderr();
let mut h = stderr.lock();
if OUTPUT_MODE.load(Ordering::Relaxed) == 1 {
let json = serde_json::json!({
"error": {
"code": code,
"message": message,
"hint": hint,
}
});
let _ = writeln!(h, "{json}");
} else {
let _ = writeln!(h, "error[{code}]: {message}");
let _ = writeln!(h, " hint: {hint}");
}
let _ = h.flush();
std::process::exit(1);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn record_output_noop_when_uninstalled() {
MAX_OUTPUT_BYTES.store(0, Ordering::Relaxed);
OUTPUT_BYTES_USED.store(0, Ordering::Relaxed);
record_output(1_000_000);
assert_eq!(OUTPUT_BYTES_USED.load(Ordering::Relaxed), 0);
}
#[test]
fn record_output_accumulates_under_budget() {
MAX_OUTPUT_BYTES.store(1024, Ordering::Relaxed);
OUTPUT_BYTES_USED.store(0, Ordering::Relaxed);
ABORTED.store(false, Ordering::Relaxed);
record_output(100);
record_output(200);
assert_eq!(OUTPUT_BYTES_USED.load(Ordering::Relaxed), 300);
MAX_OUTPUT_BYTES.store(0, Ordering::Relaxed);
OUTPUT_BYTES_USED.store(0, Ordering::Relaxed);
}
}