use std::io;
use std::time::Duration;
use tokio::process::{Child, Command};
use crate::Mechanism;
#[cfg(feature = "process-control")]
use crate::Signal;
#[cfg(feature = "limits")]
use crate::limits::ResourceLimits;
#[cfg(feature = "stats")]
use crate::stats::ProcessGroupStats;
#[cfg(unix)]
pub(crate) const SIGTERM_RAW: i32 = libc::SIGTERM;
#[cfg(not(unix))]
pub(crate) const SIGTERM_RAW: i32 = 15;
#[derive(Debug, Default)]
pub(crate) struct SkipDropKill(std::sync::atomic::AtomicUsize);
#[derive(Debug, Clone, Copy)]
pub(crate) struct ShutdownEpoch(usize);
impl SkipDropKill {
const SKIP_BIT: usize = 1;
const GEN_STEP: usize = Self::SKIP_BIT << 1;
pub(crate) fn new() -> Self {
Self(std::sync::atomic::AtomicUsize::new(0))
}
pub(crate) fn begin_shutdown(&self) -> ShutdownEpoch {
use std::sync::atomic::Ordering;
ShutdownEpoch(self.0.load(Ordering::Acquire) & !Self::SKIP_BIT)
}
pub(crate) fn request(&self, epoch: ShutdownEpoch) {
use std::sync::atomic::Ordering;
let _ = self.0.compare_exchange(
epoch.0,
epoch.0 | Self::SKIP_BIT,
Ordering::Release,
Ordering::Relaxed,
);
}
pub(crate) fn clear(&self) {
use std::sync::atomic::Ordering;
let mut cur = self.0.load(Ordering::Relaxed);
loop {
let next = cur.wrapping_add(Self::GEN_STEP) & !Self::SKIP_BIT;
match self
.0
.compare_exchange_weak(cur, next, Ordering::Release, Ordering::Relaxed)
{
Ok(_) => return,
Err(actual) => cur = actual,
}
}
}
pub(crate) fn is_set(&self) -> bool {
use std::sync::atomic::Ordering;
(self.0.load(Ordering::Acquire) & Self::SKIP_BIT) != 0
}
}
#[cfg(test)]
mod skip_drop_kill_tests {
use super::SkipDropKill;
#[test]
fn request_then_clear_re_arms_the_drop_kill() {
let latch = SkipDropKill::new();
assert!(!latch.is_set(), "a fresh latch does not skip Drop's kill");
let epoch = latch.begin_shutdown();
latch.request(epoch);
assert!(latch.is_set(), "request() spares survivors on Drop");
latch.clear();
assert!(
!latch.is_set(),
"clear() re-arms Drop's kill for a reused group"
);
}
#[test]
fn a_stale_request_does_not_override_a_concurrent_clear() {
let latch = SkipDropKill::new();
latch.clear();
let epoch = latch.begin_shutdown();
latch.clear();
latch.request(epoch);
assert!(
!latch.is_set(),
"a stale non-escalating request must not re-spare a child that a \
concurrent spawn/adopt already re-armed the backstop for"
);
}
#[test]
fn request_spares_survivors_when_no_clear_intervenes() {
let latch = SkipDropKill::new();
latch.clear(); let epoch = latch.begin_shutdown();
latch.request(epoch);
assert!(
latch.is_set(),
"an unraced non-escalating request spares survivors"
);
latch.clear();
assert!(
!latch.is_set(),
"a spawn after the spare re-arms Drop's kill"
);
}
#[test]
fn a_stale_epoch_never_matches_a_later_generation() {
let latch = SkipDropKill::new();
let stale = latch.begin_shutdown();
latch.clear(); let fresh = latch.begin_shutdown();
latch.request(stale);
assert!(
!latch.is_set(),
"a stale epoch cannot spare at a newer generation"
);
latch.request(fresh);
assert!(
latch.is_set(),
"a current-generation request spares as usual"
);
}
}
#[cfg(feature = "stats")]
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct ProcMetrics {
pub cpu_time: Option<Duration>,
pub peak_memory_bytes: Option<u64>,
}
#[cfg(feature = "stats")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ProcIdentity(u64);
#[cfg(feature = "stats")]
#[cfg_attr(all(unix, not(target_os = "linux")), allow(dead_code))]
impl ProcIdentity {
pub(crate) fn from_raw(raw: u64) -> Self {
Self(raw)
}
pub(crate) fn raw(self) -> u64 {
self.0
}
}
#[cfg(feature = "stats")]
pub(crate) fn process_metrics(pid: u32, expected: Option<ProcIdentity>) -> ProcMetrics {
imp::process_metrics(pid, expected)
}
#[cfg(feature = "stats")]
pub(crate) fn process_identity(pid: u32) -> Option<ProcIdentity> {
imp::process_identity(pid)
}
#[cfg(unix)]
pub(crate) mod pgroup;
#[cfg(unix)]
pub(crate) mod graceful;
pub(crate) mod pid_gate;
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct SpawnOptions {
#[cfg_attr(not(unix), allow(dead_code))]
pub setsid: bool,
#[cfg_attr(not(windows), allow(dead_code))]
pub creation_flags: u32,
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub kill_on_parent_death: bool,
}
#[cfg(not(any(unix, windows)))]
compile_error!(
"processkit supports only Unix and Windows targets — it requires tokio::process \
and OS job/process-group primitives unavailable on this target."
);
#[cfg_attr(windows, path = "windows.rs")]
#[cfg_attr(target_os = "linux", path = "linux.rs")]
#[cfg_attr(all(unix, not(target_os = "linux")), path = "unix.rs")]
mod imp;
pub(crate) struct Job(imp::Job);
impl Job {
#[cfg(feature = "limits")]
pub(crate) fn new(limits: &ResourceLimits) -> io::Result<Self> {
imp::Job::new(limits).map(Job)
}
#[cfg(not(feature = "limits"))]
pub(crate) fn new() -> io::Result<Self> {
imp::Job::new().map(Job)
}
pub(crate) fn spawn(&self, cmd: &mut Command, opts: &SpawnOptions) -> io::Result<Child> {
self.0.spawn(cmd, opts)
}
#[cfg(feature = "process-control")]
pub(crate) fn adopt(&self, child: &Child) -> io::Result<()> {
self.0.adopt(child)
}
pub(crate) fn kill_all(&self) -> io::Result<()> {
self.0.kill_all()
}
#[cfg(feature = "process-control")]
pub(crate) fn signal(&self, sig: Signal) -> io::Result<()> {
self.0.signal(sig)
}
#[cfg(feature = "process-control")]
pub(crate) fn suspend(&self) -> io::Result<()> {
self.0.suspend()
}
#[cfg(feature = "process-control")]
pub(crate) fn resume(&self) -> io::Result<()> {
self.0.resume()
}
#[cfg(feature = "process-control")]
pub(crate) fn members(&self) -> io::Result<Vec<u32>> {
self.0.members()
}
pub(crate) async fn graceful_shutdown(
&self,
signal: i32,
timeout: Duration,
escalate: bool,
) -> io::Result<()> {
self.0.graceful_shutdown(signal, timeout, escalate).await
}
#[cfg(feature = "stats")]
pub(crate) fn stats(&self) -> io::Result<ProcessGroupStats> {
self.0.stats()
}
pub(crate) fn mechanism(&self) -> Mechanism {
self.0.mechanism()
}
}