aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2021-12-29 02:55:24 -0500
committerJesse Luehrs <doy@tozt.net>2021-12-29 03:52:12 -0500
commit3b550f5d3dad77a56455352579fae3071b42e86d (patch)
tree393de5f206d227ec71149f478db900291357c456
parentf8780ca1e76286688b74d8a6c64d5fadf3cfd2a1 (diff)
downloadpty-process-3b550f5d3dad77a56455352579fae3071b42e86d.tar.gz
pty-process-3b550f5d3dad77a56455352579fae3071b42e86d.zip
wip another complete refactor
-rw-r--r--Cargo.toml17
-rw-r--r--examples/async-std.rs19
-rw-r--r--examples/basic.rs18
-rw-r--r--examples/interhack.rs23
-rw-r--r--examples/smol.rs19
-rw-r--r--examples/tokio.rs31
-rw-r--r--src/async_std.rs5
-rw-r--r--src/blocking/command.rs156
-rw-r--r--src/blocking/mod.rs4
-rw-r--r--src/blocking/pty.rs66
-rw-r--r--src/command.rs269
-rw-r--r--src/command/async_process.rs32
-rw-r--r--src/command/std.rs32
-rw-r--r--src/command/tokio.rs30
-rw-r--r--src/lib.rs73
-rw-r--r--src/pty.rs193
-rw-r--r--src/pty/async_io.rs5
-rw-r--r--src/pty/std.rs5
-rw-r--r--src/pty/tokio.rs101
-rw-r--r--src/smol.rs5
-rw-r--r--src/std.rs5
-rw-r--r--src/sys.rs103
-rw-r--r--src/tokio.rs5
-rw-r--r--src/types.rs50
-rw-r--r--tests/basic.rs58
-rw-r--r--tests/pipe.rs2
-rw-r--r--tests/winch.rs122
27 files changed, 758 insertions, 690 deletions
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<dyn std::error::Error + '_>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<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/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<std::process::Stdio>,
+ stdout: Option<std::process::Stdio>,
+ stderr: Option<std::process::Stdio>,
+}
+
+impl Command {
+ pub fn new<S: AsRef<std::ffi::OsStr>>(program: S) -> Self {
+ Self {
+ inner: std::process::Command::new(program),
+ stdin: None,
+ stdout: None,
+ stderr: None,
+ }
+ }
+
+ pub fn arg<S: AsRef<std::ffi::OsStr>>(&mut self, arg: S) -> &mut Self {
+ self.inner.arg(arg);
+ self
+ }
+
+ pub fn args<I, S>(&mut self, args: I) -> &mut Self
+ where
+ I: IntoIterator<Item = S>,
+ S: AsRef<std::ffi::OsStr>,
+ {
+ self.inner.args(args);
+ self
+ }
+
+ pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
+ where
+ K: AsRef<std::ffi::OsStr>,
+ V: AsRef<std::ffi::OsStr>,
+ {
+ self.inner.env(key, val);
+ self
+ }
+
+ pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self
+ where
+ I: IntoIterator<Item = (K, V)>,
+ K: AsRef<std::ffi::OsStr>,
+ V: AsRef<std::ffi::OsStr>,
+ {
+ self.inner.envs(vars);
+ self
+ }
+
+ pub fn env_remove<K: AsRef<std::ffi::OsStr>>(
+ &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<P: AsRef<std::path::Path>>(
+ &mut self,
+ dir: P,
+ ) -> &mut Self {
+ self.inner.current_dir(dir);
+ self
+ }
+
+ pub fn stdin<T: Into<std::process::Stdio>>(
+ &mut self,
+ cfg: Option<T>,
+ ) -> &mut Self {
+ self.stdin = cfg.map(Into::into);
+ self
+ }
+
+ pub fn stdout<T: Into<std::process::Stdio>>(
+ &mut self,
+ cfg: Option<T>,
+ ) -> &mut Self {
+ self.stdout = cfg.map(Into::into);
+ self
+ }
+
+ pub fn stderr<T: Into<std::process::Stdio>>(
+ &mut self,
+ cfg: Option<T>,
+ ) -> &mut Self {
+ self.stderr = cfg.map(Into::into);
+ self
+ }
+
+ pub fn spawn(
+ &mut self,
+ pty: crate::blocking::Pty,
+ ) -> crate::Result<Child> {
+ 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<Self> {
+ 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<std::fs::File> {
+ 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<usize> {
+ (&self.pt).read(buf)
+ }
+}
+
+// same as above
+impl std::io::Write for &Pty {
+ fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+ (&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<C: Impl, P: crate::pty::Impl> {
- inner: C,
- stdin_set: bool,
- stdout_set: bool,
- stderr_set: bool,
-
- _phantom: ::std::marker::PhantomData<P>,
+use async_process::unix::CommandExt as _;
+
+pub struct Command {
+ inner: async_process::Command,
+ stdin: Option<std::process::Stdio>,
+ stdout: Option<std::process::Stdio>,
+ stderr: Option<std::process::Stdio>,
}
-impl<C: Impl, P: crate::pty::Impl> Command<C, P> {
- pub fn new<S: AsRef<::std::ffi::OsStr>>(program: S) -> Self {
+impl Command {
+ 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,
+ inner: async_process::Command::new(program),
+ stdin: None,
+ stdout: None,
+ stderr: None,
}
}
- pub fn stdin<T: Into<::std::process::Stdio>>(&mut self, cfg: T) {
- self.stdin_set = true;
- self.inner.stdin_impl(cfg.into());
+ pub fn arg<S: AsRef<std::ffi::OsStr>>(&mut self, arg: S) -> &mut Self {
+ self.inner.arg(arg);
+ self
}
- pub fn stdout<T: Into<::std::process::Stdio>>(&mut self, cfg: T) {
- self.stdout_set = true;
- self.inner.stdout_impl(cfg.into());
+ pub fn args<I, S>(&mut self, args: I) -> &mut Self
+ where
+ I: IntoIterator<Item = S>,
+ S: AsRef<std::ffi::OsStr>,
+ {
+ self.inner.args(args);
+ self
}
- pub fn stderr<T: Into<::std::process::Stdio>>(&mut self, cfg: T) {
- self.stderr_set = true;
- self.inner.stderr_impl(cfg.into());
+ pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
+ where
+ K: AsRef<std::ffi::OsStr>,
+ V: AsRef<std::ffi::OsStr>,
+ {
+ self.inner.env(key, val);
+ self
}
- pub fn spawn(
+ pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self
+ where
+ I: IntoIterator<Item = (K, V)>,
+ K: AsRef<std::ffi::OsStr>,
+ V: AsRef<std::ffi::OsStr>,
+ {
+ self.inner.envs(vars);
+ self
+ }
+
+ pub fn env_remove<K: AsRef<std::ffi::OsStr>>(
&mut self,
- 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.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<P: AsRef<std::path::Path>>(
+ &mut self,
+ dir: P,
+ ) -> &mut Self {
+ self.inner.current_dir(dir);
+ self
}
-}
-impl<C: Impl, P: crate::pty::Impl> ::std::ops::Deref for Command<C, P> {
- type Target = C;
+ pub fn stdin<T: Into<std::process::Stdio>>(
+ &mut self,
+ cfg: Option<T>,
+ ) -> &mut Self {
+ self.stdin = cfg.map(Into::into);
+ self
+ }
- fn deref(&self) -> &Self::Target {
- &self.inner
+ pub fn stdout<T: Into<std::process::Stdio>>(
+ &mut self,
+ cfg: Option<T>,
+ ) -> &mut Self {
+ self.stdout = cfg.map(Into::into);
+ self
}
-}
-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
+ pub fn stderr<T: Into<std::process::Stdio>>(
+ &mut self,
+ cfg: Option<T>,
+ ) -> &mut Self {
+ self.stderr = cfg.map(Into::into);
+ self
+ }
+
+ pub fn spawn(&mut self, pty: crate::Pty) -> crate::Result<Child> {
+ 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<C, P: crate::pty::Impl> {
- inner: C,
- pty: crate::Pty<P>,
+pub struct Child {
+ inner: async_process::Child,
+ pty: crate::Pty,
}
-impl<C, P: crate::pty::Impl> Child<C, P> {
- fn new(inner: C, pty: crate::Pty<P>) -> 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<C, P: crate::pty::Impl> ::std::ops::Deref for Child<C, P> {
- type Target = C;
+impl std::ops::Deref for Child {
+ type Target = async_process::Child;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
-impl<C, P: crate::pty::Impl> ::std::ops::DerefMut for Child<C, P> {
+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<F>(&mut self, f: F)
- where
- F: FnMut() -> ::std::io::Result<()> + Send + Sync + 'static;
-
- fn spawn_impl(&mut self) -> crate::Result<Self::Child>;
-}
-
-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<F>(&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::Child> {
- 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<F>(&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::Child> {
- 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<F>(&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::Child> {
- 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: Impl> {
- pt: Pt,
- ptsname: ::std::path::PathBuf,
+pub struct Pty {
+ pt: async_io::Async<std::fs::File>,
+ 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)?;
-
+impl Pty {
+ pub fn new() -> crate::Result<Self> {
+ 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<std::fs::File> {
+ 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<Self>;
-}
+impl std::ops::Deref for Pty {
+ type Target = async_io::Async<std::fs::File>;
-/// 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<Size> 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<std::fs::File>, but without this
+// explicit impl, rust finds the AsyncRead impl for Async<std::fs::File>
+// 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<Result<usize, std::io::Error>> {
+ 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<Result<usize, std::io::Error>> {
+ 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<Result<(), std::io::Error>> {
+ 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<Result<(), std::io::Error>> {
+ 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<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
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<Self> {
- 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<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
-// 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<std::fs::File>);
-
-impl std::ops::Deref for AsyncPty {
- type Target = tokio::io::unix::AsyncFd<std::fs::File>;
-
- 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<std::io::Result<()>> {
- 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<std::io::Result<usize>> {
- 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<std::io::Result<()>> {
- 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::io::Result<()>> {
- 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<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/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<std::process::Command, Pt>;
-pub type Child = crate::Child<std::process::Child, Pt>;
-pub type Pty = crate::Pty<Pt>;
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<tokio::process::Command, Pt>;
-pub type Child = crate::Child<tokio::process::Child, Pt>;
-pub type Pty = crate::Pty<Pt>;
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<Size> 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);
+ });
+}