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::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<std::process::Child>,
dropped_env: Vec<String>,
tmpdir: Option<PathBuf>,
reporting: Reporting,
receipt: crate::receipt::RunFacts,
killed: bool,
audit: crate::audit::RunAudit,
}
impl Child {
pub(super) fn new(
inner: std::process::Child,
dropped_env: Vec<String>,
tmpdir: Option<PathBuf>,
reporting: Reporting,
receipt: crate::receipt::RunFacts,
audit: crate::audit::RunAudit,
) -> Self {
Self {
inner: Some(inner),
dropped_env,
tmpdir,
reporting,
receipt,
killed: false,
audit,
}
}
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();
}
}
fn inner(&self) -> &std::process::Child {
self.inner
.as_ref()
.unwrap_or_else(|| unreachable!("present until drop or detach"))
}
fn inner_mut(&mut self) -> &mut std::process::Child {
self.inner
.as_mut()
.unwrap_or_else(|| unreachable!("present until drop or detach"))
}
pub fn kill_all(&mut self) -> io::Result<()> {
let child = self.inner_mut();
let pid =
libc::pid_t::try_from(child.id()).map_err(|_| io::Error::other("pid out of range"))?;
let running = child.try_wait()?.is_none();
if running {
for descendant in descendants(pid) {
sys::kill(descendant);
}
}
sys::killpg(pid);
let waited = child.wait().map(drop);
self.killed |= running;
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();
waited
}
pub fn detach(mut self) -> std::process::Child {
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,
}
}
}
fn parents() -> Vec<(libc::pid_t, libc::pid_t)> {
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 rest = &stat[stat.rfind(')')? + 1..];
let ppid = rest.split_whitespace().nth(1)?.parse().ok()?;
Some((pid, ppid))
})
.collect()
}
fn descendants(pid: libc::pid_t) -> Vec<libc::pid_t> {
let parents = parents();
let mut out = Vec::new();
let mut queue = vec![pid];
while let Some(p) = queue.pop() {
for &(child, parent) in &parents {
if parent == p && !out.contains(&child) {
out.push(child);
queue.push(child);
}
}
}
out
}
impl Deref for Child {
type Target = std::process::Child;
fn deref(&self) -> &std::process::Child {
self.inner()
}
}
impl DerefMut for Child {
fn deref_mut(&mut self) -> &mut std::process::Child {
self.inner_mut()
}
}
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);
}
}
}