diff options
author | Andrew Walbran <qwandor@google.com> | 2021-03-31 17:09:26 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2021-03-31 17:09:26 +0000 |
commit | 692e6047f4808e2453d70557a06281d7e234fac1 (patch) | |
tree | 2377f0703a57702b707bb56dacd6c88c6cd73e79 | |
parent | f2b560084491ce3f9da4bc451322e80d10bde4e9 (diff) | |
parent | 1ab603b01bc271d851187790cc93f8d9e4a6f6aa (diff) | |
download | shared_child-692e6047f4808e2453d70557a06281d7e234fac1.tar.gz |
Import shared_child crate. am: ea0d2d3231 am: 1ab603b01b
Original change: https://android-review.googlesource.com/c/platform/external/rust/crates/shared_child/+/1658331
Change-Id: I6f32b4178d677c31a1e121c0f328593911d618fa
-rw-r--r-- | .cargo_vcs_info.json | 5 | ||||
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | .travis.yml | 8 | ||||
-rw-r--r-- | Android.bp | 41 | ||||
-rw-r--r-- | Cargo.toml | 27 | ||||
-rw-r--r-- | Cargo.toml.orig | 16 | ||||
-rw-r--r-- | LICENSE | 19 | ||||
-rw-r--r-- | METADATA | 19 | ||||
-rw-r--r-- | MODULE_LICENSE_MIT | 0 | ||||
-rw-r--r-- | OWNERS | 1 | ||||
-rw-r--r-- | README.md | 64 | ||||
-rw-r--r-- | README.tpl | 3 | ||||
-rw-r--r-- | TEST_MAPPING | 9 | ||||
-rw-r--r-- | appveyor.yml | 32 | ||||
-rw-r--r-- | src/lib.rs | 333 | ||||
-rw-r--r-- | src/sys/mod.rs | 9 | ||||
-rw-r--r-- | src/sys/unix.rs | 81 | ||||
-rw-r--r-- | src/sys/windows.rs | 44 | ||||
-rw-r--r-- | src/unix.rs | 46 |
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"] } @@ -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 @@ -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()); + } +} |