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 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}