From 24cc5cac002f87ed501fddb8e48a2315288fdb1f Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Thu, 24 Oct 2019 12:51:04 -0400 Subject: docs --- Cargo.toml | 4 ++ src/lib.rs | 176 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 869c550..dfdd991 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,7 @@ log = "0.4" snafu = "0.5" tokio = "0.1.22" tokio-pty-process = "0.4" + +[dev-dependencies] +crossterm = "0.11" +mio = "0.6" diff --git a/src/lib.rs b/src/lib.rs index 406e54b..2c5aafb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,143 @@ +//! This crate wraps `tokio-pty-process` in order to provide a simpler API as +//! a single stream object. +//! +//! # Overview +//! +//! When you need to interact with an interactive program as part of an +//! asynchronous application, it can be tricky to figure out the way to +//! structure the different parts that are required. This crate simplifies the +//! API down to just providing the input via an `AsyncRead` object, and then +//! getting updates about what the program is doing via results generated by a +//! stream. +//! +//! # Synopsis +//! +//! This is an example of how to run an interactive program and have it behave +//! identically to running it in the shell. Note that we have to use our own +//! `Stdin` implementation here because `tokio::io::stdin()` is actually +//! blocking, and so polling it as part of an interactive application doesn't +//! work correctly. The implementation of `Stdin` is elided here, but you can +//! see the full implementation in `examples/shell.rs` in the repository. +//! +//! ```no_run +//! # use futures::future::Future as _; +//! # use futures::stream::Stream as _; +//! # use std::io::{Read as _, Write as _}; +//! # +//! let mut argv = std::env::args(); +//! argv.next().unwrap(); +//! let cmd = argv.next().unwrap(); +//! let args: Vec<_> = argv.collect(); +//! +//! let process = +//! tokio_pty_process_stream::Process::new(&cmd, &args, Stdin::new()); +//! +//! let _raw = crossterm::RawScreen::into_raw_mode().unwrap(); +//! tokio::run( +//! process +//! .for_each(|ev| { +//! match ev { +//! tokio_pty_process_stream::Event::CommandStart { +//! .. +//! } => {} +//! tokio_pty_process_stream::Event::Output { data } => { +//! let stdout = std::io::stdout(); +//! let mut stdout = stdout.lock(); +//! stdout.write_all(&data).unwrap(); +//! stdout.flush().unwrap(); +//! } +//! tokio_pty_process_stream::Event::CommandExit { +//! .. +//! } => {} +//! } +//! futures::future::ok(()) +//! }) +//! .map_err(|e| panic!(e)), +//! ); +//! # +//! # struct EventedStdin; +//! # +//! # const STDIN: i32 = 0; +//! # +//! # impl std::io::Read for EventedStdin { +//! # fn read(&mut self, buf: &mut [u8]) -> std::io::Result { +//! # let stdin = std::io::stdin(); +//! # let mut stdin = stdin.lock(); +//! # stdin.read(buf) +//! # } +//! # } +//! # +//! # impl mio::Evented for EventedStdin { +//! # fn register( +//! # &self, +//! # poll: &mio::Poll, +//! # token: mio::Token, +//! # interest: mio::Ready, +//! # opts: mio::PollOpt, +//! # ) -> std::io::Result<()> { +//! # let fd = STDIN as std::os::unix::io::RawFd; +//! # let eventedfd = mio::unix::EventedFd(&fd); +//! # eventedfd.register(poll, token, interest, opts) +//! # } +//! # +//! # fn reregister( +//! # &self, +//! # poll: &mio::Poll, +//! # token: mio::Token, +//! # interest: mio::Ready, +//! # opts: mio::PollOpt, +//! # ) -> std::io::Result<()> { +//! # let fd = STDIN as std::os::unix::io::RawFd; +//! # let eventedfd = mio::unix::EventedFd(&fd); +//! # eventedfd.reregister(poll, token, interest, opts) +//! # } +//! # +//! # fn deregister(&self, poll: &mio::Poll) -> std::io::Result<()> { +//! # let fd = STDIN as std::os::unix::io::RawFd; +//! # let eventedfd = mio::unix::EventedFd(&fd); +//! # eventedfd.deregister(poll) +//! # } +//! # } +//! # +//! # pub struct Stdin { +//! # input: tokio::reactor::PollEvented2, +//! # } +//! # +//! # impl Stdin { +//! # pub fn new() -> Self { +//! # Default::default() +//! # } +//! # } +//! # +//! # impl Default for Stdin { +//! # fn default() -> Self { +//! # Self { +//! # input: tokio::reactor::PollEvented2::new(EventedStdin), +//! # } +//! # } +//! # } +//! # +//! # impl std::io::Read for Stdin { +//! # fn read(&mut self, buf: &mut [u8]) -> std::io::Result { +//! # self.input.read(buf) +//! # } +//! # } +//! # +//! # impl tokio::io::AsyncRead for Stdin { +//! # fn poll_read( +//! # &mut self, +//! # buf: &mut [u8], +//! # ) -> std::result::Result, tokio::io::Error> { +//! # let ready = mio::Ready::readable(); +//! # futures::try_ready!(self.input.poll_read_ready(ready)); +//! # +//! # let res = self.read(buf)?; +//! # self.input.clear_read_ready(ready)?; +//! # Ok(futures::Async::Ready(res)) +//! # } +//! # } +//! ``` + #![warn(clippy::pedantic)] #![warn(clippy::nursery)] #![allow(clippy::missing_const_for_fn)] @@ -11,34 +151,51 @@ use tokio_pty_process::{CommandExt as _, PtyMaster as _}; const READ_BUFFER_SIZE: usize = 4 * 1024; +/// Errors returned by the process stream. #[derive(Debug, snafu::Snafu)] pub enum Error { + /// failed to open a pty #[snafu(display("failed to open a pty: {}", source))] OpenPty { source: std::io::Error }, + /// failed to poll for process exit #[snafu(display("failed to poll for process exit: {}", source))] ProcessExitPoll { source: std::io::Error }, + /// failed to read from pty #[snafu(display("failed to read from pty: {}", source))] ReadPty { source: std::io::Error }, + /// failed to read from terminal #[snafu(display("failed to read from terminal: {}", source))] ReadTerminal { source: std::io::Error }, + /// failed to resize pty #[snafu(display("failed to resize pty: {}", source))] ResizePty { source: std::io::Error }, + /// failed to spawn process #[snafu(display("failed to spawn process for `{}`: {}", cmd, source))] SpawnProcess { cmd: String, source: std::io::Error }, + /// failed to write to pty #[snafu(display("failed to write to pty: {}", source))] WritePty { source: std::io::Error }, } +/// Represents events generated by the process. #[derive(Debug, PartialEq, Eq)] pub enum Event { + /// Emitted once the command has been successfully spawned. CommandStart { cmd: String, args: Vec }, + + /// Emitted every time the command produces output. Note that when a + /// process is running under a pty, both stdout and stderr are attached to + /// the single pty input - there is no way to differentiate them when + /// reading from the pty output. Output { data: Vec }, + + /// Emitted when the command has exited. CommandExit { status: std::process::ExitStatus }, } @@ -68,6 +225,13 @@ impl State { } } +/// A spawned process. +/// +/// Wraps `AsyncPtyMaster` and `Child` from `tokio-pty-process` to provide a +/// view of the process as a single stream which emits events. In particular, +/// the stream will return an event when the process starts, when it writes +/// output to the pty, and when it exits. See the `Event` type for more +/// details. pub struct Process { state: State, input: R, @@ -83,6 +247,15 @@ pub struct Process { } impl Process { + /// Creates a new process stream. + /// + /// The process is not spawned and the pty is not opened until `poll` is + /// called. + /// + /// Takes as input the command and arguments to run, as well as the + /// `AsyncRead` object to read input from. Typically you will pass in + /// something connected to stdin here, although other options may be more + /// useful for automation or testing. pub fn new(cmd: &str, args: &[String], input: R) -> Self { Self { state: State::new(), @@ -99,6 +272,9 @@ impl Process { } } + /// Requests a change to the pty's terminal size. + /// + /// This will only be applied on the next call to `poll`. pub fn resize(&mut self, rows: u16, cols: u16) { self.needs_resize = Some((rows, cols)); } -- cgit v1.2.3