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 --- 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 ++++++++ 18 files changed, 561 insertions(+), 578 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 (limited to 'src') 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, + } + } +} -- cgit v1.2.3-54-g00ecf