Skip to content

Commit

Permalink
Improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
zhiburt committed Feb 12, 2022
1 parent a4480c2 commit 4d809d6
Show file tree
Hide file tree
Showing 16 changed files with 321 additions and 214 deletions.
3 changes: 1 addition & 2 deletions examples/log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ use expectrl::{spawn, Error};

#[cfg(not(feature = "async"))]
fn main() -> Result<(), Error> {
let mut p = spawn("cat")?;
p.set_log(std::io::stdout())?;
let mut p = spawn("cat")?.with_log(std::io::stdout())?;
p.send_line("Hello World")?;

// reading doesn't apear here because
Expand Down
22 changes: 0 additions & 22 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ use std::io;
#[derive(Debug)]
pub enum Error {
IO(io::Error),
#[cfg(unix)]
Nix(ptyprocess::Error),
#[cfg(windows)]
Win(conpty::error::Error),
CommandParsing,
RegexParsing,
ExpectTimeout,
Expand All @@ -22,10 +18,6 @@ impl Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::IO(err) => write!(f, "IO error {}", err),
#[cfg(unix)]
Error::Nix(err) => write!(f, "Nix error {}", err),
#[cfg(windows)]
Error::Win(err) => write!(f, "Win error {}", err),
Error::CommandParsing => write!(f, "Can't parse a command string, please check it out"),
Error::RegexParsing => write!(f, "Can't parse a regex expression"),
Error::ExpectTimeout => write!(f, "Reached a timeout for expect type of command"),
Expand All @@ -43,20 +35,6 @@ impl From<io::Error> for Error {
}
}

#[cfg(unix)]
impl From<ptyprocess::Error> for Error {
fn from(err: ptyprocess::Error) -> Self {
Self::Nix(err)
}
}

#[cfg(windows)]
impl From<conpty::error::Error> for Error {
fn from(err: conpty::error::Error) -> Self {
Self::Win(err)
}
}

impl From<String> for Error {
fn from(message: String) -> Self {
Self::Other(message)
Expand Down
32 changes: 22 additions & 10 deletions src/interact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,36 +381,46 @@ where
// flush buffers
session.flush()?;

let origin_pty_echo = session.get_echo()?;
let origin_pty_echo = session
.get_echo()
.map_err(|e| Error::Other(format!("failed to get echo {}", e)))?;
// tcgetattr issues error if a provided fd is not a tty,
// but we can work with such input as it may be redirected.
let origin_stdin_flags = termios::tcgetattr(STDIN_FILENO);

// verify: possible controlling fd can be stdout and stderr as well?
// https://stackoverflow.com/questions/35873843/when-setting-terminal-attributes-via-tcsetattrfd-can-fd-be-either-stdout
let isatty_terminal = isatty(STDIN_FILENO)?;
let isatty_terminal =
isatty(STDIN_FILENO).map_err(|e| Error::Other(format!("failed to call isatty {}", e)))?;

if isatty_terminal {
set_raw(STDIN_FILENO)?;
set_raw(STDIN_FILENO)
.map_err(|e| Error::Other(format!("failed to set a raw tty {}", e)))?;
}

session.set_echo(true, None)?;
session
.set_echo(true, None)
.map_err(|e| Error::Other(format!("failed to set echo {}", e)))?;

let result = interact(session, options);

if isatty_terminal {
// it's suppose to be always OK.
// but we don't use unwrap just in case.
let origin_stdin_flags = origin_stdin_flags?;
let origin_stdin_flags = origin_stdin_flags
.map_err(|e| Error::Other(format!("failed to call tcgetattr {}", e)))?;

termios::tcsetattr(
STDIN_FILENO,
termios::SetArg::TCSAFLUSH,
&origin_stdin_flags,
)?;
)
.map_err(|e| Error::Other(format!("failed to call tcsetattr {}", e)))?;
}

session.set_echo(origin_pty_echo, None)?;
session
.set_echo(origin_pty_echo, None)
.map_err(|e| Error::Other(format!("failed to set echo {}", e)))?;

result
}
Expand Down Expand Up @@ -440,7 +450,9 @@ where
// fill buffer to run callbacks if there was something in.
//
// We ignore errors because there might be errors like EOCHILD etc.
let status = session.status().map_err(|e| e.into());
let status = session
.status()
.map_err(|e| Error::Other(format!("failed to call status {}", e)));
if !matches!(status, Ok(WaitStatus::StillAlive)) {
exited = true;
}
Expand Down Expand Up @@ -979,13 +991,13 @@ impl NonBlocking for Stdin {
fn set_non_blocking(&mut self) -> io::Result<()> {
use std::os::unix::io::AsRawFd;
let fd = self.as_raw_fd();
crate::stream::unix::_make_non_blocking(fd, true)
crate::process::unix::_make_non_blocking(fd, true)
}

fn set_blocking(&mut self) -> io::Result<()> {
use std::os::unix::io::AsRawFd;
let fd = self.as_raw_fd();
crate::stream::unix::_make_non_blocking(fd, false)
crate::process::unix::_make_non_blocking(fd, false)
}
}

Expand Down
22 changes: 4 additions & 18 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,13 @@ mod check_macros;
mod control_code;
mod error;
mod found;
pub mod interact;
mod needle;
mod process;
mod stream;

pub mod interact;
pub mod repl;
pub mod session;
mod stream;

pub use control_code::ControlCode;
pub use error::Error;
Expand Down Expand Up @@ -128,22 +130,6 @@ fn tokenize_command(program: &str) -> Vec<String> {
mod tests {
use super::*;

#[cfg(unix)]
#[test]
fn test_tokenize_command() {
let res = tokenize_command("prog arg1 arg2");
assert_eq!(vec!["prog", "arg1", "arg2"], res);

let res = tokenize_command("prog -k=v");
assert_eq!(vec!["prog", "-k=v"], res);

let res = tokenize_command("prog 'my text'");
assert_eq!(vec!["prog", "'my text'"], res);

let res = tokenize_command(r#"prog "my text""#);
assert_eq!(vec!["prog", r#""my text""#], res);
}

#[cfg(unix)]
#[test]
fn test_spawn_no_command() {
Expand Down
15 changes: 15 additions & 0 deletions src/process/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use std::io::{self, Read, Result, Write};

#[cfg(unix)]
pub mod unix;
#[cfg(windows)]
pub mod windows;

pub trait Process: Sized {
type Command;
type Session;

fn spawn<S: AsRef<str>>(cmd: S) -> Result<Self>;
fn spawn_command(command: Self::Command) -> Result<Self>;
fn open_session(&mut self) -> Result<Self::Session>;
}
174 changes: 174 additions & 0 deletions src/process/unix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
use super::Process;
use crate::session::stream::NonBlocking;
use ptyprocess::{stream::Stream, PtyProcess};
use std::{
io::{self, Read, Result, Write},
ops::{Deref, DerefMut},
os::unix::prelude::{AsRawFd, RawFd},
process::Command,
};

pub struct UnixProcess {
proc: PtyProcess,
}

impl Process for UnixProcess {
type Command = Command;
type Session = PtyStream;

fn spawn<S: AsRef<str>>(cmd: S) -> Result<Self> {
let args = tokenize_command(cmd.as_ref());
if args.is_empty() {
return Err(io::Error::new(
io::ErrorKind::Other,
"Failed to parse a command",
));
}

let mut command = std::process::Command::new(&args[0]);
command.args(args.iter().skip(1));

Self::spawn_command(command)
}

fn spawn_command(command: Self::Command) -> Result<Self> {
let proc = PtyProcess::spawn(command).map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("Failed to spawn a command; {}", e),
)
})?;

Ok(Self { proc })
}

fn open_session(&mut self) -> Result<Self::Session> {
let stream = self.proc.get_pty_stream().map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("Failed to create a stream; {}", e),
)
})?;
let stream = PtyStream::new(stream);
Ok(stream)
}
}

#[derive(Debug)]
pub struct PtyStream {
handle: Stream,
}

impl PtyStream {
pub fn new(stream: Stream) -> Self {
Self { handle: stream }
}
}

impl Write for PtyStream {
fn write(&mut self, buf: &[u8]) -> Result<usize> {
self.handle.write(buf)
}

fn flush(&mut self) -> Result<()> {
self.handle.flush()
}

fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> Result<usize> {
self.handle.write_vectored(bufs)
}
}

impl Read for PtyStream {
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
self.handle.read(buf)
}
}

impl AsRawFd for PtyStream {
fn as_raw_fd(&self) -> RawFd {
self.handle.as_raw_fd()
}
}

impl NonBlocking for PtyStream {
fn set_non_blocking(&mut self) -> Result<()> {
let fd = self.as_raw_fd();
_make_non_blocking(fd, true)
}

fn set_blocking(&mut self) -> Result<()> {
let fd = self.as_raw_fd();
_make_non_blocking(fd, false)
}
}

pub fn _make_non_blocking(fd: RawFd, blocking: bool) -> Result<()> {
use nix::fcntl::{fcntl, FcntlArg, OFlag};

let opt = fcntl(fd, FcntlArg::F_GETFL).map_err(nix_error_to_io)?;
let mut opt = OFlag::from_bits_truncate(opt);
opt.set(OFlag::O_NONBLOCK, blocking);
fcntl(fd, FcntlArg::F_SETFL(opt)).map_err(nix_error_to_io)?;
Ok(())
}

impl Deref for UnixProcess {
type Target = PtyProcess;

fn deref(&self) -> &Self::Target {
&self.proc
}
}

impl DerefMut for UnixProcess {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.proc
}
}

fn nix_error_to_io(err: nix::Error) -> io::Error {
match err.as_errno() {
Some(code) => io::Error::from_raw_os_error(code as _),
None => io::Error::new(
io::ErrorKind::Other,
"Unexpected error type conversion from nix to io",
),
}
}

/// Turn e.g. "prog arg1 arg2" into ["prog", "arg1", "arg2"]
/// It takes care of single and double quotes but,
///
/// It doesn't cover all edge cases.
/// So it may not be compatible with real shell arguments parsing.
#[cfg(unix)]
fn tokenize_command(program: &str) -> Vec<String> {
let re = regex::Regex::new(r#""[^"]+"|'[^']+'|[^'" ]+"#).unwrap();
let mut res = vec![];
for cap in re.captures_iter(program) {
res.push(cap[0].to_string());
}
res
}

#[cfg(test)]
mod tests {
use super::*;

#[cfg(unix)]
#[test]
fn test_tokenize_command() {
let res = tokenize_command("prog arg1 arg2");
assert_eq!(vec!["prog", "arg1", "arg2"], res);

let res = tokenize_command("prog -k=v");
assert_eq!(vec!["prog", "-k=v"], res);

let res = tokenize_command("prog 'my text'");
assert_eq!(vec!["prog", "'my text'"], res);

let res = tokenize_command(r#"prog "my text""#);
assert_eq!(vec!["prog", r#""my text""#], res);
}
}
File renamed without changes.
4 changes: 3 additions & 1 deletion src/repl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,9 @@ impl ReplSession {
quit_command: Option<Q>,
) -> Result<Self, Error> {
let session = Session::spawn(cmd)?;
let is_echo_on = session.get_echo()?;
let is_echo_on = session
.get_echo()
.map_err(|e| Error::Other(format!("failed to get echo {}", e)))?;
let prompt = prompt.as_ref().to_owned();
let quit_command = quit_command.map(|q| q.as_ref().to_owned());

Expand Down

0 comments on commit 4d809d6

Please sign in to comment.