aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Walbran <qwandor@google.com>2021-03-31 16:22:22 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2021-03-31 16:22:22 +0000
commit1ab603b01bc271d851187790cc93f8d9e4a6f6aa (patch)
tree2377f0703a57702b707bb56dacd6c88c6cd73e79
parentf2b560084491ce3f9da4bc451322e80d10bde4e9 (diff)
parentea0d2d32318cc81aca08b64d2f34cb9ee9aef3e5 (diff)
downloadshared_child-1ab603b01bc271d851187790cc93f8d9e4a6f6aa.tar.gz
Import shared_child crate. am: ea0d2d3231
Original change: https://android-review.googlesource.com/c/platform/external/rust/crates/shared_child/+/1658331 Change-Id: Ibfb1e9f1a5c40e742d3894164f7b730ec31b6db7
-rw-r--r--.cargo_vcs_info.json5
-rw-r--r--.gitignore2
-rw-r--r--.travis.yml8
-rw-r--r--Android.bp41
-rw-r--r--Cargo.toml27
-rw-r--r--Cargo.toml.orig16
-rw-r--r--LICENSE19
-rw-r--r--METADATA19
-rw-r--r--MODULE_LICENSE_MIT0
-rw-r--r--OWNERS1
-rw-r--r--README.md64
-rw-r--r--README.tpl3
-rw-r--r--TEST_MAPPING9
-rw-r--r--appveyor.yml32
-rw-r--r--src/lib.rs333
-rw-r--r--src/sys/mod.rs9
-rw-r--r--src/sys/unix.rs81
-rw-r--r--src/sys/windows.rs44
-rw-r--r--src/unix.rs46
19 files changed, 759 insertions, 0 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
new file mode 100644
index 0000000..d5c37ad
--- /dev/null
+++ b/.cargo_vcs_info.json
@@ -0,0 +1,5 @@
+{
+ "git": {
+ "sha1": "d23a8b12f0228b9a9ebe7983f44b8fec1d815b9c"
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a9d37c5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+target
+Cargo.lock
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..0d4ad53
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,8 @@
+language: rust
+os:
+ - linux
+ - osx
+rust:
+ - stable
+ - beta
+ - nightly
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..79874d6
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,41 @@
+// This file is generated by cargo2android.py --run --device --dependencies --tests.
+// Do not modify this file as changes will be overridden on upgrade.
+
+rust_library {
+ name: "libshared_child",
+ host_supported: true,
+ crate_name: "shared_child",
+ srcs: ["src/lib.rs"],
+ edition: "2015",
+ rustlibs: [
+ "liblibc",
+ ],
+}
+
+rust_defaults {
+ name: "shared_child_defaults",
+ crate_name: "shared_child",
+ srcs: ["src/lib.rs"],
+ test_suites: ["general-tests"],
+ auto_gen_config: true,
+ edition: "2015",
+ rustlibs: [
+ "liblibc",
+ ],
+}
+
+rust_test_host {
+ name: "shared_child_host_test_src_lib",
+ defaults: ["shared_child_defaults"],
+ test_options: {
+ unit_test: true,
+ },
+}
+
+rust_test {
+ name: "shared_child_device_test_src_lib",
+ defaults: ["shared_child_defaults"],
+}
+
+// dependent_library ["feature_list"]
+// libc-0.2.88 "default,std"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..e9fc07c
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,27 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies
+#
+# If you believe there's an error in this file please file an
+# issue against the rust-lang/cargo repository. If you're
+# editing this file be aware that the upstream Cargo.toml
+# will likely look very different (and much more reasonable)
+
+[package]
+name = "shared_child"
+version = "0.3.4"
+authors = ["jacko"]
+description = "a library for using child processes from multiple threads"
+documentation = "https://docs.rs/shared_child"
+keywords = ["command", "process", "child", "subprocess"]
+categories = ["os"]
+license = "MIT"
+repository = "https://github.com/oconnor663/shared_child.rs"
+[target."cfg(not(windows))".dependencies.libc]
+version = "0.2.42"
+[target."cfg(windows)".dependencies.winapi]
+version = "0.3.5"
+features = ["synchapi", "winbase", "winerror", "winnt"]
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
new file mode 100644
index 0000000..0de5303
--- /dev/null
+++ b/Cargo.toml.orig
@@ -0,0 +1,16 @@
+[package]
+name = "shared_child"
+version = "0.3.4"
+authors = ["jacko"]
+license = "MIT"
+repository = "https://github.com/oconnor663/shared_child.rs"
+documentation = "https://docs.rs/shared_child"
+description = "a library for using child processes from multiple threads"
+keywords = ["command", "process", "child", "subprocess"]
+categories = ["os"]
+
+[target.'cfg(not(windows))'.dependencies]
+libc = "0.2.42"
+
+[target.'cfg(windows)'.dependencies]
+winapi = { version = "0.3.5", features = ["synchapi", "winbase", "winerror", "winnt"] }
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..72dc60d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+The MIT License (MIT)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..57e4d01
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,19 @@
+name: "shared_child"
+description: "a library for using child processes from multiple threads"
+third_party {
+ url {
+ type: HOMEPAGE
+ value: "https://crates.io/crates/shared_child"
+ }
+ url {
+ type: ARCHIVE
+ value: "https://static.crates.io/crates/shared_child/shared_child-0.3.4.crate"
+ }
+ version: "0.3.4"
+ license_type: NOTICE
+ last_upgrade_date {
+ year: 2021
+ month: 3
+ day: 12
+ }
+}
diff --git a/MODULE_LICENSE_MIT b/MODULE_LICENSE_MIT
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_MIT
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..46fc303
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1 @@
+include platform/prebuilts/rust:/OWNERS
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..94525fa
--- /dev/null
+++ b/README.md
@@ -0,0 +1,64 @@
+# shared_child.rs [![Travis build](https://travis-ci.org/oconnor663/shared_child.rs.svg?branch=master)](https://travis-ci.org/oconnor663/shared_child.rs) [![Build status](https://ci.appveyor.com/api/projects/status/900ckow3c5awq3t5/branch/master?svg=true)](https://ci.appveyor.com/project/oconnor663/shared-child-rs/branch/master) [![crates.io](https://img.shields.io/crates/v/shared_child.svg)](https://crates.io/crates/shared_child) [![docs.rs](https://docs.rs/shared_child/badge.svg)](https://docs.rs/shared_child)
+
+A library for awaiting and killing child processes from multiple threads.
+
+- [Docs](https://docs.rs/shared_child)
+- [Crate](https://crates.io/crates/shared_child)
+- [Repo](https://github.com/oconnor663/shared_child.rs)
+
+The
+[`std::process::Child`](https://doc.rust-lang.org/std/process/struct.Child.html)
+type in the standard library provides
+[`wait`](https://doc.rust-lang.org/std/process/struct.Child.html#method.wait)
+and
+[`kill`](https://doc.rust-lang.org/std/process/struct.Child.html#method.kill)
+methods that take `&mut self`, making it impossible to kill a child process
+while another thread is waiting on it. That design works around a race
+condition in Unix's `waitpid` function, where a PID might get reused as soon
+as the wait returns, so a signal sent around the same time could
+accidentally get delivered to the wrong process.
+
+However with the newer POSIX `waitid` function, we can wait on a child
+without freeing its PID for reuse. That makes it safe to send signals
+concurrently. Windows has actually always supported this, by preventing PID
+reuse while there are still open handles to a child process. This library
+wraps `std::process::Child` for concurrent use, backed by these APIs.
+
+Compatibility note: The `libc` crate doesn't currently support `waitid` on
+NetBSD or OpenBSD, or on older versions of OSX. There [might also
+be](https://bugs.python.org/msg167016) some version of OSX where the
+`waitid` function exists but is broken. We can add a "best effort"
+workaround using `waitpid` for these platforms as we run into them. Please
+[file an issue](https://github.com/oconnor663/shared_child.rs/issues/new) if
+you hit this.
+
+## Example
+
+```rust
+use shared_child::SharedChild;
+use std::process::Command;
+use std::sync::Arc;
+
+// Spawn a child that will just sleep for a long time,
+// and put it in an Arc to share between threads.
+let mut command = Command::new("python");
+command.arg("-c").arg("import time; time.sleep(1000000000)");
+let shared_child = SharedChild::spawn(&mut command).unwrap();
+let child_arc = Arc::new(shared_child);
+
+// On another thread, wait on the child process.
+let child_arc_clone = child_arc.clone();
+let thread = std::thread::spawn(move || {
+ child_arc_clone.wait().unwrap()
+});
+
+// While the other thread is waiting, kill the child process.
+// This wouldn't be possible with e.g. Arc<Mutex<Child>> from
+// the standard library, because the waiting thread would be
+// holding the mutex.
+child_arc.kill().unwrap();
+
+// Join the waiting thread and get the exit status.
+let exit_status = thread.join().unwrap();
+assert!(!exit_status.success());
+```
diff --git a/README.tpl b/README.tpl
new file mode 100644
index 0000000..3ad7e39
--- /dev/null
+++ b/README.tpl
@@ -0,0 +1,3 @@
+# {{crate}}.rs [![Travis build](https://travis-ci.org/oconnor663/shared_child.rs.svg?branch=master)](https://travis-ci.org/oconnor663/shared_child.rs) [![Build status](https://ci.appveyor.com/api/projects/status/900ckow3c5awq3t5/branch/master?svg=true)](https://ci.appveyor.com/project/oconnor663/shared-child-rs/branch/master) [![crates.io](https://img.shields.io/crates/v/shared_child.svg)](https://crates.io/crates/shared_child) [![docs.rs](https://docs.rs/shared_child/badge.svg)](https://docs.rs/shared_child)
+
+{{readme}}
diff --git a/TEST_MAPPING b/TEST_MAPPING
new file mode 100644
index 0000000..f4635ab
--- /dev/null
+++ b/TEST_MAPPING
@@ -0,0 +1,9 @@
+// Generated by update_crate_tests.py for tests that depend on this crate.
+{
+ "presubmit": [
+ {
+ // Tests currently depend on Python, so commenting out until that is fixed.
+ //"name": "shared_child_device_test_src_lib"
+ }
+ ]
+}
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..11d0f88
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,32 @@
+environment:
+ matrix:
+ - TARGET: x86_64-pc-windows-msvc
+ VERSION: 1.16.0
+ - TARGET: i686-pc-windows-msvc
+ VERSION: 1.16.0
+ - TARGET: i686-pc-windows-gnu
+ VERSION: 1.16.0
+ - TARGET: x86_64-pc-windows-msvc
+ VERSION: beta
+ - TARGET: i686-pc-windows-msvc
+ VERSION: beta
+ - TARGET: i686-pc-windows-gnu
+ VERSION: beta
+ - TARGET: x86_64-pc-windows-msvc
+ VERSION: nightly
+ - TARGET: i686-pc-windows-msvc
+ VERSION: nightly
+ - TARGET: i686-pc-windows-gnu
+ VERSION: nightly
+install:
+ - ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-${env:VERSION}-${env:TARGET}.exe"
+ - rust-%VERSION%-%TARGET%.exe /VERYSILENT /NORESTART /DIR="C:\Program Files (x86)\Rust"
+ - SET PATH=%PATH%;C:\Program Files (x86)\Rust\bin
+ - SET PATH=%PATH%;C:\MinGW\bin
+ - rustc -V
+ - cargo -V
+
+build: false
+
+test_script:
+ - cargo test --verbose
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..ee77aa4
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,333 @@
+//! A library for awaiting and killing child processes from multiple threads.
+//!
+//! - [Docs](https://docs.rs/shared_child)
+//! - [Crate](https://crates.io/crates/shared_child)
+//! - [Repo](https://github.com/oconnor663/shared_child.rs)
+//!
+//! The
+//! [`std::process::Child`](https://doc.rust-lang.org/std/process/struct.Child.html)
+//! type in the standard library provides
+//! [`wait`](https://doc.rust-lang.org/std/process/struct.Child.html#method.wait)
+//! and
+//! [`kill`](https://doc.rust-lang.org/std/process/struct.Child.html#method.kill)
+//! methods that take `&mut self`, making it impossible to kill a child process
+//! while another thread is waiting on it. That design works around a race
+//! condition in Unix's `waitpid` function, where a PID might get reused as soon
+//! as the wait returns, so a signal sent around the same time could
+//! accidentally get delivered to the wrong process.
+//!
+//! However with the newer POSIX `waitid` function, we can wait on a child
+//! without freeing its PID for reuse. That makes it safe to send signals
+//! concurrently. Windows has actually always supported this, by preventing PID
+//! reuse while there are still open handles to a child process. This library
+//! wraps `std::process::Child` for concurrent use, backed by these APIs.
+//!
+//! Compatibility note: The `libc` crate doesn't currently support `waitid` on
+//! NetBSD or OpenBSD, or on older versions of OSX. There [might also
+//! be](https://bugs.python.org/msg167016) some version of OSX where the
+//! `waitid` function exists but is broken. We can add a "best effort"
+//! workaround using `waitpid` for these platforms as we run into them. Please
+//! [file an issue](https://github.com/oconnor663/shared_child.rs/issues/new) if
+//! you hit this.
+//!
+//! # Example
+//!
+//! ```rust
+//! use shared_child::SharedChild;
+//! use std::process::Command;
+//! use std::sync::Arc;
+//!
+//! // Spawn a child that will just sleep for a long time,
+//! // and put it in an Arc to share between threads.
+//! let mut command = Command::new("python");
+//! command.arg("-c").arg("import time; time.sleep(1000000000)");
+//! let shared_child = SharedChild::spawn(&mut command).unwrap();
+//! let child_arc = Arc::new(shared_child);
+//!
+//! // On another thread, wait on the child process.
+//! let child_arc_clone = child_arc.clone();
+//! let thread = std::thread::spawn(move || {
+//! child_arc_clone.wait().unwrap()
+//! });
+//!
+//! // While the other thread is waiting, kill the child process.
+//! // This wouldn't be possible with e.g. Arc<Mutex<Child>> from
+//! // the standard library, because the waiting thread would be
+//! // holding the mutex.
+//! child_arc.kill().unwrap();
+//!
+//! // Join the waiting thread and get the exit status.
+//! let exit_status = thread.join().unwrap();
+//! assert!(!exit_status.success());
+//! ```
+
+use std::io;
+use std::process::{Child, Command, ExitStatus};
+use std::sync::{Condvar, Mutex};
+
+mod sys;
+
+// Publish the Unix-only SharedChildExt trait.
+#[cfg(unix)]
+pub mod unix;
+
+#[derive(Debug)]
+pub struct SharedChild {
+ // This lock provides shared access to kill() and wait(). We never hold it
+ // during a blocking wait, though, so that non-blocking waits and kills can
+ // go through. (Blocking waits use libc::waitid with the WNOWAIT flag.)
+ child: Mutex<Child>,
+
+ // When there are multiple waiting threads, one of them will actually wait
+ // on the child, and the rest will block on this condvar.
+ state_lock: Mutex<ChildState>,
+ state_condvar: Condvar,
+}
+
+impl SharedChild {
+ /// Spawn a new `SharedChild` from a `std::process::Command`.
+ pub fn spawn(command: &mut Command) -> io::Result<SharedChild> {
+ let child = command.spawn()?;
+ Ok(SharedChild {
+ child: Mutex::new(child),
+ state_lock: Mutex::new(NotWaiting),
+ state_condvar: Condvar::new(),
+ })
+ }
+
+ /// Return the child process ID.
+ pub fn id(&self) -> u32 {
+ self.child.lock().unwrap().id()
+ }
+
+ fn get_handle(&self) -> sys::Handle {
+ sys::get_handle(&self.child.lock().unwrap())
+ }
+
+ /// Wait for the child to exit, blocking the current thread, and return its
+ /// exit status.
+ pub fn wait(&self) -> io::Result<ExitStatus> {
+ let mut state = self.state_lock.lock().unwrap();
+ loop {
+ match *state {
+ NotWaiting => {
+ // Either no one is waiting on the child yet, or a previous
+ // waiter failed. That means we need to do it ourselves.
+ // Break out of this loop.
+ break;
+ }
+ Waiting => {
+ // Another thread is already waiting on the child. We'll
+ // block until it signal us on the condvar, then loop again.
+ // Spurious wakeups could bring us here multiple times
+ // though, see the Condvar docs.
+ state = self.state_condvar.wait(state).unwrap();
+ }
+ Exited(exit_status) => return Ok(exit_status),
+ }
+ }
+
+ // If we get here, we have the state lock, and we're the thread
+ // responsible for waiting on the child. Set the state to Waiting and
+ // then release the state lock, so that other threads can observe it
+ // while we block. Afterwards we must leave the Waiting state before
+ // this function exits, or other waiters will deadlock.
+ *state = Waiting;
+ drop(state);
+
+ // Block until the child exits without reaping it. (On Unix, that means
+ // we need to call libc::waitid with the WNOWAIT flag. On Windows
+ // waiting never reaps.) That makes it safe for another thread to kill
+ // while we're here, without racing against some process reusing the
+ // child's PID. Having only one thread in this section is important,
+ // because POSIX doesn't guarantee much about what happens when multiple
+ // threads wait on a child at the same time:
+ // http://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_13
+ let noreap_result = sys::wait_without_reaping(self.get_handle());
+
+ // Now either we hit an error, or the child has exited and needs to be
+ // reaped. Retake the state lock and handle all the different exit
+ // cases. No matter what happened/happens, we'll leave the Waiting state
+ // and signal the state condvar.
+ let mut state = self.state_lock.lock().unwrap();
+ // The child has already exited, so this wait should clean up without blocking.
+ let final_result = noreap_result.and_then(|_| self.child.lock().unwrap().wait());
+ *state = if let Ok(exit_status) = final_result {
+ Exited(exit_status)
+ } else {
+ NotWaiting
+ };
+ self.state_condvar.notify_all();
+ final_result
+ }
+
+ /// Return the child's exit status if it has already exited. If the child is
+ /// still running, return `Ok(None)`.
+ pub fn try_wait(&self) -> io::Result<Option<ExitStatus>> {
+ let mut status = self.state_lock.lock().unwrap();
+
+ // Unlike wait() above, we don't loop on the Condvar here. If the status
+ // is Waiting or Exited, we return immediately. However, if the status
+ // is NotWaiting, we'll do a non-blocking wait below, in case the child
+ // has already exited.
+ match *status {
+ NotWaiting => {}
+ Waiting => return Ok(None),
+ Exited(exit_status) => return Ok(Some(exit_status)),
+ };
+
+ // No one is waiting on the child. Check to see if it's already exited.
+ // If it has, put ourselves in the Exited state. (There can't be any
+ // other waiters to signal, because the state was NotWaiting when we
+ // started, and we're still holding the status lock.)
+ if sys::try_wait_without_reaping(self.get_handle())? {
+ // The child has exited. Reap it. This should not block.
+ let exit_status = self.child.lock().unwrap().wait()?;
+ *status = Exited(exit_status);
+ Ok(Some(exit_status))
+ } else {
+ Ok(None)
+ }
+ }
+
+ /// Send a kill signal to the child. On Unix this sends SIGKILL, and you
+ /// should call `wait` afterwards to avoid leaving a zombie. If the process
+ /// has already been waited on, this returns `Ok(())` and does nothing.
+ pub fn kill(&self) -> io::Result<()> {
+ let status = self.state_lock.lock().unwrap();
+ if let Exited(_) = *status {
+ return Ok(());
+ }
+ // The child is still running. Kill it. This assumes that the wait
+ // functions above will never hold the child lock during a blocking
+ // wait.
+ self.child.lock().unwrap().kill()
+ }
+
+ /// Consume the `SharedChild` and return the `std::process::Child` it
+ /// contains.
+ ///
+ /// We never reap the child process except through `Child::wait`, so the
+ /// child object's inner state is correct, even if it was waited on while it
+ /// was shared.
+ pub fn into_inner(self) -> Child {
+ self.child.into_inner().unwrap()
+ }
+}
+
+#[derive(Debug)]
+enum ChildState {
+ NotWaiting,
+ Waiting,
+ Exited(ExitStatus),
+}
+
+use ChildState::*;
+
+#[cfg(test)]
+mod tests {
+ use super::{sys, SharedChild};
+ use std;
+ use std::process::Command;
+ use std::sync::Arc;
+
+ pub fn true_cmd() -> Command {
+ let mut cmd = Command::new("python");
+ cmd.arg("-c").arg("");
+ cmd
+ }
+
+ pub fn sleep_forever_cmd() -> Command {
+ let mut cmd = Command::new("python");
+ cmd.arg("-c").arg("import time; time.sleep(1000000)");
+ cmd
+ }
+
+ #[test]
+ fn test_wait() {
+ let child = SharedChild::spawn(&mut true_cmd()).unwrap();
+ // Test the id() function while we're at it.
+ let id = child.id();
+ assert!(id > 0);
+ let status = child.wait().unwrap();
+ assert_eq!(status.code().unwrap(), 0);
+ }
+
+ #[test]
+ fn test_kill() {
+ let child = SharedChild::spawn(&mut sleep_forever_cmd()).unwrap();
+ child.kill().unwrap();
+ let status = child.wait().unwrap();
+ assert!(!status.success());
+ }
+
+ #[test]
+ fn test_try_wait() {
+ let child = SharedChild::spawn(&mut sleep_forever_cmd()).unwrap();
+ let maybe_status = child.try_wait().unwrap();
+ assert_eq!(maybe_status, None);
+ child.kill().unwrap();
+ // The child will handle that signal asynchronously, so we check it
+ // repeatedly in a busy loop.
+ let mut maybe_status = None;
+ while let None = maybe_status {
+ maybe_status = child.try_wait().unwrap();
+ }
+ assert!(maybe_status.is_some());
+ assert!(!maybe_status.unwrap().success());
+ }
+
+ #[test]
+ fn test_many_waiters() {
+ let child = Arc::new(SharedChild::spawn(&mut sleep_forever_cmd()).unwrap());
+ let mut threads = Vec::new();
+ for _ in 0..10 {
+ let clone = child.clone();
+ threads.push(std::thread::spawn(move || clone.wait()));
+ }
+ child.kill().unwrap();
+ for thread in threads {
+ thread.join().unwrap().unwrap();
+ }
+ }
+
+ #[test]
+ fn test_waitid_after_exit_doesnt_hang() {
+ // There are ominous reports (https://bugs.python.org/issue10812) of a
+ // broken waitid implementation on OSX, which might hang forever if it
+ // tries to wait on a child that's already exited.
+ let child = true_cmd().spawn().unwrap();
+ sys::wait_without_reaping(sys::get_handle(&child)).unwrap();
+ // At this point the child has definitely exited. Wait again to test
+ // that a second wait doesn't block.
+ sys::wait_without_reaping(sys::get_handle(&child)).unwrap();
+ }
+
+ #[test]
+ fn test_into_inner_before_wait() {
+ let shared_child = SharedChild::spawn(&mut sleep_forever_cmd()).unwrap();
+ let mut child = shared_child.into_inner();
+ child.kill().unwrap();
+ child.wait().unwrap();
+ }
+
+ #[test]
+ fn test_into_inner_after_wait() {
+ // This makes sure the child's inner state is valid. If we used waitpid
+ // on the side, the inner child would try to wait again and cause an
+ // error.
+ let shared_child = SharedChild::spawn(&mut sleep_forever_cmd()).unwrap();
+ shared_child.kill().unwrap();
+ shared_child.wait().unwrap();
+ let mut child = shared_child.into_inner();
+ // The child has already been waited on, so kill should be an error.
+ let kill_err = child.kill().unwrap_err();
+ if cfg!(windows) {
+ assert_eq!(std::io::ErrorKind::PermissionDenied, kill_err.kind());
+ } else {
+ assert_eq!(std::io::ErrorKind::InvalidInput, kill_err.kind());
+ }
+ // But wait should succeed.
+ child.wait().unwrap();
+ }
+}
diff --git a/src/sys/mod.rs b/src/sys/mod.rs
new file mode 100644
index 0000000..2ce2496
--- /dev/null
+++ b/src/sys/mod.rs
@@ -0,0 +1,9 @@
+#[cfg(unix)]
+#[path = "unix.rs"]
+mod sys;
+
+#[cfg(windows)]
+#[path = "windows.rs"]
+mod sys;
+
+pub use self::sys::*;
diff --git a/src/sys/unix.rs b/src/sys/unix.rs
new file mode 100644
index 0000000..46fe5b2
--- /dev/null
+++ b/src/sys/unix.rs
@@ -0,0 +1,81 @@
+extern crate libc;
+
+use std;
+use std::io;
+use std::process::Child;
+
+// A handle on Unix is just the PID.
+pub struct Handle(u32);
+
+pub fn get_handle(child: &Child) -> Handle {
+ Handle(child.id())
+}
+
+// This blocks until a child exits, without reaping the child.
+pub fn wait_without_reaping(handle: Handle) -> io::Result<()> {
+ loop {
+ let ret = unsafe {
+ let mut siginfo = std::mem::zeroed();
+ libc::waitid(
+ libc::P_PID,
+ handle.0 as libc::id_t,
+ &mut siginfo,
+ libc::WEXITED | libc::WNOWAIT,
+ )
+ };
+ if ret == 0 {
+ return Ok(());
+ }
+ let error = io::Error::last_os_error();
+ if error.kind() != io::ErrorKind::Interrupted {
+ return Err(error);
+ }
+ // We were interrupted. Loop and retry.
+ }
+}
+
+// This checks whether the child has already exited, without reaping the child.
+pub fn try_wait_without_reaping(handle: Handle) -> io::Result<bool> {
+ let mut siginfo: libc::siginfo_t;
+ let ret = unsafe {
+ // Darwin doesn't touch the siginfo_t struct if the child hasn't exited
+ // yet. It expects us to have zeroed it ahead of time:
+ //
+ // The state of the siginfo structure in this case
+ // is undefined. Some implementations bzero it, some
+ // (like here) leave it untouched for efficiency.
+ //
+ // Thus the most portable check for "no matching pid with
+ // WNOHANG" is to store a zero into si_pid before
+ // invocation, then check for a non-zero value afterwards.
+ //
+ // https://github.com/opensource-apple/xnu/blob/0a798f6738bc1db01281fc08ae024145e84df927/bsd/kern/kern_exit.c#L2150-L2156
+ //
+ // XXX: The siginfo_t struct has padding. Does that make it unsound to
+ // initialize it this way?
+ siginfo = std::mem::zeroed();
+ libc::waitid(
+ libc::P_PID,
+ handle.0 as libc::id_t,
+ &mut siginfo,
+ libc::WEXITED | libc::WNOWAIT | libc::WNOHANG,
+ )
+ };
+ if ret != 0 {
+ // EINTR should be impossible here
+ Err(io::Error::last_os_error())
+ } else if siginfo.si_signo == libc::SIGCHLD {
+ // The child has exited.
+ Ok(true)
+ } else if siginfo.si_signo == 0 {
+ // The child has not exited.
+ Ok(false)
+ } else {
+ // This should be impossible if we called waitid correctly. But it will
+ // show up on macOS if we forgot to zero the siginfo_t above, for example.
+ Err(io::Error::new(
+ io::ErrorKind::Other,
+ format!("unexpected si_signo from waitid: {}", siginfo.si_signo),
+ ))
+ }
+}
diff --git a/src/sys/windows.rs b/src/sys/windows.rs
new file mode 100644
index 0000000..db927bc
--- /dev/null
+++ b/src/sys/windows.rs
@@ -0,0 +1,44 @@
+extern crate winapi;
+
+use self::winapi::shared::winerror::WAIT_TIMEOUT;
+use self::winapi::um::synchapi::WaitForSingleObject;
+use self::winapi::um::winbase::{WAIT_OBJECT_0, INFINITE};
+use self::winapi::um::winnt::HANDLE;
+use std::io;
+use std::os::windows::io::{AsRawHandle, RawHandle};
+use std::process::Child;
+
+pub struct Handle(RawHandle);
+
+// Kind of like a child PID on Unix, it's important not to keep the handle
+// around after the child has been cleaned up. The best solution would be to
+// have the handle actually borrow the child, but we need to keep the child
+// unborrowed. Instead we just avoid storing them.
+pub fn get_handle(child: &Child) -> Handle {
+ Handle(child.as_raw_handle())
+}
+
+// This is very similar to libstd's Child::wait implementation, because the
+// basic wait on Windows doesn't reap. The main difference is that this can be
+// called without &mut Child.
+pub fn wait_without_reaping(handle: Handle) -> io::Result<()> {
+ let wait_ret = unsafe { WaitForSingleObject(handle.0 as HANDLE, INFINITE) };
+ if wait_ret != WAIT_OBJECT_0 {
+ Err(io::Error::last_os_error())
+ } else {
+ Ok(())
+ }
+}
+
+pub fn try_wait_without_reaping(handle: Handle) -> io::Result<bool> {
+ let wait_ret = unsafe { WaitForSingleObject(handle.0 as HANDLE, 0) };
+ if wait_ret == WAIT_OBJECT_0 {
+ // Child has exited.
+ Ok(true)
+ } else if wait_ret == WAIT_TIMEOUT {
+ // Child has not exited yet.
+ Ok(false)
+ } else {
+ Err(io::Error::last_os_error())
+ }
+}
diff --git a/src/unix.rs b/src/unix.rs
new file mode 100644
index 0000000..f53ddd6
--- /dev/null
+++ b/src/unix.rs
@@ -0,0 +1,46 @@
+//! Unix-only extensions, for sending signals.
+
+extern crate libc;
+
+use std::io;
+
+pub trait SharedChildExt {
+ /// Send a signal to the child process with `libc::kill`. If the process
+ /// has already been waited on, this returns `Ok(())` and does nothing.
+ fn send_signal(&self, signal: libc::c_int) -> io::Result<()>;
+}
+
+impl SharedChildExt for super::SharedChild {
+ fn send_signal(&self, signal: libc::c_int) -> io::Result<()> {
+ let status = self.state_lock.lock().unwrap();
+ if let super::ChildState::Exited(_) = *status {
+ return Ok(());
+ }
+ // The child is still running. Signal it. Holding the state lock
+ // is important to prevent a PID race.
+ // This assumes that the wait methods will never hold the child
+ // lock during a blocking wait, since we need it to get the pid.
+ let pid = self.id() as libc::pid_t;
+ match unsafe { libc::kill(pid, signal) } {
+ -1 => Err(io::Error::last_os_error()),
+ _ => Ok(()),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::libc;
+ use super::SharedChildExt;
+ use std::os::unix::process::ExitStatusExt;
+ use tests::*;
+ use SharedChild;
+
+ #[test]
+ fn test_send_signal() {
+ let child = SharedChild::spawn(&mut sleep_forever_cmd()).unwrap();
+ child.send_signal(libc::SIGABRT).unwrap();
+ let status = child.wait().unwrap();
+ assert_eq!(Some(libc::SIGABRT), status.signal());
+ }
+}