use tokio::process::Child;
use tokio::runtime::Handle;
use tokio::task::{AbortHandle, JoinHandle};
pub(super) struct ChildScope {
child: Option<Child>,
aborts: Vec<AbortHandle>,
armed: bool,
}
impl ChildScope {
pub(super) fn new(child: Child) -> Self {
Self {
child: Some(child),
aborts: Vec::new(),
armed: true,
}
}
pub(super) fn register<T>(&mut self, task: &Option<JoinHandle<T>>) {
if let Some(task) = task {
self.aborts.push(task.abort_handle());
}
}
pub(super) fn child_mut(&mut self) -> &mut Child {
match &mut self.child {
Some(child) => child,
None => unreachable!("ChildScope::child_mut called after the child was taken"),
}
}
pub(super) fn disarm(&mut self) {
self.armed = false;
}
}
impl Drop for ChildScope {
fn drop(&mut self) {
if !self.armed {
return;
}
for abort in &self.aborts {
abort.abort();
}
if let Some(mut child) = self.child.take() {
let _ = child.start_kill();
if let Ok(handle) = Handle::try_current() {
handle.spawn(async move {
let _ = child.wait().await;
});
}
}
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use tokio::process::Command;
use super::ChildScope;
fn spawn_sleeper() -> tokio::process::Child {
Command::new("/bin/sleep")
.arg("30")
.kill_on_drop(false)
.spawn()
.expect("spawn sleep")
}
#[tokio::test]
async fn dropping_an_armed_scope_aborts_tasks_and_kills_the_child() {
let child = spawn_sleeper();
let some_task = Some(tokio::spawn(async {
tokio::time::sleep(Duration::from_secs(30)).await;
}));
let mut scope = ChildScope::new(child);
scope.register(&some_task);
drop(scope);
let Some(task) = some_task else {
unreachable!("task was just registered")
};
assert!(task.await.unwrap_err().is_cancelled());
}
#[tokio::test]
async fn disarming_the_scope_leaves_the_child_and_tasks_alone() {
let some_task = Some(tokio::spawn(async {
tokio::time::sleep(Duration::from_millis(10)).await;
}));
let mut scope = ChildScope::new(spawn_sleeper());
scope.register(&some_task);
scope.disarm();
let Some(task) = some_task else {
unreachable!("task was just registered")
};
task.await.expect("task completed");
assert!(
scope.child_mut().try_wait().expect("try_wait").is_none(),
"a disarmed scope must not kill the child"
);
scope.child_mut().start_kill().expect("kill child");
scope.child_mut().wait().await.expect("reap child");
}
}