use std::cell::Cell;
use std::io::Read;
use std::os::unix::process::{CommandExt, ExitStatusExt};
use std::path::Path;
use std::process::{Command, ExitStatus, Stdio};
use std::sync::{Mutex, MutexGuard, OnceLock};
use std::time::{Duration, Instant};
pub struct Interrupted {
pub status: ExitStatus,
pub stderr: Vec<u8>,
pub left: Vec<String>,
}
#[derive(Clone, Copy)]
pub enum Inherit {
Default,
IgnoreInt,
}
pub fn big_input(mib: usize) -> &'static [u8] {
static BIG: OnceLock<Vec<u8>> = OnceLock::new();
const BLOCK: usize = 4 << 20;
let v = BIG.get_or_init(|| {
let block = super::random(BLOCK, 0x5157_0013);
let mut v = Vec::with_capacity(256 << 20);
while v.len() < 256 << 20 {
v.extend_from_slice(&block);
}
v
});
&v[..mib << 20]
}
const HANG_DEADLINE: Duration = Duration::from_secs(30);
const IGNORED_SIGNAL_DEADLINE: Duration = Duration::from_secs(600);
thread_local! {
static REFERENCE_HUNG: Cell<bool> = const { Cell::new(false) };
}
pub fn take_reference_hung() -> bool {
REFERENCE_HUNG.with(|h| h.replace(false))
}
fn is_stuffr(exe: &Path) -> bool {
match (
std::fs::canonicalize(exe),
std::fs::canonicalize(env!("CARGO_BIN_EXE_stuffr")),
) {
(Ok(a), Ok(b)) => a == b,
_ => false,
}
}
static INTERRUPT_LOCK: Mutex<()> = Mutex::new(());
pub fn interrupt_lock() -> MutexGuard<'static, ()> {
INTERRUPT_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
pub fn fast_size(mib: usize) -> usize {
(mib * 3).min(256)
}
pub fn listing(dir: &Path) -> Vec<String> {
let mut v: Vec<String> = std::fs::read_dir(dir)
.unwrap()
.flatten()
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
v.sort();
v
}
pub fn interrupt(
exe: &Path,
name: &str,
args: &[&str],
dir: &Path,
inherit: Inherit,
sig: i32,
ready: &dyn Fn(&str) -> bool,
) -> Option<Interrupted> {
interrupt_env(exe, name, args, dir, &[], inherit, sig, ready)
}
#[allow(clippy::too_many_arguments)]
pub fn interrupt_env(
exe: &Path,
name: &str,
args: &[&str],
dir: &Path,
env: &[(&str, &str)],
inherit: Inherit,
sig: i32,
ready: &dyn Fn(&str) -> bool,
) -> Option<Interrupted> {
let mut cmd = Command::new(exe);
cmd.arg0(name).args(args).current_dir(dir);
cmd.envs(env.iter().copied());
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped());
unsafe {
cmd.pre_exec(move || {
let int = match inherit {
Inherit::Default => libc::SIG_DFL,
Inherit::IgnoreInt => libc::SIG_IGN,
};
libc::signal(libc::SIGINT, int);
for s in [libc::SIGTERM, libc::SIGHUP, libc::SIGQUIT, libc::SIGPIPE] {
libc::signal(s, libc::SIG_DFL);
}
Ok(())
});
}
REFERENCE_HUNG.with(|h| h.set(false));
let mut child = super::spawn(&mut cmd).expect("spawn");
let deadline = Instant::now() + Duration::from_secs(30);
loop {
if child.try_wait().unwrap().is_some() {
return None;
}
if listing(dir).iter().any(|n| ready(n)) {
break;
}
assert!(
Instant::now() < deadline,
"{name} {args:?}: never became ready"
);
std::thread::sleep(Duration::from_millis(2));
}
let mut pipe = child.stderr.take().expect("piped stderr");
let reader = std::thread::spawn(move || {
let mut v = Vec::new();
let _ = pipe.read_to_end(&mut v);
v
});
unsafe { libc::kill(child.id() as i32, sig) };
let hang_at = Instant::now()
+ match inherit {
Inherit::Default => HANG_DEADLINE,
Inherit::IgnoreInt => IGNORED_SIGNAL_DEADLINE,
};
let status = loop {
if let Some(st) = child.try_wait().unwrap() {
break Some(st);
}
if Instant::now() >= hang_at {
break None;
}
std::thread::sleep(Duration::from_millis(5));
};
let Some(status) = status else {
let _ = child.kill();
let _ = child.wait();
let stderr = reader.join().unwrap_or_default();
let note = format!(
"{name} {args:?}: hung after the signal ({sig}); stderr so far: {:?}",
String::from_utf8_lossy(&stderr)
);
assert!(!is_stuffr(exe), "{note}");
eprintln!("note: reference {note}; attempt counted as a miss");
REFERENCE_HUNG.with(|h| h.set(true));
return None;
};
let stderr = reader.join().unwrap_or_default();
Some(Interrupted {
status,
stderr,
left: listing(dir),
})
}
#[allow(clippy::too_many_arguments)]
pub fn interrupt_with_input(
exe: &Path,
name: &str,
args: &[&str],
dir: &Path,
file: &str,
input: &dyn Fn(usize) -> Vec<u8>,
inherit: Inherit,
sig: i32,
ready: &dyn Fn(&str) -> bool,
) -> Option<Interrupted> {
interrupt_with_input_env(exe, name, args, dir, &[], file, input, inherit, sig, ready)
}
#[allow(clippy::too_many_arguments)]
pub fn interrupt_with_input_env(
exe: &Path,
name: &str,
args: &[&str],
dir: &Path,
env: &[(&str, &str)],
file: &str,
input: &dyn Fn(usize) -> Vec<u8>,
inherit: Inherit,
sig: i32,
ready: &dyn Fn(&str) -> bool,
) -> Option<Interrupted> {
let mut hung = false;
for mib in [64, 256] {
let _ = std::fs::remove_dir_all(dir);
std::fs::create_dir_all(dir).unwrap();
std::fs::write(dir.join(file), input(mib)).unwrap();
match interrupt_env(exe, name, args, dir, env, inherit, sig, ready) {
Some(r) if !r.status.success() => return Some(r),
_ => hung |= take_reference_hung(),
}
}
if hung {
eprintln!("note: {name} {args:?}: the reference hung after the signal; comparison skipped");
return None;
}
panic!("{name} {args:?}: finished before the signal even at the larger size");
}
pub fn killed_by(r: &Interrupted, sig: i32) -> bool {
r.status.signal() == Some(sig)
}
pub fn stored_gzip(data: &[u8]) -> Vec<u8> {
let mut v = vec![0x1f, 0x8b, 8, 0, 0, 0, 0, 0, 0, 3];
let mut chunks = data.chunks(65535).peekable();
if chunks.peek().is_none() {
v.extend_from_slice(&[1, 0, 0, 0xff, 0xff]);
}
while let Some(c) = chunks.next() {
v.push(u8::from(chunks.peek().is_none()));
v.extend_from_slice(&(c.len() as u16).to_le_bytes());
v.extend_from_slice(&(!(c.len() as u16)).to_le_bytes());
v.extend_from_slice(c);
}
v.extend_from_slice(&super::crc32(data).to_le_bytes());
v.extend_from_slice(&(data.len() as u32).to_le_bytes());
v
}
pub fn has_data<'a>(dir: &'a Path, named: &'a dyn Fn(&str) -> bool) -> impl Fn(&str) -> bool + 'a {
move |n| named(n) && std::fs::metadata(dir.join(n)).is_ok_and(|m| m.len() > 0)
}