From 3b550f5d3dad77a56455352579fae3071b42e86d Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Wed, 29 Dec 2021 02:55:24 -0500 Subject: wip another complete refactor --- Cargo.toml | 17 ++- examples/async-std.rs | 19 +-- examples/basic.rs | 18 +-- examples/interhack.rs | 23 ++-- examples/smol.rs | 19 +-- examples/tokio.rs | 31 ++--- src/async_std.rs | 5 - src/blocking/command.rs | 156 +++++++++++++++++++++++++ src/blocking/mod.rs | 4 + src/blocking/pty.rs | 66 +++++++++++ src/command.rs | 269 ++++++++++++++++++------------------------- src/command/async_process.rs | 32 ----- src/command/std.rs | 32 ----- src/command/tokio.rs | 30 ----- src/lib.rs | 73 +++--------- src/pty.rs | 193 +++++++++---------------------- src/pty/async_io.rs | 5 - src/pty/std.rs | 5 - src/pty/tokio.rs | 101 ---------------- src/smol.rs | 5 - src/std.rs | 5 - src/sys.rs | 103 +++++++++++++++++ src/tokio.rs | 5 - src/types.rs | 50 ++++++++ tests/basic.rs | 58 +++++----- tests/pipe.rs | 2 +- tests/winch.rs | 122 ++++++++++++++++++-- 27 files changed, 758 insertions(+), 690 deletions(-) delete mode 100644 src/async_std.rs create mode 100644 src/blocking/command.rs create mode 100644 src/blocking/mod.rs create mode 100644 src/blocking/pty.rs delete mode 100644 src/command/async_process.rs delete mode 100644 src/command/std.rs delete mode 100644 src/command/tokio.rs delete mode 100644 src/pty/async_io.rs delete mode 100644 src/pty/std.rs delete mode 100644 src/pty/tokio.rs delete mode 100644 src/smol.rs delete mode 100644 src/std.rs create mode 100644 src/sys.rs delete mode 100644 src/tokio.rs create mode 100644 src/types.rs diff --git a/Cargo.toml b/Cargo.toml index aeefa41..2ff64e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,28 +14,25 @@ include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"] [dependencies] libc = "0.2.112" -nix = "0.23.0" +nix = "0.23.1" async-io = { version = "1.6.0", optional = true } async-process = { version = "1.3.0", optional = true } -tokio = { version = "1.14.0", optional = true, features = ["fs", "process", "net"] } -futures = { version = "0.3.17", optional = true } +futures-io = { version = "0.3.19", optional = true } [dev-dependencies] -async-std = { version = "1.10.0", features = ["unstable"] } async-executor = "1.4.1" +async-std = { version = "1.10.0", features = ["unstable"] } regex = "1.5.4" smol = "1.2.5" term_size = "0.3.2" -tokio = { version = "1.14.0", features = [ "rt-multi-thread", "macros", "io-std", "io-util", "time" ] } +tokio = { version = "1.15.0", features = ["full"] } +tokio-util = { version = "0.6.9", features = ["compat"] } [features] -default = ["backend-std"] +default = [] -backend-std = [] -backend-async-std = ["async-io", "async-process"] -backend-smol = ["async-io", "async-process"] -backend-tokio = ["tokio", "futures"] +async = ["async-io", "async-process", "futures-io"] [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 880c91b..6bf9412 100644 --- a/examples/async-std.rs +++ b/examples/async-std.rs @@ -1,13 +1,13 @@ mod raw_guard; -#[cfg(feature = "backend-async-std")] +#[cfg(feature = "async")] mod main { use async_std::io::prelude::WriteExt as _; use async_std::io::ReadExt as _; use async_std::prelude::FutureExt as _; pub async fn run( - child: &mut pty_process::async_std::Child, + child: &pty_process::Child, ) -> std::result::Result<(), Box> { let _raw = super::raw_guard::RawGuard::new(); @@ -55,17 +55,18 @@ mod main { } } -#[cfg(feature = "backend-async-std")] +#[cfg(feature = "async")] fn main() { use std::os::unix::process::ExitStatusExt as _; let status = async_std::task::block_on(async { - let pty = pty_process::async_std::Pty::new().unwrap(); + let pty = pty_process::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(); + let mut child = pty_process::Command::new("tac") + // .args(&["500"]) + .spawn(pty) + .unwrap(); + main::run(&child).await.unwrap(); child.status().await.unwrap() }); std::process::exit( @@ -75,7 +76,7 @@ fn main() { ); } -#[cfg(not(feature = "backend-async-std"))] +#[cfg(not(feature = "async"))] fn main() { unimplemented!() } diff --git a/examples/basic.rs b/examples/basic.rs index 1e5913b..c9b5f6c 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,11 +1,10 @@ mod raw_guard; -#[cfg(feature = "backend-std")] mod main { use std::io::{Read as _, Write as _}; use std::os::unix::io::AsRawFd as _; - pub fn run(child: &mut pty_process::std::Child) { + pub fn run(child: &mut pty_process::blocking::Child) { let _raw = super::raw_guard::RawGuard::new(); let mut buf = [0_u8; 4096]; let pty = child.pty().as_raw_fd(); @@ -70,15 +69,15 @@ mod main { } } -#[cfg(feature = "backend-std")] fn main() { use std::os::unix::process::ExitStatusExt as _; - let pty = pty_process::std::Pty::new().unwrap(); + let pty = pty_process::blocking::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(); + let mut child = pty_process::blocking::Command::new("tac") + // .args(&["500"]) + .spawn(pty) + .unwrap(); main::run(&mut child); @@ -89,8 +88,3 @@ fn main() { .unwrap_or_else(|| status.signal().unwrap_or(0) + 128), ); } - -#[cfg(not(feature = "backend-std"))] -fn main() { - unimplemented!() -} diff --git a/examples/interhack.rs b/examples/interhack.rs index e636282..9d44bbf 100644 --- a/examples/interhack.rs +++ b/examples/interhack.rs @@ -1,11 +1,11 @@ mod raw_guard; -#[cfg(feature = "backend-smol")] +#[cfg(feature = "async")] mod main { use smol::io::{AsyncReadExt as _, AsyncWriteExt as _}; pub async fn run( - child: &mut pty_process::smol::Child, + child: &pty_process::Child, ) -> std::result::Result<(), Box> { let _raw = super::raw_guard::RawGuard::new(); @@ -71,11 +71,7 @@ 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; } } @@ -93,7 +89,7 @@ mod main { } } -#[cfg(feature = "backend-smol")] +#[cfg(feature = "async")] fn main() { use std::os::unix::process::ExitStatusExt as _; @@ -103,12 +99,11 @@ fn main() { (80, 24) }; let status = smol::block_on(async { - let pty = pty_process::smol::Pty::new().unwrap(); + let pty = pty_process::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(&mut child).await.unwrap(); + let mut child = + pty_process::Command::new("nethack").spawn(pty).unwrap(); + main::run(&child).await.unwrap(); child.status().await.unwrap() }); std::process::exit( @@ -118,7 +113,7 @@ fn main() { ); } -#[cfg(not(feature = "backend-smol"))] +#[cfg(not(feature = "async"))] fn main() { unimplemented!() } diff --git a/examples/smol.rs b/examples/smol.rs index b0b90e4..e8b1c4c 100644 --- a/examples/smol.rs +++ b/examples/smol.rs @@ -1,11 +1,11 @@ mod raw_guard; -#[cfg(feature = "backend-smol")] +#[cfg(feature = "async")] mod main { use smol::io::{AsyncReadExt as _, AsyncWriteExt as _}; pub async fn run( - child: &mut pty_process::smol::Child, + child: &pty_process::Child, ) -> std::result::Result<(), Box> { let _raw = super::raw_guard::RawGuard::new(); @@ -54,17 +54,18 @@ mod main { } } -#[cfg(feature = "backend-smol")] +#[cfg(feature = "async")] fn main() { use std::os::unix::process::ExitStatusExt as _; let status = smol::block_on(async { - let pty = pty_process::smol::Pty::new().unwrap(); + let pty = pty_process::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(); + let mut child = pty_process::Command::new("tac") + // .args(&["500"]) + .spawn(pty) + .unwrap(); + main::run(&child).await.unwrap(); child.status().await.unwrap() }); std::process::exit( @@ -74,7 +75,7 @@ fn main() { ); } -#[cfg(not(feature = "backend-smol"))] +#[cfg(not(feature = "async"))] fn main() { unimplemented!() } diff --git a/examples/tokio.rs b/examples/tokio.rs index bc62592..16083c7 100644 --- a/examples/tokio.rs +++ b/examples/tokio.rs @@ -1,11 +1,12 @@ mod raw_guard; -#[cfg(feature = "backend-tokio")] +#[cfg(feature = "async")] mod main { use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; + use tokio_util::compat::FuturesAsyncReadCompatExt as _; pub async fn run( - child: &mut pty_process::tokio::Child, + child: &pty_process::Child, ) -> std::result::Result<(), Box> { let _raw = super::raw_guard::RawGuard::new(); @@ -16,17 +17,18 @@ mod main { let mut stdout = tokio::io::stdout(); loop { + let mut pty = child.pty().compat(); tokio::select! { bytes = stdin.read(&mut in_buf) => match bytes { Ok(bytes) => { - child.pty_mut().write_all(&in_buf[..bytes]).await.unwrap(); + pty.write_all(&in_buf[..bytes]).await.unwrap(); } Err(e) => { eprintln!("stdin read failed: {:?}", e); break; } }, - bytes = child.pty_mut().read(&mut out_buf) => match bytes { + bytes = pty.read(&mut out_buf) => match bytes { Ok(bytes) => { stdout.write_all(&out_buf[..bytes]).await.unwrap(); stdout.flush().await.unwrap(); @@ -36,9 +38,7 @@ mod main { break; } }, - // _ = child.status_no_drop() => { - // break; - // } + _ = child.status_no_drop() => break, } } @@ -46,18 +46,19 @@ mod main { } } -#[cfg(feature = "backend-tokio")] +#[cfg(feature = "async")] #[tokio::main] async fn main() { use std::os::unix::process::ExitStatusExt as _; - let pty = pty_process::tokio::Pty::new().unwrap(); + let pty = pty_process::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(); + let mut child = pty_process::Command::new("tac") + // .args(&["500"]) + .spawn(pty) + .unwrap(); + main::run(&child).await.unwrap(); + let status = child.status().await.unwrap(); std::process::exit( status .code() @@ -65,7 +66,7 @@ async fn main() { ); } -#[cfg(not(feature = "backend-tokio"))] +#[cfg(not(feature = "async"))] fn main() { unimplemented!() } diff --git a/src/async_std.rs b/src/async_std.rs deleted file mode 100644 index 8d0e877..0000000 --- a/src/async_std.rs +++ /dev/null @@ -1,5 +0,0 @@ -type Pt = async_io::Async; - -pub type Command = crate::Command; -pub type Child = crate::Child; -pub type Pty = crate::Pty; diff --git a/src/blocking/command.rs b/src/blocking/command.rs new file mode 100644 index 0000000..3de0f3e --- /dev/null +++ b/src/blocking/command.rs @@ -0,0 +1,156 @@ +use std::os::unix::process::CommandExt as _; + +pub struct Command { + inner: std::process::Command, + stdin: Option, + stdout: Option, + stderr: Option, +} + +impl Command { + pub fn new>(program: S) -> Self { + Self { + inner: std::process::Command::new(program), + stdin: None, + stdout: None, + stderr: None, + } + } + + pub fn arg>(&mut self, arg: S) -> &mut Self { + self.inner.arg(arg); + self + } + + pub fn args(&mut self, args: I) -> &mut Self + where + I: IntoIterator, + S: AsRef, + { + self.inner.args(args); + self + } + + pub fn env(&mut self, key: K, val: V) -> &mut Self + where + K: AsRef, + V: AsRef, + { + self.inner.env(key, val); + self + } + + pub fn envs(&mut self, vars: I) -> &mut Self + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + self.inner.envs(vars); + self + } + + pub fn env_remove>( + &mut self, + key: K, + ) -> &mut Self { + self.inner.env_remove(key); + self + } + + pub fn env_clear(&mut self) -> &mut Self { + self.inner.env_clear(); + self + } + + pub fn current_dir>( + &mut self, + dir: P, + ) -> &mut Self { + self.inner.current_dir(dir); + self + } + + pub fn stdin>( + &mut self, + cfg: Option, + ) -> &mut Self { + self.stdin = cfg.map(Into::into); + self + } + + pub fn stdout>( + &mut self, + cfg: Option, + ) -> &mut Self { + self.stdout = cfg.map(Into::into); + self + } + + pub fn stderr>( + &mut self, + cfg: Option, + ) -> &mut Self { + self.stderr = cfg.map(Into::into); + self + } + + pub fn spawn( + &mut self, + pty: crate::blocking::Pty, + ) -> crate::Result { + let (stdin, stdout, stderr, pre_exec) = crate::sys::setup_subprocess( + &pty, + pty.pts().map_err(crate::error::spawn)?, + ) + .map_err(crate::error::spawn)?; + + self.inner.stdin(self.stdin.take().unwrap_or(stdin)); + self.inner.stdout(self.stdout.take().unwrap_or(stdout)); + self.inner.stderr(self.stderr.take().unwrap_or(stderr)); + + // safe because setsid() and close() are async-signal-safe functions + // and ioctl() is a raw syscall (which is inherently + // async-signal-safe). + unsafe { self.inner.pre_exec(pre_exec) }; + + let child = self.inner.spawn().map_err(crate::error::spawn)?; + + Ok(Child::new(child, pty)) + } +} + +pub struct Child { + inner: std::process::Child, + pty: crate::blocking::Pty, +} + +impl Child { + fn new(inner: std::process::Child, pty: crate::blocking::Pty) -> Self { + Self { inner, pty } + } + + #[must_use] + pub fn pty(&self) -> &crate::blocking::Pty { + &self.pty + } + + #[must_use] + pub fn pty_mut(&mut self) -> &mut crate::blocking::Pty { + &mut self.pty + } +} + +impl std::ops::Deref for Child { + type Target = std::process::Child; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl std::ops::DerefMut for Child { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} diff --git a/src/blocking/mod.rs b/src/blocking/mod.rs new file mode 100644 index 0000000..a45b132 --- /dev/null +++ b/src/blocking/mod.rs @@ -0,0 +1,4 @@ +mod command; +pub use command::{Child, Command}; +mod pty; +pub use pty::Pty; diff --git a/src/blocking/pty.rs b/src/blocking/pty.rs new file mode 100644 index 0000000..7900b50 --- /dev/null +++ b/src/blocking/pty.rs @@ -0,0 +1,66 @@ +pub struct Pty { + pt: std::fs::File, + ptsname: std::path::PathBuf, +} + +impl Pty { + pub fn new() -> crate::Result { + let (pt, ptsname) = + crate::sys::create_pt().map_err(crate::error::create_pty)?; + Ok(Self { pt, ptsname }) + } + + pub fn resize(&self, size: crate::Size) -> crate::error::Result<()> { + crate::sys::set_term_size(self, size) + .map_err(crate::error::set_term_size) + } + + pub(crate) fn pts(&self) -> std::io::Result { + let fh = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(&self.ptsname)?; + Ok(fh) + } +} + +impl std::ops::Deref for Pty { + type Target = std::fs::File; + + fn deref(&self) -> &Self::Target { + &self.pt + } +} + +impl std::ops::DerefMut for Pty { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.pt + } +} + +impl std::os::unix::io::AsRawFd for Pty { + fn as_raw_fd(&self) -> std::os::unix::io::RawFd { + self.pt.as_raw_fd() + } +} + +// there is a Read impl for &std::fs::File, but without this explicit impl, +// rust finds the Read impl for std::fs::File first, and then complains that +// it requires &mut self, because method resolution/autoderef doesn't take +// mutability into account +impl std::io::Read for &Pty { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + (&self.pt).read(buf) + } +} + +// same as above +impl std::io::Write for &Pty { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + (&self.pt).write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + (&self.pt).flush() + } +} diff --git a/src/command.rs b/src/command.rs index b3d1104..bbfc1e8 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,198 +1,153 @@ -use ::std::os::unix::io::{AsRawFd as _, FromRawFd as _}; - -#[cfg(any(feature = "backend-async-std", feature = "backend-smol"))] -mod async_process; -#[cfg(feature = "backend-std")] -mod std; -#[cfg(feature = "backend-tokio")] -mod tokio; - -pub struct Command { - inner: C, - stdin_set: bool, - stdout_set: bool, - stderr_set: bool, - - _phantom: ::std::marker::PhantomData

, +use async_process::unix::CommandExt as _; + +pub struct Command { + inner: async_process::Command, + stdin: Option, + stdout: Option, + stderr: Option, } -impl Command { - pub fn new>(program: S) -> Self { +impl Command { + pub fn new>(program: S) -> Self { Self { - inner: C::new_impl(program.as_ref()), - stdin_set: false, - stdout_set: false, - stderr_set: false, - - _phantom: ::std::marker::PhantomData, + inner: async_process::Command::new(program), + stdin: None, + stdout: None, + stderr: None, } } - pub fn stdin>(&mut self, cfg: T) { - self.stdin_set = true; - self.inner.stdin_impl(cfg.into()); + pub fn arg>(&mut self, arg: S) -> &mut Self { + self.inner.arg(arg); + self } - pub fn stdout>(&mut self, cfg: T) { - self.stdout_set = true; - self.inner.stdout_impl(cfg.into()); + pub fn args(&mut self, args: I) -> &mut Self + where + I: IntoIterator, + S: AsRef, + { + self.inner.args(args); + self } - pub fn stderr>(&mut self, cfg: T) { - self.stderr_set = true; - self.inner.stderr_impl(cfg.into()); + pub fn env(&mut self, key: K, val: V) -> &mut Self + where + K: AsRef, + V: AsRef, + { + self.inner.env(key, val); + self } - pub fn spawn( + pub fn envs(&mut self, vars: I) -> &mut Self + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + self.inner.envs(vars); + self + } + + pub fn env_remove>( &mut self, - pty: crate::Pty

, - ) -> crate::Result::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.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()?; - set_controlling_terminal(pts_fd)?; - - // in the parent, destructors will handle closing these file - // descriptors (other than pt, used by the parent to - // communicate with the child) when the function ends, but in - // the child, we end by calling exec(), which doesn't call - // destructors. - - nix::unistd::close(pt_fd)?; - nix::unistd::close(pts_fd)?; - // at this point, stdin/stdout/stderr have already been - // reopened as fds 0/1/2 in the child, so we can (and should) - // close the originals - nix::unistd::close(stdin)?; - nix::unistd::close(stdout)?; - nix::unistd::close(stderr)?; - - Ok(()) - }; - // safe because setsid() and close() are async-signal-safe functions - // and ioctl() is a raw syscall (which is inherently - // async-signal-safe). - unsafe { self.inner.pre_exec_impl(pre_exec) }; + key: K, + ) -> &mut Self { + self.inner.env_remove(key); + self + } - let child = self.inner.spawn_impl()?; + pub fn env_clear(&mut self) -> &mut Self { + self.inner.env_clear(); + self + } - Ok(Child::new(child, pty)) + pub fn current_dir>( + &mut self, + dir: P, + ) -> &mut Self { + self.inner.current_dir(dir); + self } -} -impl ::std::ops::Deref for Command { - type Target = C; + pub fn stdin>( + &mut self, + cfg: Option, + ) -> &mut Self { + self.stdin = cfg.map(Into::into); + self + } - fn deref(&self) -> &Self::Target { - &self.inner + pub fn stdout>( + &mut self, + cfg: Option, + ) -> &mut Self { + self.stdout = cfg.map(Into::into); + self } -} -impl ::std::ops::DerefMut for Command { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner + pub fn stderr>( + &mut self, + cfg: Option, + ) -> &mut Self { + self.stderr = cfg.map(Into::into); + self + } + + pub fn spawn(&mut self, pty: crate::Pty) -> crate::Result { + let (stdin, stdout, stderr, pre_exec) = crate::sys::setup_subprocess( + &pty, + pty.pts().map_err(crate::error::spawn)?, + ) + .map_err(crate::error::spawn)?; + + self.inner.stdin(self.stdin.take().unwrap_or(stdin)); + self.inner.stdout(self.stdout.take().unwrap_or(stdout)); + self.inner.stderr(self.stderr.take().unwrap_or(stderr)); + + // safe because setsid() and close() are async-signal-safe functions + // and ioctl() is a raw syscall (which is inherently + // async-signal-safe). + unsafe { self.inner.pre_exec(pre_exec) }; + + let child = self.inner.spawn().map_err(crate::error::spawn)?; + + Ok(Child::new(child, pty)) } } -pub struct Child { - inner: C, - pty: crate::Pty

, +pub struct Child { + inner: async_process::Child, + pty: crate::Pty, } -impl Child { - fn new(inner: C, pty: crate::Pty

) -> Self { +impl Child { + fn new(inner: async_process::Child, pty: crate::Pty) -> 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 { - self.pty.pt() - } - - /// Returns a mutable 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. - /// - /// 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 { - self.pty.pt_mut() - } - - /// Causes the pty to change its size. - /// - /// This will additionally cause a `SIGWINCH` signal to be sent to the - /// running process. - /// - /// # Errors - /// * `Error::SetTermSize`: error setting terminal size - pub fn resize_pty( - &self, - size: crate::pty::Size, - ) -> crate::error::Result<()> { - self.pty.resize(size) + #[must_use] + pub fn pty(&self) -> &crate::Pty { + &self.pty + } + + #[must_use] + pub fn pty_mut(&mut self) -> &mut crate::Pty { + &mut self.pty } } -impl ::std::ops::Deref for Child { - type Target = C; +impl std::ops::Deref for Child { + type Target = async_process::Child; fn deref(&self) -> &Self::Target { &self.inner } } -impl ::std::ops::DerefMut for Child { +impl std::ops::DerefMut for Child { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner } } - -pub trait Impl { - type Child; - - 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(&mut self, f: F) - where - F: FnMut() -> ::std::io::Result<()> + Send + Sync + 'static; - - fn spawn_impl(&mut self) -> crate::Result; -} - -fn set_controlling_terminal( - fd: ::std::os::unix::io::RawFd, -) -> nix::Result<()> { - // safe because std::fs::File is required to contain a valid file - // descriptor - unsafe { set_controlling_terminal_unsafe(fd, ::std::ptr::null()) } - .map(|_| ()) -} - -nix::ioctl_write_ptr_bad!( - set_controlling_terminal_unsafe, - libc::TIOCSCTTY, - libc::c_int -); diff --git a/src/command/async_process.rs b/src/command/async_process.rs deleted file mode 100644 index a0aaa47..0000000 --- a/src/command/async_process.rs +++ /dev/null @@ -1,32 +0,0 @@ -use async_process::unix::CommandExt as _; - -impl super::Impl for async_process::Command { - type Child = async_process::Child; - - 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(&mut self, f: F) - where - F: FnMut() -> ::std::io::Result<()> + Send + Sync + 'static, - { - self.pre_exec(f); - } - - fn spawn_impl(&mut self) -> crate::Result { - self.spawn().map_err(crate::error::spawn) - } -} diff --git a/src/command/std.rs b/src/command/std.rs deleted file mode 100644 index 2a89db5..0000000 --- a/src/command/std.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::os::unix::process::CommandExt as _; - -impl super::Impl for std::process::Command { - type Child = std::process::Child; - - 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(&mut self, f: F) - where - F: FnMut() -> ::std::io::Result<()> + Send + Sync + 'static, - { - self.pre_exec(f); - } - - fn spawn_impl(&mut self) -> crate::Result { - self.spawn().map_err(crate::error::spawn) - } -} diff --git a/src/command/tokio.rs b/src/command/tokio.rs deleted file mode 100644 index c63b18a..0000000 --- a/src/command/tokio.rs +++ /dev/null @@ -1,30 +0,0 @@ -impl super::Impl for tokio::process::Command { - type Child = tokio::process::Child; - - 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(&mut self, f: F) - where - F: FnMut() -> ::std::io::Result<()> + Send + Sync + 'static, - { - self.pre_exec(f); - } - - fn spawn_impl(&mut self) -> crate::Result { - self.spawn().map_err(crate::error::spawn) - } -} diff --git a/src/lib.rs b/src/lib.rs index 5ccaed5..bdc01a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,50 +1,3 @@ -//! This crate adds a helper method to -//! [`std::process::Command`](::std::process::Command) (and optionally its -//! equivalent in various async frameworks) to allocate a pty to spawn the -//! process into. This allows for manipulation of interactive programs. -//! -//! The basic functionality is provided by the [`Command`](Command) trait in -//! this crate: -//! -//! ```no_run -//! use pty_process::Command as _; -//! -//! let mut cmd = std::process::Command::new("nethack"); -//! let child = cmd.spawn_pty(Some(&pty_process::Size::new(24, 80))).unwrap(); -//! ``` -//! -//! The `child` instance returned by the call to -//! [`spawn_pty`](Command::spawn_pty) is a thin wrapper around the `Child` -//! struct associated with the `Command` module used. You can use it -//! identically to how you would use the normal -//! [`std::process::Child`](::std::process::Child) instance, but it also -//! provides additional methods for interacting with the pty: -//! -//! ```no_run -//! # use pty_process::Command as _; -//! # -//! # let mut cmd = std::process::Command::new("nethack"); -//! # let mut child = cmd -//! # .spawn_pty(Some(&pty_process::Size::new(24, 80))).unwrap(); -//! use std::io::Write as _; -//! -//! child.pty().write_all(b"foo\n").unwrap(); -//! child.resize_pty(&pty_process::Size::new(30, 100)).unwrap(); -//! let status = child.wait().unwrap(); -//! ``` -//! -//! The available implementations are gated by features: -//! * `backend-std`: Add an implementation for -//! [`std::process::Command`](::std::process::Command). Enabled by default. -//! * `backend-smol`: Add an implementation for -//! [`smol::process::Command`](::smol::process::Command). -//! * `backend-async-std`: Add an implementation for -//! [`async_std::process::Command`](::async_std::process::Command). -//! * `backend-tokio`: Add an implementation for -//! [`tokio::process::Command`](::tokio::process::Command). -//! -//! Any number of backends may be enabled, depending on your needs. - #![warn(clippy::cargo)] #![warn(clippy::pedantic)] #![warn(clippy::nursery)] @@ -58,18 +11,20 @@ #![allow(clippy::too_many_lines)] #![allow(clippy::type_complexity)] -mod command; -pub use command::{Child, Command}; mod error; pub use error::{Error, Result, Source}; -mod pty; -pub use pty::{Pty, Size}; +mod types; +pub use types::Size; + +mod sys; -#[cfg(feature = "backend-async-std")] -pub mod async_std; -#[cfg(feature = "backend-smol")] -pub mod smol; -#[cfg(feature = "backend-std")] -pub mod std; -#[cfg(feature = "backend-tokio")] -pub mod tokio; +pub mod blocking; + +#[cfg(feature = "async")] +mod command; +#[cfg(feature = "async")] +pub use command::{Child, Command}; +#[cfg(feature = "async")] +mod pty; +#[cfg(feature = "async")] +pub use pty::Pty; diff --git a/src/pty.rs b/src/pty.rs index 72c59e1..f01e014 100644 --- a/src/pty.rs +++ b/src/pty.rs @@ -1,167 +1,86 @@ -use ::std::os::unix::io::{AsRawFd as _, FromRawFd as _, IntoRawFd as _}; - -#[cfg(any(feature = "backend-async-std", feature = "backend-smol"))] -pub mod async_io; -#[cfg(feature = "backend-std")] -pub mod std; -#[cfg(feature = "backend-tokio")] -pub mod tokio; - -pub struct Pty { - pt: Pt, - ptsname: ::std::path::PathBuf, +pub struct Pty { + pt: async_io::Async, + ptsname: std::path::PathBuf, } -impl Pty { - pub fn new() -> crate::error::Result { - 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)?; - +impl Pty { + pub fn new() -> crate::Result { + let (pt, ptsname) = + crate::sys::create_pt().map_err(crate::error::create_pty)?; + let pt = + async_io::Async::new(pt).map_err(crate::error::create_pty)?; 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) + crate::sys::set_term_size(self, size) .map_err(crate::error::set_term_size) } - fn pts(&self) -> crate::error::Result<::std::fs::File> { - let fh = ::std::fs::OpenOptions::new() + pub(crate) fn pts(&self) -> std::io::Result { + let fh = std::fs::OpenOptions::new() .read(true) .write(true) - .open(&self.ptsname) - .map_err(crate::error::create_pty)?; + .open(&self.ptsname)?; 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)) - } } -pub trait Impl: ::std::os::unix::io::AsRawFd + Sized { - fn new_from_fh(fh: ::std::fs::File) -> crate::Result; -} +impl std::ops::Deref for Pty { + type Target = async_io::Async; -/// Represents the size of the pty. -#[derive(Debug, Clone, Copy)] -pub struct Size { - row: u16, - col: u16, - xpixel: u16, - ypixel: u16, + fn deref(&self) -> &Self::Target { + &self.pt + } } -impl Size { - /// Returns a [`Size`](Size) instance with the given number of rows and - /// columns. - #[must_use] - pub fn new(row: u16, col: u16) -> Self { - Self { - row, - col, - xpixel: 0, - ypixel: 0, - } +impl std::ops::DerefMut for Pty { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.pt } +} - /// Returns a [`Size`](Size) instance with the given number of rows and - /// columns, as well as the given pixel dimensions. - #[must_use] - pub fn new_with_pixel( - row: u16, - col: u16, - xpixel: u16, - ypixel: u16, - ) -> Self { - Self { - row, - col, - xpixel, - ypixel, - } +impl std::os::unix::io::AsRawFd for Pty { + fn as_raw_fd(&self) -> std::os::unix::io::RawFd { + self.pt.as_raw_fd() } } -impl From for nix::pty::Winsize { - fn from(size: Size) -> Self { - Self { - ws_row: size.row, - ws_col: size.col, - ws_xpixel: size.xpixel, - ws_ypixel: size.ypixel, - } +// there is an AsyncRead impl for &Async, but without this +// explicit impl, rust finds the AsyncRead impl for Async +// first, and then complains that it requires &mut self, because method +// resolution/autoderef doesn't take mutability into account +impl futures_io::AsyncRead for &Pty { + fn poll_read( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut [u8], + ) -> std::task::Poll> { + std::pin::Pin::new(&mut &self.pt).poll_read(cx, buf) } } -fn create_pt( -) -> crate::error::Result<(::std::os::unix::io::RawFd, ::std::path::PathBuf)> -{ - let pt = nix::pty::posix_openpt( - nix::fcntl::OFlag::O_RDWR | nix::fcntl::OFlag::O_NOCTTY, - ) - .map_err(crate::error::create_pty)?; - nix::pty::grantpt(&pt).map_err(crate::error::create_pty)?; - nix::pty::unlockpt(&pt).map_err(crate::error::create_pty)?; - - let ptsname = nix::pty::ptsname_r(&pt) - .map_err(crate::error::create_pty)? - .into(); - - let pt_fd = pt.into_raw_fd(); - - Ok((pt_fd, ptsname)) -} +// same as above +impl futures_io::AsyncWrite for &Pty { + fn poll_write( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + std::pin::Pin::new(&mut &self.pt).poll_write(cx, buf) + } -nix::ioctl_write_ptr_bad!( - set_term_size_unsafe, - libc::TIOCSWINSZ, - nix::pty::Winsize -); + fn poll_flush( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::pin::Pin::new(&mut &self.pt).poll_flush(cx) + } -fn set_term_size( - fd: ::std::os::unix::io::RawFd, - size: Size, -) -> nix::Result<()> { - let size = size.into(); - // safe because std::fs::File is required to contain a valid file - // descriptor and size is guaranteed to be initialized because it's a - // normal rust value, and nix::pty::Winsize is a repr(C) struct with the - // same layout as `struct winsize` from sys/ioctl.h. - unsafe { - set_term_size_unsafe(fd, ::std::ptr::NonNull::from(&size).as_ptr()) + fn poll_close( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::pin::Pin::new(&mut &self.pt).poll_close(cx) } - .map(|_| ()) } diff --git a/src/pty/async_io.rs b/src/pty/async_io.rs deleted file mode 100644 index d76c92c..0000000 --- a/src/pty/async_io.rs +++ /dev/null @@ -1,5 +0,0 @@ -impl super::Impl for async_io::Async { - fn new_from_fh(fh: std::fs::File) -> crate::Result { - Self::new(fh).map_err(crate::error::create_pty) - } -} diff --git a/src/pty/std.rs b/src/pty/std.rs deleted file mode 100644 index f88e7ba..0000000 --- a/src/pty/std.rs +++ /dev/null @@ -1,5 +0,0 @@ -impl super::Impl for std::fs::File { - fn new_from_fh(fh: std::fs::File) -> crate::Result { - Ok(fh) - } -} diff --git a/src/pty/tokio.rs b/src/pty/tokio.rs deleted file mode 100644 index ff0b280..0000000 --- a/src/pty/tokio.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::io::{Read as _, Write as _}; - -impl super::Impl for AsyncPty { - fn new_from_fh(fh: std::fs::File) -> crate::Result { - 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 -// doesn't actually support having both a read and a write operation happening -// on it simultaneously - if you poll the future returned by .read() at any -// point, .write().await will never complete (because it is trying to wait for -// the read to finish before processing the write, which will never happen). -// this unfortunately shows up in patterns like select! pretty frequently, so -// we need to do this the complicated way/: -pub struct AsyncPty(tokio::io::unix::AsyncFd); - -impl std::ops::Deref for AsyncPty { - type Target = tokio::io::unix::AsyncFd; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl std::ops::DerefMut for AsyncPty { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl std::os::unix::io::AsRawFd for AsyncPty { - fn as_raw_fd(&self) -> std::os::unix::io::RawFd { - self.0.as_raw_fd() - } -} - -impl tokio::io::AsyncRead for AsyncPty { - fn poll_read( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - buf: &mut tokio::io::ReadBuf, - ) -> std::task::Poll> { - loop { - let mut guard = futures::ready!(self.0.poll_read_ready(cx))?; - let mut b = [0_u8; 4096]; - match guard.try_io(|inner| inner.get_ref().read(&mut b)) { - Ok(Ok(bytes)) => { - // XXX this is safe, but not particularly efficient - buf.clear(); - buf.initialize_unfilled_to(bytes); - buf.set_filled(bytes); - buf.filled_mut().copy_from_slice(&b[..bytes]); - return std::task::Poll::Ready(Ok(())); - } - Ok(Err(e)) => return std::task::Poll::Ready(Err(e)), - Err(_would_block) => continue, - } - } - } -} - -impl tokio::io::AsyncWrite for AsyncPty { - fn poll_write( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - buf: &[u8], - ) -> std::task::Poll> { - loop { - let mut guard = futures::ready!(self.0.poll_write_ready(cx))?; - match guard.try_io(|inner| inner.get_ref().write(buf)) { - Ok(result) => return std::task::Poll::Ready(result), - Err(_would_block) => continue, - } - } - } - - fn poll_flush( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - loop { - let mut guard = futures::ready!(self.0.poll_write_ready(cx))?; - match guard.try_io(|inner| inner.get_ref().flush()) { - Ok(_) => return std::task::Poll::Ready(Ok(())), - Err(_would_block) => continue, - } - } - } - - fn poll_shutdown( - self: std::pin::Pin<&mut Self>, - _cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - std::task::Poll::Ready(Ok(())) - } -} diff --git a/src/smol.rs b/src/smol.rs deleted file mode 100644 index 8d0e877..0000000 --- a/src/smol.rs +++ /dev/null @@ -1,5 +0,0 @@ -type Pt = async_io::Async; - -pub type Command = crate::Command; -pub type Child = crate::Child; -pub type Pty = crate::Pty; diff --git a/src/std.rs b/src/std.rs deleted file mode 100644 index 4bc0eb0..0000000 --- a/src/std.rs +++ /dev/null @@ -1,5 +0,0 @@ -type Pt = std::fs::File; - -pub type Command = crate::Command; -pub type Child = crate::Child; -pub type Pty = crate::Pty; diff --git a/src/sys.rs b/src/sys.rs new file mode 100644 index 0000000..9cf6a44 --- /dev/null +++ b/src/sys.rs @@ -0,0 +1,103 @@ +use std::os::unix::io::{FromRawFd as _, IntoRawFd as _}; + +pub fn create_pt() -> nix::Result<(std::fs::File, std::path::PathBuf)> { + let pt = nix::pty::posix_openpt( + nix::fcntl::OFlag::O_RDWR | nix::fcntl::OFlag::O_NOCTTY, + )?; + nix::pty::grantpt(&pt)?; + nix::pty::unlockpt(&pt)?; + + let ptsname = nix::pty::ptsname_r(&pt)?.into(); + + let pt_fd = pt.into_raw_fd(); + + // Safety: 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((pt, ptsname)) +} + +pub fn set_term_size( + fh: &impl std::os::unix::io::AsRawFd, + size: crate::Size, +) -> nix::Result<()> { + let size = size.into(); + let fd = fh.as_raw_fd(); + + // Safety: std::fs::File is required to contain a valid file descriptor + // and size is guaranteed to be initialized because it's a normal rust + // value, and nix::pty::Winsize is a repr(C) struct with the same layout + // as `struct winsize` from sys/ioctl.h. + unsafe { + set_term_size_unsafe(fd, std::ptr::NonNull::from(&size).as_ptr()) + } + .map(|_| ()) +} + +pub fn setup_subprocess( + pt: &impl std::os::unix::io::AsRawFd, + pts: impl std::os::unix::io::IntoRawFd, +) -> nix::Result<( + std::process::Stdio, + std::process::Stdio, + std::process::Stdio, + impl FnMut() -> std::io::Result<()>, +)> { + let pt_fd = pt.as_raw_fd(); + let pts_fd = pts.into_raw_fd(); + + let stdin = nix::unistd::dup(pts_fd)?; + let stdout = nix::unistd::dup(pts_fd)?; + let stderr = nix::unistd::dup(pts_fd)?; + + // Safety: these file descriptors were all just returned from dup, so they + // must be valid + Ok(( + unsafe { std::process::Stdio::from_raw_fd(stdin) }, + unsafe { std::process::Stdio::from_raw_fd(stdout) }, + unsafe { std::process::Stdio::from_raw_fd(stderr) }, + move || { + nix::unistd::setsid()?; + set_controlling_terminal(pts_fd)?; + + // in the parent, destructors will handle closing these file + // descriptors (other than pt, used by the parent to + // communicate with the child) when the function ends, but in + // the child, we end by calling exec(), which doesn't call + // destructors. + + nix::unistd::close(pt_fd)?; + nix::unistd::close(pts_fd)?; + // at this point, stdin/stdout/stderr have already been + // reopened as fds 0/1/2 in the child, so we can (and should) + // close the originals + nix::unistd::close(stdin)?; + nix::unistd::close(stdout)?; + nix::unistd::close(stderr)?; + + Ok(()) + }, + )) +} + +fn set_controlling_terminal(fd: std::os::unix::io::RawFd) -> nix::Result<()> { + // Safety: std::fs::File is required to contain a valid file descriptor + unsafe { set_controlling_terminal_unsafe(fd, std::ptr::null()) } + .map(|_| ()) +} + +nix::ioctl_write_ptr_bad!( + set_term_size_unsafe, + libc::TIOCSWINSZ, + nix::pty::Winsize +); + +nix::ioctl_write_ptr_bad!( + set_controlling_terminal_unsafe, + libc::TIOCSCTTY, + libc::c_int +); diff --git a/src/tokio.rs b/src/tokio.rs deleted file mode 100644 index e2efa56..0000000 --- a/src/tokio.rs +++ /dev/null @@ -1,5 +0,0 @@ -type Pt = crate::pty::tokio::AsyncPty; - -pub type Command = crate::Command; -pub type Child = crate::Child; -pub type Pty = crate::Pty; diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..dd432eb --- /dev/null +++ b/src/types.rs @@ -0,0 +1,50 @@ +/// Represents the size of the pty. +#[derive(Debug, Clone, Copy)] +pub struct Size { + row: u16, + col: u16, + xpixel: u16, + ypixel: u16, +} + +impl Size { + /// Returns a [`Size`](Size) instance with the given number of rows and + /// columns. + #[must_use] + pub fn new(row: u16, col: u16) -> Self { + Self { + row, + col, + xpixel: 0, + ypixel: 0, + } + } + + /// Returns a [`Size`](Size) instance with the given number of rows and + /// columns, as well as the given pixel dimensions. + #[must_use] + pub fn new_with_pixel( + row: u16, + col: u16, + xpixel: u16, + ypixel: u16, + ) -> Self { + Self { + row, + col, + xpixel, + ypixel, + } + } +} + +impl From for nix::pty::Winsize { + fn from(size: Size) -> Self { + Self { + ws_row: size.row, + ws_col: size.col, + ws_xpixel: size.xpixel, + ws_ypixel: size.ypixel, + } + } +} diff --git a/tests/basic.rs b/tests/basic.rs index e164357..a623f13 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -1,11 +1,12 @@ -#[cfg(feature = "backend-std")] #[test] -fn test_cat_std() { +fn test_cat_blocking() { use std::io::{Read as _, Write as _}; - let pty = pty_process::std::Pty::new().unwrap(); + let pty = pty_process::blocking::Pty::new().unwrap(); pty.resize(pty_process::Size::new(24, 80)).unwrap(); - let mut child = pty_process::std::Command::new("cat").spawn(pty).unwrap(); + let mut child = pty_process::blocking::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 @@ -25,16 +26,16 @@ fn test_cat_std() { assert_eq!(status.code().unwrap(), 0); } -#[cfg(feature = "backend-smol")] +#[cfg(feature = "async")] #[test] -fn test_cat_smol() { - use smol::io::{AsyncReadExt as _, AsyncWriteExt as _}; +fn test_cat_async_std() { + use async_std::io::prelude::WriteExt as _; + use async_std::io::ReadExt as _; - let status = smol::block_on(async { - let pty = pty_process::smol::Pty::new().unwrap(); + let status = async_std::task::block_on(async { + let pty = pty_process::Pty::new().unwrap(); pty.resize(pty_process::Size::new(24, 80)).unwrap(); - let mut child = - pty_process::smol::Command::new("cat").spawn(pty).unwrap(); + let mut child = pty_process::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 @@ -43,7 +44,7 @@ fn test_cat_smol() { // 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. - smol::Timer::after(std::time::Duration::from_secs(1)).await; + async_std::task::sleep(std::time::Duration::from_secs(1)).await; let mut buf = [0u8; 1024]; let bytes = child.pty().read(&mut buf).await.unwrap(); @@ -55,18 +56,15 @@ fn test_cat_smol() { assert_eq!(status.code().unwrap(), 0); } -#[cfg(feature = "backend-async-std")] +#[cfg(feature = "async")] #[test] -fn test_cat_async_std() { - use async_std::io::prelude::WriteExt as _; - use async_std::io::ReadExt as _; +fn test_cat_smol() { + use smol::io::{AsyncReadExt as _, AsyncWriteExt as _}; - let status = async_std::task::block_on(async { - let pty = pty_process::async_std::Pty::new().unwrap(); + let status = smol::block_on(async { + let pty = pty_process::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(); + let mut child = pty_process::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 @@ -75,7 +73,7 @@ fn test_cat_async_std() { // 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; + smol::Timer::after(std::time::Duration::from_secs(1)).await; let mut buf = [0u8; 1024]; let bytes = child.pty().read(&mut buf).await.unwrap(); @@ -87,18 +85,18 @@ fn test_cat_async_std() { assert_eq!(status.code().unwrap(), 0); } -#[cfg(feature = "backend-tokio")] +#[cfg(feature = "async")] #[test] fn test_cat_tokio() { use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; + use tokio_util::compat::FuturesAsyncReadCompatExt as _; async fn async_test_cat_tokio() { - let pty = pty_process::tokio::Pty::new().unwrap(); + let pty = pty_process::Pty::new().unwrap(); pty.resize(pty_process::Size::new(24, 80)).unwrap(); - let mut child = - pty_process::tokio::Command::new("cat").spawn(pty).unwrap(); + let mut child = pty_process::Command::new("cat").spawn(pty).unwrap(); - child.pty_mut().write_all(b"foo\n").await.unwrap(); + child.pty_mut().compat().write_all(b"foo\n").await.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). @@ -108,12 +106,12 @@ fn test_cat_tokio() { tokio::time::sleep(std::time::Duration::from_secs(1)).await; let mut buf = [0u8; 1024]; - let bytes = child.pty_mut().read(&mut buf).await.unwrap(); + let bytes = child.pty_mut().compat().read(&mut buf).await.unwrap(); assert_eq!(&buf[..bytes], b"foo\r\nfoo\r\n"); - child.pty_mut().write_all(&[4u8]).await.unwrap(); + child.pty_mut().compat().write_all(&[4u8]).await.unwrap(); - let status = child.wait().await.unwrap(); + let status = child.status().await.unwrap(); assert_eq!(status.code().unwrap(), 0); } tokio::runtime::Runtime::new().unwrap().block_on(async { diff --git a/tests/pipe.rs b/tests/pipe.rs index b1e0540..1bf49f2 100644 --- a/tests/pipe.rs +++ b/tests/pipe.rs @@ -19,7 +19,7 @@ fn test_pipe_basic() { assert_eq!(output.stdout, b"10\n9\n8\n7\n6\n5\n4\n3\n2\n1\n"); } -#[cfg(feature = "backend-async-std")] +#[cfg(feature = "todo")] // TODO (hangs because i'm still overriding the configured fds) // #[test] fn test_pipe_async() { diff --git a/tests/winch.rs b/tests/winch.rs index 326efcc..9d26751 100644 --- a/tests/winch.rs +++ b/tests/winch.rs @@ -1,22 +1,22 @@ -use std::io::{Read as _, Write as _}; - -#[cfg(feature = "backend-std")] #[test] -fn test_winch() { - let pty = pty_process::std::Pty::new().unwrap(); +fn test_winch_std() { + use std::io::{Read as _, Write as _}; + + let pty = pty_process::blocking::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 child = pty_process::blocking::Command::new("perl") + .args(&[ + "-E", + "$|++; $SIG{WINCH} = sub { say 'WINCH' }; say 'started'; <>", + ]) + .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.pty().resize(pty_process::Size::new(25, 80)).unwrap(); let bytes = child.pty().read(&mut buf).unwrap(); assert_eq!(&buf[..bytes], b"WINCH\r\n"); @@ -25,3 +25,101 @@ fn test_winch() { let status = child.wait().unwrap(); assert_eq!(status.code().unwrap(), 0); } + +#[cfg(feature = "async")] +#[test] +fn test_winch_async_std() { + use async_std::io::prelude::WriteExt as _; + use async_std::io::ReadExt as _; + + let status = async_std::task::block_on(async { + let pty = pty_process::Pty::new().unwrap(); + pty.resize(pty_process::Size::new(24, 80)).unwrap(); + let mut child = pty_process::Command::new("perl") + .args(&[ + "-E", + "$|++; $SIG{WINCH} = sub { say 'WINCH' }; say 'started'; <>", + ]) + .spawn(pty) + .unwrap(); + + let mut buf = [0u8; 1024]; + let bytes = child.pty().read(&mut buf).await.unwrap(); + assert_eq!(&buf[..bytes], b"started\r\n"); + + child.pty().resize(pty_process::Size::new(25, 80)).unwrap(); + + let bytes = child.pty().read(&mut buf).await.unwrap(); + assert_eq!(&buf[..bytes], b"WINCH\r\n"); + + child.pty().write_all(b"\n").await.unwrap(); + child.status().await.unwrap() + }); + assert_eq!(status.code().unwrap(), 0); +} + +#[cfg(feature = "async")] +#[test] +fn test_winch_smol() { + use smol::io::{AsyncReadExt as _, AsyncWriteExt as _}; + + let status = smol::block_on(async { + let pty = pty_process::Pty::new().unwrap(); + pty.resize(pty_process::Size::new(24, 80)).unwrap(); + let mut child = pty_process::Command::new("perl") + .args(&[ + "-E", + "$|++; $SIG{WINCH} = sub { say 'WINCH' }; say 'started'; <>", + ]) + .spawn(pty) + .unwrap(); + + let mut buf = [0u8; 1024]; + let bytes = child.pty().read(&mut buf).await.unwrap(); + assert_eq!(&buf[..bytes], b"started\r\n"); + + child.pty().resize(pty_process::Size::new(25, 80)).unwrap(); + + let bytes = child.pty().read(&mut buf).await.unwrap(); + assert_eq!(&buf[..bytes], b"WINCH\r\n"); + + child.pty().write_all(b"\n").await.unwrap(); + child.status().await.unwrap() + }); + assert_eq!(status.code().unwrap(), 0); +} + +#[cfg(feature = "async")] +#[test] +fn test_winch_tokio() { + use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; + use tokio_util::compat::FuturesAsyncReadCompatExt as _; + + async fn async_test_cat_tokio() -> std::process::ExitStatus { + let pty = pty_process::Pty::new().unwrap(); + pty.resize(pty_process::Size::new(24, 80)).unwrap(); + let mut child = pty_process::Command::new("perl") + .args(&[ + "-E", + "$|++; $SIG{WINCH} = sub { say 'WINCH' }; say 'started'; <>", + ]) + .spawn(pty) + .unwrap(); + + let mut buf = [0u8; 1024]; + let bytes = child.pty().compat().read(&mut buf).await.unwrap(); + assert_eq!(&buf[..bytes], b"started\r\n"); + + child.pty().resize(pty_process::Size::new(25, 80)).unwrap(); + + let bytes = child.pty().compat().read(&mut buf).await.unwrap(); + assert_eq!(&buf[..bytes], b"WINCH\r\n"); + + child.pty().compat().write_all(b"\n").await.unwrap(); + child.status().await.unwrap() + } + tokio::runtime::Runtime::new().unwrap().block_on(async { + let status = async_test_cat_tokio().await; + assert_eq!(status.code().unwrap(), 0); + }); +} -- cgit v1.2.3-54-g00ecf