use std::{
collections::HashMap,
path::PathBuf,
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc, Mutex, MutexGuard, OnceLock,
},
time::Duration,
};
use subc_control::ModuleProtocol;
use tracing::warn;
use crate::live_children::{ExecutableIdentity, LiveChild};
#[cfg_attr(not(unix), allow(dead_code))]
#[derive(Debug, Clone)]
pub(crate) struct RosterEntry {
pub(crate) module_id: String,
pub(crate) pid: u32,
pub(crate) protocol: ModuleProtocol,
pub(crate) start_time: Option<u64>,
recorded: RecordedIdentity,
drain_budget: Arc<Mutex<Duration>>,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct RecordedIdentity {
pub(crate) start_time: Option<u64>,
pub(crate) executable: Option<ExecutableIdentity>,
pub(crate) cgroup_name: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct DaemonShutdownFlag(Arc<AtomicBool>);
impl DaemonShutdownFlag {
pub(crate) fn is_set(&self) -> bool {
self.0.load(Ordering::SeqCst)
}
#[cfg(unix)]
fn set(&self) {
self.0.store(true, Ordering::SeqCst);
}
}
#[derive(Debug, Default)]
struct RosterInner {
next_key: AtomicU64,
closed: DaemonShutdownFlag,
live: Mutex<HashMap<u64, RosterEntry>>,
record_path: OnceLock<PathBuf>,
}
impl RosterInner {
fn write_record(&self, live: &HashMap<u64, RosterEntry>) {
let Some(path) = self.record_path.get() else {
return;
};
let mut entries: Vec<(&u64, &RosterEntry)> = live.iter().collect();
entries.sort_by_key(|(key, _)| **key);
let children: Vec<LiveChild> = entries
.into_iter()
.map(|(_, entry)| LiveChild {
module_id: entry.module_id.clone(),
pid: entry.pid,
protocol: entry.protocol,
start_time: entry.recorded.start_time,
executable: entry.recorded.executable,
cgroup_name: entry.recorded.cgroup_name.clone(),
})
.collect();
if let Err(error) = crate::live_children::write_record(path, &children) {
warn!(
path = %path.display(),
%error,
"could not rewrite the live-children record; a crash now could leave orphans the next boot cannot find"
);
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct ChildRoster {
inner: Arc<RosterInner>,
drain_budget: Arc<Mutex<Duration>>,
}
impl Default for ChildRoster {
fn default() -> Self {
Self {
inner: Arc::default(),
drain_budget: Arc::new(Mutex::new(crate::supervise::DEFAULT_DRAIN_TIMEOUT)),
}
}
}
#[derive(Debug)]
pub(crate) struct RosterGuard {
inner: Arc<RosterInner>,
key: u64,
}
impl Drop for RosterGuard {
fn drop(&mut self) {
let mut live = lock(&self.inner.live);
if live.remove(&self.key).is_some() {
self.inner.write_record(&live);
}
}
}
impl ChildRoster {
pub(crate) fn for_module(&self, drain_budget: Arc<Mutex<Duration>>) -> Self {
Self {
inner: Arc::clone(&self.inner),
drain_budget,
}
}
pub(crate) fn is_closed(&self) -> bool {
self.inner.closed.is_set()
}
pub(crate) fn shutdown_flag(&self) -> DaemonShutdownFlag {
self.inner.closed.clone()
}
pub(crate) fn record_to(&self, path: PathBuf) {
let _ = self.inner.record_path.set(path);
}
pub(crate) fn admit(
&self,
module_id: String,
pid: u32,
protocol: ModuleProtocol,
start_time: Option<u64>,
recorded: RecordedIdentity,
) -> RosterGuard {
let key = self.inner.next_key.fetch_add(1, Ordering::Relaxed);
let mut live = lock(&self.inner.live);
live.insert(
key,
RosterEntry {
module_id,
pid,
protocol,
start_time,
recorded,
drain_budget: Arc::clone(&self.drain_budget),
},
);
self.inner.write_record(&live);
drop(live);
RosterGuard {
inner: Arc::clone(&self.inner),
key,
}
}
#[cfg(unix)]
fn live(&self) -> Vec<(u64, RosterEntry)> {
lock(&self.inner.live)
.iter()
.map(|(key, entry)| (*key, entry.clone()))
.collect()
}
#[cfg(unix)]
pub(crate) fn close(&self) {
self.inner.closed.set();
}
}
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[cfg(unix)]
pub(crate) use unix_shutdown::end_children_for_daemon_shutdown;
#[cfg(unix)]
mod unix_shutdown {
use std::{collections::HashSet, future::Future, time::Duration};
use rustix::process::{kill_process, Pid, Signal};
use tokio::time::{sleep, Instant};
use tracing::{debug, info, warn};
use super::{lock, ChildRoster, RosterEntry};
use subc_control::ModuleProtocol;
const CHILD_SHUTDOWN_CAP: Duration = Duration::from_secs(25);
const TERM_TO_KILL: Duration = Duration::from_millis(500);
const CHILD_REAP_BOUND: Duration = Duration::from_millis(250);
const POLL: Duration = Duration::from_millis(10);
pub(crate) async fn end_children_for_daemon_shutdown(
roster: &ChildRoster,
already_escalated: bool,
escalate: impl Future<Output = ()>,
) {
roster.close();
tokio::pin!(escalate);
let started = Instant::now();
let mut termed = HashSet::new();
let mut killed = HashSet::new();
let mut last_kill: Option<Instant> = None;
let mut escalated = already_escalated;
if !escalated {
for (key, entry) in roster.live() {
if entry.protocol == ModuleProtocol::None {
signal(&entry, Signal::TERM);
termed.insert(key);
}
}
}
loop {
let live = roster.live();
if live.is_empty() {
return;
}
let now = Instant::now();
for (key, entry) in &live {
if killed.contains(key) {
continue;
}
let deadline = started + budget(entry);
let kill_at = match entry.protocol {
ModuleProtocol::None => deadline,
ModuleProtocol::Subc => deadline + TERM_TO_KILL,
};
if escalated || now >= kill_at {
warn!(
module_id = %entry.module_id,
pid = entry.pid,
escalated,
"supervised child did not exit during daemon shutdown; sending SIGKILL"
);
signal(entry, Signal::KILL);
killed.insert(*key);
last_kill = Some(now);
} else if now >= deadline && termed.insert(*key) {
warn!(
module_id = %entry.module_id,
pid = entry.pid,
"supervised module still running at its shutdown deadline after EOF; sending SIGTERM"
);
signal(entry, Signal::TERM);
}
}
if live.iter().all(|(key, _)| killed.contains(key))
&& last_kill.is_some_and(|at| now >= at + CHILD_REAP_BOUND)
{
return;
}
if escalated {
sleep(POLL).await;
continue;
}
tokio::select! {
biased;
_ = escalate.as_mut() => {
info!("second SIGTERM: killing remaining supervised children without further grace");
escalated = true;
}
_ = sleep(POLL) => {}
}
}
}
fn budget(entry: &RosterEntry) -> Duration {
(*lock(&entry.drain_budget)).min(CHILD_SHUTDOWN_CAP)
}
fn signal(entry: &RosterEntry, signal: Signal) {
if let Some(expected) = entry.start_time {
if crate::provenance::process_start_time(entry.pid) != Some(expected) {
debug!(
module_id = %entry.module_id,
pid = entry.pid,
"supervised child already gone; not signalling its pid"
);
return;
}
}
let Some(pid) = i32::try_from(entry.pid).ok().and_then(Pid::from_raw) else {
return;
};
if let Err(error) = kill_process(pid, signal) {
debug!(
module_id = %entry.module_id,
pid = entry.pid,
?signal,
%error,
"signal to supervised child failed; it has most likely already exited"
);
}
}
}