rusty_forkfork/
fork.rs

1//-
2// Copyright 2018 Jason Lingle
3//
4// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
5// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
7// option. This file may not be copied, modified, or distributed
8// except according to those terms.
9
10use std::env;
11use std::fmt::Debug;
12use std::fs;
13use std::hash::{Hash, Hasher};
14use std::io::{self, BufRead, Seek};
15use std::panic;
16use std::process;
17
18use crate::child_wrapper::ChildWrapper;
19use crate::cmdline;
20use crate::error::*;
21
22const OCCURS_ENV: &str = "RUSTY_FORK_OCCURS";
23const OCCURS_TERM_LENGTH: usize = 17; /* ':' plus 16 hexits */
24
25pub trait TestExitStatus<E: Debug> {
26    fn status(self) -> std::result::Result<(), E>;
27}
28
29impl<T, E: Debug> TestExitStatus<E> for std::result::Result<T, E> {
30    fn status(self) -> std::result::Result<(), E> {
31        match self {
32            Ok(_) => Ok(()),
33            Err(err) => Err(err),
34        }
35    }
36}
37
38impl<T> TestExitStatus<()> for Option<T> {
39    fn status(self) -> std::result::Result<(), ()> {
40        match self {
41            Some(_) => Ok(()),
42            None => Err(()),
43        }
44    }
45}
46
47impl TestExitStatus<()> for () {
48    fn status(self) -> std::result::Result<(), ()> {
49        Ok(())
50    }
51}
52
53/// Simulate a process fork.
54///
55/// The function documentation here only lists information unique to calling it
56/// directly; please see the crate documentation for more details on how the
57/// forking process works.
58///
59/// Since this is not a true process fork, the calling code must be structured
60/// to ensure that the child process, upon starting from the same entry point,
61/// also reaches this same `fork()` call. Recursive forks are supported; the
62/// child branch is taken from all child processes of the fork even if it is
63/// not directly the child of a particular branch. However, encountering the
64/// same fork point more than once in a single execution sequence of a child
65/// process is not (e.g., putting this call in a recursive function) and
66/// results in unspecified behaviour.
67///
68/// The child's output is buffered into an anonymous temporary file. Before
69/// this call returns, this output is copied to the parent's standard output
70/// (passing through the redirect mechanism Rust test uses).
71///
72/// `test_name` must exactly match the full path of the test function being
73/// run.
74///
75/// `fork_id` is a unique identifier identifying this particular fork location.
76/// This *must* be stable across processes of the same executable; pointers are
77/// not suitable stable, and string constants may not be suitably unique. The
78/// [`rusty_fork_id!()`](macro.rusty_fork_id.html) macro is the recommended way
79/// to supply this parameter.
80///
81/// If this is the parent process, `in_parent` is invoked, and the return value
82/// becomes the return value from this function. The callback is passed a
83/// handle to the file which receives the child's output. If is the callee's
84/// responsibility to wait for the child to exit. If this is the child process,
85/// `in_child` is invoked, and when the callback returns, the child process
86/// exits.
87///
88/// If `in_parent` returns or panics before the child process has terminated,
89/// the child process is killed.
90///
91/// If `in_child` panics, the child process exits with a failure code
92/// immediately rather than let the panic propagate out of the `fork()` call.
93///
94/// `process_modifier` is invoked on the `std::process::Command` immediately
95/// before spawning the new process. The callee may modify the process
96/// parameters if desired, but should not do anything that would modify or
97/// remove any environment variables beginning with `RUSTY_FORK_`.
98///
99/// ## Panics
100///
101/// Panics if the environment indicates that there are already at least 16
102/// levels of fork nesting.
103///
104/// Panics if `std::env::current_exe()` fails determine the path to the current
105/// executable.
106///
107/// Panics if any argument to the current process is not valid UTF-8.
108pub fn fork<ID, MODIFIER, PARENT, CHILD, R, T, E: Debug>(
109    test_name: &str,
110    fork_id: ID,
111    process_modifier: MODIFIER,
112    in_parent: PARENT,
113    in_child: CHILD,
114) -> Result<R>
115where
116    ID: Hash,
117    MODIFIER: FnOnce(&mut process::Command),
118    PARENT: FnOnce(&mut ChildWrapper, &mut fs::File) -> R,
119    T: TestExitStatus<E>,
120    CHILD: FnOnce() -> T,
121{
122    let fork_id = id_str(fork_id);
123
124    // Erase the generics so we don't instantiate the actual implementation for
125    // every single test
126    let mut return_value = None;
127    let mut process_modifier = Some(process_modifier);
128    let mut in_parent = Some(in_parent);
129    let mut in_child = Some(in_child);
130
131    fork_impl(
132        test_name,
133        fork_id,
134        &mut |cmd| process_modifier.take().unwrap()(cmd),
135        &mut |child, file| return_value = Some(in_parent.take().unwrap()(child, file)),
136        &mut || in_child.take().unwrap()(),
137    )
138    .map(|_| return_value.unwrap())
139}
140
141fn fork_impl<E: Debug, T: TestExitStatus<E>>(
142    test_name: &str,
143    fork_id: String,
144    process_modifier: &mut dyn FnMut(&mut process::Command),
145    in_parent: &mut dyn FnMut(&mut ChildWrapper, &mut fs::File),
146    in_child: &mut dyn FnMut() -> T,
147) -> Result<()> {
148    let mut occurs = env::var(OCCURS_ENV).unwrap_or_else(|_| String::new());
149    if occurs.contains(&fork_id) {
150        match panic::catch_unwind(panic::AssertUnwindSafe(in_child)) {
151            Ok(test_result) => match test_result.status() {
152                Ok(_) => process::exit(0),
153                Err(err) => {
154                    eprintln!("Test failure cause by: {:?}", err);
155                    process::exit(70 /* EX_SOFTWARE */)
156                }
157            },
158            // Assume that the default panic handler already printed something
159            //
160            // We don't use process::abort() since it produces core dumps on
161            // some systems and isn't something more special than a normal
162            // panic.
163            Err(_) => process::exit(70 /* EX_SOFTWARE */),
164        }
165    } else {
166        // Prevent misconfiguration creating a fork bomb
167        if occurs.len() > 16 * OCCURS_TERM_LENGTH {
168            panic!("rusty-fork: Not forking due to >=16 levels of recursion");
169        }
170
171        let file = tempfile::tempfile()?;
172
173        struct KillOnDrop(ChildWrapper, fs::File);
174        impl Drop for KillOnDrop {
175            fn drop(&mut self) {
176                // Kill the child if it hasn't exited yet
177                let _ = self.0.kill();
178
179                // Copy the child's output to our own
180                // Awkwardly, `print!()` and `println!()` are our only gateway
181                // to putting things in the captured output. Generally test
182                // output really is text, so work on that assumption and read
183                // line-by-line, converting lossily into UTF-8 so we can
184                // println!() it.
185                let _ = self.1.seek(io::SeekFrom::Start(0));
186
187                let mut buf = Vec::new();
188                let mut br = io::BufReader::new(&mut self.1);
189                loop {
190                    // We can't use read_line() or lines() since they break if
191                    // there's any non-UTF-8 output at all. \n occurs at the
192                    // end of the line endings on all major platforms, so we
193                    // can just use that as a delimiter.
194                    if br.read_until(b'\n', &mut buf).is_err() {
195                        break;
196                    }
197                    if buf.is_empty() {
198                        break;
199                    }
200
201                    // not println!() because we already have a line ending
202                    // from above.
203                    print!("{}", String::from_utf8_lossy(&buf));
204                    buf.clear();
205                }
206            }
207        }
208
209        occurs.push_str(&fork_id);
210        let mut command =
211            process::Command::new(env::current_exe().expect("current_exe() failed, cannot fork"));
212        command
213            .args(cmdline::strip_cmdline(env::args())?)
214            .args(cmdline::RUN_TEST_ARGS)
215            .arg(test_name)
216            .env(OCCURS_ENV, &occurs)
217            .stdin(process::Stdio::null())
218            .stdout(file.try_clone()?)
219            .stderr(file.try_clone()?);
220        process_modifier(&mut command);
221
222        let mut child = command
223            .spawn()
224            .map(ChildWrapper::new)
225            .map(|p| KillOnDrop(p, file))?;
226
227        let ret = in_parent(&mut child.0, &mut child.1);
228
229        Ok(ret)
230    }
231}
232
233fn id_str<ID: Hash>(id: ID) -> String {
234    let mut hasher = fnv::FnvHasher::default();
235    id.hash(&mut hasher);
236
237    return format!(":{:016X}", hasher.finish());
238}
239
240#[cfg(test)]
241mod test {
242    use std::io::Read;
243    use std::thread;
244
245    use super::*;
246
247    fn sleep(ms: u64) {
248        thread::sleep(::std::time::Duration::from_millis(ms));
249    }
250
251    fn capturing_output(cmd: &mut process::Command) {
252        // Only actually capture stdout since we can't use
253        // wait_with_output() since it for some reason consumes the `Child`.
254        cmd.stdout(process::Stdio::piped())
255            .stderr(process::Stdio::inherit());
256    }
257
258    fn inherit_output(cmd: &mut process::Command) {
259        cmd.stdout(process::Stdio::inherit())
260            .stderr(process::Stdio::inherit());
261    }
262
263    fn wait_for_child_output(child: &mut ChildWrapper, _file: &mut fs::File) -> String {
264        let mut output = String::new();
265        child
266            .inner_mut()
267            .stdout
268            .as_mut()
269            .unwrap()
270            .read_to_string(&mut output)
271            .unwrap();
272        assert!(child.wait().unwrap().success());
273        output
274    }
275
276    fn wait_for_child(child: &mut ChildWrapper, _file: &mut fs::File) {
277        assert!(child.wait().unwrap().success());
278    }
279
280    #[test]
281    fn fork_basically_works() {
282        let status = fork(
283            "fork::test::fork_basically_works",
284            rusty_fork_id!(),
285            |_| (),
286            |child, _| child.wait().unwrap(),
287            || println!("hello from child"),
288        )
289        .unwrap();
290        assert!(status.success());
291    }
292
293    #[test]
294    fn child_output_captured_and_repeated() {
295        let output = fork(
296            "fork::test::child_output_captured_and_repeated",
297            rusty_fork_id!(),
298            capturing_output,
299            wait_for_child_output,
300            || {
301                fork(
302                    "fork::test::child_output_captured_and_repeated",
303                    rusty_fork_id!(),
304                    |_| (),
305                    wait_for_child,
306                    || println!("hello from child"),
307                )
308                .unwrap()
309            },
310        )
311        .unwrap();
312        assert!(output.contains("hello from child"));
313    }
314
315    #[test]
316    fn child_killed_if_parent_exits_first() {
317        let output = fork(
318            "fork::test::child_killed_if_parent_exits_first",
319            rusty_fork_id!(),
320            capturing_output,
321            wait_for_child_output,
322            || {
323                fork(
324                    "fork::test::child_killed_if_parent_exits_first",
325                    rusty_fork_id!(),
326                    inherit_output,
327                    |_, _| (),
328                    || {
329                        sleep(1_000);
330                        println!("hello from child");
331                    },
332                )
333                .unwrap()
334            },
335        )
336        .unwrap();
337
338        sleep(2_000);
339        assert!(
340            !output.contains("hello from child"),
341            "Had unexpected output:\n{}",
342            output
343        );
344    }
345
346    #[test]
347    fn child_killed_if_parent_panics_first() {
348        let output = fork(
349            "fork::test::child_killed_if_parent_panics_first",
350            rusty_fork_id!(),
351            capturing_output,
352            wait_for_child_output,
353            || {
354                assert!(panic::catch_unwind(panic::AssertUnwindSafe(|| fork(
355                    "fork::test::child_killed_if_parent_panics_first",
356                    rusty_fork_id!(),
357                    inherit_output,
358                    |_, _| panic!("testing a panic, nothing to see here"),
359                    || {
360                        sleep(1_000);
361                        println!("hello from child");
362                    }
363                )
364                .unwrap()))
365                .is_err());
366            },
367        )
368        .unwrap();
369
370        sleep(2_000);
371        assert!(
372            !output.contains("hello from child"),
373            "Had unexpected output:\n{}",
374            output
375        );
376    }
377
378    #[test]
379    fn child_aborted_if_panics() {
380        let status = fork(
381            "fork::test::child_aborted_if_panics",
382            rusty_fork_id!(),
383            |_| (),
384            |child, _| child.wait().unwrap(),
385            || panic!("testing a panic, nothing to see here"),
386        )
387        .unwrap();
388        assert_eq!(70, status.code().unwrap());
389    }
390}