diff --git a/README.md b/README.md index 95451423..d0837051 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,10 @@ Simple example for interacting via ftp: ```rust,no_run use rexpect::spawn; use rexpect::error::*; +use std::time; fn do_ftp() -> Result<(), Error> { - let mut p = spawn("ftp speedtest.tele2.net", Some(30_000))?; + let mut p = spawn("ftp speedtest.tele2.net", Some(time::Duration::from_secs(30)))?; p.exp_regex("Name \\(.*\\):")?; p.send_line("anonymous")?; p.exp_string("Password")?; @@ -55,9 +56,10 @@ fn main() { ```rust,no_run use rexpect::spawn_bash; use rexpect::error::*; +use std::time; fn do_bash() -> Result<(), Error> { - let mut p = spawn_bash(Some(2000))?; + let mut p = spawn_bash(Some(time::Duration::from_secs(2)))?; // case 1: wait until program is done p.send_line("hostname")?; @@ -105,9 +107,10 @@ goes into nirvana. There are two functions to ensure that: ```rust,no_run use rexpect::spawn_bash; use rexpect::error::*; +use std::time; fn do_bash_jobcontrol() -> Result<(), Error> { - let mut p = spawn_bash(Some(1000))?; + let mut p = spawn_bash(Some(time::Duration::from_secs(1)))?; p.execute("ping 8.8.8.8", "bytes of data")?; p.send_control('z')?; p.wait_for_prompt()?; diff --git a/examples/bash.rs b/examples/bash.rs index a95ac46b..95530823 100644 --- a/examples/bash.rs +++ b/examples/bash.rs @@ -1,8 +1,9 @@ use rexpect::error::Error; use rexpect::spawn_bash; +use std::time; fn main() -> Result<(), Error> { - let mut p = spawn_bash(Some(1000))?; + let mut p = spawn_bash(Some(time::Duration::from_secs(1)))?; p.execute("ping 8.8.8.8", "bytes")?; p.send_control('z')?; p.wait_for_prompt()?; diff --git a/examples/bash_read.rs b/examples/bash_read.rs index a5e37258..efbde1a0 100644 --- a/examples/bash_read.rs +++ b/examples/bash_read.rs @@ -1,8 +1,10 @@ +use std::time; + use rexpect::error::Error; use rexpect::spawn_bash; fn main() -> Result<(), Error> { - let mut p = spawn_bash(Some(2000))?; + let mut p = spawn_bash(Some(time::Duration::from_secs(2)))?; // case 1: wait until program is done p.send_line("hostname")?; diff --git a/examples/exit_code.rs b/examples/exit_code.rs index 67486de6..d9f81c3a 100644 --- a/examples/exit_code.rs +++ b/examples/exit_code.rs @@ -1,19 +1,23 @@ use rexpect::error::Error; use rexpect::process::wait; use rexpect::spawn; +use std::time; /// The following code emits: /// cat exited with code 0, all good! /// cat exited with code 1 /// 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))?; + let p = spawn("cat /etc/passwd", Some(time::Duration::from_secs(2)))?; match p.process.wait() { Ok(wait::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))?; + let mut p = spawn( + "cat /this/does/not/exist", + Some(time::Duration::from_secs(2)), + )?; match p.process.wait() { Ok(wait::WaitStatus::Exited(_, 0)) => println!("cat succeeded"), Ok(wait::WaitStatus::Exited(_, c)) => { diff --git a/examples/ftp.rs b/examples/ftp.rs index d1571879..27c56da8 100644 --- a/examples/ftp.rs +++ b/examples/ftp.rs @@ -1,8 +1,12 @@ use rexpect::error::Error; use rexpect::spawn; +use std::time; fn main() -> Result<(), Error> { - let mut p = spawn("ftp speedtest.tele2.net", Some(2000))?; + let mut p = spawn( + "ftp speedtest.tele2.net", + Some(time::Duration::from_secs(2)), + )?; p.exp_regex("Name \\(.*\\):")?; p.send_line("anonymous")?; p.exp_string("Password")?; diff --git a/examples/repl.rs b/examples/repl.rs index 4a305e46..4bf59d7b 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -3,6 +3,7 @@ use rexpect::error::Error; use rexpect::session::PtyReplSession; use rexpect::spawn; +use std::time; fn ed_session() -> Result { let mut ed = PtyReplSession { @@ -12,7 +13,7 @@ fn ed_session() -> Result { // used for `wait_for_prompt()` prompt: "> ".to_owned(), - pty_session: spawn("/bin/ed -p '> '", Some(2000))?, + pty_session: spawn("/bin/ed -p '> '", Some(time::Duration::from_secs(2)))?, // command which is sent when the instance of this struct is dropped // in the below example this is not needed, but if you don't explicitly // exit a REPL then rexpect tries to send a SIGTERM and depending on the repl diff --git a/examples/tcp.rs b/examples/tcp.rs index 5f3c11f4..9ed80890 100644 --- a/examples/tcp.rs +++ b/examples/tcp.rs @@ -1,11 +1,12 @@ use rexpect::spawn_stream; use std::error::Error; use std::net::TcpStream; +use std::time; fn main() -> Result<(), Box> { let tcp = TcpStream::connect("www.google.com:80")?; let tcp_w = tcp.try_clone()?; - let mut session = spawn_stream(tcp, tcp_w, Some(2000)); + let mut session = spawn_stream(tcp, tcp_w, Some(time::Duration::from_secs(2))); session.send_line("GET / HTTP/1.1")?; session.send_line("Host: www.google.com")?; session.send_line("Accept-Language: fr")?; diff --git a/src/lib.rs b/src/lib.rs index cc656a03..49df311c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,9 +18,10 @@ //! //! use rexpect::spawn; //! use rexpect::error::Error; +//! use std::time; //! //! fn main() -> Result<(), Error> { -//! let mut p = spawn("ftp speedtest.tele2.net", Some(2000))?; +//! let mut p = spawn("ftp speedtest.tele2.net", Some(time::Duration::from_secs(2)))?; //! p.exp_regex("Name \\(.*\\):")?; //! p.send_line("anonymous")?; //! p.exp_string("Password")?; @@ -48,9 +49,10 @@ //! ```no_run //! use rexpect::spawn_bash; //! use rexpect::error::Error; +//! use std::time; //! //! fn main() -> Result<(), Error> { -//! let mut p = spawn_bash(Some(30_000))?; +//! let mut p = spawn_bash(Some(time::Duration::from_secs(30)))?; //! p.execute("ping 8.8.8.8", "bytes of data")?; //! p.send_control('z')?; //! p.wait_for_prompt()?; @@ -74,7 +76,8 @@ pub mod reader; pub mod session; pub use reader::ReadUntil; -pub use session::{spawn, spawn_bash, spawn_python, spawn_stream, spawn_with_options}; +pub use session::Builder; +pub use session::{spawn, spawn_bash, spawn_python, spawn_stream}; // include the README.md here to test its doc #[doc = include_str!("../README.md")] diff --git a/src/process.rs b/src/process.rs index db1c2507..4e7ee207 100644 --- a/src/process.rs +++ b/src/process.rs @@ -147,8 +147,8 @@ impl PtyProcess { /// At the drop of `PtyProcess` the running process is killed. This is blocking forever if /// the process does not react to a normal kill. If `kill_timeout` is set the process is /// `kill -9`ed after duration - pub fn set_kill_timeout(&mut self, timeout_ms: Option) { - self.kill_timeout = timeout_ms.map(time::Duration::from_millis); + pub fn set_kill_timeout(&mut self, timeout: Option) { + self.kill_timeout = timeout; } /// Get status of child process, non-blocking. diff --git a/src/reader.rs b/src/reader.rs index a6ae6e54..42011e6d 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -104,9 +104,12 @@ pub fn find(needle: &ReadUntil, buffer: &str, eof: bool) -> Option<(usize, usize /// + `None`: `read_until` is blocking forever. This is probably not what you want /// + `Some(millis)`: after millis milliseconds a timeout error is raised /// - `strip_ansi_escape_codes`: Whether to filter out escape codes, such as colors. +/// + `Some`: the frequency with which the process will check the output. +/// + `None`: will be 1/1000 of a `timeout` (or `100ms` if `timeout` is None) #[derive(Default)] pub struct Options { - pub timeout_ms: Option, + pub timeout: Option, + pub poll_frequency: Option, pub strip_ansi_escape_codes: bool, } @@ -120,6 +123,7 @@ pub struct NBReader { buffer: String, eof: bool, timeout: Option, + poll_frequency: time::Duration, } impl NBReader { @@ -167,13 +171,23 @@ impl NBReader { // don't do error handling as on an error it was most probably // the main thread which exited (remote hangup) }); + + // Set `poll_frequency` based on the `timeout` (or 100ms in case `timeout` is None) if it is None + let poll_frequency = match options.poll_frequency { + Some(poll_frequency) => poll_frequency, + None => options + .timeout + .map_or(time::Duration::from_millis(100), |t| t / 1000), + }; + // allocate string with a initial capacity of 1024, so when appending chars // we don't need to reallocate memory often NBReader { reader: rx, buffer: String::with_capacity(1024), eof: false, - timeout: options.timeout_ms.map(time::Duration::from_millis), + timeout: options.timeout, + poll_frequency, } } @@ -279,7 +293,7 @@ impl NBReader { } } // nothing matched: wait a little - thread::sleep(time::Duration::from_millis(100)); + thread::sleep(self.poll_frequency); } } @@ -406,8 +420,9 @@ mod tests { let mut r = NBReader::new( f, Options { - timeout_ms: None, + timeout: None, strip_ansi_escape_codes: true, + poll_frequency: None, }, ); let bytes = r @@ -423,8 +438,9 @@ mod tests { let mut r = NBReader::new( f, Options { - timeout_ms: None, + timeout: None, strip_ansi_escape_codes: true, + poll_frequency: None, }, ); let bytes = r diff --git a/src/session.rs b/src/session.rs index 07399812..623f38d2 100644 --- a/src/session.rs +++ b/src/session.rs @@ -9,6 +9,7 @@ use std::io::LineWriter; use std::io::prelude::*; use std::ops::{Deref, DerefMut}; use std::process::Command; +use std::time; use tempfile; pub struct StreamSession { @@ -135,10 +136,11 @@ impl StreamSession { /// ``` /// use rexpect::{spawn, ReadUntil}; /// # use rexpect::error::Error; + /// use std::time; /// /// # fn main() { /// # || -> Result<(), Error> { - /// let mut s = spawn("cat", Some(1000))?; + /// let mut s = spawn("cat", Some(time::Duration::from_secs(1)))?; /// s.send_line("hello, polly!")?; /// s.exp_any(vec![ReadUntil::String("hello".into()), /// ReadUntil::EOF])?; @@ -179,10 +181,11 @@ impl DerefMut for PtySession { /// /// use rexpect::spawn; /// # use rexpect::error::Error; +/// use std::time; /// /// # fn main() { /// # || -> Result<(), Error> { -/// let mut s = spawn("cat", Some(1000))?; +/// let mut s = spawn("cat", Some(time::Duration::from_secs(1)))?; /// s.send_line("hello, polly!")?; /// let line = s.read_line()?; /// assert_eq!("hello, polly!", line); @@ -199,6 +202,89 @@ impl PtySession { } } +/// Process factory, which can be used in order to configure the properties of a command. +#[derive(Default)] +pub struct Builder { + /// A command to spawn + pub(super) command: Option, + /// If Some: all `exp_*` commands time out after `timeout`, if None: never times out. + /// By default timeout is set to 30s + pub(super) timeout: Option, + /// If Some: the frequency with which the process will check the output. + /// If None: will be 1/1000 of a `timeout` (or `100ms` if `timeout` is None) + pub(super) poll_frequency: Option, + /// Whether to filter out escape codes, such as colors. + pub(super) strip_ansi_escape_codes: bool, +} + +impl Builder { + pub fn new(command: Command) -> Self { + Self { + command: Some(command), + timeout: Some(time::Duration::from_secs(30)), + ..Default::default() + } + } + + /// Set the command which will be executed + pub fn command(mut self, command: Command) -> Self { + self.command = Some(command); + self + } + + /// Set the timeout for the command. + /// + /// If Some: all `exp_*` commands time out after `timeout`, if None: never times out. + /// It's highly recommended to put a timeout there, as otherwise in case of + /// a problem the program just hangs instead of exiting with an + /// error message indicating where it stopped. + /// For automation 30s (the default in pexpect) is a good value. + pub fn timeout(mut self, timeout: Option) -> Self { + self.timeout = timeout; + self + } + + /// Set the poll frequency with which the process will check the output. + /// + /// Keep `None` to make it relative to the `timeout`. + pub fn poll_frequency(mut self, poll_frequency: Option) -> Self { + self.poll_frequency = poll_frequency; + self + } + + /// Set filtering out escape codes, such as colors. + pub fn strip_ansi_escape_codes(mut self) -> Self { + self.strip_ansi_escape_codes = true; + self + } + + pub fn spawn(self) -> Result { + let command = self.command.ok_or(Error::EmptyProgramName)?; + + #[cfg(feature = "which")] + { + let _ = which::which(command.get_program())?; + } + let mut process = PtyProcess::new(command)?; + process.set_kill_timeout(self.timeout); + + let Self { + timeout, + poll_frequency, + strip_ansi_escape_codes, + .. + } = self; + + let options = Options { + timeout, + poll_frequency, + strip_ansi_escape_codes, + }; + + PtySession::new(process, options) + } +} + /// Turn e.g. "prog arg1 arg2" into ["prog", "arg1", "arg2"] /// Also takes care of single and double quotes fn tokenize_command(program: &str) -> Result, Error> { @@ -212,13 +298,13 @@ fn tokenize_command(program: &str) -> Result, Error> { /// /// - `program`: This is split at spaces and turned into a `process::Command` /// if you wish more control over this, use `spawn_command` -/// - `timeout`: If Some: all `exp_*` commands time out after x milliseconds, if None: never times +/// - `timeout`: If Some: all `exp_*` commands time out after `timeout`, if None: never times /// out. /// It's highly recommended to put a timeout there, as otherwise in case of /// a problem the program just hangs instead of exiting with an /// error message indicating where it stopped. /// For automation 30'000 (30s, the default in pexpect) is a good value. -pub fn spawn(program: &str, timeout_ms: Option) -> Result { +pub fn spawn(program: &str, timeout: Option) -> Result { if program.is_empty() { return Err(Error::EmptyProgramName); } @@ -227,30 +313,16 @@ pub fn spawn(program: &str, timeout_ms: Option) -> Result) -> Result { - spawn_with_options( - command, - Options { - timeout_ms, - strip_ansi_escape_codes: false, - }, - ) + spawn_command(command, timeout) } /// See `spawn` -pub fn spawn_with_options(command: Command, options: Options) -> Result { - #[cfg(feature = "which")] - { - let _ = which::which(command.get_program())?; - } - let mut process = PtyProcess::new(command)?; - process.set_kill_timeout(options.timeout_ms); - - PtySession::new(process, options) +pub fn spawn_command( + command: Command, + timeout: Option, +) -> Result { + let builder = Builder::new(command); + builder.timeout(timeout).spawn() } /// A repl session: e.g. bash or the python shell: @@ -299,10 +371,11 @@ impl PtyReplSession { /// ``` /// use rexpect::spawn_bash; /// # use rexpect::error::Error; + /// use std::time; /// /// # fn main() { /// # || -> Result<(), Error> { - /// let mut p = spawn_bash(Some(1000))?; + /// let mut p = spawn_bash(Some(time::Duration::from_secs(1)))?; /// p.execute("cat <(echo ready) -", "ready")?; /// p.send_line("hans")?; /// p.exp_string("hans")?; @@ -381,7 +454,7 @@ impl Drop for PtyReplSession { /// Also: if you start a program you should use `execute` and not `send_line`. /// /// For an example see the README -pub fn spawn_bash(timeout: Option) -> Result { +pub fn spawn_bash(timeout: Option) -> Result { // unfortunately working with a temporary tmpfile is the only // way to guarantee that we are "in step" with the prompt // all other attempts were futile, especially since we cannot @@ -423,7 +496,7 @@ pub fn spawn_bash(timeout: Option) -> Result { /// Spawn the python shell /// /// This is just a proof of concept implementation (and serves for documentation purposes) -pub fn spawn_python(timeout: Option) -> Result { +pub fn spawn_python(timeout: Option) -> Result { spawn_command(Command::new("python"), timeout).map(|p| PtyReplSession { prompt: ">>> ".to_owned(), pty_session: p, @@ -436,14 +509,15 @@ pub fn spawn_python(timeout: Option) -> Result { pub fn spawn_stream( reader: R, writer: W, - timeout_ms: Option, + timeout: Option, ) -> StreamSession { StreamSession::new( reader, writer, Options { - timeout_ms, + timeout, strip_ansi_escape_codes: false, + poll_frequency: None, }, ) } @@ -454,7 +528,7 @@ mod tests { #[test] fn test_read_line() -> Result<(), Error> { - let mut s = spawn("cat", Some(100000))?; + let mut s = spawn("cat", Some(time::Duration::from_secs(100)))?; s.send_line("hans")?; assert_eq!("hans", s.read_line()?); let should = crate::process::wait::WaitStatus::Signaled( @@ -468,7 +542,8 @@ mod tests { #[test] fn test_expect_eof_timeout() -> Result<(), Error> { - let mut p = spawn("sleep 3", Some(1000)).expect("cannot run sleep 3"); + let mut p = + spawn("sleep 3", Some(time::Duration::from_secs(1))).expect("cannot run sleep 3"); match p.exp_eof() { Ok(_) => panic!("should raise Timeout"), Err(Error::Timeout { .. }) => {} @@ -479,13 +554,14 @@ mod tests { #[test] fn test_expect_eof_timeout2() { - let mut p = spawn("sleep 1", Some(1100)).expect("cannot run sleep 1"); + let mut p = + spawn("sleep 1", Some(time::Duration::from_millis(1100))).expect("cannot run sleep 1"); assert!(p.exp_eof().is_ok(), "expected eof"); } #[test] fn test_expect_string() -> Result<(), Error> { - let mut p = spawn("cat", Some(1000)).expect("cannot run cat"); + let mut p = spawn("cat", Some(time::Duration::from_secs(1))).expect("cannot run cat"); p.send_line("hello world!")?; p.exp_string("hello world!")?; p.send_line("hello heaven!")?; @@ -495,7 +571,7 @@ mod tests { #[test] fn test_read_string_before() -> Result<(), Error> { - let mut p = spawn("cat", Some(1000)).expect("cannot run cat"); + let mut p = spawn("cat", Some(time::Duration::from_secs(1))).expect("cannot run cat"); p.send_line("lorem ipsum dolor sit amet")?; assert_eq!("lorem ipsum dolor sit ", p.exp_string("amet")?); Ok(()) @@ -503,7 +579,7 @@ mod tests { #[test] fn test_expect_any() -> Result<(), Error> { - let mut p = spawn("cat", Some(1000)).expect("cannot run cat"); + let mut p = spawn("cat", Some(time::Duration::from_secs(1))).expect("cannot run cat"); p.send_line("Hi")?; match p.exp_any(vec![ ReadUntil::NBytes(3), @@ -517,7 +593,7 @@ mod tests { #[test] fn test_expect_empty_command_error() { - let p = spawn("", Some(1000)); + let p = spawn("", Some(time::Duration::from_secs(1))); match p { Ok(_) => panic!("should raise an error"), Err(Error::EmptyProgramName) => {} @@ -527,7 +603,7 @@ mod tests { #[test] fn test_kill_timeout() -> Result<(), Error> { - let mut p = spawn_bash(Some(1000))?; + let mut p = spawn_bash(Some(time::Duration::from_secs(1)))?; p.execute("cat <(echo ready) -", "ready")?; Ok(()) // p is dropped here and kill is sent immediately to bash @@ -536,7 +612,7 @@ mod tests { #[test] fn test_bash() -> Result<(), Error> { - let mut p = spawn_bash(Some(1000))?; + let mut p = spawn_bash(Some(time::Duration::from_secs(1)))?; p.send_line("cd /tmp/")?; p.wait_for_prompt()?; p.send_line("pwd")?; @@ -546,7 +622,7 @@ mod tests { #[test] fn test_bash_control_chars() -> Result<(), Error> { - let mut p = spawn_bash(Some(1000))?; + let mut p = spawn_bash(Some(time::Duration::from_secs(1)))?; p.execute("cat <(echo ready) -", "ready")?; p.send_control('c')?; // abort: SIGINT p.wait_for_prompt()?;