]>
Commit | Line | Data |
---|---|---|
476ff2be | 1 | // Copyright 2016 The Rust Project Developers. See the COPYRIGHT |
85aaf69f SL |
2 | // file at the top-level directory of this distribution and at |
3 | // http://rust-lang.org/COPYRIGHT. | |
4 | // | |
5 | // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or | |
6 | // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license | |
7 | // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your | |
8 | // option. This file may not be copied, modified, or distributed | |
9 | // except according to those terms. | |
10 | ||
c34b1796 | 11 | use os::unix::prelude::*; |
85aaf69f | 12 | |
7453a54e | 13 | use collections::hash_map::{HashMap, Entry}; |
85aaf69f | 14 | use env; |
9346a6ac | 15 | use ffi::{OsString, OsStr, CString, CStr}; |
85aaf69f | 16 | use fmt; |
476ff2be SL |
17 | use io; |
18 | use libc::{self, c_int, gid_t, uid_t, c_char}; | |
85aaf69f | 19 | use ptr; |
62682a34 SL |
20 | use sys::fd::FileDesc; |
21 | use sys::fs::{File, OpenOptions}; | |
7453a54e | 22 | use sys::pipe::{self, AnonPipe}; |
85aaf69f SL |
23 | |
24 | //////////////////////////////////////////////////////////////////////////////// | |
25 | // Command | |
26 | //////////////////////////////////////////////////////////////////////////////// | |
27 | ||
85aaf69f | 28 | pub struct Command { |
7453a54e SL |
29 | // Currently we try hard to ensure that the call to `.exec()` doesn't |
30 | // actually allocate any memory. While many platforms try to ensure that | |
31 | // memory allocation works after a fork in a multithreaded process, it's | |
32 | // been observed to be buggy and somewhat unreliable, so we do our best to | |
33 | // just not do it at all! | |
34 | // | |
35 | // Along those lines, the `argv` and `envp` raw pointers here are exactly | |
36 | // what's gonna get passed to `execvp`. The `argv` array starts with the | |
37 | // `program` and ends with a NULL, and the `envp` pointer, if present, is | |
38 | // also null-terminated. | |
39 | // | |
40 | // Right now we don't support removing arguments, so there's no much fancy | |
41 | // support there, but we support adding and removing environment variables, | |
42 | // so a side table is used to track where in the `envp` array each key is | |
43 | // located. Whenever we add a key we update it in place if it's already | |
44 | // present, and whenever we remove a key we update the locations of all | |
45 | // other keys. | |
46 | program: CString, | |
47 | args: Vec<CString>, | |
48 | env: Option<HashMap<OsString, (usize, CString)>>, | |
49 | argv: Vec<*const c_char>, | |
50 | envp: Option<Vec<*const c_char>>, | |
51 | ||
52 | cwd: Option<CString>, | |
53 | uid: Option<uid_t>, | |
54 | gid: Option<gid_t>, | |
7453a54e SL |
55 | saw_nul: bool, |
56 | closures: Vec<Box<FnMut() -> io::Result<()> + Send + Sync>>, | |
57 | stdin: Option<Stdio>, | |
58 | stdout: Option<Stdio>, | |
59 | stderr: Option<Stdio>, | |
60 | } | |
61 | ||
62 | // passed back to std::process with the pipes connected to the child, if any | |
63 | // were requested | |
64 | pub struct StdioPipes { | |
65 | pub stdin: Option<AnonPipe>, | |
66 | pub stdout: Option<AnonPipe>, | |
67 | pub stderr: Option<AnonPipe>, | |
68 | } | |
69 | ||
70 | // passed to do_exec() with configuration of what the child stdio should look | |
71 | // like | |
476ff2be SL |
72 | pub struct ChildPipes { |
73 | pub stdin: ChildStdio, | |
74 | pub stdout: ChildStdio, | |
75 | pub stderr: ChildStdio, | |
7453a54e SL |
76 | } |
77 | ||
476ff2be | 78 | pub enum ChildStdio { |
7453a54e SL |
79 | Inherit, |
80 | Explicit(c_int), | |
81 | Owned(FileDesc), | |
82 | } | |
83 | ||
84 | pub enum Stdio { | |
85 | Inherit, | |
86 | Null, | |
87 | MakePipe, | |
88 | Fd(FileDesc), | |
85aaf69f SL |
89 | } |
90 | ||
91 | impl Command { | |
92 | pub fn new(program: &OsStr) -> Command { | |
7453a54e SL |
93 | let mut saw_nul = false; |
94 | let program = os2c(program, &mut saw_nul); | |
85aaf69f | 95 | Command { |
5bcae85e | 96 | argv: vec![program.as_ptr(), ptr::null()], |
3b2f2976 | 97 | program, |
85aaf69f SL |
98 | args: Vec::new(), |
99 | env: None, | |
7453a54e | 100 | envp: None, |
85aaf69f SL |
101 | cwd: None, |
102 | uid: None, | |
103 | gid: None, | |
3b2f2976 | 104 | saw_nul, |
7453a54e SL |
105 | closures: Vec::new(), |
106 | stdin: None, | |
107 | stdout: None, | |
108 | stderr: None, | |
85aaf69f SL |
109 | } |
110 | } | |
111 | ||
112 | pub fn arg(&mut self, arg: &OsStr) { | |
7453a54e SL |
113 | // Overwrite the trailing NULL pointer in `argv` and then add a new null |
114 | // pointer. | |
115 | let arg = os2c(arg, &mut self.saw_nul); | |
116 | self.argv[self.args.len() + 1] = arg.as_ptr(); | |
5bcae85e | 117 | self.argv.push(ptr::null()); |
7453a54e SL |
118 | |
119 | // Also make sure we keep track of the owned value to schedule a | |
120 | // destructor for this memory. | |
121 | self.args.push(arg); | |
85aaf69f | 122 | } |
7453a54e SL |
123 | |
124 | fn init_env_map(&mut self) -> (&mut HashMap<OsString, (usize, CString)>, | |
125 | &mut Vec<*const c_char>) { | |
85aaf69f | 126 | if self.env.is_none() { |
7453a54e SL |
127 | let mut map = HashMap::new(); |
128 | let mut envp = Vec::new(); | |
129 | for (k, v) in env::vars_os() { | |
130 | let s = pair_to_key(&k, &v, &mut self.saw_nul); | |
131 | envp.push(s.as_ptr()); | |
132 | map.insert(k, (envp.len() - 1, s)); | |
133 | } | |
5bcae85e | 134 | envp.push(ptr::null()); |
7453a54e SL |
135 | self.env = Some(map); |
136 | self.envp = Some(envp); | |
85aaf69f | 137 | } |
7453a54e | 138 | (self.env.as_mut().unwrap(), self.envp.as_mut().unwrap()) |
85aaf69f | 139 | } |
7453a54e | 140 | |
85aaf69f | 141 | pub fn env(&mut self, key: &OsStr, val: &OsStr) { |
7453a54e SL |
142 | let new_key = pair_to_key(key, val, &mut self.saw_nul); |
143 | let (map, envp) = self.init_env_map(); | |
144 | ||
a7813a04 | 145 | // If `key` is already present then we just update `envp` in place |
7453a54e SL |
146 | // (and store the owned value), but if it's not there we override the |
147 | // trailing NULL pointer, add a new NULL pointer, and store where we | |
148 | // were located. | |
149 | match map.entry(key.to_owned()) { | |
150 | Entry::Occupied(mut e) => { | |
151 | let (i, ref mut s) = *e.get_mut(); | |
152 | envp[i] = new_key.as_ptr(); | |
153 | *s = new_key; | |
154 | } | |
155 | Entry::Vacant(e) => { | |
156 | let len = envp.len(); | |
157 | envp[len - 1] = new_key.as_ptr(); | |
5bcae85e | 158 | envp.push(ptr::null()); |
7453a54e SL |
159 | e.insert((len - 1, new_key)); |
160 | } | |
161 | } | |
85aaf69f | 162 | } |
7453a54e | 163 | |
85aaf69f | 164 | pub fn env_remove(&mut self, key: &OsStr) { |
7453a54e SL |
165 | let (map, envp) = self.init_env_map(); |
166 | ||
167 | // If we actually ended up removing a key, then we need to update the | |
168 | // position of all keys that come after us in `envp` because they're all | |
169 | // one element sooner now. | |
170 | if let Some((i, _)) = map.remove(key) { | |
171 | envp.remove(i); | |
172 | ||
173 | for (_, &mut (ref mut j, _)) in map.iter_mut() { | |
174 | if *j >= i { | |
175 | *j -= 1; | |
176 | } | |
177 | } | |
178 | } | |
85aaf69f | 179 | } |
7453a54e | 180 | |
85aaf69f | 181 | pub fn env_clear(&mut self) { |
7453a54e | 182 | self.env = Some(HashMap::new()); |
5bcae85e | 183 | self.envp = Some(vec![ptr::null()]); |
85aaf69f | 184 | } |
7453a54e | 185 | |
85aaf69f | 186 | pub fn cwd(&mut self, dir: &OsStr) { |
7453a54e | 187 | self.cwd = Some(os2c(dir, &mut self.saw_nul)); |
85aaf69f | 188 | } |
7453a54e SL |
189 | pub fn uid(&mut self, id: uid_t) { |
190 | self.uid = Some(id); | |
92a42be0 | 191 | } |
7453a54e SL |
192 | pub fn gid(&mut self, id: gid_t) { |
193 | self.gid = Some(id); | |
85aaf69f | 194 | } |
92a42be0 | 195 | |
476ff2be SL |
196 | pub fn saw_nul(&self) -> bool { |
197 | self.saw_nul | |
198 | } | |
199 | pub fn get_envp(&self) -> &Option<Vec<*const c_char>> { | |
200 | &self.envp | |
201 | } | |
202 | pub fn get_argv(&self) -> &Vec<*const c_char> { | |
203 | &self.argv | |
204 | } | |
205 | ||
206 | #[allow(dead_code)] | |
207 | pub fn get_cwd(&self) -> &Option<CString> { | |
208 | &self.cwd | |
209 | } | |
210 | #[allow(dead_code)] | |
211 | pub fn get_uid(&self) -> Option<uid_t> { | |
212 | self.uid | |
213 | } | |
214 | #[allow(dead_code)] | |
215 | pub fn get_gid(&self) -> Option<gid_t> { | |
216 | self.gid | |
217 | } | |
218 | ||
219 | pub fn get_closures(&mut self) -> &mut Vec<Box<FnMut() -> io::Result<()> + Send + Sync>> { | |
220 | &mut self.closures | |
221 | } | |
222 | ||
7453a54e SL |
223 | pub fn before_exec(&mut self, |
224 | f: Box<FnMut() -> io::Result<()> + Send + Sync>) { | |
225 | self.closures.push(f); | |
85aaf69f | 226 | } |
85aaf69f | 227 | |
7453a54e SL |
228 | pub fn stdin(&mut self, stdin: Stdio) { |
229 | self.stdin = Some(stdin); | |
85aaf69f | 230 | } |
476ff2be | 231 | |
7453a54e SL |
232 | pub fn stdout(&mut self, stdout: Stdio) { |
233 | self.stdout = Some(stdout); | |
234 | } | |
476ff2be | 235 | |
7453a54e SL |
236 | pub fn stderr(&mut self, stderr: Stdio) { |
237 | self.stderr = Some(stderr); | |
85aaf69f SL |
238 | } |
239 | ||
476ff2be | 240 | pub fn setup_io(&self, default: Stdio, needs_stdin: bool) |
54a0048b SL |
241 | -> io::Result<(StdioPipes, ChildPipes)> { |
242 | let null = Stdio::Null; | |
243 | let default_stdin = if needs_stdin {&default} else {&null}; | |
244 | let stdin = self.stdin.as_ref().unwrap_or(default_stdin); | |
7453a54e SL |
245 | let stdout = self.stdout.as_ref().unwrap_or(&default); |
246 | let stderr = self.stderr.as_ref().unwrap_or(&default); | |
54a0048b SL |
247 | let (their_stdin, our_stdin) = stdin.to_child_stdio(true)?; |
248 | let (their_stdout, our_stdout) = stdout.to_child_stdio(false)?; | |
249 | let (their_stderr, our_stderr) = stderr.to_child_stdio(false)?; | |
7453a54e SL |
250 | let ours = StdioPipes { |
251 | stdin: our_stdin, | |
252 | stdout: our_stdout, | |
253 | stderr: our_stderr, | |
254 | }; | |
255 | let theirs = ChildPipes { | |
256 | stdin: their_stdin, | |
257 | stdout: their_stdout, | |
258 | stderr: their_stderr, | |
259 | }; | |
260 | Ok((ours, theirs)) | |
62682a34 | 261 | } |
7453a54e | 262 | } |
62682a34 | 263 | |
7453a54e SL |
264 | fn os2c(s: &OsStr, saw_nul: &mut bool) -> CString { |
265 | CString::new(s.as_bytes()).unwrap_or_else(|_e| { | |
266 | *saw_nul = true; | |
267 | CString::new("<string-with-nul>").unwrap() | |
268 | }) | |
269 | } | |
270 | ||
271 | impl Stdio { | |
476ff2be | 272 | pub fn to_child_stdio(&self, readable: bool) |
7453a54e SL |
273 | -> io::Result<(ChildStdio, Option<AnonPipe>)> { |
274 | match *self { | |
476ff2be SL |
275 | Stdio::Inherit => { |
276 | Ok((ChildStdio::Inherit, None)) | |
277 | }, | |
7453a54e SL |
278 | |
279 | // Make sure that the source descriptors are not an stdio | |
280 | // descriptor, otherwise the order which we set the child's | |
281 | // descriptors may blow away a descriptor which we are hoping to | |
282 | // save. For example, suppose we want the child's stderr to be the | |
283 | // parent's stdout, and the child's stdout to be the parent's | |
284 | // stderr. No matter which we dup first, the second will get | |
285 | // overwritten prematurely. | |
286 | Stdio::Fd(ref fd) => { | |
287 | if fd.raw() >= 0 && fd.raw() <= libc::STDERR_FILENO { | |
54a0048b | 288 | Ok((ChildStdio::Owned(fd.duplicate()?), None)) |
7453a54e SL |
289 | } else { |
290 | Ok((ChildStdio::Explicit(fd.raw()), None)) | |
291 | } | |
292 | } | |
293 | ||
294 | Stdio::MakePipe => { | |
54a0048b | 295 | let (reader, writer) = pipe::anon_pipe()?; |
7453a54e SL |
296 | let (ours, theirs) = if readable { |
297 | (writer, reader) | |
298 | } else { | |
299 | (reader, writer) | |
300 | }; | |
301 | Ok((ChildStdio::Owned(theirs.into_fd()), Some(ours))) | |
302 | } | |
303 | ||
304 | Stdio::Null => { | |
305 | let mut opts = OpenOptions::new(); | |
306 | opts.read(readable); | |
307 | opts.write(!readable); | |
308 | let path = unsafe { | |
309 | CStr::from_ptr("/dev/null\0".as_ptr() as *const _) | |
310 | }; | |
54a0048b | 311 | let fd = File::open_c(&path, &opts)?; |
7453a54e SL |
312 | Ok((ChildStdio::Owned(fd.into_fd()), None)) |
313 | } | |
314 | } | |
85aaf69f | 315 | } |
7453a54e | 316 | } |
85aaf69f | 317 | |
041b39d2 XL |
318 | impl From<AnonPipe> for Stdio { |
319 | fn from(pipe: AnonPipe) -> Stdio { | |
320 | Stdio::Fd(pipe.into_fd()) | |
321 | } | |
322 | } | |
323 | ||
324 | impl From<File> for Stdio { | |
325 | fn from(file: File) -> Stdio { | |
326 | Stdio::Fd(file.into_fd()) | |
327 | } | |
328 | } | |
329 | ||
7453a54e | 330 | impl ChildStdio { |
476ff2be | 331 | pub fn fd(&self) -> Option<c_int> { |
7453a54e SL |
332 | match *self { |
333 | ChildStdio::Inherit => None, | |
334 | ChildStdio::Explicit(fd) => Some(fd), | |
335 | ChildStdio::Owned(ref fd) => Some(fd.raw()), | |
85aaf69f SL |
336 | } |
337 | } | |
338 | } | |
339 | ||
7453a54e SL |
340 | fn pair_to_key(key: &OsStr, value: &OsStr, saw_nul: &mut bool) -> CString { |
341 | let (key, value) = (key.as_bytes(), value.as_bytes()); | |
342 | let mut v = Vec::with_capacity(key.len() + value.len() + 1); | |
343 | v.extend(key); | |
344 | v.push(b'='); | |
345 | v.extend(value); | |
346 | CString::new(v).unwrap_or_else(|_e| { | |
347 | *saw_nul = true; | |
348 | CString::new("foo=bar").unwrap() | |
349 | }) | |
350 | } | |
351 | ||
352 | impl fmt::Debug for Command { | |
353 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
54a0048b | 354 | write!(f, "{:?}", self.program)?; |
7453a54e | 355 | for arg in &self.args { |
54a0048b | 356 | write!(f, " {:?}", arg)?; |
7453a54e SL |
357 | } |
358 | Ok(()) | |
359 | } | |
360 | } | |
361 | ||
7453a54e SL |
362 | /// Unix exit statuses |
363 | #[derive(PartialEq, Eq, Clone, Copy, Debug)] | |
364 | pub struct ExitStatus(c_int); | |
365 | ||
366 | impl ExitStatus { | |
476ff2be SL |
367 | pub fn new(status: c_int) -> ExitStatus { |
368 | ExitStatus(status) | |
369 | } | |
370 | ||
7453a54e SL |
371 | fn exited(&self) -> bool { |
372 | unsafe { libc::WIFEXITED(self.0) } | |
373 | } | |
85aaf69f | 374 | |
7453a54e SL |
375 | pub fn success(&self) -> bool { |
376 | self.code() == Some(0) | |
377 | } | |
85aaf69f | 378 | |
7453a54e SL |
379 | pub fn code(&self) -> Option<i32> { |
380 | if self.exited() { | |
381 | Some(unsafe { libc::WEXITSTATUS(self.0) }) | |
382 | } else { | |
383 | None | |
384 | } | |
385 | } | |
85aaf69f | 386 | |
7453a54e SL |
387 | pub fn signal(&self) -> Option<i32> { |
388 | if !self.exited() { | |
389 | Some(unsafe { libc::WTERMSIG(self.0) }) | |
390 | } else { | |
391 | None | |
392 | } | |
393 | } | |
85aaf69f SL |
394 | } |
395 | ||
a7813a04 XL |
396 | impl From<c_int> for ExitStatus { |
397 | fn from(a: c_int) -> ExitStatus { | |
398 | ExitStatus(a) | |
399 | } | |
400 | } | |
401 | ||
7453a54e SL |
402 | impl fmt::Display for ExitStatus { |
403 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
404 | if let Some(code) = self.code() { | |
405 | write!(f, "exit code: {}", code) | |
406 | } else { | |
407 | let signal = self.signal().unwrap(); | |
408 | write!(f, "signal: {}", signal) | |
9346a6ac | 409 | } |
7453a54e SL |
410 | } |
411 | } | |
85aaf69f | 412 | |
c30ab7b3 | 413 | #[cfg(all(test, not(target_os = "emscripten")))] |
62682a34 SL |
414 | mod tests { |
415 | use super::*; | |
62682a34 SL |
416 | |
417 | use ffi::OsStr; | |
418 | use mem; | |
419 | use ptr; | |
420 | use libc; | |
7453a54e | 421 | use sys::cvt; |
62682a34 | 422 | |
b039eaaf SL |
423 | macro_rules! t { |
424 | ($e:expr) => { | |
425 | match $e { | |
426 | Ok(t) => t, | |
427 | Err(e) => panic!("received error for `{}`: {}", stringify!($e), e), | |
428 | } | |
429 | } | |
430 | } | |
431 | ||
cc61c64b XL |
432 | // Android with api less than 21 define sig* functions inline, so it is not |
433 | // available for dynamic link. Implementing sigemptyset and sigaddset allow us | |
434 | // to support older Android version (independent of libc version). | |
435 | // The following implementations are based on https://git.io/vSkNf | |
436 | ||
62682a34 SL |
437 | #[cfg(not(target_os = "android"))] |
438 | extern { | |
cc61c64b XL |
439 | #[cfg_attr(target_os = "netbsd", link_name = "__sigemptyset14")] |
440 | fn sigemptyset(set: *mut libc::sigset_t) -> libc::c_int; | |
441 | ||
b039eaaf | 442 | #[cfg_attr(target_os = "netbsd", link_name = "__sigaddset14")] |
92a42be0 | 443 | fn sigaddset(set: *mut libc::sigset_t, signum: libc::c_int) -> libc::c_int; |
62682a34 SL |
444 | } |
445 | ||
cc61c64b XL |
446 | #[cfg(target_os = "android")] |
447 | unsafe fn sigemptyset(set: *mut libc::sigset_t) -> libc::c_int { | |
448 | libc::memset(set as *mut _, 0, mem::size_of::<libc::sigset_t>()); | |
449 | return 0; | |
450 | } | |
451 | ||
62682a34 | 452 | #[cfg(target_os = "android")] |
92a42be0 | 453 | unsafe fn sigaddset(set: *mut libc::sigset_t, signum: libc::c_int) -> libc::c_int { |
9cc50fc6 SL |
454 | use slice; |
455 | ||
92a42be0 | 456 | let raw = slice::from_raw_parts_mut(set as *mut u8, mem::size_of::<libc::sigset_t>()); |
62682a34 SL |
457 | let bit = (signum - 1) as usize; |
458 | raw[bit / 8] |= 1 << (bit % 8); | |
459 | return 0; | |
460 | } | |
461 | ||
e9174d1e | 462 | // See #14232 for more information, but it appears that signal delivery to a |
cc61c64b XL |
463 | // newly spawned process may just be raced in the macOS, so to prevent this |
464 | // test from being flaky we ignore it on macOS. | |
62682a34 | 465 | #[test] |
e9174d1e | 466 | #[cfg_attr(target_os = "macos", ignore)] |
92a42be0 | 467 | #[cfg_attr(target_os = "nacl", ignore)] // no signals on NaCl. |
8bb4bdeb XL |
468 | // When run under our current QEMU emulation test suite this test fails, |
469 | // although the reason isn't very clear as to why. For now this test is | |
470 | // ignored there. | |
471 | #[cfg_attr(target_arch = "arm", ignore)] | |
3b2f2976 | 472 | #[cfg_attr(target_arch = "aarch64", ignore)] |
62682a34 SL |
473 | fn test_process_mask() { |
474 | unsafe { | |
475 | // Test to make sure that a signal mask does not get inherited. | |
7453a54e | 476 | let mut cmd = Command::new(OsStr::new("cat")); |
62682a34 | 477 | |
92a42be0 SL |
478 | let mut set: libc::sigset_t = mem::uninitialized(); |
479 | let mut old_set: libc::sigset_t = mem::uninitialized(); | |
cc61c64b | 480 | t!(cvt(sigemptyset(&mut set))); |
b039eaaf | 481 | t!(cvt(sigaddset(&mut set, libc::SIGINT))); |
92a42be0 | 482 | t!(cvt(libc::pthread_sigmask(libc::SIG_SETMASK, &set, &mut old_set))); |
62682a34 | 483 | |
7453a54e SL |
484 | cmd.stdin(Stdio::MakePipe); |
485 | cmd.stdout(Stdio::MakePipe); | |
486 | ||
54a0048b | 487 | let (mut cat, mut pipes) = t!(cmd.spawn(Stdio::Null, true)); |
7453a54e SL |
488 | let stdin_write = pipes.stdin.take().unwrap(); |
489 | let stdout_read = pipes.stdout.take().unwrap(); | |
62682a34 | 490 | |
92a42be0 SL |
491 | t!(cvt(libc::pthread_sigmask(libc::SIG_SETMASK, &old_set, |
492 | ptr::null_mut()))); | |
62682a34 | 493 | |
92a42be0 | 494 | t!(cvt(libc::kill(cat.id() as libc::pid_t, libc::SIGINT))); |
62682a34 SL |
495 | // We need to wait until SIGINT is definitely delivered. The |
496 | // easiest way is to write something to cat, and try to read it | |
497 | // back: if SIGINT is unmasked, it'll get delivered when cat is | |
498 | // next scheduled. | |
499 | let _ = stdin_write.write(b"Hello"); | |
500 | drop(stdin_write); | |
501 | ||
502 | // Either EOF or failure (EPIPE) is okay. | |
503 | let mut buf = [0; 5]; | |
504 | if let Ok(ret) = stdout_read.read(&mut buf) { | |
8bb4bdeb | 505 | assert_eq!(ret, 0); |
62682a34 SL |
506 | } |
507 | ||
b039eaaf | 508 | t!(cat.wait()); |
62682a34 SL |
509 | } |
510 | } | |
511 | } |