use super::supervisor_pin::{Pin, end_tree};
use super::supervisor_proc::{Look, Sample, children, command_line_of, describe, directory_of, look_at, stat_of, tick};
use std::collections::HashMap;
use std::process::Command;
use std::sync::{Mutex, Once};
use std::time::{Duration, Instant};
pub const SHARED_BACKSTOP_SECONDS: &str = "600";
pub const NO_PROGRESS_WINDOW: Duration = Duration::from_secs(5);
pub const CPU_BUDGET: Duration = Duration::from_secs(10);
pub const POLL: Duration = Duration::from_millis(100);
pub const UNCLAIMED_GRACE: Duration = Duration::from_secs(3);
pub const UNCLAIMED_EXIT_STATUS: i32 = 101;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Limits {
pub no_progress: Duration,
pub cpu_budget: Duration,
pub backstop: Duration,
}
pub const SHARED: Limits = Limits { no_progress: NO_PROGRESS_WINDOW, cpu_budget: CPU_BUDGET, backstop: Duration::from_secs(600) };
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Reason {
NoProgress(Duration),
CpuBudget(Duration),
Backstop(Duration),
}
impl std::fmt::Display for Reason {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return match self {
Reason::NoProgress(idle) => write!(
formatter,
"no progress for {:.1} s: no task was runnable, waiting for the disk or held from outside, and processor time, bytes moved, the work of waited-for children and the set of processes stood still (a deadlock or a wait on something that never happens)",
idle.as_secs_f64()
),
Reason::CpuBudget(used) => write!(formatter, "it used {:.1} s of processor time (a loop)", used.as_secs_f64()),
Reason::Backstop(elapsed) => write!(formatter, "it was still running after the {:.0} s wall-clock backstop", elapsed.as_secs_f64()),
};
}
}
type Previous = (u64, u64, u64, Vec<(i32, u64)>);
#[derive(Clone, Debug)]
pub struct Judge {
limits: Limits,
last_change: Duration,
previous: Option<Previous>,
cpu_peak_ticks: u64,
tick: Duration,
}
impl Judge {
pub fn new(limits: Limits, tick: Duration) -> Judge {
return Judge { limits, last_change: Duration::ZERO, previous: None, cpu_peak_ticks: 0, tick };
}
pub fn observe(&mut self, elapsed: Duration, sample: &Sample, held_by_test: bool) -> Option<Reason> {
self.cpu_peak_ticks = self.cpu_peak_ticks.max(sample.cpu_ticks);
let used: Duration = self.tick.saturating_mul(u32::try_from(self.cpu_peak_ticks).unwrap_or(u32::MAX));
if used >= self.limits.cpu_budget {
return Some(Reason::CpuBudget(used));
}
if elapsed >= self.limits.backstop {
return Some(Reason::Backstop(elapsed));
}
let moved: bool = self.previous.as_ref().is_none_or(|(cpu, activity, faults, processes)| {
return *cpu != sample.cpu_ticks || *activity != sample.activity || *faults != sample.child_faults || *processes != sample.processes;
});
if moved || sample.runnable || sample.disk_wait || sample.held || held_by_test {
self.last_change = elapsed;
}
self.previous = Some((sample.cpu_ticks, sample.activity, sample.child_faults, sample.processes.clone()));
let idle: Duration = elapsed.saturating_sub(self.last_change);
if idle >= self.limits.no_progress {
return Some(Reason::NoProgress(idle));
}
return None;
}
}
#[derive(Clone, Debug)]
struct Kill {
pid: i32,
message: String,
at: Instant,
}
struct Watched {
judge: Judge,
began: Instant,
pin: Pin,
}
#[derive(Default)]
struct Registry {
watched: HashMap<i32, Watched>,
kills: Vec<Kill>,
holds: HashMap<i32, u32>,
disk_wait_looks: HashMap<i32, u32>,
looks: HashMap<i32, u32>,
}
static REGISTRY: Mutex<Option<Registry>> = Mutex::new(None);
fn with_registry<T>(action: impl FnOnce(&mut Registry) -> T) -> T {
let mut guard = REGISTRY.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
return action(guard.get_or_insert_with(Registry::default));
}
pub fn is_shared_command_line(arguments: &[&str]) -> bool {
return arguments.len() > 3
&& arguments[0].rsplit('/').next() == Some("timeout")
&& arguments[1] == "--signal=KILL"
&& arguments[2] == SHARED_BACKSTOP_SECONDS;
}
pub fn is_shared(command: &Command) -> bool {
let program: String = command.get_program().to_string_lossy().into_owned();
let arguments: Vec<String> = command.get_args().map(|argument| { return argument.to_string_lossy().into_owned(); }).collect();
let mut line: Vec<&str> = vec![program.as_str()];
line.extend(arguments.iter().map(String::as_str));
return is_shared_command_line(line.as_slice());
}
pub fn watch(pid: u32, limits: Limits) {
let watched: i32 = i32::try_from(pid).expect("a process ID fits");
ensure_monitor();
let started: u64 = stat_of(watched).expect("the child to watch is running").started;
let pin: Pin = Pin::open(watched, started).expect("the child to watch can be pinned");
with_registry(|registry| { registry.watched.insert(watched, Watched { judge: Judge::new(limits, tick()), began: Instant::now(), pin }); });
}
pub fn is_watching(pid: u32) -> bool {
let Ok(watched) = i32::try_from(pid) else { return false };
return with_registry(|registry| { return registry.watched.contains_key(&watched); });
}
pub fn take_kill(pid: u32) -> Option<String> {
let ended: i32 = i32::try_from(pid).ok()?;
return with_registry(|registry| {
let position: usize = registry.kills.iter().rposition(|kill| { return kill.pid == ended; })?;
return Some(registry.kills.remove(position).message);
});
}
pub fn disk_wait_looks(pid: u32) -> u32 {
let Ok(watched) = i32::try_from(pid) else { return 0 };
return with_registry(|registry| { return registry.disk_wait_looks.get(&watched).copied().unwrap_or(0); });
}
pub fn looks_of(pid: u32) -> u32 {
let Ok(watched) = i32::try_from(pid) else { return 0 };
return with_registry(|registry| { return registry.looks.get(&watched).copied().unwrap_or(0); });
}
pub struct IdleHold {
pid: i32,
}
impl Drop for IdleHold {
fn drop(&mut self) {
with_registry(|registry| {
if let Some(count) = registry.holds.get_mut(&self.pid) {
*count -= 1;
if *count == 0 {
registry.holds.remove(&self.pid);
}
}
});
}
}
pub fn hold_idle_judgment(pid: u32) -> IdleHold {
let held: i32 = i32::try_from(pid).expect("a process ID fits");
with_registry(|registry| { *registry.holds.entry(held).or_default() += 1; });
return IdleHold { pid: held };
}
extern "C" fn fail_unclaimed_at_exit() {
if let Some(message) = unclaimed_kill(Instant::now(), Duration::ZERO) {
eprintln!("a command the supervisor ended was never reported by its test, so the process exits with a failure:\n{message}");
unsafe { libc::_exit(UNCLAIMED_EXIT_STATUS) };
}
}
pub fn ensure_monitor() {
static STARTED: Once = Once::new();
STARTED.call_once(|| {
std::thread::Builder::new()
.name(String::from("binary-supervisor"))
.spawn(monitor)
.expect("the supervisor thread starts");
unsafe { libc::atexit(fail_unclaimed_at_exit) };
});
}
fn monitor() {
let own: i32 = i32::try_from(std::process::id()).expect("a process ID fits");
let tick: Duration = tick();
loop {
std::thread::sleep(POLL);
discover(own, tick);
let now: Instant = Instant::now();
let ids: Vec<i32> = with_registry(|registry| { return registry.watched.keys().copied().collect(); });
for pid in ids {
judge_one(pid, now);
}
abort_on_unclaimed(now);
}
}
fn discover(own: i32, tick: Duration) {
for pid in children(own) {
let Some(stat) = stat_of(pid) else { continue };
if stat.parent != own || with_registry(|registry| { return registry.watched.get(&pid).is_some_and(|watched| { return watched.pin.started() == stat.started; }); }) {
continue;
}
let line: Vec<String> = command_line_of(pid);
let words: Vec<&str> = line.iter().map(String::as_str).collect();
if !is_shared_command_line(words.as_slice()) {
continue;
}
let Some(pin) = Pin::open(pid, stat.started) else { continue };
with_registry(|registry| {
registry.watched.insert(pid, Watched { judge: Judge::new(SHARED, tick), began: Instant::now(), pin });
});
}
}
fn judge_one(pid: i32, now: Instant) {
let Some(started) = with_registry(|registry| { return registry.watched.get(&pid).map(|watched| { return watched.pin.started(); }); }) else { return };
let sample: Sample = match look_at(pid, started) {
Look::Gone | Look::Exited => {
with_registry(|registry| {
if registry.watched.get(&pid).is_some_and(|watched| { return watched.pin.started() == started; }) {
registry.watched.remove(&pid);
}
});
return;
}
Look::Seen(seen) => seen,
};
let verdict: Option<Reason> = with_registry(|registry| {
*registry.looks.entry(pid).or_default() += 1;
if sample.disk_wait {
*registry.disk_wait_looks.entry(pid).or_default() += 1;
}
let held: bool = registry.holds.contains_key(&pid);
let watched: &mut Watched = registry.watched.get_mut(&pid)?;
let began: Instant = watched.began;
return watched.judge.observe(now.saturating_duration_since(began), &sample, held);
});
let Some(reason) = verdict else { return };
let line: String = command_line_of(pid).join(" ");
let message: String = format!(
"the harness supervisor ended the command `{line}` in {}: {reason}\nthe tasks of its tree when it was ended:\n{}",
directory_of(pid),
describe(pid)
);
let Some(watched) = with_registry(|registry| { return registry.watched.remove(&pid); }) else { return };
end_command(&watched.pin, message.as_str(), &end_tree);
eprintln!("{message}");
}
pub fn end_command(pin: &Pin, message: &str, end: &dyn Fn(&Pin)) {
with_registry(|registry| { registry.kills.push(Kill { pid: pin.pid(), message: message.to_string(), at: Instant::now() }); });
end(pin);
with_registry(|registry| {
if let Some(kill) = registry.kills.iter_mut().rev().find(|kill| { return kill.pid == pin.pid(); }) {
kill.at = Instant::now();
}
});
}
pub fn kill_recorded(pid: i32) -> bool {
return with_registry(|registry| { return registry.kills.iter().any(|kill| { return kill.pid == pid; }); });
}
pub fn unclaimed_kill(now: Instant, grace: Duration) -> Option<String> {
return with_registry(|registry| {
return registry.kills.iter().find(|kill| { return now.saturating_duration_since(kill.at) >= grace; }).map(|kill| { return kill.message.clone(); });
});
}
pub fn plant_kill(pid: i32, message: &str, at: Instant) {
with_registry(|registry| { registry.kills.push(Kill { pid, message: message.to_string(), at }); });
}
fn abort_on_unclaimed(now: Instant) {
if let Some(message) = unclaimed_kill(now, UNCLAIMED_GRACE) {
eprintln!("a command the supervisor ended was never reported by its test, so the process aborts:\n{message}");
std::process::abort();
}
}