use std::io;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use crate::unix::Proc;
use crate::violation::{Coverage, Sink, Violation, Violations, lock};
use super::sys;
#[derive(Debug)]
pub(super) enum Reporting {
Off,
Unavailable(String),
On { record: Sink, done: Arc<AtomicBool> },
}
#[derive(Debug)]
pub struct Child {
inner: Option<Proc>,
pid: u32,
dropped_env: Vec<String>,
tmpdir: Option<PathBuf>,
reporting: Reporting,
receipt: crate::receipt::RunFacts,
killed: bool,
audit: crate::audit::RunAudit,
#[cfg(feature = "proxy")]
proxy: Option<procage_proxy::Run>,
}
impl Child {
pub(super) fn new(
inner: Proc,
dropped_env: Vec<String>,
tmpdir: Option<PathBuf>,
reporting: Reporting,
receipt: crate::receipt::RunFacts,
audit: crate::audit::RunAudit,
) -> Self {
Self {
pid: inner.pid(),
inner: Some(inner),
dropped_env,
tmpdir,
reporting,
receipt,
killed: false,
audit,
#[cfg(feature = "proxy")]
proxy: None,
}
}
#[cfg(feature = "proxy")]
pub(super) fn set_proxy(&mut self, run: Option<procage_proxy::Run>) {
self.proxy = run;
}
pub fn audit(&mut self) -> Option<crate::Audit> {
self.inner_mut().try_wait().ok()??;
let ended = self.kill_all().is_ok();
Some(self.audit.finish(ended))
}
pub fn receipt(&mut self) -> Option<crate::Receipt> {
let status = self.inner_mut().try_wait().ok()??;
let ended = self.kill_all().is_ok();
let record = self.violations();
let audit = self.audit.finish(ended);
Some(self.receipt.receipt(status, &record, self.killed, &audit))
}
fn settle(&mut self) {
let Reporting::On { record, done } = &self.reporting else {
return;
};
let reaped = self
.inner
.as_mut()
.is_some_and(|c| matches!(c.try_wait(), Ok(Some(_))));
if !reaped {
return;
}
let start = Instant::now();
while !done.load(Ordering::Acquire) && start.elapsed() < Duration::from_secs(1) {
std::thread::sleep(Duration::from_millis(2));
}
if done.load(Ordering::Acquire) {
lock(record).finish();
}
}
pub(crate) fn inner(&self) -> &Proc {
self.inner
.as_ref()
.unwrap_or_else(|| unreachable!("present until drop or detach"))
}
pub(crate) fn inner_mut(&mut self) -> &mut Proc {
self.inner
.as_mut()
.unwrap_or_else(|| unreachable!("present until drop or detach"))
}
pub fn kill_all(&mut self) -> io::Result<()> {
self.signal()?;
let waited = self.inner_mut().wait().map(drop);
self.end(waited)
}
pub(crate) fn signal(&mut self) -> io::Result<()> {
let pid =
libc::pid_t::try_from(self.pid).map_err(|_| io::Error::other("pid out of range"))?;
let running = self.inner_mut().try_wait()?.is_none();
if running {
let below = descendants(pid);
sys::kill(pid);
for descendant in below {
sys::kill(descendant);
}
}
kill_session(pid);
self.killed |= running;
Ok(())
}
pub(crate) fn end(&mut self, waited: io::Result<()>) -> io::Result<()> {
#[cfg(feature = "proxy")]
drop(self.proxy.take());
if let Reporting::On { done, .. } = &self.reporting {
let start = Instant::now();
while !done.load(Ordering::Acquire) && start.elapsed() < Duration::from_secs(2) {
std::thread::sleep(Duration::from_millis(2));
}
}
self.settle();
if let Reporting::On { record, .. } = &self.reporting {
lock(record).close();
}
waited
}
pub fn detach(self) -> std::process::Child {
match self.release() {
Proc::Std(c) => c,
#[cfg(feature = "tokio")]
Proc::Tokio(_) => unreachable!("an async handle detaches through AsyncChild"),
}
}
pub(crate) fn release(mut self) -> Proc {
self.tmpdir = None;
self.inner
.take()
.unwrap_or_else(|| unreachable!("present until drop or detach"))
}
pub fn dropped_env(&self) -> &[String] {
&self.dropped_env
}
pub fn tmpdir(&self) -> Option<&Path> {
self.tmpdir.as_deref()
}
pub fn violations(&mut self) -> Violations {
self.settle();
match &self.reporting {
Reporting::Off => Violations::empty(Coverage::Off),
Reporting::Unavailable(reason) => {
Violations::empty(Coverage::Unavailable(reason.clone()))
}
Reporting::On { record, .. } => lock(record).snapshot(),
}
}
pub fn violation_receiver(&mut self) -> Option<std::sync::mpsc::Receiver<Violation>> {
match &self.reporting {
Reporting::On { record, .. } => lock(record).take_receiver(),
_ => None,
}
}
#[cfg(feature = "tokio")]
pub(crate) fn violation_stream(&mut self) -> Option<crate::ViolationStream> {
match &self.reporting {
Reporting::On { record, .. } => lock(record).take_stream(),
_ => None,
}
}
}
pub(super) fn kill_session(sid: libc::pid_t) {
let mut killed = Vec::new();
for _ in 0..64 {
let members: Vec<libc::pid_t> = processes()
.into_iter()
.filter(|p| p.session == sid && !p.zombie && !killed.contains(&p.pid))
.map(|p| p.pid)
.collect();
if members.is_empty() {
break;
}
for member in members {
sys::kill(member);
killed.push(member);
}
}
sys::killpg(sid);
}
fn processes() -> Vec<Process> {
let Ok(entries) = std::fs::read_dir("/proc") else {
return Vec::new();
};
entries
.flatten()
.filter_map(|e| {
let pid: libc::pid_t = e.file_name().to_str()?.parse().ok()?;
let stat = std::fs::read_to_string(e.path().join("stat")).ok()?;
let mut rest = stat[stat.rfind(')')? + 1..].split_whitespace();
let zombie = rest.next()? == "Z";
let ppid = rest.next()?.parse().ok()?;
let session = rest.nth(1)?.parse().ok()?;
Some(Process {
pid,
ppid,
session,
zombie,
})
})
.collect()
}
struct Process {
pid: libc::pid_t,
ppid: libc::pid_t,
session: libc::pid_t,
zombie: bool,
}
fn descendants(pid: libc::pid_t) -> Vec<libc::pid_t> {
let all = processes();
let mut out = Vec::new();
let mut queue = vec![pid];
while let Some(p) = queue.pop() {
for proc in &all {
if proc.ppid == p && !out.contains(&proc.pid) {
out.push(proc.pid);
queue.push(proc.pid);
}
}
}
out
}
impl Deref for Child {
type Target = std::process::Child;
fn deref(&self) -> &std::process::Child {
match self.inner() {
Proc::Std(c) => c,
#[cfg(feature = "tokio")]
Proc::Tokio(_) => unreachable!("an async handle derefs through AsyncChild"),
}
}
}
impl DerefMut for Child {
fn deref_mut(&mut self) -> &mut std::process::Child {
match self.inner_mut() {
Proc::Std(c) => c,
#[cfg(feature = "tokio")]
Proc::Tokio(_) => unreachable!("an async handle derefs through AsyncChild"),
}
}
}
impl Drop for Child {
fn drop(&mut self) {
if self.inner.is_some() {
let _ = self.kill_all();
}
if let Some(dir) = self.tmpdir.take() {
let _ = std::fs::remove_dir_all(dir);
}
}
}