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}