darwin.rs

  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}