use std::{
sync::{Arc, atomic::AtomicBool, atomic::Ordering},
thread,
time::{Duration, Instant},
};
use tokio_util::sync::CancellationToken;
use crate::process_group::{kill_target, terminate_target};
use crate::terminate::targets_group;
use crate::{AppError, AppResult, ErrorCode, SignalPolicy};
const POLL_INTERVAL: Duration = Duration::from_millis(10);
#[derive(Debug)]
pub(in crate::persistent) struct CancelThread {
stop: Arc<AtomicBool>,
cancel: CancellationToken,
cancelled: Arc<AtomicBool>,
thread: thread::JoinHandle<AppResult<()>>,
}
impl CancelThread {
pub(in crate::persistent) fn is_cancel_requested(&self) -> bool {
self.cancel.is_cancelled() || self.cancelled.load(Ordering::SeqCst)
}
pub(in crate::persistent) fn stop(self) -> AppResult<()> {
if self.is_cancel_requested() {
self.cancelled.store(true, Ordering::SeqCst);
}
self.stop.store(true, Ordering::SeqCst);
self.thread
.join()
.map_err(|_| AppError::new(ErrorCode::Internal, "process cancel thread panicked"))?
}
}
pub(in crate::persistent) fn spawn_cancel_thread(
pid: u32,
cancel: CancellationToken,
cancelled: Arc<AtomicBool>,
signal: SignalPolicy,
grace_period: Duration,
) -> AppResult<CancelThread> {
let stop = Arc::new(AtomicBool::new(false));
let thread_stop = Arc::clone(&stop);
let thread_cancel = cancel.clone();
let thread_cancelled = Arc::clone(&cancelled);
let group = targets_group(signal);
let thread = thread::spawn(move || {
while !thread_stop.load(Ordering::SeqCst) {
if thread_cancel.is_cancelled() {
thread_cancelled.store(true, Ordering::SeqCst);
let _ = terminate_target(pid, group);
escalate_after_grace(&thread_stop, pid, group, grace_period);
return Ok(());
}
thread::sleep(POLL_INTERVAL);
}
Ok(())
});
Ok(CancelThread {
stop,
cancel,
cancelled,
thread,
})
}
fn escalate_after_grace(stop: &AtomicBool, pid: u32, group: bool, grace_period: Duration) {
let deadline = Instant::now() + grace_period;
while !stop.load(Ordering::SeqCst) {
if Instant::now() >= deadline {
let _ = kill_target(pid, group);
return;
}
thread::sleep(POLL_INTERVAL);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cancel_thread_records_requested_cancellation_before_stop() {
let cancel = CancellationToken::new();
let cancelled = Arc::new(AtomicBool::new(false));
let thread = spawn_cancel_thread(
0,
cancel.clone(),
Arc::clone(&cancelled),
SignalPolicy::default(),
Duration::from_millis(1),
)
.unwrap();
cancel.cancel();
while !thread.is_cancel_requested() {
std::thread::sleep(Duration::from_millis(1));
}
assert!(thread.is_cancel_requested());
thread.stop().unwrap();
assert!(cancelled.load(Ordering::SeqCst));
}
}