diff options
author | Jesse Luehrs <doy@tozt.net> | 2021-12-28 03:33:52 -0500 |
---|---|---|
committer | Jesse Luehrs <doy@tozt.net> | 2021-12-28 05:28:28 -0500 |
commit | f8780ca1e76286688b74d8a6c64d5fadf3cfd2a1 (patch) | |
tree | b1e0fe6a378f3a8810e0332ca572a86185fd556c | |
parent | b181b63a69d5db78769c1c3723a9940f66491466 (diff) | |
download | pty-process-f8780ca1e76286688b74d8a6c64d5fadf3cfd2a1.tar.gz pty-process-f8780ca1e76286688b74d8a6c64d5fadf3cfd2a1.zip |
wip
-rw-r--r-- | Cargo.toml | 3 | ||||
-rw-r--r-- | examples/async-std.rs | 26 | ||||
-rw-r--r-- | examples/basic.rs | 28 | ||||
-rw-r--r-- | examples/interhack.rs | 18 | ||||
-rw-r--r-- | examples/smol.rs | 27 | ||||
-rw-r--r-- | examples/tokio.rs | 19 | ||||
-rw-r--r-- | src/async_std.rs | 7 | ||||
-rw-r--r-- | src/command.rs | 172 | ||||
-rw-r--r-- | src/command/async_process.rs | 33 | ||||
-rw-r--r-- | src/command/std.rs | 33 | ||||
-rw-r--r-- | src/command/tokio.rs | 34 | ||||
-rw-r--r-- | src/lib.rs | 2 | ||||
-rw-r--r-- | src/pty.rs | 83 | ||||
-rw-r--r-- | src/pty/async_io.rs | 49 | ||||
-rw-r--r-- | src/pty/std.rs | 44 | ||||
-rw-r--r-- | src/pty/tokio.rs | 72 | ||||
-rw-r--r-- | src/smol.rs | 7 | ||||
-rw-r--r-- | src/std.rs | 6 | ||||
-rw-r--r-- | src/tokio.rs | 6 | ||||
-rw-r--r-- | tests/basic.rs | 28 | ||||
-rw-r--r-- | tests/pipe.rs | 68 | ||||
-rw-r--r-- | tests/winch.rs | 18 |
22 files changed, 394 insertions, 389 deletions
@@ -36,3 +36,6 @@ backend-std = [] backend-async-std = ["async-io", "async-process"] backend-smol = ["async-io", "async-process"] backend-tokio = ["tokio", "futures"] + +[patch.crates-io] +async-process = { git = "https://github.com/doy/async-process", branch = "status-drop" } diff --git a/examples/async-std.rs b/examples/async-std.rs index 6e1b941..880c91b 100644 --- a/examples/async-std.rs +++ b/examples/async-std.rs @@ -7,7 +7,7 @@ mod main { use async_std::prelude::FutureExt as _; pub async fn run( - child: &pty_process::async_std::Child, + child: &mut pty_process::async_std::Child, ) -> std::result::Result<(), Box<dyn std::error::Error + '_>> { let _raw = super::raw_guard::RawGuard::new(); @@ -38,18 +38,18 @@ mod main { stdout.flush().await.unwrap(); } Err(e) => { - // EIO means that the process closed the other - // end of the pty - if e.raw_os_error() != Some(libc::EIO) { - eprintln!("pty read failed: {:?}", e); - } + eprintln!("pty read failed: {:?}", e); break; } } } }); - ex.run(input.race(output)).await; + let wait = async { + child.status_no_drop().await.unwrap(); + }; + + ex.run(input.race(output).race(wait)).await; Ok(()) } @@ -57,15 +57,15 @@ mod main { #[cfg(feature = "backend-async-std")] fn main() { - use pty_process::Command as _; use std::os::unix::process::ExitStatusExt as _; let status = async_std::task::block_on(async { - let mut child = async_std::process::Command::new("sleep") - .args(&["500"]) - .spawn_pty(Some(&pty_process::Size::new(24, 80))) - .unwrap(); - main::run(&child).await.unwrap(); + let pty = pty_process::async_std::Pty::new().unwrap(); + pty.resize(pty_process::Size::new(24, 80)).unwrap(); + let mut cmd = pty_process::async_std::Command::new("tac"); + // cmd.args(&["500"]); + let mut child = cmd.spawn(pty).unwrap(); + main::run(&mut child).await.unwrap(); child.status().await.unwrap() }); std::process::exit( diff --git a/examples/basic.rs b/examples/basic.rs index bee6758..1e5913b 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -5,7 +5,7 @@ mod main { use std::io::{Read as _, Write as _}; use std::os::unix::io::AsRawFd as _; - pub fn run(child: &pty_process::std::Child) { + pub fn run(child: &mut pty_process::std::Child) { let _raw = super::raw_guard::RawGuard::new(); let mut buf = [0_u8; 4096]; let pty = child.pty().as_raw_fd(); @@ -34,11 +34,7 @@ mod main { stdout.flush().unwrap(); } Err(e) => { - // EIO means that the process closed the other - // end of the pty - if e.raw_os_error() != Some(libc::EIO) { - eprintln!("pty read failed: {:?}", e); - } + eprintln!("pty read failed: {:?}", e); break; } }; @@ -62,21 +58,29 @@ mod main { break; } } + match child.try_wait() { + Ok(Some(_)) => break, + Ok(None) => {} + Err(e) => { + println!("wait failed: {:?}", e); + break; + } + } } } } #[cfg(feature = "backend-std")] fn main() { - use pty_process::Command as _; use std::os::unix::process::ExitStatusExt as _; - let mut child = std::process::Command::new("sleep") - .args(&["500"]) - .spawn_pty(Some(&pty_process::Size::new(24, 80))) - .unwrap(); + let pty = pty_process::std::Pty::new().unwrap(); + pty.resize(pty_process::Size::new(24, 80)).unwrap(); + let mut cmd = pty_process::std::Command::new("tac"); + // cmd.args(&["500"]); + let mut child = cmd.spawn(pty).unwrap(); - main::run(&child); + main::run(&mut child); let status = child.wait().unwrap(); std::process::exit( diff --git a/examples/interhack.rs b/examples/interhack.rs index 8caebb4..e636282 100644 --- a/examples/interhack.rs +++ b/examples/interhack.rs @@ -5,7 +5,7 @@ mod main { use smol::io::{AsyncReadExt as _, AsyncWriteExt as _}; pub async fn run( - child: &pty_process::smol::Child, + child: &mut pty_process::smol::Child, ) -> std::result::Result<(), Box<dyn std::error::Error>> { let _raw = super::raw_guard::RawGuard::new(); @@ -82,7 +82,12 @@ mod main { } }); - ex.run(smol::future::or(input, output)).await; + let wait = async { + child.status_no_drop().await.unwrap(); + }; + + ex.run(smol::future::or(smol::future::or(input, output), wait)) + .await; Ok(()) } @@ -90,7 +95,6 @@ mod main { #[cfg(feature = "backend-smol")] fn main() { - use pty_process::Command as _; use std::os::unix::process::ExitStatusExt as _; let (w, h) = if let Some((w, h)) = term_size::dimensions() { @@ -99,10 +103,12 @@ fn main() { (80, 24) }; let status = smol::block_on(async { - let mut child = smol::process::Command::new("nethack") - .spawn_pty(Some(&pty_process::Size::new(h, w))) + let pty = pty_process::smol::Pty::new().unwrap(); + pty.resize(pty_process::Size::new(h, w)).unwrap(); + let mut child = pty_process::smol::Command::new("nethack") + .spawn(pty) .unwrap(); - main::run(&child).await.unwrap(); + main::run(&mut child).await.unwrap(); child.status().await.unwrap() }); std::process::exit( diff --git a/examples/smol.rs b/examples/smol.rs index d519fbe..b0b90e4 100644 --- a/examples/smol.rs +++ b/examples/smol.rs @@ -5,7 +5,7 @@ mod main { use smol::io::{AsyncReadExt as _, AsyncWriteExt as _}; pub async fn run( - child: &pty_process::smol::Child, + child: &mut pty_process::smol::Child, ) -> std::result::Result<(), Box<dyn std::error::Error>> { let _raw = super::raw_guard::RawGuard::new(); @@ -36,18 +36,19 @@ mod main { stdout.flush().await.unwrap(); } Err(e) => { - // EIO means that the process closed the other - // end of the pty - if e.raw_os_error() != Some(libc::EIO) { - eprintln!("pty read failed: {:?}", e); - } + eprintln!("pty read failed: {:?}", e); break; } } } }); - ex.run(smol::future::or(input, output)).await; + let wait = async { + child.status_no_drop().await.unwrap(); + }; + + ex.run(smol::future::or(smol::future::or(input, output), wait)) + .await; Ok(()) } @@ -55,15 +56,15 @@ mod main { #[cfg(feature = "backend-smol")] fn main() { - use pty_process::Command as _; use std::os::unix::process::ExitStatusExt as _; let status = smol::block_on(async { - let mut child = smol::process::Command::new("sleep") - .args(&["500"]) - .spawn_pty(Some(&pty_process::Size::new(24, 80))) - .unwrap(); - main::run(&child).await.unwrap(); + let pty = pty_process::smol::Pty::new().unwrap(); + pty.resize(pty_process::Size::new(24, 80)).unwrap(); + let mut cmd = pty_process::smol::Command::new("tac"); + // cmd.args(&["500"]); + let mut child = cmd.spawn(pty).unwrap(); + main::run(&mut child).await.unwrap(); child.status().await.unwrap() }); std::process::exit( diff --git a/examples/tokio.rs b/examples/tokio.rs index 0dc007f..bc62592 100644 --- a/examples/tokio.rs +++ b/examples/tokio.rs @@ -32,14 +32,13 @@ mod main { stdout.flush().await.unwrap(); } Err(e) => { - // EIO means that the process closed the other - // end of the pty - if e.raw_os_error() != Some(libc::EIO) { - eprintln!("pty read failed: {:?}", e); - } + eprintln!("pty read failed: {:?}", e); break; } }, + // _ = child.status_no_drop() => { + // break; + // } } } @@ -50,13 +49,13 @@ mod main { #[cfg(feature = "backend-tokio")] #[tokio::main] async fn main() { - use pty_process::Command as _; use std::os::unix::process::ExitStatusExt as _; - let mut child = tokio::process::Command::new("sleep") - .args(&["500"]) - .spawn_pty(Some(&pty_process::Size::new(24, 80))) - .unwrap(); + let pty = pty_process::tokio::Pty::new().unwrap(); + pty.resize(pty_process::Size::new(24, 80)).unwrap(); + let mut cmd = pty_process::tokio::Command::new("tac"); + // cmd.args(&["500"]); + let mut child = cmd.spawn(pty).unwrap(); main::run(&mut child).await.unwrap(); let status = child.wait().await.unwrap(); std::process::exit( diff --git a/src/async_std.rs b/src/async_std.rs index 2a66647..8d0e877 100644 --- a/src/async_std.rs +++ b/src/async_std.rs @@ -1,2 +1,5 @@ -pub type Child = - crate::Child<async_process::Child, crate::pty::async_io::Pty>; +type Pt = async_io::Async<std::fs::File>; + +pub type Command = crate::Command<async_process::Command, Pt>; +pub type Child = crate::Child<async_process::Child, Pt>; +pub type Pty = crate::Pty<Pt>; diff --git a/src/command.rs b/src/command.rs index 303b885..b3d1104 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,6 +1,4 @@ -use crate::pty::Pty as _; - -use ::std::os::unix::io::AsRawFd as _; +use ::std::os::unix::io::{AsRawFd as _, FromRawFd as _}; #[cfg(any(feature = "backend-async-std", feature = "backend-smol"))] mod async_process; @@ -9,47 +7,59 @@ mod std; #[cfg(feature = "backend-tokio")] mod tokio; -/// Adds methods to the existing `Command` struct. -/// -/// This trait is automatically implemented for a backend's `Command` struct -/// when that backend's feature is enabled. -pub trait Command { - type Child; - type Pty; +pub struct Command<C: Impl, P: crate::pty::Impl> { + inner: C, + stdin_set: bool, + stdout_set: bool, + stderr_set: bool, - /// Creates a new pty, associates the command's stdin/stdout/stderr with - /// that pty, and then calls `spawn`. This will override any previous - /// calls to `stdin`/`stdout`/`stderr`. - /// - /// # Errors - /// * `Error::CreatePty`: error creating pty - /// * `Error::SetTermSize`: error setting terminal size - /// * `Error::Spawn`: error spawning subprocess - fn spawn_pty( - &mut self, - size: Option<&crate::pty::Size>, - ) -> crate::error::Result<Child<Self::Child, Self::Pty>>; + _phantom: ::std::marker::PhantomData<P>, } -impl<T> Command for T -where - T: Impl, - T::Pty: crate::pty::Pty, - <<T as Impl>::Pty as crate::pty::Pty>::Pt: ::std::os::unix::io::AsRawFd, -{ - type Child = T::Child; - type Pty = T::Pty; +impl<C: Impl, P: crate::pty::Impl> Command<C, P> { + pub fn new<S: AsRef<::std::ffi::OsStr>>(program: S) -> Self { + Self { + inner: C::new_impl(program.as_ref()), + stdin_set: false, + stdout_set: false, + stderr_set: false, + + _phantom: ::std::marker::PhantomData, + } + } + + pub fn stdin<T: Into<::std::process::Stdio>>(&mut self, cfg: T) { + self.stdin_set = true; + self.inner.stdin_impl(cfg.into()); + } + + pub fn stdout<T: Into<::std::process::Stdio>>(&mut self, cfg: T) { + self.stdout_set = true; + self.inner.stdout_impl(cfg.into()); + } + + pub fn stderr<T: Into<::std::process::Stdio>>(&mut self, cfg: T) { + self.stderr_set = true; + self.inner.stderr_impl(cfg.into()); + } - fn spawn_pty( + pub fn spawn( &mut self, - size: Option<&crate::pty::Size>, - ) -> crate::error::Result<Child<Self::Child, Self::Pty>> { - let (pty, pts, stdin, stdout, stderr) = setup_pty::<Self::Pty>(size)?; + pty: crate::Pty<P>, + ) -> crate::Result<Child<<C as Impl>::Child, P>> { + let (pts, stdin, stdout, stderr) = pty.setup()?; let pt_fd = pty.pt().as_raw_fd(); let pts_fd = pts.as_raw_fd(); - self.std_fds(stdin, stdout, stderr); + self.inner + .stdin_impl(unsafe { ::std::process::Stdio::from_raw_fd(stdin) }); + self.inner.stdout_impl(unsafe { + ::std::process::Stdio::from_raw_fd(stdout) + }); + self.inner.stderr_impl(unsafe { + ::std::process::Stdio::from_raw_fd(stderr) + }); let pre_exec = move || { nix::unistd::setsid()?; @@ -75,30 +85,44 @@ where // safe because setsid() and close() are async-signal-safe functions // and ioctl() is a raw syscall (which is inherently // async-signal-safe). - unsafe { self.pre_exec_impl(pre_exec) }; + unsafe { self.inner.pre_exec_impl(pre_exec) }; + + let child = self.inner.spawn_impl()?; + + Ok(Child::new(child, pty)) + } +} + +impl<C: Impl, P: crate::pty::Impl> ::std::ops::Deref for Command<C, P> { + type Target = C; - let child = self.spawn_impl().map_err(crate::error::spawn)?; + fn deref(&self) -> &Self::Target { + &self.inner + } +} - Ok(Child { child, pty }) +impl<C: Impl, P: crate::pty::Impl> ::std::ops::DerefMut for Command<C, P> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner } } -/// Wrapper struct adding pty methods to the normal `Child` struct. -pub struct Child<C, P> { - child: C, - pty: P, +pub struct Child<C, P: crate::pty::Impl> { + inner: C, + pty: crate::Pty<P>, } -impl<C, P> Child<C, P> -where - P: crate::pty::Pty, -{ +impl<C, P: crate::pty::Impl> Child<C, P> { + fn new(inner: C, pty: crate::Pty<P>) -> Self { + Self { inner, pty } + } + /// Returns a reference to the pty. /// /// The underlying pty instance is guaranteed to implement /// [`AsRawFd`](::std::os::unix::io::AsRawFd), as well as the appropriate /// `Read` and `Write` traits for the associated backend. - pub fn pty(&self) -> &P::Pt { + pub fn pty(&self) -> &P { self.pty.pt() } @@ -111,7 +135,7 @@ where /// This method is primarily useful for the tokio backend, since tokio's /// `AsyncRead` and `AsyncWrite` traits have methods which take mutable /// references. - pub fn pty_mut(&mut self) -> &mut P::Pt { + pub fn pty_mut(&mut self) -> &mut P { self.pty.pt_mut() } @@ -124,70 +148,38 @@ where /// * `Error::SetTermSize`: error setting terminal size pub fn resize_pty( &self, - size: &crate::pty::Size, + size: crate::pty::Size, ) -> crate::error::Result<()> { self.pty.resize(size) } } -impl<C, P> ::std::ops::Deref for Child<C, P> { +impl<C, P: crate::pty::Impl> ::std::ops::Deref for Child<C, P> { type Target = C; fn deref(&self) -> &Self::Target { - &self.child + &self.inner } } -impl<C, P> ::std::ops::DerefMut for Child<C, P> { +impl<C, P: crate::pty::Impl> ::std::ops::DerefMut for Child<C, P> { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.child + &mut self.inner } } -// XXX shouldn't be pub? pub trait Impl { type Child; - type Pty; - fn std_fds( - &mut self, - stdin: ::std::os::unix::io::RawFd, - stdout: ::std::os::unix::io::RawFd, - stderr: ::std::os::unix::io::RawFd, - ); + fn new_impl(program: &::std::ffi::OsStr) -> Self; + fn stdin_impl(&mut self, cfg: ::std::process::Stdio); + fn stdout_impl(&mut self, cfg: ::std::process::Stdio); + fn stderr_impl(&mut self, cfg: ::std::process::Stdio); unsafe fn pre_exec_impl<F>(&mut self, f: F) where F: FnMut() -> ::std::io::Result<()> + Send + Sync + 'static; - fn spawn_impl(&mut self) -> ::std::io::Result<Self::Child>; -} - -fn setup_pty<P>( - size: Option<&crate::pty::Size>, -) -> crate::error::Result<( - P, - ::std::fs::File, - ::std::os::unix::io::RawFd, - ::std::os::unix::io::RawFd, - ::std::os::unix::io::RawFd, -)> -where - P: crate::pty::Pty, -{ - let pty = P::new()?; - if let Some(size) = size { - pty.resize(size)?; - } - - let pts = pty.pts()?; - let pts_fd = pts.as_raw_fd(); - - let stdin = nix::unistd::dup(pts_fd).map_err(crate::error::create_pty)?; - let stdout = - nix::unistd::dup(pts_fd).map_err(crate::error::create_pty)?; - let stderr = - nix::unistd::dup(pts_fd).map_err(crate::error::create_pty)?; - Ok((pty, pts, stdin, stdout, stderr)) + fn spawn_impl(&mut self) -> crate::Result<Self::Child>; } fn set_controlling_terminal( diff --git a/src/command/async_process.rs b/src/command/async_process.rs index 4a1d042..a0aaa47 100644 --- a/src/command/async_process.rs +++ b/src/command/async_process.rs @@ -1,23 +1,22 @@ use async_process::unix::CommandExt as _; -use std::os::unix::io::FromRawFd as _; impl super::Impl for async_process::Command { type Child = async_process::Child; - type Pty = crate::pty::async_io::Pty; - fn std_fds( - &mut self, - stdin: ::std::os::unix::io::RawFd, - stdout: ::std::os::unix::io::RawFd, - stderr: ::std::os::unix::io::RawFd, - ) { - // safe because the fds are valid (otherwise pty.pts() or dup() would - // have returned an Err and we would have exited early) and are not - // owned by any other structure (since dup() returns a fresh copy of - // the file descriptor), allowing from_raw_fd to take ownership of it. - self.stdin(unsafe { std::process::Stdio::from_raw_fd(stdin) }) - .stdout(unsafe { std::process::Stdio::from_raw_fd(stdout) }) - .stderr(unsafe { std::process::Stdio::from_raw_fd(stderr) }); + fn new_impl(program: &::std::ffi::OsStr) -> Self { + Self::new(program) + } + + fn stdin_impl(&mut self, cfg: ::std::process::Stdio) { + self.stdin(cfg); + } + + fn stdout_impl(&mut self, cfg: ::std::process::Stdio) { + self.stdout(cfg); + } + + fn stderr_impl(&mut self, cfg: ::std::process::Stdio) { + self.stderr(cfg); } unsafe fn pre_exec_impl<F>(&mut self, f: F) @@ -27,7 +26,7 @@ impl super::Impl for async_process::Command { self.pre_exec(f); } - fn spawn_impl(&mut self) -> ::std::io::Result<Self::Child> { - self.spawn() + fn spawn_impl(&mut self) -> crate::Result<Self::Child> { + self.spawn().map_err(crate::error::spawn) } } diff --git a/src/command/std.rs b/src/command/std.rs index 5c08808..2a89db5 100644 --- a/src/command/std.rs +++ b/src/command/std.rs @@ -1,23 +1,22 @@ -use std::os::unix::io::FromRawFd as _; use std::os::unix::process::CommandExt as _; impl super::Impl for std::process::Command { type Child = std::process::Child; - type Pty = crate::pty::std::Pty; - fn std_fds( - &mut self, - stdin: ::std::os::unix::io::RawFd, - stdout: ::std::os::unix::io::RawFd, - stderr: ::std::os::unix::io::RawFd, - ) { - // safe because the fds are valid (otherwise pty.pts() or dup() would - // have returned an Err and we would have exited early) and are not - // owned by any other structure (since dup() returns a fresh copy of - // the file descriptor), allowing from_raw_fd to take ownership of it. - self.stdin(unsafe { std::process::Stdio::from_raw_fd(stdin) }) - .stdout(unsafe { std::process::Stdio::from_raw_fd(stdout) }) - .stderr(unsafe { std::process::Stdio::from_raw_fd(stderr) }); + fn new_impl(program: &::std::ffi::OsStr) -> Self { + Self::new(program) + } + + fn stdin_impl(&mut self, cfg: ::std::process::Stdio) { + self.stdin(cfg); + } + + fn stdout_impl(&mut self, cfg: ::std::process::Stdio) { + self.stdout(cfg); + } + + fn stderr_impl(&mut self, cfg: ::std::process::Stdio) { + self.stderr(cfg); } unsafe fn pre_exec_impl<F>(&mut self, f: F) @@ -27,7 +26,7 @@ impl super::Impl for std::process::Command { self.pre_exec(f); } - fn spawn_impl(&mut self) -> ::std::io::Result<Self::Child> { - self.spawn() + fn spawn_impl(&mut self) -> crate::Result<Self::Child> { + self.spawn().map_err(crate::error::spawn) } } diff --git a/src/command/tokio.rs b/src/command/tokio.rs index 58d2c26..c63b18a 100644 --- a/src/command/tokio.rs +++ b/src/command/tokio.rs @@ -1,22 +1,20 @@ -use std::os::unix::io::FromRawFd as _; - impl super::Impl for tokio::process::Command { type Child = tokio::process::Child; - type Pty = crate::pty::tokio::Pty; - fn std_fds( - &mut self, - stdin: ::std::os::unix::io::RawFd, - stdout: ::std::os::unix::io::RawFd, - stderr: ::std::os::unix::io::RawFd, - ) { - // safe because the fds are valid (otherwise pty.pts() or dup() would - // have returned an Err and we would have exited early) and are not - // owned by any other structure (since dup() returns a fresh copy of - // the file descriptor), allowing from_raw_fd to take ownership of it. - self.stdin(unsafe { std::process::Stdio::from_raw_fd(stdin) }) - .stdout(unsafe { std::process::Stdio::from_raw_fd(stdout) }) - .stderr(unsafe { std::process::Stdio::from_raw_fd(stderr) }); + fn new_impl(program: &::std::ffi::OsStr) -> Self { + Self::new(program) + } + + fn stdin_impl(&mut self, cfg: ::std::process::Stdio) { + self.stdin(cfg); + } + + fn stdout_impl(&mut self, cfg: ::std::process::Stdio) { + self.stdout(cfg); + } + + fn stderr_impl(&mut self, cfg: ::std::process::Stdio) { + self.stderr(cfg); } unsafe fn pre_exec_impl<F>(&mut self, f: F) @@ -26,7 +24,7 @@ impl super::Impl for tokio::process::Command { self.pre_exec(f); } - fn spawn_impl(&mut self) -> ::std::io::Result<Self::Child> { - self.spawn() + fn spawn_impl(&mut self) -> crate::Result<Self::Child> { + self.spawn().map_err(crate::error::spawn) } } @@ -63,7 +63,7 @@ pub use command::{Child, Command}; mod error; pub use error::{Error, Result, Source}; mod pty; -pub use pty::Size; +pub use pty::{Pty, Size}; #[cfg(feature = "backend-async-std")] pub mod async_std; @@ -1,4 +1,4 @@ -use ::std::os::unix::io::IntoRawFd as _; +use ::std::os::unix::io::{AsRawFd as _, FromRawFd as _, IntoRawFd as _}; #[cfg(any(feature = "backend-async-std", feature = "backend-smol"))] pub mod async_io; @@ -7,19 +7,76 @@ pub mod std; #[cfg(feature = "backend-tokio")] pub mod tokio; -pub trait Pty { - type Pt; +pub struct Pty<Pt: Impl> { + pt: Pt, + ptsname: ::std::path::PathBuf, +} + +impl<Pt: Impl> Pty<Pt> { + pub fn new() -> crate::error::Result<Self> { + let (pt_fd, ptsname) = create_pt()?; + + // safe because posix_openpt (or the previous functions operating on + // the result) would have returned an Err (causing us to return early) + // if the file descriptor was invalid. additionally, into_raw_fd gives + // up ownership over the file descriptor, allowing the newly created + // File object to take full ownership. + let fh = unsafe { ::std::fs::File::from_raw_fd(pt_fd) }; + let pt = Pt::new_from_fh(fh)?; + + Ok(Self { pt, ptsname }) + } + + pub fn pt(&self) -> &Pt { + &self.pt + } + + pub fn pt_mut(&mut self) -> &mut Pt { + &mut self.pt + } + + pub fn resize(&self, size: crate::Size) -> crate::error::Result<()> { + set_term_size(self.pt().as_raw_fd(), size) + .map_err(crate::error::set_term_size) + } + + fn pts(&self) -> crate::error::Result<::std::fs::File> { + let fh = ::std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(&self.ptsname) + .map_err(crate::error::create_pty)?; + Ok(fh) + } + + pub(crate) fn setup( + &self, + ) -> crate::Result<( + ::std::fs::File, + ::std::os::unix::io::RawFd, + ::std::os::unix::io::RawFd, + ::std::os::unix::io::RawFd, + )> { + let pts = self.pts()?; + let pts_fd = pts.as_raw_fd(); + + let stdin = + nix::unistd::dup(pts_fd).map_err(crate::error::create_pty)?; + let stdout = + nix::unistd::dup(pts_fd).map_err(crate::error::create_pty)?; + let stderr = + nix::unistd::dup(pts_fd).map_err(crate::error::create_pty)?; + + Ok((pts, stdin, stdout, stderr)) + } +} - fn new() -> crate::error::Result<Self> - where - Self: Sized; - fn pt(&self) -> &Self::Pt; - fn pt_mut(&mut self) -> &mut Self::Pt; - fn pts(&self) -> crate::error::Result<::std::fs::File>; - fn resize(&self, size: &super::Size) -> crate::error::Result<()>; +pub trait Impl: ::std::os::unix::io::AsRawFd + Sized { + fn new_from_fh(fh: ::std::fs::File) -> crate::Result<Self>; } /// Represents the size of the pty. +#[derive(Debug, Clone, Copy)] pub struct Size { row: u16, col: u16, @@ -58,8 +115,8 @@ impl Size { } } -impl From<&Size> for nix::pty::Winsize { - fn from(size: &Size) -> Self { +impl From<Size> for nix::pty::Winsize { + fn from(size: Size) -> Self { Self { ws_row: size.row, ws_col: size.col, @@ -96,7 +153,7 @@ nix::ioctl_write_ptr_bad!( fn set_term_size( fd: ::std::os::unix::io::RawFd, - size: &Size, + size: Size, ) -> nix::Result<()> { let size = size.into(); // safe because std::fs::File is required to contain a valid file diff --git a/src/pty/async_io.rs b/src/pty/async_io.rs index 304f403..d76c92c 100644 --- a/src/pty/async_io.rs +++ b/src/pty/async_io.rs @@ -1,48 +1,5 @@ -use std::os::unix::io::{AsRawFd as _, FromRawFd as _}; - -pub struct Pty { - pt: async_io::Async<std::fs::File>, - ptsname: std::path::PathBuf, -} - -impl super::Pty for Pty { - type Pt = async_io::Async<std::fs::File>; - - fn new() -> crate::error::Result<Self> { - let (pt_fd, ptsname) = super::create_pt()?; - - // safe because posix_openpt (or the previous functions operating on - // the result) would have returned an Err (causing us to return early) - // if the file descriptor was invalid. additionally, into_raw_fd gives - // up ownership over the file descriptor, allowing the newly created - // File object to take full ownership. - let pt = unsafe { std::fs::File::from_raw_fd(pt_fd) }; - - let pt = - async_io::Async::new(pt).map_err(crate::error::create_pty)?; - - Ok(Self { pt, ptsname }) - } - - fn pt(&self) -> &Self::Pt { - &self.pt - } - - fn pt_mut(&mut self) -> &mut Self::Pt { - &mut self.pt - } - - fn pts(&self) -> crate::error::Result<std::fs::File> { - let fh = std::fs::OpenOptions::new() - .read(true) - .write(true) - .open(&self.ptsname) - .map_err(crate::error::create_pty)?; - Ok(fh) - } - - fn resize(&self, size: &super::Size) -> crate::error::Result<()> { - super::set_term_size(self.pt().as_raw_fd(), size) - .map_err(crate::error::set_term_size) +impl super::Impl for async_io::Async<std::fs::File> { + fn new_from_fh(fh: std::fs::File) -> crate::Result<Self> { + Self::new(fh).map_err(crate::error::create_pty) } } diff --git a/src/pty/std.rs b/src/pty/std.rs index c907052..f88e7ba 100644 --- a/src/pty/std.rs +++ b/src/pty/std.rs @@ -1,45 +1,5 @@ -use std::os::unix::io::{AsRawFd as _, FromRawFd as _}; - -pub struct Pty { - pt: std::fs::File, - ptsname: std::path::PathBuf, -} - -impl super::Pty for Pty { - type Pt = std::fs::File; - - fn new() -> crate::error::Result<Self> { - let (pt_fd, ptsname) = super::create_pt()?; - - // safe because posix_openpt (or the previous functions operating on - // the result) would have returned an Err (causing us to return early) - // if the file descriptor was invalid. additionally, into_raw_fd gives - // up ownership over the file descriptor, allowing the newly created - // File object to take full ownership. - let pt = unsafe { std::fs::File::from_raw_fd(pt_fd) }; - - Ok(Self { pt, ptsname }) - } - - fn pt(&self) -> &Self::Pt { - &self.pt - } - - fn pt_mut(&mut self) -> &mut Self::Pt { - &mut self.pt - } - - fn pts(&self) -> crate::error::Result<std::fs::File> { - let fh = std::fs::OpenOptions::new() - .read(true) - .write(true) - .open(&self.ptsname) - .map_err(crate::error::create_pty)?; +impl super::Impl for std::fs::File { + fn new_from_fh(fh: std::fs::File) -> crate::Result<Self> { Ok(fh) } - - fn resize(&self, size: &super::Size) -> crate::error::Result<()> { - super::set_term_size(self.pt().as_raw_fd(), size) - .map_err(crate::error::set_term_size) - } } diff --git a/src/pty/tokio.rs b/src/pty/tokio.rs index e11bffb..ff0b280 100644 --- a/src/pty/tokio.rs +++ b/src/pty/tokio.rs @@ -1,5 +1,13 @@ use std::io::{Read as _, Write as _}; -use std::os::unix::io::{AsRawFd as _, FromRawFd as _}; + +impl super::Impl for AsyncPty { + fn new_from_fh(fh: std::fs::File) -> crate::Result<Self> { + Ok(Self( + tokio::io::unix::AsyncFd::new(fh) + .map_err(crate::error::create_pty)?, + )) + } +} // ideally i would just be able to use tokio::fs::File::from_std on the // std::fs::File i create from the pty fd, but it appears that tokio::fs::File @@ -91,65 +99,3 @@ impl tokio::io::AsyncWrite for AsyncPty { std::task::Poll::Ready(Ok(())) } } - -pub struct Pty { - pt: AsyncPty, - ptsname: std::path::PathBuf, -} - -impl super::Pty for Pty { - type Pt = AsyncPty; - - fn new() -> crate::error::Result<Self> { - let (pt_fd, ptsname) = super::create_pt()?; - - let bits = nix::fcntl::fcntl(pt_fd, nix::fcntl::FcntlArg::F_GETFL) - .map_err(crate::error::create_pty)?; - // this should be safe because i am just using the return value of - // F_GETFL directly, but for whatever reason nix doesn't like - // from_bits(bits) (it claims it has an unknown field) - let opts = unsafe { - nix::fcntl::OFlag::from_bits_unchecked( - bits | nix::fcntl::OFlag::O_NONBLOCK.bits(), - ) - }; - nix::fcntl::fcntl(pt_fd, nix::fcntl::FcntlArg::F_SETFL(opts)) - .map_err(crate::error::create_pty)?; - - // safe because posix_openpt (or the previous functions operating on - // the result) would have returned an Err (causing us to return early) - // if the file descriptor was invalid. additionally, into_raw_fd gives - // up ownership over the file descriptor, allowing the newly created - // File object to take full ownership. - let pt = unsafe { std::fs::File::from_raw_fd(pt_fd) }; - - let pt = AsyncPty( - tokio::io::unix::AsyncFd::new(pt) - .map_err(crate::error::create_pty)?, - ); - - Ok(Self { pt, ptsname }) - } - - fn pt(&self) -> &Self::Pt { - &self.pt - } - - fn pt_mut(&mut self) -> &mut Self::Pt { - &mut self.pt - } - - fn pts(&self) -> crate::error::Result<std::fs::File> { - let fh = std::fs::OpenOptions::new() - .read(true) - .write(true) - .open(&self.ptsname) - .map_err(crate::error::create_pty)?; - Ok(fh) - } - - fn resize(&self, size: &super::Size) -> crate::error::Result<()> { - super::set_term_size(self.pt().as_raw_fd(), size) - .map_err(crate::error::set_term_size) - } -} diff --git a/src/smol.rs b/src/smol.rs index 2a66647..8d0e877 100644 --- a/src/smol.rs +++ b/src/smol.rs @@ -1,2 +1,5 @@ -pub type Child = - crate::Child<async_process::Child, crate::pty::async_io::Pty>; +type Pt = async_io::Async<std::fs::File>; + +pub type Command = crate::Command<async_process::Command, Pt>; +pub type Child = crate::Child<async_process::Child, Pt>; +pub type Pty = crate::Pty<Pt>; @@ -1 +1,5 @@ -pub type Child = crate::Child<std::process::Child, crate::pty::std::Pty>; +type Pt = std::fs::File; + +pub type Command = crate::Command<std::process::Command, Pt>; +pub type Child = crate::Child<std::process::Child, Pt>; +pub type Pty = crate::Pty<Pt>; diff --git a/src/tokio.rs b/src/tokio.rs index b0ad53c..e2efa56 100644 --- a/src/tokio.rs +++ b/src/tokio.rs @@ -1 +1,5 @@ -pub type Child = crate::Child<tokio::process::Child, crate::pty::tokio::Pty>; +type Pt = crate::pty::tokio::AsyncPty; + +pub type Command = crate::Command<tokio::process::Command, Pt>; +pub type Child = crate::Child<tokio::process::Child, Pt>; +pub type Pty = crate::Pty<Pt>; diff --git a/tests/basic.rs b/tests/basic.rs index 9ff0a33..e164357 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -1,13 +1,11 @@ -use pty_process::Command as _; - #[cfg(feature = "backend-std")] #[test] fn test_cat_std() { use std::io::{Read as _, Write as _}; - let mut child = std::process::Command::new("cat") - .spawn_pty(Some(&pty_process::Size::new(24, 80))) - .unwrap(); + let pty = pty_process::std::Pty::new().unwrap(); + pty.resize(pty_process::Size::new(24, 80)).unwrap(); + let mut child = pty_process::std::Command::new("cat").spawn(pty).unwrap(); child.pty().write_all(b"foo\n").unwrap(); // the pty will echo the written bytes back immediately, but the @@ -33,9 +31,10 @@ fn test_cat_smol() { use smol::io::{AsyncReadExt as _, AsyncWriteExt as _}; let status = smol::block_on(async { - let mut child = smol::process::Command::new("cat") - .spawn_pty(Some(&pty_process::Size::new(24, 80))) - .unwrap(); + let pty = pty_process::smol::Pty::new().unwrap(); + pty.resize(pty_process::Size::new(24, 80)).unwrap(); + let mut child = + pty_process::smol::Command::new("cat").spawn(pty).unwrap(); child.pty().write_all(b"foo\n").await.unwrap(); // the pty will echo the written bytes back immediately, but the @@ -63,8 +62,10 @@ fn test_cat_async_std() { use async_std::io::ReadExt as _; let status = async_std::task::block_on(async { - let mut child = async_std::process::Command::new("cat") - .spawn_pty(Some(&pty_process::Size::new(24, 80))) + let pty = pty_process::async_std::Pty::new().unwrap(); + pty.resize(pty_process::Size::new(24, 80)).unwrap(); + let mut child = pty_process::async_std::Command::new("cat") + .spawn(pty) .unwrap(); child.pty().write_all(b"foo\n").await.unwrap(); @@ -92,9 +93,10 @@ fn test_cat_tokio() { use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; async fn async_test_cat_tokio() { - let mut child = tokio::process::Command::new("cat") - .spawn_pty(Some(&pty_process::Size::new(24, 80))) - .unwrap(); + let pty = pty_process::tokio::Pty::new().unwrap(); + pty.resize(pty_process::Size::new(24, 80)).unwrap(); + let mut child = + pty_process::tokio::Command::new("cat").spawn(pty).unwrap(); child.pty_mut().write_all(b"foo\n").await.unwrap(); // the pty will echo the written bytes back immediately, but the diff --git a/tests/pipe.rs b/tests/pipe.rs new file mode 100644 index 0000000..b1e0540 --- /dev/null +++ b/tests/pipe.rs @@ -0,0 +1,68 @@ +#[test] +fn test_pipe_basic() { + use std::os::unix::io::FromRawFd as _; + + let (read_fd, write_fd) = nix::unistd::pipe().unwrap(); + + let mut child_from = std::process::Command::new("seq"); + child_from.args(["1", "10"]); + child_from.stdout(unsafe { std::process::Stdio::from_raw_fd(write_fd) }); + + let mut child_to = std::process::Command::new("tac"); + child_to.stdin(unsafe { std::process::Stdio::from_raw_fd(read_fd) }); + child_to.stdout(std::process::Stdio::piped()); + + assert!(child_from.status().unwrap().success()); + nix::unistd::close(write_fd).unwrap(); + let output = child_to.output().unwrap(); + assert!(output.status.success()); + assert_eq!(output.stdout, b"10\n9\n8\n7\n6\n5\n4\n3\n2\n1\n"); +} + +#[cfg(feature = "backend-async-std")] +// TODO (hangs because i'm still overriding the configured fds) +// #[test] +fn test_pipe_async() { + use async_std::io::ReadExt as _; + use std::os::unix::io::FromRawFd as _; + + let (status_from, status_to) = async_std::task::block_on(async { + let (read_fd, write_fd) = nix::unistd::pipe().unwrap(); + + let pty_from = pty_process::async_std::Pty::new().unwrap(); + pty_from.resize(pty_process::Size::new(24, 80)).unwrap(); + let mut cmd_from = pty_process::async_std::Command::new("seq"); + cmd_from.args(["1", "10"]); + cmd_from + .stdout(unsafe { std::process::Stdio::from_raw_fd(write_fd) }); + let mut child_from = cmd_from.spawn(pty_from).unwrap(); + + let pty_to = pty_process::async_std::Pty::new().unwrap(); + pty_to.resize(pty_process::Size::new(24, 80)).unwrap(); + let mut cmd_to = pty_process::async_std::Command::new("tac"); + cmd_to.stdin(unsafe { std::process::Stdio::from_raw_fd(read_fd) }); + let mut child_to = cmd_to.spawn(pty_to).unwrap(); + + // the pty will echo the written bytes back immediately, but the + // subprocess needs to generate its own output, which takes time, so + // we can't just read immediately (we may just get the echoed bytes). + // because the output generation is happening in the subprocess, we + // also don't have any way to know when (or if!) the subprocess will + // decide to send its output, so sleeping is the best we can do. + async_std::task::sleep(std::time::Duration::from_secs(1)).await; + + let mut buf = [0u8; 1024]; + let bytes = child_to.pty().read(&mut buf).await.unwrap(); + assert_eq!( + &buf[..bytes], + b"10\r\n9\r\n8\r\n7\r\n6\r\n5\r\n4\r\n3\r\n2\r\n1\r\n" + ); + + ( + child_from.status().await.unwrap(), + child_to.status().await.unwrap(), + ) + }); + assert_eq!(status_from.code().unwrap(), 0); + assert_eq!(status_to.code().unwrap(), 0); +} diff --git a/tests/winch.rs b/tests/winch.rs index 234afeb..326efcc 100644 --- a/tests/winch.rs +++ b/tests/winch.rs @@ -1,22 +1,22 @@ -use pty_process::Command as _; use std::io::{Read as _, Write as _}; #[cfg(feature = "backend-std")] #[test] fn test_winch() { - let mut child = std::process::Command::new("perl") - .args(&[ - "-E", - "$|++; $SIG{WINCH} = sub { say 'WINCH' }; say 'started'; <>", - ]) - .spawn_pty(Some(&pty_process::Size::new(24, 80))) - .unwrap(); + let pty = pty_process::std::Pty::new().unwrap(); + pty.resize(pty_process::Size::new(24, 80)).unwrap(); + let mut cmd = pty_process::std::Command::new("perl"); + cmd.args(&[ + "-E", + "$|++; $SIG{WINCH} = sub { say 'WINCH' }; say 'started'; <>", + ]); + let mut child = cmd.spawn(pty).unwrap(); let mut buf = [0u8; 1024]; let bytes = child.pty().read(&mut buf).unwrap(); assert_eq!(&buf[..bytes], b"started\r\n"); - child.resize_pty(&pty_process::Size::new(25, 80)).unwrap(); + child.resize_pty(pty_process::Size::new(25, 80)).unwrap(); let bytes = child.pty().read(&mut buf).unwrap(); assert_eq!(&buf[..bytes], b"WINCH\r\n"); |