diff --git a/examples/exit_code.rs b/examples/exit_code.rs index 67486de6..e341fd51 100644 --- a/examples/exit_code.rs +++ b/examples/exit_code.rs @@ -1,5 +1,5 @@ use rexpect::error::Error; -use rexpect::process::wait; +use rexpect::process::WaitStatus; use rexpect::spawn; /// The following code emits: @@ -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()?); } diff --git a/src/process.rs b/src/process.rs index bbe0683b..c1db0b7c 100644 --- a/src/process.rs +++ b/src/process.rs @@ -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, @@ -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 /// @@ -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"); @@ -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() { @@ -173,26 +175,26 @@ impl PtyProcess { /// # } /// ``` /// - pub fn status(&self) -> Option { + pub fn status(&self) -> Option { 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 { + pub fn wait(&self) -> Result { 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 { + pub fn exit(&mut self) -> Result { 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) } @@ -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 { + pub fn kill(&mut self, sig: Signal) -> Result { 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)?; } } } @@ -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"); } } @@ -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] @@ -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(()) } diff --git a/src/reader.rs b/src/reader.rs index b49c298f..6b1efd09 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -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) -> 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. @@ -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(); @@ -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(); diff --git a/src/session.rs b/src/session.rs index 273cbb8c..f655d0be 100644 --- a/src/session.rs +++ b/src/session.rs @@ -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 @@ -245,13 +253,7 @@ fn tokenize_command(program: &str) -> Result, Error> { /// See [`spawn`] pub fn spawn_command(command: Command, timeout_ms: Option) -> Result { - spawn_with_options( - command, - Options { - timeout_ms, - strip_ansi_escape_codes: false, - }, - ) + spawn_with_options(command, Options::new().timeout_ms(timeout_ms)) } /// See [`spawn`] @@ -271,25 +273,45 @@ pub fn spawn_with_options(command: Command, options: Options) -> Result>> " for python + pub pty_session: PtySession, pub prompt: String, + pub quit_command: Option, + 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, + /// 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) -> 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 { @@ -423,8 +445,8 @@ pub fn spawn_bash(timeout: Option) -> Result { 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, }; @@ -456,14 +478,7 @@ pub fn spawn_stream( writer: W, timeout_ms: Option, ) -> StreamSession { - StreamSession::new( - reader, - writer, - Options { - timeout_ms, - strip_ansi_escape_codes: false, - }, - ) + StreamSession::new(reader, writer, Options::new().timeout_ms(timeout_ms)) } #[cfg(test)]