use std::io; use std::sync::Mutex; use std::time::{Duration, Instant}; use crate::{Child, Group, Launch}; pub(crate) struct Escalation { steps: Vec<(String, Duration)>, next: usize, deadline: Option, pub forced: bool, } impl Escalation { pub fn new(mut steps: Vec<(String, Duration)>) -> Self { steps.push(("SIGKILL".into(), Duration::from_secs(5))); Self { steps, next: 0, deadline: None, forced: false, } } pub fn cancelled(&self) -> bool { self.deadline.is_some() } pub fn request(&mut self, force: bool) -> bool { let changed = !self.cancelled() || (force && !self.forced); self.deadline.get_or_insert_with(Instant::now); self.forced |= force; changed } pub fn advance(&mut self, mut signal: impl FnMut(&str) -> io::Result) -> io::Result<()> { let Some(mut deadline) = self.deadline else { return Ok(()); }; let mut error = None; if self.forced { self.next = self.next.max(self.steps.len() - 1); } while self.next < self.steps.len() && (self.forced || Instant::now() >= deadline) { let (name, grace) = &self.steps[self.next]; if let Err(e) = signal(name) { error.get_or_insert(e); } self.next += 1; deadline = Instant::now() + *grace; } self.deadline = Some(deadline); error.map_or(Ok(()), Err) } fn expired(&self) -> bool { self.next == self.steps.len() && self.deadline.is_some_and(|at| Instant::now() >= at) } } /// An explicitly retained scope: command exits do not end descendant ownership. /// Cancellation serializes with both launching and `if_running` publication. pub struct Scope { group: Group, stop: Mutex, } impl Scope { pub fn new(host: &std::path::Path, steps: Vec<(String, Duration)>) -> io::Result { Ok(Self { group: Group::new(host)?, stop: Mutex::new(Escalation::new(steps)), }) } pub fn is_cancelled(&self) -> bool { self.stop.lock().unwrap().cancelled() } pub fn if_running(&self, action: impl FnOnce() -> T) -> Option { let stop = self.stop.lock().unwrap(); if stop.cancelled() { None } else { Some(action()) } } pub(crate) fn spawn(&self, launch: Launch<'_>) -> io::Result { let stop = self.stop.lock().unwrap(); if stop.cancelled() { return Err(io::Error::new( io::ErrorKind::Interrupted, "Task run was cancelled", )); } crate::spawn(Launch { join: Some(&self.group), group: false, ..launch }) } pub fn cancel(&self) { self.stop.lock().unwrap().request(false); } /// Bounded, resumable shutdown. The caller can include its own work in settling. pub fn terminate(&self, settled: impl Fn() -> bool) -> bool { self.cancel(); loop { { let mut stop = self.stop.lock().unwrap(); if !self.group.alive() && settled() { self.group.close(); return true; } let _ = stop.advance(|signal| self.group.signal(signal)); if stop.expired() { return false; } } std::thread::sleep(Duration::from_millis(5)); } } pub fn force_close(&self) { self.stop.lock().unwrap().request(true); self.group.close(); } }