1use mach2::exception_types::{
2 EXC_MASK_ALL, EXCEPTION_DEFAULT, exception_behavior_t, exception_mask_t,
3};
4use mach2::port::{MACH_PORT_NULL, mach_port_t};
5use mach2::thread_status::{THREAD_STATE_NONE, thread_state_flavor_t};
6use smol::Unblock;
7use std::collections::BTreeMap;
8use std::ffi::{CString, OsStr, OsString};
9use std::io;
10use std::os::unix::ffi::OsStrExt;
11use std::os::unix::io::FromRawFd;
12use std::os::unix::process::ExitStatusExt;
13use std::path::{Path, PathBuf};
14use std::process::{ExitStatus, Output};
15use std::ptr;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum Stdio {
19 /// A new pipe should be arranged to connect the parent and child processes.
20 #[default]
21 Piped,
22 /// The child inherits from the corresponding parent descriptor.
23 Inherit,
24 /// This stream will be ignored (redirected to `/dev/null`).
25 Null,
26}
27
28impl Stdio {
29 pub fn piped() -> Self {
30 Self::Piped
31 }
32
33 pub fn inherit() -> Self {
34 Self::Inherit
35 }
36
37 pub fn null() -> Self {
38 Self::Null
39 }
40}
41
42unsafe extern "C" {
43 fn posix_spawnattr_setexceptionports_np(
44 attr: *mut libc::posix_spawnattr_t,
45 mask: exception_mask_t,
46 new_port: mach_port_t,
47 behavior: exception_behavior_t,
48 new_flavor: thread_state_flavor_t,
49 ) -> libc::c_int;
50
51 fn posix_spawn_file_actions_addchdir_np(
52 file_actions: *mut libc::posix_spawn_file_actions_t,
53 path: *const libc::c_char,
54 ) -> libc::c_int;
55
56 fn posix_spawn_file_actions_addinherit_np(
57 file_actions: *mut libc::posix_spawn_file_actions_t,
58 filedes: libc::c_int,
59 ) -> libc::c_int;
60
61 static environ: *const *mut libc::c_char;
62}
63
64#[derive(Debug)]
65pub struct Command {
66 program: OsString,
67 args: Vec<OsString>,
68 envs: BTreeMap<OsString, Option<OsString>>,
69 env_clear: bool,
70 current_dir: Option<PathBuf>,
71 stdin_cfg: Option<Stdio>,
72 stdout_cfg: Option<Stdio>,
73 stderr_cfg: Option<Stdio>,
74 kill_on_drop: bool,
75}
76
77impl Command {
78 pub fn new(program: impl AsRef<OsStr>) -> Self {
79 Self {
80 program: program.as_ref().to_owned(),
81 args: Vec::new(),
82 envs: BTreeMap::new(),
83 env_clear: false,
84 current_dir: None,
85 stdin_cfg: None,
86 stdout_cfg: None,
87 stderr_cfg: None,
88 kill_on_drop: false,
89 }
90 }
91
92 pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
93 self.args.push(arg.as_ref().to_owned());
94 self
95 }
96
97 pub fn args<I, S>(&mut self, args: I) -> &mut Self
98 where
99 I: IntoIterator<Item = S>,
100 S: AsRef<OsStr>,
101 {
102 self.args
103 .extend(args.into_iter().map(|a| a.as_ref().to_owned()));
104 self
105 }
106
107 pub fn env(&mut self, key: impl AsRef<OsStr>, val: impl AsRef<OsStr>) -> &mut Self {
108 self.envs
109 .insert(key.as_ref().to_owned(), Some(val.as_ref().to_owned()));
110 self
111 }
112
113 pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self
114 where
115 I: IntoIterator<Item = (K, V)>,
116 K: AsRef<OsStr>,
117 V: AsRef<OsStr>,
118 {
119 for (key, val) in vars {
120 self.envs
121 .insert(key.as_ref().to_owned(), Some(val.as_ref().to_owned()));
122 }
123 self
124 }
125
126 pub fn env_remove(&mut self, key: impl AsRef<OsStr>) -> &mut Self {
127 let key = key.as_ref().to_owned();
128 if self.env_clear {
129 self.envs.remove(&key);
130 } else {
131 self.envs.insert(key, None);
132 }
133 self
134 }
135
136 pub fn env_clear(&mut self) -> &mut Self {
137 self.env_clear = true;
138 self.envs.clear();
139 self
140 }
141
142 pub fn current_dir(&mut self, dir: impl AsRef<Path>) -> &mut Self {
143 self.current_dir = Some(dir.as_ref().to_owned());
144 self
145 }
146
147 pub fn stdin(&mut self, cfg: Stdio) -> &mut Self {
148 self.stdin_cfg = Some(cfg);
149 self
150 }
151
152 pub fn stdout(&mut self, cfg: Stdio) -> &mut Self {
153 self.stdout_cfg = Some(cfg);
154 self
155 }
156
157 pub fn stderr(&mut self, cfg: Stdio) -> &mut Self {
158 self.stderr_cfg = Some(cfg);
159 self
160 }
161
162 pub fn kill_on_drop(&mut self, kill_on_drop: bool) -> &mut Self {
163 self.kill_on_drop = kill_on_drop;
164 self
165 }
166
167 pub fn spawn(&mut self) -> io::Result<Child> {
168 let current_dir = self
169 .current_dir
170 .as_deref()
171 .unwrap_or_else(|| Path::new("."));
172
173 // Optimization: if no environment modifications were requested, pass None
174 // to spawn_posix so it uses the `environ` global directly, avoiding a
175 // full copy of the environment. This matches std::process::Command behavior.
176 let envs = if self.env_clear || !self.envs.is_empty() {
177 let mut result = BTreeMap::<OsString, OsString>::new();
178 if !self.env_clear {
179 for (key, val) in std::env::vars_os() {
180 result.insert(key, val);
181 }
182 }
183 for (key, maybe_val) in &self.envs {
184 if let Some(val) = maybe_val {
185 result.insert(key.clone(), val.clone());
186 } else {
187 result.remove(key);
188 }
189 }
190 Some(result.into_iter().collect::<Vec<_>>())
191 } else {
192 None
193 };
194
195 spawn_posix_spawn(
196 &self.program,
197 &self.args,
198 current_dir,
199 envs.as_deref(),
200 self.stdin_cfg.unwrap_or_default(),
201 self.stdout_cfg.unwrap_or_default(),
202 self.stderr_cfg.unwrap_or_default(),
203 self.kill_on_drop,
204 )
205 }
206
207 pub async fn output(&mut self) -> io::Result<Output> {
208 self.stdin_cfg.get_or_insert(Stdio::null());
209 self.stdout_cfg.get_or_insert(Stdio::piped());
210 self.stderr_cfg.get_or_insert(Stdio::piped());
211
212 let child = self.spawn()?;
213 child.output().await
214 }
215
216 pub async fn status(&mut self) -> io::Result<ExitStatus> {
217 let mut child = self.spawn()?;
218 child.status().await
219 }
220}
221
222#[derive(Debug)]
223pub struct Child {
224 pid: libc::pid_t,
225 pub stdin: Option<Unblock<std::fs::File>>,
226 pub stdout: Option<Unblock<std::fs::File>>,
227 pub stderr: Option<Unblock<std::fs::File>>,
228 kill_on_drop: bool,
229 status: Option<ExitStatus>,
230}
231
232impl Drop for Child {
233 fn drop(&mut self) {
234 if self.kill_on_drop && self.status.is_none() {
235 let _ = self.kill();
236 }
237 }
238}
239
240impl Child {
241 pub fn id(&self) -> u32 {
242 self.pid as u32
243 }
244
245 pub fn kill(&mut self) -> io::Result<()> {
246 let result = unsafe { libc::kill(self.pid, libc::SIGKILL) };
247 if result == -1 {
248 Err(io::Error::last_os_error())
249 } else {
250 Ok(())
251 }
252 }
253
254 pub fn try_status(&mut self) -> io::Result<Option<ExitStatus>> {
255 if let Some(status) = self.status {
256 return Ok(Some(status));
257 }
258
259 let mut status: libc::c_int = 0;
260 let result = unsafe { libc::waitpid(self.pid, &mut status, libc::WNOHANG) };
261
262 if result == -1 {
263 Err(io::Error::last_os_error())
264 } else if result == 0 {
265 Ok(None)
266 } else {
267 let exit_status = ExitStatus::from_raw(status);
268 self.status = Some(exit_status);
269 Ok(Some(exit_status))
270 }
271 }
272
273 pub fn status(
274 &mut self,
275 ) -> impl std::future::Future<Output = io::Result<ExitStatus>> + Send + 'static {
276 self.stdin.take();
277
278 let pid = self.pid;
279 let cached_status = self.status;
280
281 async move {
282 if let Some(status) = cached_status {
283 return Ok(status);
284 }
285
286 smol::unblock(move || {
287 let mut status: libc::c_int = 0;
288 let result = unsafe { libc::waitpid(pid, &mut status, 0) };
289 if result == -1 {
290 Err(io::Error::last_os_error())
291 } else {
292 Ok(ExitStatus::from_raw(status))
293 }
294 })
295 .await
296 }
297 }
298
299 pub async fn output(mut self) -> io::Result<Output> {
300 use futures_lite::AsyncReadExt;
301
302 let status = self.status();
303
304 let stdout = self.stdout.take();
305 let stdout_future = async move {
306 let mut data = Vec::new();
307 if let Some(mut stdout) = stdout {
308 stdout.read_to_end(&mut data).await?;
309 }
310 io::Result::Ok(data)
311 };
312
313 let stderr = self.stderr.take();
314 let stderr_future = async move {
315 let mut data = Vec::new();
316 if let Some(mut stderr) = stderr {
317 stderr.read_to_end(&mut data).await?;
318 }
319 io::Result::Ok(data)
320 };
321
322 let (stdout_data, stderr_data) =
323 futures_lite::future::try_zip(stdout_future, stderr_future).await?;
324 let status = status.await?;
325
326 Ok(Output {
327 status,
328 stdout: stdout_data,
329 stderr: stderr_data,
330 })
331 }
332}
333
334fn spawn_posix_spawn(
335 program: &OsStr,
336 args: &[OsString],
337 current_dir: &Path,
338 envs: Option<&[(OsString, OsString)]>,
339 stdin_cfg: Stdio,
340 stdout_cfg: Stdio,
341 stderr_cfg: Stdio,
342 kill_on_drop: bool,
343) -> io::Result<Child> {
344 let program_cstr = CString::new(program.as_bytes()).map_err(|_| invalid_input_error())?;
345
346 let current_dir_cstr =
347 CString::new(current_dir.as_os_str().as_bytes()).map_err(|_| invalid_input_error())?;
348
349 let mut argv_cstrs = vec![program_cstr.clone()];
350 for arg in args {
351 let cstr = CString::new(arg.as_bytes()).map_err(|_| invalid_input_error())?;
352 argv_cstrs.push(cstr);
353 }
354 let mut argv_ptrs: Vec<*mut libc::c_char> = argv_cstrs
355 .iter()
356 .map(|s| s.as_ptr() as *mut libc::c_char)
357 .collect();
358 argv_ptrs.push(ptr::null_mut());
359
360 let envp: Vec<CString> = if let Some(envs) = envs {
361 envs.iter()
362 .map(|(key, value)| {
363 let mut env_str = key.as_bytes().to_vec();
364 env_str.push(b'=');
365 env_str.extend_from_slice(value.as_bytes());
366 CString::new(env_str)
367 })
368 .collect::<Result<Vec<_>, _>>()
369 .map_err(|_| invalid_input_error())?
370 } else {
371 Vec::new()
372 };
373 let mut envp_ptrs: Vec<*mut libc::c_char> = envp
374 .iter()
375 .map(|s| s.as_ptr() as *mut libc::c_char)
376 .collect();
377 envp_ptrs.push(ptr::null_mut());
378
379 let (stdin_read, stdin_write) = match stdin_cfg {
380 Stdio::Piped => {
381 let (r, w) = create_pipe()?;
382 (Some(r), Some(w))
383 }
384 Stdio::Null => {
385 let fd = open_dev_null(libc::O_RDONLY)?;
386 (Some(fd), None)
387 }
388 Stdio::Inherit => (None, None),
389 };
390
391 let (stdout_read, stdout_write) = match stdout_cfg {
392 Stdio::Piped => {
393 let (r, w) = create_pipe()?;
394 (Some(r), Some(w))
395 }
396 Stdio::Null => {
397 let fd = open_dev_null(libc::O_WRONLY)?;
398 (None, Some(fd))
399 }
400 Stdio::Inherit => (None, None),
401 };
402
403 let (stderr_read, stderr_write) = match stderr_cfg {
404 Stdio::Piped => {
405 let (r, w) = create_pipe()?;
406 (Some(r), Some(w))
407 }
408 Stdio::Null => {
409 let fd = open_dev_null(libc::O_WRONLY)?;
410 (None, Some(fd))
411 }
412 Stdio::Inherit => (None, None),
413 };
414
415 let mut attr: libc::posix_spawnattr_t = ptr::null_mut();
416 let mut file_actions: libc::posix_spawn_file_actions_t = ptr::null_mut();
417
418 unsafe {
419 cvt_nz(libc::posix_spawnattr_init(&mut attr))?;
420 cvt_nz(libc::posix_spawn_file_actions_init(&mut file_actions))?;
421
422 cvt_nz(libc::posix_spawnattr_setflags(
423 &mut attr,
424 libc::POSIX_SPAWN_CLOEXEC_DEFAULT as libc::c_short,
425 ))?;
426
427 cvt_nz(posix_spawnattr_setexceptionports_np(
428 &mut attr,
429 EXC_MASK_ALL,
430 MACH_PORT_NULL,
431 EXCEPTION_DEFAULT as exception_behavior_t,
432 THREAD_STATE_NONE,
433 ))?;
434
435 cvt_nz(posix_spawn_file_actions_addchdir_np(
436 &mut file_actions,
437 current_dir_cstr.as_ptr(),
438 ))?;
439
440 if let Some(fd) = stdin_read {
441 cvt_nz(libc::posix_spawn_file_actions_adddup2(
442 &mut file_actions,
443 fd,
444 libc::STDIN_FILENO,
445 ))?;
446 cvt_nz(posix_spawn_file_actions_addinherit_np(
447 &mut file_actions,
448 libc::STDIN_FILENO,
449 ))?;
450 }
451
452 if let Some(fd) = stdout_write {
453 cvt_nz(libc::posix_spawn_file_actions_adddup2(
454 &mut file_actions,
455 fd,
456 libc::STDOUT_FILENO,
457 ))?;
458 cvt_nz(posix_spawn_file_actions_addinherit_np(
459 &mut file_actions,
460 libc::STDOUT_FILENO,
461 ))?;
462 }
463
464 if let Some(fd) = stderr_write {
465 cvt_nz(libc::posix_spawn_file_actions_adddup2(
466 &mut file_actions,
467 fd,
468 libc::STDERR_FILENO,
469 ))?;
470 cvt_nz(posix_spawn_file_actions_addinherit_np(
471 &mut file_actions,
472 libc::STDERR_FILENO,
473 ))?;
474 }
475
476 let mut pid: libc::pid_t = 0;
477
478 let spawn_result = libc::posix_spawnp(
479 &mut pid,
480 program_cstr.as_ptr(),
481 &file_actions,
482 &attr,
483 argv_ptrs.as_ptr(),
484 if envs.is_some() {
485 envp_ptrs.as_ptr()
486 } else {
487 environ
488 },
489 );
490
491 libc::posix_spawnattr_destroy(&mut attr);
492 libc::posix_spawn_file_actions_destroy(&mut file_actions);
493
494 if let Some(fd) = stdin_read {
495 libc::close(fd);
496 }
497 if let Some(fd) = stdout_write {
498 libc::close(fd);
499 }
500 if let Some(fd) = stderr_write {
501 libc::close(fd);
502 }
503
504 cvt_nz(spawn_result)?;
505
506 Ok(Child {
507 pid,
508 stdin: stdin_write.map(|fd| Unblock::new(std::fs::File::from_raw_fd(fd))),
509 stdout: stdout_read.map(|fd| Unblock::new(std::fs::File::from_raw_fd(fd))),
510 stderr: stderr_read.map(|fd| Unblock::new(std::fs::File::from_raw_fd(fd))),
511 kill_on_drop,
512 status: None,
513 })
514 }
515}
516
517fn create_pipe() -> io::Result<(libc::c_int, libc::c_int)> {
518 let mut fds: [libc::c_int; 2] = [0; 2];
519 let result = unsafe { libc::pipe(fds.as_mut_ptr()) };
520 if result == -1 {
521 return Err(io::Error::last_os_error());
522 }
523 Ok((fds[0], fds[1]))
524}
525
526fn open_dev_null(flags: libc::c_int) -> io::Result<libc::c_int> {
527 let fd = unsafe { libc::open(c"/dev/null".as_ptr() as *const libc::c_char, flags) };
528 if fd == -1 {
529 return Err(io::Error::last_os_error());
530 }
531 Ok(fd)
532}
533
534/// Zero means `Ok()`, all other values are treated as raw OS errors. Does not look at `errno`.
535/// Mirrored after Rust's std `cvt_nz` function.
536fn cvt_nz(error: libc::c_int) -> io::Result<()> {
537 if error == 0 {
538 Ok(())
539 } else {
540 Err(io::Error::from_raw_os_error(error))
541 }
542}
543
544fn invalid_input_error() -> io::Error {
545 io::Error::new(
546 io::ErrorKind::InvalidInput,
547 "invalid argument: path or argument contains null byte",
548 )
549}
550
551#[cfg(test)]
552mod tests {
553 use super::*;
554 use futures_lite::AsyncWriteExt;
555
556 #[test]
557 fn test_spawn_echo() {
558 smol::block_on(async {
559 let output = Command::new("/bin/echo")
560 .args(["-n", "hello world"])
561 .output()
562 .await
563 .expect("failed to run command");
564
565 assert!(output.status.success());
566 assert_eq!(output.stdout, b"hello world");
567 });
568 }
569
570 #[test]
571 fn test_spawn_cat_stdin() {
572 smol::block_on(async {
573 let mut child = Command::new("/bin/cat")
574 .stdin(Stdio::piped())
575 .stdout(Stdio::piped())
576 .spawn()
577 .expect("failed to spawn");
578
579 if let Some(ref mut stdin) = child.stdin {
580 stdin
581 .write_all(b"hello from stdin")
582 .await
583 .expect("failed to write");
584 stdin.close().await.expect("failed to close");
585 }
586 drop(child.stdin.take());
587
588 let output = child.output().await.expect("failed to get output");
589 assert!(output.status.success());
590 assert_eq!(output.stdout, b"hello from stdin");
591 });
592 }
593
594 #[test]
595 fn test_spawn_stderr() {
596 smol::block_on(async {
597 let output = Command::new("/bin/sh")
598 .args(["-c", "echo error >&2"])
599 .output()
600 .await
601 .expect("failed to run command");
602
603 assert!(output.status.success());
604 assert_eq!(output.stderr, b"error\n");
605 });
606 }
607
608 #[test]
609 fn test_spawn_exit_code() {
610 smol::block_on(async {
611 let output = Command::new("/bin/sh")
612 .args(["-c", "exit 42"])
613 .output()
614 .await
615 .expect("failed to run command");
616
617 assert!(!output.status.success());
618 assert_eq!(output.status.code(), Some(42));
619 });
620 }
621
622 #[test]
623 fn test_spawn_current_dir() {
624 smol::block_on(async {
625 let output = Command::new("/bin/pwd")
626 .current_dir("/tmp")
627 .output()
628 .await
629 .expect("failed to run command");
630
631 assert!(output.status.success());
632 let pwd = String::from_utf8_lossy(&output.stdout);
633 assert!(pwd.trim() == "/tmp" || pwd.trim() == "/private/tmp");
634 });
635 }
636
637 #[test]
638 fn test_spawn_env() {
639 smol::block_on(async {
640 let output = Command::new("/bin/sh")
641 .args(["-c", "echo $MY_TEST_VAR"])
642 .env("MY_TEST_VAR", "test_value")
643 .output()
644 .await
645 .expect("failed to run command");
646
647 assert!(output.status.success());
648 assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "test_value");
649 });
650 }
651
652 #[test]
653 fn test_spawn_status() {
654 smol::block_on(async {
655 let status = Command::new("/usr/bin/true")
656 .status()
657 .await
658 .expect("failed to run command");
659
660 assert!(status.success());
661
662 let status = Command::new("/usr/bin/false")
663 .status()
664 .await
665 .expect("failed to run command");
666
667 assert!(!status.success());
668 });
669 }
670
671 #[test]
672 fn test_env_remove_removes_set_env() {
673 smol::block_on(async {
674 let output = Command::new("/bin/sh")
675 .args(["-c", "echo ${MY_VAR:-unset}"])
676 .env("MY_VAR", "set_value")
677 .env_remove("MY_VAR")
678 .output()
679 .await
680 .expect("failed to run command");
681
682 assert!(output.status.success());
683 assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "unset");
684 });
685 }
686
687 #[test]
688 fn test_env_remove_removes_inherited_env() {
689 smol::block_on(async {
690 // SAFETY: This test is single-threaded and we clean up the var at the end
691 unsafe { std::env::set_var("TEST_INHERITED_VAR", "inherited_value") };
692
693 let output = Command::new("/bin/sh")
694 .args(["-c", "echo ${TEST_INHERITED_VAR:-unset}"])
695 .env_remove("TEST_INHERITED_VAR")
696 .output()
697 .await
698 .expect("failed to run command");
699
700 assert!(output.status.success());
701 assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "unset");
702
703 // SAFETY: Cleaning up test env var
704 unsafe { std::env::remove_var("TEST_INHERITED_VAR") };
705 });
706 }
707
708 #[test]
709 fn test_env_after_env_remove() {
710 smol::block_on(async {
711 let output = Command::new("/bin/sh")
712 .args(["-c", "echo ${MY_VAR:-unset}"])
713 .env_remove("MY_VAR")
714 .env("MY_VAR", "new_value")
715 .output()
716 .await
717 .expect("failed to run command");
718
719 assert!(output.status.success());
720 assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "new_value");
721 });
722 }
723
724 #[test]
725 fn test_env_remove_after_env_clear() {
726 smol::block_on(async {
727 let output = Command::new("/bin/sh")
728 .args(["-c", "echo ${MY_VAR:-unset}"])
729 .env_clear()
730 .env("MY_VAR", "set_value")
731 .env_remove("MY_VAR")
732 .output()
733 .await
734 .expect("failed to run command");
735
736 assert!(output.status.success());
737 assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "unset");
738 });
739 }
740
741 #[test]
742 fn test_stdio_null_stdin() {
743 smol::block_on(async {
744 let child = Command::new("/bin/cat")
745 .stdin(Stdio::null())
746 .stdout(Stdio::piped())
747 .spawn()
748 .expect("failed to spawn");
749
750 let output = child.output().await.expect("failed to get output");
751 assert!(output.status.success());
752 assert!(
753 output.stdout.is_empty(),
754 "stdin from /dev/null should produce no output from cat"
755 );
756 });
757 }
758
759 #[test]
760 fn test_stdio_null_stdout() {
761 smol::block_on(async {
762 let mut child = Command::new("/bin/echo")
763 .args(["hello"])
764 .stdout(Stdio::null())
765 .spawn()
766 .expect("failed to spawn");
767
768 assert!(
769 child.stdout.is_none(),
770 "stdout should be None when Stdio::null() is used"
771 );
772
773 let status = child.status().await.expect("failed to get status");
774 assert!(status.success());
775 });
776 }
777
778 #[test]
779 fn test_stdio_null_stderr() {
780 smol::block_on(async {
781 let mut child = Command::new("/bin/sh")
782 .args(["-c", "echo error >&2"])
783 .stderr(Stdio::null())
784 .spawn()
785 .expect("failed to spawn");
786
787 assert!(
788 child.stderr.is_none(),
789 "stderr should be None when Stdio::null() is used"
790 );
791
792 let status = child.status().await.expect("failed to get status");
793 assert!(status.success());
794 });
795 }
796
797 #[test]
798 fn test_stdio_piped_stdin() {
799 smol::block_on(async {
800 let mut child = Command::new("/bin/cat")
801 .stdin(Stdio::piped())
802 .stdout(Stdio::piped())
803 .spawn()
804 .expect("failed to spawn");
805
806 assert!(
807 child.stdin.is_some(),
808 "stdin should be Some when Stdio::piped() is used"
809 );
810
811 if let Some(ref mut stdin) = child.stdin {
812 stdin
813 .write_all(b"piped input")
814 .await
815 .expect("failed to write");
816 stdin.close().await.expect("failed to close");
817 }
818 drop(child.stdin.take());
819
820 let output = child.output().await.expect("failed to get output");
821 assert!(output.status.success());
822 assert_eq!(output.stdout, b"piped input");
823 });
824 }
825}