Skip to main content

mio_child_process/
lib.rs

1#![deny(missing_docs)]
2
3//! A simple crate that allows child processes to be handled with mio
4//!
5//! # usage
6//!
7//! ``` rust
8//! extern crate mio_child_process;
9//! extern crate mio;
10//!
11//! use mio::{Poll, Token, Ready, PollOpt, Events, Evented};
12//! use std::process::{Command, Stdio};
13//! use mio_child_process::{CommandAsync, ProcessEvent};
14//! use std::sync::mpsc::TryRecvError;
15//!
16//! fn main() {
17//! let mut process = Command::new("ping");
18//!    if cfg!(target_os = "linux") {
19//!        process.arg("-c").arg("4");
20//!    }
21//!    let mut process = process.arg("8.8.8.8")
22//!         .stdout(Stdio::piped())
23//!         .stderr(Stdio::piped())
24//!         .spawn_async()
25//!         .expect("Could not spawn process");
26//!     let poll = Poll::new().expect("Could not spawn poll");
27//!     let mut events = Events::with_capacity(10);
28//!     let token = Token(1);
29//!     process.register(&poll, token, Ready::all(), PollOpt::edge()).expect("Could not register");
30//!     'outer: loop {
31//!         poll.poll(&mut events, None).expect("Could not poll");
32//!         for event in &events {
33//!             assert_eq!(event.token(), token);
34//!             loop {
35//!                 let result = match process.try_recv() {
36//!                     Ok(r) => r,
37//!                     Err(TryRecvError::Empty) => continue,
38//!                     Err(TryRecvError::Disconnected) => panic!("Could not receive from process"),
39//!                 };
40//!                 println!("{:?}", result);
41//!
42//!                 match result {
43//!                     ProcessEvent::Exit(exit_status) => {
44//!                         assert!(exit_status.success());
45//!                         break 'outer;
46//!                     },
47//!                     ProcessEvent::IoError(_, _) | ProcessEvent::CommandError(_) => {
48//!                         assert!(false);
49//!                     },
50//!                     _ => {}
51//!                 }
52//!             }
53//!         }
54//!     }
55//! }
56//! ```
57//!
58//! # Notes
59//!
60//! StdioChannel::Stdout will only be emitted when `.stdout(Stdio::piped())` is passed to the `Command`.
61//!
62//! StdioChannel::Stderr will only be emitted when `.stderr(Stdio::piped())` is passed to the `Command`.
63//!
64//! # Threads
65//!
66//! Internally a thread gets spawned for each std stream it's listening to (stdout and stderr).
67//! Another thread is started, that is in a blocking wait until the child process is done.
68//! This means that mio-child-process uses between 1 to 3 threads for every process that gets started.
69
70extern crate mio;
71extern crate mio_extras;
72#[cfg(target_os = "windows")]
73extern crate winapi;
74
75use mio::{Evented, Poll, PollOpt, Ready, Token};
76use mio_extras::channel::{channel, Receiver, Sender};
77use std::io::{Error, ErrorKind, Read, Result, Write};
78use std::process::{Child, Command, ExitStatus};
79use std::thread::spawn;
80
81#[cfg(test)]
82mod test;
83
84/// Extension trait to implement an async spawner on the Command struct
85pub trait CommandAsync {
86    /// Spawn an async child process
87    fn spawn_async(&mut self) -> Result<Process>;
88}
89
90impl CommandAsync for Command {
91    fn spawn_async(&mut self) -> Result<Process> {
92        let child = self.spawn()?;
93        Ok(Process::from_child(child))
94    }
95}
96
97/// An async child process
98pub struct Process {
99    receiver: Receiver<ProcessEvent>,
100    stdin: Option<std::process::ChildStdin>,
101    id: u32,
102}
103
104impl Process {
105    /// Try to receive an event from the child. This should be called after mio's Poll returns an event for this token
106    pub fn try_recv(&mut self) -> std::result::Result<ProcessEvent, std::sync::mpsc::TryRecvError> {
107        self.receiver.try_recv()
108    }
109
110    pub(crate) fn from_child(mut child: Child) -> Process {
111        let (sender, receiver) = channel();
112        if let Some(stdout) = child.stdout.take() {
113            spawn(create_reader(stdout, sender.clone(), StdioChannel::Stdout));
114        }
115        if let Some(stderr) = child.stderr.take() {
116            spawn(create_reader(stderr, sender.clone(), StdioChannel::Stderr));
117        }
118        let stdin = child.stdin.take();
119        let id = child.id();
120        spawn(move || {
121            let result = match child.wait_with_output() {
122                Err(e) => {
123                    let _ = sender.send(ProcessEvent::CommandError(e));
124                    return;
125                }
126                Ok(r) => r,
127            };
128            if !result.stdout.is_empty() {
129                if SendResult::Abort
130                    == try_send_buffer(&result.stdout, StdioChannel::Stdout, &sender)
131                {
132                    return;
133                }
134            }
135            if !result.stderr.is_empty() {
136                if SendResult::Abort
137                    == try_send_buffer(&result.stderr, StdioChannel::Stderr, &sender)
138                {
139                    return;
140                }
141            }
142            let _ = sender.send(ProcessEvent::Exit(result.status));
143        });
144        Process {
145            receiver,
146            stdin,
147            id,
148        }
149    }
150
151    /// Kill the process child, and all it's children.
152    #[cfg(target_os = "windows")]
153    pub fn kill(&mut self) -> Result<()> {
154        use std::collections::HashMap;
155        use std::io::Error;
156        use std::mem;
157        use winapi::um::handleapi::{CloseHandle, INVALID_HANDLE_VALUE};
158        use winapi::um::processthreadsapi::{OpenProcess, TerminateProcess};
159        use winapi::um::tlhelp32::{
160            CreateToolhelp32Snapshot, Process32First, Process32Next, PROCESSENTRY32,
161            TH32CS_SNAPPROCESS,
162        };
163        use winapi::um::winnt::PROCESS_TERMINATE;
164
165        // We first need to make a list of all processes and their parents
166        // Then we'll go through the processes and list their ids and parent ids
167        // then we'll look up the current pid, find all processes that have this pid as their parent, and kill those first
168        type ParentID = u32;
169        type ChildID = u32;
170        let mut processes = HashMap::<ParentID, Vec<ChildID>>::new();
171
172        let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) };
173        if snapshot == INVALID_HANDLE_VALUE {
174            unsafe {
175                CloseHandle(snapshot);
176            }
177            return Err(Error::last_os_error());
178        }
179
180        let mut process_entry_32: PROCESSENTRY32 = unsafe { mem::zeroed() };
181        process_entry_32.dwSize = mem::size_of::<PROCESSENTRY32>() as u32;
182        if 0 == unsafe { Process32First(snapshot, &mut process_entry_32) } {
183            unsafe {
184                CloseHandle(snapshot);
185            }
186            return Err(Error::last_os_error());
187        }
188
189        // Push first entry
190        processes
191            .entry(process_entry_32.th32ParentProcessID)
192            .or_insert_with(Vec::new)
193            .push(process_entry_32.th32ProcessID);
194
195        while unsafe { Process32Next(snapshot, &mut process_entry_32) } != 0 {
196            // Push subsequent entries
197            processes
198                .entry(process_entry_32.th32ParentProcessID)
199                .or_insert_with(Vec::new)
200                .push(process_entry_32.th32ProcessID);
201        }
202
203        unsafe {
204            CloseHandle(snapshot);
205        }
206
207        // Kill all children, then kills the process with the given `pid`
208        fn kill_pid(pid: u32, processes: &HashMap<ParentID, Vec<ChildID>>) -> Result<()> {
209            if let Some(children) = processes.get(&pid) {
210                for child in children {
211                    kill_pid(*child, processes)?;
212                }
213            }
214            // open a handle to the given pid
215            let handle = unsafe { OpenProcess(PROCESS_TERMINATE, 0, pid) };
216            if handle.is_null() {
217                // handle not gotten
218                Err(Error::last_os_error())
219            } else if 0 == unsafe { TerminateProcess(handle, 0) } {
220                // handle not terminated
221                Err(Error::last_os_error())
222            } else {
223                Ok(())
224            }
225        }
226
227        kill_pid(self.id(), &processes)
228    }
229
230    /// Kill the process child, and all it's children.
231    #[cfg(target_os = "linux")]
232    pub fn kill(&mut self) -> Result<()> {
233        extern crate libc;
234        use std::io::Error;
235
236        let result = unsafe { libc::kill(self.id() as i32, libc::SIGKILL) };
237        if result == 0 {
238            Ok(())
239        } else {
240            Err(Error::last_os_error())
241        }
242    }
243
244    /// Returns the OS-assigned process identifier associated with this child.
245    pub fn id(&self) -> u32 {
246        self.id
247    }
248}
249
250impl Write for Process {
251    /// Write a buffer to the Stdin stream of this child process.
252    ///
253    /// If the child is not created with `.stdin(Stdio::piped())`,
254    /// This function will return an error `ErrorKind::NotConnected`.
255    fn write(&mut self, buf: &[u8]) -> Result<usize> {
256        match self.stdin.as_mut() {
257            Some(ref mut stdin) => stdin.write(buf),
258            None => Err(Error::from(ErrorKind::NotConnected)),
259        }
260    }
261
262    /// Flushed the Stdin stream of this child process.
263    ///
264    /// If the child is not created with `.stdin(Stdio::piped())`,
265    /// This function will return an error `ErrorKind::NotConnected`.
266    fn flush(&mut self) -> Result<()> {
267        match self.stdin.as_mut() {
268            Some(ref mut stdin) => stdin.flush(),
269            None => Err(Error::from(ErrorKind::NotConnected)),
270        }
271    }
272}
273
274impl Evented for Process {
275    /// Register the child process with the poll so it can notify when the child process updates
276    fn register(&self, poll: &Poll, token: Token, interest: Ready, ops: PollOpt) -> Result<()> {
277        self.receiver.register(poll, token, interest, ops)
278    }
279    /// Reregister the child process with the poll so it can notify when the child process updates
280    fn reregister(&self, poll: &Poll, token: Token, interest: Ready, ops: PollOpt) -> Result<()> {
281        self.receiver.reregister(poll, token, interest, ops)
282    }
283    /// Deregister the child process with the poll
284    fn deregister(&self, poll: &Poll) -> Result<()> {
285        self.receiver.deregister(poll)
286    }
287}
288
289#[derive(PartialEq, Eq, Debug)]
290enum SendResult {
291    Abort,
292    Ok,
293}
294
295fn try_send_buffer(
296    buffer: &[u8],
297    channel: StdioChannel,
298    sender: &Sender<ProcessEvent>,
299) -> SendResult {
300    let str = match std::str::from_utf8(buffer) {
301        Ok(s) => s,
302        Err(e) => {
303            let _ = sender.send(ProcessEvent::Utf8Error(channel, e));
304            return SendResult::Abort;
305        }
306    };
307    if str.is_empty() {
308        println!("Aborting try_send_buffer because we're sending empty strings");
309        println!("Channel: {:?}", channel);
310        return SendResult::Abort;
311    }
312    if let Err(_) = sender.send(ProcessEvent::Data(channel, String::from(str))) {
313        SendResult::Abort
314    } else {
315        SendResult::Ok
316    }
317}
318
319fn create_reader<T: Read + 'static>(
320    mut stream: T,
321    sender: Sender<ProcessEvent>,
322    channel: StdioChannel,
323) -> impl FnOnce() {
324    move || {
325        let mut buffer = [0u8; 1024];
326        loop {
327            match stream.read(&mut buffer[..]) {
328                Ok(0) => {
329                    // if we read 0 bytes from the stream, that means the stream ended
330                    break;
331                }
332                Ok(n) => {
333                    if SendResult::Abort == try_send_buffer(&buffer[..n], channel, &sender) {
334                        break;
335                    }
336                }
337                Err(e) => {
338                    let _ = sender.send(ProcessEvent::IoError(channel, e));
339                    break;
340                }
341            }
342        }
343    }
344}
345
346/// An event received from the child process
347#[derive(Debug)]
348pub enum ProcessEvent {
349    /// Data is received on an StdioChannel. The String is the UTF8 interpretation of this data.
350    Data(StdioChannel, String),
351
352    /// There was an issue with starting or closing a child process.
353    CommandError(std::io::Error),
354
355    /// There was an issue while reading the stdout or stderr stream.
356    IoError(StdioChannel, std::io::Error),
357
358    /// There was an issue with translating the byte array from the stdout or stderr stream into UTF8
359    Utf8Error(StdioChannel, std::str::Utf8Error),
360
361    /// The process exited with the given ExitStatus.
362    Exit(ExitStatus),
363}
364
365/// Describes what channel the ProcessEvent came from.
366#[derive(Copy, Clone, Debug)]
367pub enum StdioChannel {
368    /// The Stdout stream of a child process
369    Stdout,
370
371    /// The Stdout stream of a child process
372    Stderr,
373}