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