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}