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