bawawa/
process.rs

1use crate::{
2    Command, Control, Error, ErrorKind, Result, ResultExt as _, StandardError, StandardInput,
3    StandardOutput,
4};
5use futures::prelude::*;
6use tokio_process::{ChildStderr, ChildStdin, ChildStdout, CommandExt as _};
7
8/// a `Process` object to monitor the execution of a [`Command`].
9///
10/// If the `Process` is dropped, the associated `Process` will be terminated.
11///
12/// A process is a future where the output Item is the exit status.
13///
14/// [`Command`]: ./struct.Command.html
15pub struct Process {
16    command: Command,
17    process: tokio_process::Child,
18}
19
20impl Process {
21    /// attempt to run the given [`Command`].
22    ///
23    /// # Error
24    ///
25    /// the function may fail if between the time the [`Program`]
26    /// object was constructed and the call of this function the `program`
27    /// situation as changed (permission, renamed, removed...).
28    ///
29    /// [`Program`]: ./struct.Program.html
30    /// [`Command`]: ./struct.Command.html
31    pub fn spawn(command: Command) -> Result<Self> {
32        let mut cmd = command.process_command();
33        let process = cmd
34            .spawn_async()
35            .chain_err(|| ErrorKind::CannotSpawnCommand(command.clone()))?;
36        Ok(Process { command, process })
37    }
38
39    pub fn stdin(&mut self) -> &mut Option<ChildStdin> {
40        self.process.stdin()
41    }
42
43    pub fn stdout(&mut self) -> &mut Option<ChildStdout> {
44        self.process.stdout()
45    }
46
47    pub fn stderr(&mut self) -> &mut Option<ChildStderr> {
48        self.process.stderr()
49    }
50}
51
52impl Control for Process {
53    #[inline]
54    fn command(&self) -> &Command {
55        &self.command
56    }
57
58    /// Returns the OS-assigned process identifier associated with this process.
59    #[inline]
60    fn id(&self) -> u32 {
61        self.process.id()
62    }
63
64    /// force the process to finish
65    ///
66    /// this is equivalent to `SIGKILL` on unix platform
67    #[inline]
68    fn kill(&mut self) -> Result<()> {
69        self.process
70            .kill()
71            .chain_err(|| ErrorKind::CannotKillProcess(self.command().clone(), self.id()))
72    }
73}
74
75impl<'a> StandardInput<'a> for Process {
76    #[inline]
77    fn standard_input(&mut self) -> &mut ChildStdin {
78        match self.process.stdin() {
79            None => unreachable!(),
80            Some(stdin) => stdin,
81        }
82    }
83}
84
85impl<'a> StandardOutput<'a> for Process {
86    #[inline]
87    fn standard_output(&mut self) -> &mut ChildStdout {
88        match self.process.stdout() {
89            None => unreachable!(),
90            Some(stdout) => stdout,
91        }
92    }
93}
94
95impl<'a> StandardError<'a> for Process {
96    #[inline]
97    fn standard_error(&mut self) -> &mut ChildStderr {
98        match self.process.stderr() {
99            None => unreachable!(),
100            Some(stderr) => stderr,
101        }
102    }
103}
104
105impl Future for Process {
106    type Item = <tokio_process::Child as Future>::Item;
107    type Error = Error;
108
109    #[inline]
110    fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
111        self.process
112            .poll()
113            .chain_err(|| ErrorKind::Poll(self.command.clone()))
114    }
115}
116
117#[cfg(test)]
118mod test {
119    use super::*;
120    use crate::Program;
121    use tokio_codec::LinesCodec;
122
123    #[test]
124    fn echo_stdout() -> Result<()> {
125        let mut cmd = Command::new(Program::new("rustc".to_owned())?);
126        cmd.arguments(&["--version"]);
127
128        let mut captured = Process::spawn(cmd)?
129            .capture_stdout(LinesCodec::new())
130            .wait();
131
132        let rustc_version: String = captured.next().unwrap()?;
133
134        assert!(rustc_version.starts_with("rustc"));
135
136        Ok(())
137    }
138
139    #[test]
140    fn cat_stdin_stderr() -> Result<()> {
141        let mut cmd = Command::new(Program::new("rustc".to_owned())?);
142        cmd.arguments(&["file-that-does-not-exist"]);
143
144        let mut captured = Process::spawn(cmd)?
145            .capture_stderr(LinesCodec::new())
146            .wait();
147
148        assert_eq!(
149            captured.next().unwrap()?,
150            "error: couldn\'t read file-that-does-not-exist: No such file or directory (os error 2)",
151        );
152
153        Ok(())
154    }
155
156    fn send_and_check<P, I>(process: P, item: I) -> Result<P>
157    where
158        P: Stream<Item = I, Error = Error> + Sink<SinkItem = I, SinkError = Error>,
159        I: std::fmt::Debug + Clone + PartialEq + Eq,
160    {
161        let process = process.send(item.clone()).wait()?;
162        let mut captured = Stream::wait(process);
163
164        assert_eq!(captured.next().unwrap()?, item);
165
166        Ok(captured.into_inner())
167    }
168
169    #[cfg(windows)]
170    #[test]
171    fn windows__stdin_stdout() -> Result<()> {
172        // TODO: write a test to check standard input capture on windows
173
174        Ok(())
175    }
176
177    #[cfg(unix)]
178    #[test]
179    fn unix_cat_stdin_stdout() -> Result<()> {
180        let cmd = Command::new(Program::new("cat".to_owned())?);
181
182        let process = Process::spawn(cmd)?
183            .capture_stdout(LinesCodec::new())
184            .send_stdin(LinesCodec::new());
185
186        let process = send_and_check(process, "Hello World!".to_owned())?;
187        let _process = send_and_check(process, "Bawawa".to_owned())?;
188
189        Ok(())
190    }
191}