use std::io::Write;
use std::sync::{Mutex, MutexGuard, PoisonError};
use std::time::{Duration, Instant};
use alpha2_media::CancelToken;
use alpha2_os::Termination;
pub(crate) struct Terminations {
state: Mutex<State>,
on_idle: fn(Termination),
}
#[derive(Default)]
struct State {
requested: Option<Termination>,
busy: usize,
running: Vec<(u64, CancelToken)>,
next: u64,
}
impl Terminations {
pub(crate) const fn new(on_idle: fn(Termination)) -> Self {
Self {
state: Mutex::new(State {
requested: None,
busy: 0,
running: Vec::new(),
next: 0,
}),
on_idle,
}
}
fn lock(&self) -> MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(crate) fn request(&self, termination: Termination) -> bool {
let mut state = self.lock();
state.requested.get_or_insert(termination);
for (_, token) in &state.running {
token.cancel();
}
state.busy == 0
}
pub(crate) fn requested(&self) -> Option<Termination> {
self.lock().requested
}
pub(crate) fn begin_request(&'static self) -> RequestGuard {
self.lock().busy += 1;
RequestGuard { owner: self }
}
pub(crate) fn hold_busy(&self) {
self.lock().busy += 1;
}
pub(crate) fn export_cancel(&'static self) -> ExportCancel {
let token = CancelToken::default();
let mut state = self.lock();
if state.requested.is_some() {
token.cancel();
}
let id = state.next;
state.next += 1;
state.running.push((id, token.clone()));
ExportCancel {
token,
id,
owner: self,
}
}
fn wait_idle(&self, limit: Duration) {
let started = Instant::now();
while self.lock().busy != 0 && started.elapsed() < limit {
std::thread::sleep(Duration::from_millis(20));
}
}
}
pub(crate) struct RequestGuard {
owner: &'static Terminations,
}
impl Drop for RequestGuard {
fn drop(&mut self) {
let mut state = self.owner.lock();
state.busy = state.busy.saturating_sub(1);
let idle = (state.busy == 0).then_some(state.requested).flatten();
drop(state);
if let Some(termination) = idle {
(self.owner.on_idle)(termination);
}
}
}
pub(crate) struct ExportCancel {
token: CancelToken,
id: u64,
owner: &'static Terminations,
}
impl std::ops::Deref for ExportCancel {
type Target = CancelToken;
fn deref(&self) -> &CancelToken {
&self.token
}
}
impl Drop for ExportCancel {
fn drop(&mut self) {
self.owner.lock().running.retain(|(id, _)| *id != self.id);
}
}
static TERMINATIONS: Terminations = Terminations::new(exit_on_termination);
fn exit_on_termination(termination: Termination) {
crate::exit_unflushed(termination.exit_code());
}
pub(crate) fn terminations() -> &'static Terminations {
&TERMINATIONS
}
pub(crate) fn requested() -> Option<Termination> {
TERMINATIONS.requested()
}
pub(crate) fn export_cancel() -> ExportCancel {
TERMINATIONS.export_cancel()
}
pub(crate) fn cancelled_by_termination(message: String) -> String {
match requested() {
Some(termination) if !crate::protocol::is_cancelled(&message) => {
crate::protocol::cancelled(format!(
"native export was cancelled: the host was asked to terminate ({})",
termination.name()
))
}
_ => message,
}
}
pub(crate) fn install() -> std::io::Result<()> {
alpha2_os::on_termination(|termination| {
let _ = writeln!(
std::io::stderr(),
"alpha2-host: {} received; cancelling the running request",
termination.name()
);
if TERMINATIONS.request(termination) {
exit_on_termination(termination);
}
if termination.closing() {
TERMINATIONS.wait_idle(Duration::from_secs(4));
}
})
}
#[cfg(test)]
mod tests {
use super::*;
static IDLE: Mutex<Vec<&'static str>> = Mutex::new(Vec::new());
fn record_idle(termination: Termination) {
IDLE.lock().expect("idle").push(termination.name());
}
fn leaked() -> &'static Terminations {
Box::leak(Box::new(Terminations::new(record_idle)))
}
fn sigterm() -> Termination {
Termination::new("SIGTERM", 143, false)
}
#[test]
fn a_termination_cancels_every_running_export_and_any_that_starts_after_it() {
let terminations = leaked();
let request = terminations.begin_request();
let running = terminations.export_cancel();
let ended = terminations.export_cancel();
drop(ended);
assert!(!running.is_cancelled());
assert!(
!terminations.request(sigterm()),
"a process answering a request is not idle"
);
assert!(running.is_cancelled());
assert_eq!(terminations.requested(), Some(sigterm()));
assert!(terminations.export_cancel().is_cancelled());
drop(running);
assert!(IDLE.lock().expect("idle").is_empty());
drop(request);
assert_eq!(*IDLE.lock().expect("idle"), ["SIGTERM"]);
assert!(terminations.request(sigterm()), "idle once answered");
}
#[test]
fn an_idle_process_can_exit_at_once() {
let terminations = leaked();
assert!(terminations.request(sigterm()));
}
}