Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions examples/exit_code.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use rexpect::error::Error;
use rexpect::process::wait;
use rexpect::process::WaitStatus;
use rexpect::spawn;

/// The following code emits:
Expand All @@ -8,15 +8,15 @@ use rexpect::spawn;
/// Output (stdout and stderr): cat: /this/does/not/exist: No such file or directory
fn main() -> Result<(), Error> {
let p = spawn("cat /etc/passwd", Some(2000))?;
match p.process.wait() {
Ok(wait::WaitStatus::Exited(_, 0)) => println!("cat exited with code 0, all good!"),
match p.process().wait() {
Ok(WaitStatus::Exited(_, 0)) => println!("cat exited with code 0, all good!"),
_ => println!("cat exited with code >0, or it was killed"),
}

let mut p = spawn("cat /this/does/not/exist", Some(2000))?;
match p.process.wait() {
Ok(wait::WaitStatus::Exited(_, 0)) => println!("cat succeeded"),
Ok(wait::WaitStatus::Exited(_, c)) => {
match p.process().wait() {
Ok(WaitStatus::Exited(_, 0)) => println!("cat succeeded"),
Ok(WaitStatus::Exited(_, c)) => {
println!("Cat failed with exit code {c}");
println!("Output (stdout and stderr): {}", p.exp_eof()?);
}
Expand Down
32 changes: 17 additions & 15 deletions src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use nix;
use nix::fcntl::{OFlag, open};
use nix::libc::STDERR_FILENO;
use nix::pty::{PtyMaster, grantpt, posix_openpt, unlockpt};
pub use nix::sys::{signal, wait};
use nix::sys::{stat, termios};
use nix::unistd::{
ForkResult, Pid, close, dup, dup2_stderr, dup2_stdin, dup2_stdout, fork, setsid,
Expand All @@ -18,6 +17,10 @@ use std::os::unix::process::CommandExt;
use std::process::Command;
use std::{thread, time};

pub use nix::sys::{signal, wait};
pub use signal::Signal;
pub use wait::WaitStatus;

/// Start a process in a forked tty to interact with it like you would
/// within a terminal
///
Expand All @@ -42,8 +45,7 @@ use std::{thread, time};
/// # fn main() {
///
/// let mut process = PtyProcess::new(Command::new("cat")).expect("could not execute cat");
/// let fd = dup(&process.pty).unwrap();
/// let f = File::from(fd);
/// let f = process.get_file_handle().unwrap();
/// let mut writer = LineWriter::new(&f);
/// let mut reader = BufReader::new(&f);
/// process.exit().expect("could not terminate process");
Expand Down Expand Up @@ -161,7 +163,7 @@ impl PtyProcess {
/// # Example
/// ```rust,no_run
///
/// use rexpect::process::{self, wait::WaitStatus};
/// use rexpect::process::{self, WaitStatus};
/// use std::process::Command;
///
/// # fn main() {
Expand All @@ -173,26 +175,26 @@ impl PtyProcess {
/// # }
/// ```
///
pub fn status(&self) -> Option<wait::WaitStatus> {
pub fn status(&self) -> Option<WaitStatus> {
wait::waitpid(self.child_pid, Some(wait::WaitPidFlag::WNOHANG)).ok()
}

/// Wait until process has exited (non-blocking).
///
/// If the process doesn't terminate this will block forever.
pub fn wait(&self) -> Result<wait::WaitStatus, Error> {
pub fn wait(&self) -> Result<WaitStatus, Error> {
wait::waitpid(self.child_pid, None).map_err(Error::from)
}

/// Regularly exit the process (blocking).
///
/// This method is blocking until the process is dead
pub fn exit(&mut self) -> Result<wait::WaitStatus, Error> {
pub fn exit(&mut self) -> Result<WaitStatus, Error> {
self.kill(signal::SIGTERM)
}

/// Kill the process with a specific signal (non-blocking).
pub fn signal(&mut self, sig: signal::Signal) -> Result<(), Error> {
pub fn signal(&mut self, sig: Signal) -> Result<(), Error> {
signal::kill(self.child_pid, sig).map_err(Error::from)
}

Expand All @@ -206,26 +208,26 @@ impl PtyProcess {
///
/// If `kill_timeout` is set and a repeated sending of signal does not result in the process
/// being killed, then `kill -9` is sent after the `kill_timeout` duration has elapsed.
pub fn kill(&mut self, sig: signal::Signal) -> Result<wait::WaitStatus, Error> {
pub fn kill(&mut self, sig: Signal) -> Result<WaitStatus, Error> {
let start = time::Instant::now();
loop {
match signal::kill(self.child_pid, sig) {
Ok(_) => {}
// process was already killed before -> ignore
Err(nix::errno::Errno::ESRCH) => {
return Ok(wait::WaitStatus::Exited(Pid::from_raw(0), 0));
return Ok(WaitStatus::Exited(Pid::from_raw(0), 0));
}
Err(e) => return Err(Error::from(e)),
}

match self.status() {
Some(status) if status != wait::WaitStatus::StillAlive => return Ok(status),
Some(status) if status != WaitStatus::StillAlive => return Ok(status),
Some(_) | None => thread::sleep(time::Duration::from_millis(100)),
}
// kill -9 if timeout is reached
if let Some(timeout) = self.kill_timeout {
if start.elapsed() > timeout {
signal::kill(self.child_pid, signal::Signal::SIGKILL).map_err(Error::from)?;
signal::kill(self.child_pid, Signal::SIGKILL).map_err(Error::from)?;
}
}
}
Expand All @@ -234,7 +236,7 @@ impl PtyProcess {

impl Drop for PtyProcess {
fn drop(&mut self) {
if let Some(wait::WaitStatus::StillAlive) = self.status() {
if let Some(WaitStatus::StillAlive) = self.status() {
self.exit().expect("cannot exit");
}
}
Expand All @@ -243,7 +245,7 @@ impl Drop for PtyProcess {
#[cfg(test)]
mod tests {
use super::*;
use nix::sys::{signal, wait};
use nix::sys::wait;
use std::io::{BufRead, BufReader, LineWriter, Write};

#[test]
Expand All @@ -263,7 +265,7 @@ mod tests {
thread::sleep(time::Duration::from_millis(100));
writer.write_all(&[3])?; // send ^C
writer.flush()?;
let should = wait::WaitStatus::Signaled(process.child_pid, signal::Signal::SIGINT, false);
let should = WaitStatus::Signaled(process.child_pid, Signal::SIGINT, false);
assert_eq!(should, wait::waitpid(process.child_pid, None).unwrap());
Ok(())
}
Expand Down
32 changes: 18 additions & 14 deletions src/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@ pub struct Options {
pub strip_ansi_escape_codes: bool,
}

impl Options {
pub fn new() -> Self {
Default::default()
}

pub fn timeout_ms(mut self, timeout_ms: Option<u64>) -> Self {
self.timeout_ms = timeout_ms;
self
}

pub fn strip_ansi_escape_codes(mut self, yes: bool) -> Self {
self.strip_ansi_escape_codes = yes;
self
}
}

/// Non blocking reader
///
/// Typically you'd need that to check for output of a process without blocking your thread.
Expand Down Expand Up @@ -408,13 +424,7 @@ mod tests {
#[test]
fn test_skip_partial_ansi_code() {
let f = io::Cursor::new("\x1b[31;1;4mHello\x1b[1");
let mut r = NBReader::new(
f,
Options {
timeout_ms: None,
strip_ansi_escape_codes: true,
},
);
let mut r = NBReader::new(f, Options::new().strip_ansi_escape_codes(true));
let bytes = r
.read_until(&ReadUntil::String("Hello".to_owned()))
.unwrap();
Expand All @@ -425,13 +435,7 @@ mod tests {
#[test]
fn test_skip_ansi_codes() {
let f = io::Cursor::new("\x1b[31;1;4mHello\x1b[0m");
let mut r = NBReader::new(
f,
Options {
timeout_ms: None,
strip_ansi_escape_codes: true,
},
);
let mut r = NBReader::new(f, Options::new().strip_ansi_escape_codes(true));
let bytes = r
.read_until(&ReadUntil::String("Hello".to_owned()))
.unwrap();
Expand Down
63 changes: 39 additions & 24 deletions src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,14 @@ impl PtySession {
let stream = StreamSession::new(reader, f, options);
Ok(Self { process, stream })
}

pub fn process(&self) -> &PtyProcess {
&self.process
}

pub fn process_mut(&mut self) -> &mut PtyProcess {
&mut self.process
}
}

/// Start command in background in a pty session (pty fork) and return a struct
Expand Down Expand Up @@ -245,13 +253,7 @@ fn tokenize_command(program: &str) -> Result<Vec<String>, Error> {

/// See [`spawn`]
pub fn spawn_command(command: Command, timeout_ms: Option<u64>) -> Result<PtySession, Error> {
spawn_with_options(
command,
Options {
timeout_ms,
strip_ansi_escape_codes: false,
},
)
spawn_with_options(command, Options::new().timeout_ms(timeout_ms))
}

/// See [`spawn`]
Expand All @@ -271,25 +273,45 @@ pub fn spawn_with_options(command: Command, options: Options) -> Result<PtySessi
/// You have a prompt where a user inputs commands and the shell
/// executes it and writes some output
pub struct PtyReplSession {
/// The prompt, used for `wait_for_prompt`, e.g. ">>> " for python
pub pty_session: PtySession,
pub prompt: String,
pub quit_command: Option<String>,
pub echo_on: bool,
}

/// The `pty_session` you prepared before (initiating the shell, maybe set a custom prompt, etc.)
impl PtyReplSession {
/// Start a REPL session
///
/// `prompt`: used for [`Self::wait_for_prompt`], e.g. ">>> " for python
///
/// See [`spawn_bash`] for an example
pub pty_session: PtySession,
pub fn new(pty_session: PtySession, prompt: String) -> Self {
Self {
pty_session,
prompt,
quit_command: None,
echo_on: false,
}
}

/// If set, then the `quit_command` is called when this object is dropped
/// you need to provide this if the shell you're testing is not killed by just sending
/// SIGTERM
pub quit_command: Option<String>,
/// Called when this object is dropped.
///
/// You need to provide this if the shell you're testing is not killed by just sending
/// SIGTERM.
pub fn quit_command(mut self, cmd: Option<String>) -> Self {
self.quit_command = cmd;
self
}

/// Set this to true if the repl has echo on (i.e. sends user input to stdout)
///
/// Although echo is set off at pty fork (see `PtyProcess::new`) a few repls still
/// seem to be able to send output.
/// You may need to try with true first, and if tests fail set this to false.
pub echo_on: bool,
pub fn echo_on(mut self, yes: bool) -> Self {
self.echo_on = yes;
self
}
}

impl PtyReplSession {
Expand Down Expand Up @@ -423,8 +445,8 @@ pub fn spawn_bash(timeout: Option<u64>) -> Result<PtyReplSession, Error> {
spawn_command(c, timeout).and_then(|p| {
let new_prompt = "[REXPECT_PROMPT>";
let mut pb = PtyReplSession {
prompt: new_prompt.to_owned(),
pty_session: p,
prompt: new_prompt.to_owned(),
quit_command: Some("quit".to_owned()),
echo_on: false,
};
Expand Down Expand Up @@ -456,14 +478,7 @@ pub fn spawn_stream<R: Read + Send + 'static, W: Write>(
writer: W,
timeout_ms: Option<u64>,
) -> StreamSession<W> {
StreamSession::new(
reader,
writer,
Options {
timeout_ms,
strip_ansi_escape_codes: false,
},
)
StreamSession::new(reader, writer, Options::new().timeout_ms(timeout_ms))
}

#[cfg(test)]
Expand Down
Loading