process: Add unit tests for dropping killing dropped children

This commit is contained in:
Ivan Petkov
2019-06-24 16:57:20 -07:00
parent fa5da27d98
commit e90e33d5df
5 changed files with 196 additions and 29 deletions
+13
View File
@@ -0,0 +1,13 @@
use std::io;
/// An interface for killing a running process.
pub(crate) trait Kill {
/// Forcefully kill the process.
fn kill(&mut self) -> io::Result<()>;
}
impl<'a, T: 'a + Kill> Kill for &'a mut T {
fn kill(&mut self) -> io::Result<()> {
(**self).kill()
}
}
+160 -16
View File
@@ -172,8 +172,9 @@ extern crate log;
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
use std::process::{Command, ExitStatus, Output, Stdio}; use std::process::{Command, ExitStatus, Output, Stdio};
use futures::{Future, Poll, IntoFuture}; use futures::{Async, Future, Poll, IntoFuture};
use futures::future::{Either, ok}; use futures::future::{Either, ok};
use kill::Kill;
use std::fmt; use std::fmt;
use tokio_io::io::{read_to_end}; use tokio_io::io::{read_to_end};
use tokio_io::{AsyncWrite, AsyncRead, IoFuture}; use tokio_io::{AsyncWrite, AsyncRead, IoFuture};
@@ -187,6 +188,8 @@ mod imp;
#[cfg(windows)] #[cfg(windows)]
mod imp; mod imp;
mod kill;
/// Extensions provided by this crate to the `Command` type in the standard /// Extensions provided by this crate to the `Command` type in the standard
/// library. /// library.
/// ///
@@ -336,11 +339,10 @@ impl CommandExt for Command {
fn spawn_async_with_handle(&mut self, handle: &Handle) -> io::Result<Child> { fn spawn_async_with_handle(&mut self, handle: &Handle) -> io::Result<Child> {
imp::spawn_child(self, handle) imp::spawn_child(self, handle)
.map(|spawned_child| Child { .map(|spawned_child| Child {
child: spawned_child.child, child: ChildDropGuard::new(spawned_child.child),
stdin: spawned_child.stdin.map(|inner| ChildStdin { inner }), stdin: spawned_child.stdin.map(|inner| ChildStdin { inner }),
stdout: spawned_child.stdout.map(|inner| ChildStdout { inner }), stdout: spawned_child.stdout.map(|inner| ChildStdout { inner }),
stderr: spawned_child.stderr.map(|inner| ChildStderr { inner }), stderr: spawned_child.stderr.map(|inner| ChildStderr { inner }),
kill_on_drop: true,
}) })
} }
@@ -373,6 +375,58 @@ impl CommandExt for Command {
} }
} }
/// A drop guard which ensures the child process is killed on drop to maintain
/// the contract of dropping a Future leads to "cancellation".
#[derive(Debug)]
struct ChildDropGuard<T: Kill> {
inner: T,
kill_on_drop: bool,
}
impl<T: Kill> ChildDropGuard<T> {
fn new(inner: T) -> Self {
Self {
inner,
kill_on_drop: true,
}
}
fn forget(&mut self) {
self.kill_on_drop = false;
}
}
impl<T: Kill> Kill for ChildDropGuard<T> {
fn kill(&mut self) -> io::Result<()> {
self.inner.kill()
}
}
impl<T: Kill> Drop for ChildDropGuard<T> {
fn drop(&mut self) {
if self.kill_on_drop {
drop(self.kill());
}
}
}
impl<T: Future + Kill> Future for ChildDropGuard<T> {
type Item = T::Item;
type Error = T::Error;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
let ret = self.inner.poll();
if let Ok(Async::Ready(_)) = ret {
// Avoid the overhead of trying to kill a reaped process
self.kill_on_drop = false;
}
ret
}
}
/// Representation of a child process spawned onto an event loop. /// Representation of a child process spawned onto an event loop.
/// ///
/// This type is also a future which will yield the `ExitStatus` of the /// This type is also a future which will yield the `ExitStatus` of the
@@ -389,8 +443,7 @@ impl CommandExt for Command {
#[must_use = "futures do nothing unless polled"] #[must_use = "futures do nothing unless polled"]
#[derive(Debug)] #[derive(Debug)]
pub struct Child { pub struct Child {
child: imp::Child, child: ChildDropGuard<imp::Child>,
kill_on_drop: bool,
stdin: Option<ChildStdin>, stdin: Option<ChildStdin>,
stdout: Option<ChildStdout>, stdout: Option<ChildStdout>,
stderr: Option<ChildStderr>, stderr: Option<ChildStderr>,
@@ -399,7 +452,7 @@ pub struct Child {
impl Child { impl Child {
/// Returns the OS-assigned process identifier associated with this child. /// Returns the OS-assigned process identifier associated with this child.
pub fn id(&self) -> u32 { pub fn id(&self) -> u32 {
self.child.id() self.child.inner.id()
} }
/// Forces the child to exit. /// Forces the child to exit.
@@ -497,7 +550,7 @@ impl Child {
/// # } /// # }
/// ``` /// ```
pub fn forget(mut self) { pub fn forget(mut self) {
self.kill_on_drop = false; self.child.forget();
} }
} }
@@ -506,15 +559,7 @@ impl Future for Child {
type Error = io::Error; type Error = io::Error;
fn poll(&mut self) -> Poll<ExitStatus, io::Error> { fn poll(&mut self) -> Poll<ExitStatus, io::Error> {
self.child.poll_exit() self.child.poll()
}
}
impl Drop for Child {
fn drop(&mut self) {
if self.kill_on_drop {
drop(self.kill());
}
} }
} }
@@ -708,3 +753,102 @@ mod sys {
} }
} }
} }
#[cfg(test)]
mod test {
use futures::{Async, Future, Poll};
use kill::Kill;
use std::io;
use super::ChildDropGuard;
struct Mock {
num_kills: usize,
num_polls: usize,
poll_result: Poll<(), ()>,
}
impl Mock {
fn new() -> Self {
Self::with_result(Ok(Async::NotReady))
}
fn with_result(result: Poll<(), ()>) -> Self {
Self {
num_kills: 0,
num_polls: 0,
poll_result: result,
}
}
}
impl Kill for Mock {
fn kill(&mut self) -> io::Result<()> {
self.num_kills += 1;
Ok(())
}
}
impl Future for Mock {
type Item = ();
type Error = ();
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
self.num_polls += 1;
self.poll_result
}
}
#[test]
fn kills_on_drop() {
let mut mock = Mock::new();
{
let guard = ChildDropGuard::new(&mut mock);
drop(guard);
}
assert_eq!(1, mock.num_kills);
assert_eq!(0, mock.num_polls);
}
#[test]
fn no_kill_if_reaped() {
let mut mock_pending = Mock::with_result(Ok(Async::NotReady));
let mut mock_reaped = Mock::with_result(Ok(Async::Ready(())));
let mut mock_err = Mock::with_result(Err(()));
{
let mut guard = ChildDropGuard::new(&mut mock_pending);
let _ = guard.poll();
let mut guard = ChildDropGuard::new(&mut mock_reaped);
let _ = guard.poll();
let mut guard = ChildDropGuard::new(&mut mock_err);
let _ = guard.poll();
}
assert_eq!(1, mock_pending.num_kills);
assert_eq!(1, mock_pending.num_polls);
assert_eq!(0, mock_reaped.num_kills);
assert_eq!(1, mock_reaped.num_polls);
assert_eq!(1, mock_err.num_kills);
assert_eq!(1, mock_err.num_polls);
}
#[test]
fn no_kill_on_forget() {
let mut mock = Mock::new();
{
let mut guard = ChildDropGuard::new(&mut mock);
guard.forget();
drop(guard);
}
assert_eq!(0, mock.num_kills);
assert_eq!(0, mock.num_polls);
}
}
+11 -4
View File
@@ -30,11 +30,12 @@ mod reap;
use futures::future::FlattenStream; use futures::future::FlattenStream;
use futures::{Future, Poll}; use futures::{Future, Poll};
use kill::Kill;
use self::mio::{Poll as MioPoll, PollOpt, Ready, Token}; use self::mio::{Poll as MioPoll, PollOpt, Ready, Token};
use self::mio::unix::{EventedFd, UnixReady}; use self::mio::unix::{EventedFd, UnixReady};
use self::mio::event::Evented; use self::mio::event::Evented;
use self::orphan::{AtomicOrphanQueue, OrphanQueue, Wait}; use self::orphan::{AtomicOrphanQueue, OrphanQueue, Wait};
use self::reap::{Kill, Reaper}; use self::reap::Reaper;
use self::tokio_signal::unix::Signal; use self::tokio_signal::unix::Signal;
use std::fmt; use std::fmt;
use std::io; use std::io;
@@ -113,16 +114,22 @@ pub(crate) fn spawn_child(cmd: &mut process::Command, handle: &Handle) -> io::Re
} }
impl Child { impl Child {
pub fn id(&self) -> u32 { pub fn id(&self) -> u32 {
self.inner.id() self.inner.id()
} }
}
pub fn kill(&mut self) -> io::Result<()> { impl Kill for Child {
fn kill(&mut self) -> io::Result<()> {
self.inner.kill() self.inner.kill()
} }
}
pub fn poll_exit(&mut self) -> Poll<ExitStatus, io::Error> { impl Future for Child {
type Item = ExitStatus;
type Error = io::Error;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
self.inner.poll() self.inner.poll()
} }
} }
+1 -6
View File
@@ -1,15 +1,10 @@
use futures::{Async, Future, Poll, Stream}; use futures::{Async, Future, Poll, Stream};
use kill::Kill;
use std::io; use std::io;
use std::ops::Deref; use std::ops::Deref;
use std::process::ExitStatus; use std::process::ExitStatus;
use super::orphan::{OrphanQueue, Wait}; use super::orphan::{OrphanQueue, Wait};
/// An interface for killing a running process.
pub(crate) trait Kill {
/// Forcefully kill the process.
fn kill(&mut self) -> io::Result<()>;
}
/// Orchestrates between registering interest for receiving signals when a /// Orchestrates between registering interest for receiving signals when a
/// child process has exited, and attempting to poll for process completion. /// child process has exited, and attempting to poll for process completion.
#[derive(Debug)] #[derive(Debug)]
+11 -3
View File
@@ -27,7 +27,8 @@ use std::ptr;
use futures::future::Fuse; use futures::future::Fuse;
use futures::sync::oneshot; use futures::sync::oneshot;
use futures::{Future, Poll, Async} ; use futures::{Future, Poll, Async};
use kill::Kill;
use self::mio_named_pipes::NamedPipe; use self::mio_named_pipes::NamedPipe;
use self::winapi::shared::minwindef::*; use self::winapi::shared::minwindef::*;
use self::winapi::shared::winerror::*; use self::winapi::shared::winerror::*;
@@ -86,12 +87,19 @@ impl Child {
pub fn id(&self) -> u32 { pub fn id(&self) -> u32 {
self.child.id() self.child.id()
} }
}
pub fn kill(&mut self) -> io::Result<()> { impl Kill for Child {
fn kill(&mut self) -> io::Result<()> {
self.child.kill() self.child.kill()
} }
}
pub fn poll_exit(&mut self) -> Poll<ExitStatus, io::Error> { impl Future for Child {
type Item = ExitStatus;
type Error = io::Error;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
loop { loop {
if let Some(ref mut w) = self.waiting { if let Some(ref mut w) = self.waiting {
match w.rx.poll().expect("should not be canceled") { match w.rx.poll().expect("should not be canceled") {