use std::thread;
use std::time::{Duration, Instant};
use sysinfo::{Pid, Signal, System};
use crate::record::{Record, Sameness, Tenant};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Policy {
pub unconfirmed: Unconfirmed,
pub escalate_after: Option<Duration>,
pub deadline: Duration,
pub poll: Duration,
}
impl Default for Policy {
fn default() -> Self {
Self {
unconfirmed: Unconfirmed::Refuse,
escalate_after: Some(Duration::from_secs(2)),
deadline: Duration::from_secs(5),
poll: Duration::from_millis(100),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Unconfirmed {
Refuse,
Signal,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Outcome {
AlreadyGone,
Left,
Refused(Sameness),
Stubborn,
}
#[must_use]
pub fn evict(record: &Record, policy: &Policy) -> Outcome {
let Some(live) = Tenant::look_up(record.tenant.pid) else {
return Outcome::AlreadyGone;
};
match record.tenant.compare(&live) {
Sameness::Same => {}
Sameness::Inconclusive if policy.unconfirmed == Unconfirmed::Signal => {}
refused => return Outcome::Refused(refused),
}
let pid = Pid::from_u32(record.tenant.pid);
let mut system = System::new();
let _delivered = signal(&mut system, pid, Signal::Term);
let started = Instant::now();
let mut escalated = policy.escalate_after.is_none();
loop {
if Tenant::look_up(record.tenant.pid).is_none_or(|now| {
record.tenant.compare(&now) == Sameness::Different
}) {
return Outcome::Left;
}
let waited = started.elapsed();
if waited >= policy.deadline {
return Outcome::Stubborn;
}
if !escalated && policy.escalate_after.is_some_and(|after| waited >= after) {
escalated = true;
signal(&mut system, pid, Signal::Kill);
}
thread::sleep(policy.poll);
}
}
fn signal(system: &mut System, pid: Pid, signal: Signal) -> bool {
system.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
system
.process(pid)
.and_then(|process| process.kill_with(signal))
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::{Compat, Identity, Run};
fn record(tenant: Tenant) -> Record {
Record::new(Identity::new(Run::from_raw(1), Compat::from_raw(1)), tenant)
}
#[test]
fn a_pid_that_is_gone_needs_no_signal() {
let outcome = evict(
&record(Tenant {
pid: 0,
started_at: Some(1),
image: None,
}),
&Policy::default(),
);
assert_eq!(outcome, Outcome::AlreadyGone);
}
#[test]
fn a_pid_that_no_longer_matches_its_record_is_never_signalled() {
let outcome = evict(
&record(Tenant {
pid: std::process::id(),
started_at: Some(1),
image: None,
}),
&Policy::default(),
);
assert_eq!(outcome, Outcome::Refused(Sameness::Different));
}
#[test]
fn an_unconfirmable_pid_is_refused_by_default() {
let outcome = evict(
&record(Tenant {
pid: std::process::id(),
started_at: None,
image: None,
}),
&Policy::default(),
);
assert_eq!(outcome, Outcome::Refused(Sameness::Inconclusive));
}
}