Skip to main content

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