//! A process a test starts, held so that it is killed and reaped however the test ends. //! //! A `std::process::Child` is not killed when it is dropped, so a test that failed before its //! last line left what it had started running after the test binary had ended: `wai http //! origin` holding its port, `wai dev` with the services it started, one more of each for every //! failed run. The integration tests that start a process that outlives a call include this file //! (`#[path = "support/running.rs"]`). #![allow(dead_code)] use std::process::Child; use std::time::{Duration, Instant}; /// How long [`Running`] waits for a process it has killed to be reaped. `SIGKILL` ends a /// process at once unless it is stuck in the kernel (on a disk or a mount that does not /// answer); a drop that waited on such a one without a bound would hang the test binary /// instead of failing it. const REAPED_WITHIN: Duration = Duration::from_secs(10); /// A process a test started, killed and reaped when it is dropped: at the end of the test, or /// while a failed assertion unwinds through it. /// /// It reads as the [`Child`] it holds: a test still kills it where it means to, waits for it, /// and asks whether it has ended. pub struct Running { child: Child, /// Whether the child leads a process group of its own, which is killed with it. group: bool, } impl Running { pub fn new(child: Child) -> Running { Running { child, group: false } } /// A child spawned as the leader of a process group of its own (`process_group(0)`), such /// as `wai dev`: the whole group is killed with it, so the services it started, which are in /// its group, end with it, where killing it alone would leave them running. pub fn group(child: Child) -> Running { Running { child, group: true } } /// Kills the process (with its group, for [`Running::group`]) and reaps it, waiting up to /// [`REAPED_WITHIN`]: whether it was reaped. A process that has already ended is reaped and /// not signalled (`Child::kill` does nothing then, as its pid, once reaped, may be another /// process's), and neither is its group: `wai dev` that ends of itself stops its services. pub fn stop(&mut self) -> bool { // The group first, while its leader is not yet reaped: until then the leader's pid, // which is the group's id, is no other process's. #[cfg(unix)] if self.group && matches!(self.child.try_wait(), Ok(None)) { let group = format!("-{}", self.child.id()); let _ = std::process::Command::new("kill").args(["-KILL", "--", &group]).stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null()).status(); } let _ = self.child.kill(); let t = Instant::now(); loop { match self.child.try_wait() { Ok(Some(_)) => return true, Ok(None) if t.elapsed() < REAPED_WITHIN => std::thread::sleep(Duration::from_millis(10)), _ => return false, } } } } impl std::ops::Deref for Running { type Target = Child; fn deref(&self) -> &Child { &self.child } } impl std::ops::DerefMut for Running { fn deref_mut(&mut self) -> &mut Child { &mut self.child } } impl Drop for Running { fn drop(&mut self) { if !self.stop() { eprintln!("process {} was killed and not reaped within {} s", self.child.id(), REAPED_WITHIN.as_secs()); } } }