diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9803fab2..6c0ea6bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,24 @@ jobs: with: command: check + + fmt: + name: format + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1 + with: + toolchain: 1.60.0 + - run: rustup component add rustfmt + - name: cargo-fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: -- --check + + # We need some "accummulation" job here because bors fails (timeouts) to # listen on matrix builds. # Hence, we have some kind of dummy here that bors can listen on @@ -41,6 +59,7 @@ jobs: if: ${{ success() }} needs: - check + - fmt runs-on: ubuntu-latest steps: - name: CI succeeded diff --git a/examples/bash.rs b/examples/bash.rs index 735297e3..3c009ac0 100644 --- a/examples/bash.rs +++ b/examples/bash.rs @@ -1,6 +1,6 @@ extern crate rexpect; -use rexpect::spawn_bash; use rexpect::errors::*; +use rexpect::spawn_bash; fn run() -> Result<()> { let mut p = spawn_bash(Some(1000))?; diff --git a/examples/bash_read.rs b/examples/bash_read.rs index 96f7404f..b8ccc42c 100644 --- a/examples/bash_read.rs +++ b/examples/bash_read.rs @@ -1,7 +1,6 @@ extern crate rexpect; -use rexpect::spawn_bash; use rexpect::errors::*; - +use rexpect::spawn_bash; fn run() -> Result<()> { let mut p = spawn_bash(Some(2000))?; @@ -19,7 +18,10 @@ fn run() -> Result<()> { let (_, words) = p.exp_regex("[0-9]+")?; let (_, bytes) = p.exp_regex("[0-9]+")?; p.wait_for_prompt()?; // go sure `wc` is really done - println!("/etc/passwd has {} lines, {} words, {} chars", lines, words, bytes); + println!( + "/etc/passwd has {} lines, {} words, {} chars", + lines, words, bytes + ); // case 3: read while program is still executing p.execute("ping 8.8.8.8", "bytes of data")?; // returns when it sees "bytes of data" in output diff --git a/examples/exit_code.rs b/examples/exit_code.rs index 1b85d1e1..60ba8c95 100644 --- a/examples/exit_code.rs +++ b/examples/exit_code.rs @@ -1,16 +1,14 @@ extern crate rexpect; -use rexpect::spawn; use rexpect::errors::*; use rexpect::process::wait; - +use rexpect::spawn; /// 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 exit_code_fun() -> Result<()> { - 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!"), @@ -23,7 +21,7 @@ fn exit_code_fun() -> Result<()> { Ok(wait::WaitStatus::Exited(_, c)) => { println!("Cat failed with exit code {}", c); println!("Output (stdout and stderr): {}", p.exp_eof()?); - }, + } // for other possible return types of wait() // see here: https://tailhook.github.io/rotor/nix/sys/wait/enum.WaitStatus.html _ => println!("cat was probably killed"), @@ -32,7 +30,6 @@ fn exit_code_fun() -> Result<()> { Ok(()) } - fn main() { exit_code_fun().unwrap_or_else(|e| panic!("cat function failed with {}", e)); } diff --git a/examples/ftp.rs b/examples/ftp.rs index d3ec7e7e..738e0493 100644 --- a/examples/ftp.rs +++ b/examples/ftp.rs @@ -1,7 +1,7 @@ extern crate rexpect; -use rexpect::spawn; use rexpect::errors::*; +use rexpect::spawn; fn do_ftp() -> Result<()> { let mut p = spawn("ftp speedtest.tele2.net", Some(2000))?; @@ -19,7 +19,6 @@ fn do_ftp() -> Result<()> { Ok(()) } - fn main() { do_ftp().unwrap_or_else(|e| panic!("ftp job failed with {}", e)); } diff --git a/examples/repl.rs b/examples/repl.rs index 084c96a5..e426c291 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -2,9 +2,9 @@ extern crate rexpect; -use rexpect::spawn; -use rexpect::session::PtyReplSession; use rexpect::errors::*; +use rexpect::session::PtyReplSession; +use rexpect::spawn; fn ed_session() -> Result { let mut ed = PtyReplSession { @@ -40,4 +40,4 @@ fn do_ed_repl() -> Result<()> { fn main() { do_ed_repl().unwrap_or_else(|e| panic!("ed session failed with {}", e)); -} \ No newline at end of file +} diff --git a/examples/tcp.rs b/examples/tcp.rs index a8998737..5f3c11f4 100644 --- a/examples/tcp.rs +++ b/examples/tcp.rs @@ -1,9 +1,8 @@ use rexpect::spawn_stream; -use std::net::TcpStream; use std::error::Error; +use std::net::TcpStream; -fn main() -> Result<(), Box> -{ +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)); @@ -13,4 +12,4 @@ fn main() -> Result<(), Box> session.send_line("")?; session.exp_string("HTTP/1.1 200 OK")?; Ok(()) -} \ No newline at end of file +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..36755d22 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +# default diff --git a/src/lib.rs b/src/lib.rs index 2a94808e..79402909 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,16 +79,16 @@ //! ``` pub mod process; -pub mod session; pub mod reader; +pub mod session; -pub use session::{spawn, spawn_bash, spawn_python, spawn_stream}; pub use reader::ReadUntil; +pub use session::{spawn, spawn_bash, spawn_python, spawn_stream}; pub mod errors { use std::time; // Create the Error, ErrorKind, ResultExt, and Result types - error_chain::error_chain!{ + error_chain::error_chain! { errors { EOF(expected:String, got:String, exit_code:Option) { description("End of filestream (usually stdout) occurred, most probably\ diff --git a/src/process.rs b/src/process.rs index 754cdeda..1e891623 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,19 +1,19 @@ //! Start a process via pty +use crate::errors::*; +use nix; +use nix::fcntl::{open, OFlag}; +use nix::libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}; +use nix::pty::{grantpt, posix_openpt, unlockpt, PtyMaster}; +pub use nix::sys::{signal, wait}; +use nix::sys::{stat, termios}; +use nix::unistd::{dup, dup2, fork, setsid, ForkResult, Pid}; use std; use std::fs::File; -use std::process::Command; +use std::os::unix::io::{AsRawFd, FromRawFd}; use std::os::unix::process::CommandExt; -use std::os::unix::io::{FromRawFd, AsRawFd}; -use std::{thread, time}; -use nix::pty::{posix_openpt, grantpt, unlockpt, PtyMaster}; -use nix::fcntl::{OFlag, open}; -use nix; -use nix::sys::{stat, termios}; -use nix::unistd::{fork, ForkResult, setsid, dup, dup2, Pid}; -use nix::libc::{STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO}; -pub use nix::sys::{wait, signal}; -use crate::errors::*; // load error-chain +use std::process::Command; +use std::{thread, time}; // load error-chain /// Start a process in a forked tty so you can interact with it the same as you would /// within a terminal @@ -60,7 +60,6 @@ pub struct PtyProcess { kill_timeout: Option, } - #[cfg(target_os = "linux")] use nix::pty::ptsname_r; @@ -69,8 +68,8 @@ use nix::pty::ptsname_r; /// instead of using a static mutex this calls ioctl with TIOCPTYGNAME directly /// based on https://blog.tarq.io/ptsname-on-osx-with-rust/ fn ptsname_r(fd: &PtyMaster) -> nix::Result { - use std::ffi::CStr; use nix::libc::{ioctl, TIOCPTYGNAME}; + use std::ffi::CStr; // the buffer size on OSX is 128, defined by sys/ttycom.h let mut buf: [i8; 128] = [0; 128]; @@ -103,9 +102,11 @@ impl PtyProcess { match fork()? { ForkResult::Child => { setsid()?; // create new session with child as session leader - let slave_fd = open(std::path::Path::new(&slave_name), - OFlag::O_RDWR, - stat::Mode::empty())?; + let slave_fd = open( + std::path::Path::new(&slave_name), + OFlag::O_RDWR, + stat::Mode::empty(), + )?; // assign stdin, stdout, stderr to the tty, just like a terminal does dup2(slave_fd, STDIN_FILENO)?; @@ -120,16 +121,14 @@ impl PtyProcess { command.exec(); Err(nix::Error::last()) } - ForkResult::Parent { child: child_pid } => { - Ok(PtyProcess { - pty: master_fd, - child_pid: child_pid, - kill_timeout: None, - }) - } + ForkResult::Parent { child: child_pid } => Ok(PtyProcess { + pty: master_fd, + child_pid: child_pid, + kill_timeout: None, + }), } }() - .chain_err(|| format!("could not execute {:?}", command)) + .chain_err(|| format!("could not execute {:?}", command)) } /// Get handle to pty fork for reading/writing @@ -189,8 +188,7 @@ impl PtyProcess { /// Non-blocking variant of `kill()` (doesn't wait for process to be killed) pub fn signal(&mut self, sig: signal::Signal) -> Result<()> { - signal::kill(self.child_pid, sig) - .chain_err(|| "failed to send signal to process")?; + signal::kill(self.child_pid, sig).chain_err(|| "failed to send signal to process")?; Ok(()) } @@ -214,7 +212,6 @@ impl PtyProcess { Err(e) => return Err(format!("kill resulted in error: {:?}", e).into()), } - match self.status() { Some(status) if status != wait::WaitStatus::StillAlive => return Ok(status), Some(_) | None => thread::sleep(time::Duration::from_millis(100)), @@ -243,9 +240,9 @@ impl Drop for PtyProcess { #[cfg(test)] mod tests { use super::*; - use std::io::{BufReader, LineWriter}; - use nix::sys::{wait, signal}; + use nix::sys::{signal, wait}; use std::io::prelude::*; + use std::io::{BufReader, LineWriter}; #[test] /// Open cat, write string, read back string twice, send Ctrl^C and check that cat exited @@ -271,6 +268,6 @@ mod tests { assert_eq!(should, wait::waitpid(process.child_pid, None).unwrap()); Ok(()) }() - .unwrap_or_else(|e| panic!("test_cat failed: {}", e)); + .unwrap_or_else(|e| panic!("test_cat failed: {}", e)); } } diff --git a/src/reader.rs b/src/reader.rs index f872302b..1c33803c 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -1,12 +1,12 @@ //! Unblocking reader which supports waiting for strings/regexes and EOF to be present -use std::io::{self, BufReader}; -use std::io::prelude::*; -use std::sync::mpsc::{channel, Receiver}; -use std::{thread, result}; -use std::{time, fmt}; use crate::errors::*; // load error-chain pub use regex::Regex; +use std::io::prelude::*; +use std::io::{self, BufReader}; +use std::sync::mpsc::{channel, Receiver}; +use std::{fmt, time}; +use std::{result, thread}; #[derive(Debug)] enum PipeError { @@ -70,7 +70,13 @@ pub fn find(needle: &ReadUntil, buffer: &str, eof: bool) -> Option<(usize, usize None } } - &ReadUntil::EOF => if eof { Some((0, buffer.len())) } else { None }, + &ReadUntil::EOF => { + if eof { + Some((0, buffer.len())) + } else { + None + } + } &ReadUntil::NBytes(n) => { if n <= buffer.len() { Some((0, n)) @@ -242,13 +248,16 @@ impl NBReader { // ran into timeout if let Some(timeout) = self.timeout { if start.elapsed() > timeout { - return Err(ErrorKind::Timeout(needle.to_string(), - self.buffer.clone() - .replace("\n", "`\\n`\n") - .replace("\r", "`\\r`") - .replace('\u{1b}', "`^`"), - timeout) - .into()); + return Err(ErrorKind::Timeout( + needle.to_string(), + self.buffer + .clone() + .replace("\n", "`\\n`\n") + .replace("\r", "`\\r`") + .replace('\u{1b}', "`^`"), + timeout, + ) + .into()); } } // nothing matched: wait a little @@ -277,9 +286,11 @@ mod tests { fn test_expect_melon() { let f = io::Cursor::new("a melon\r\n"); let mut r = NBReader::new(f, None); - assert_eq!(("a melon".to_string(), "\r\n".to_string()), - r.read_until(&ReadUntil::String("\r\n".to_string())) - .expect("cannot read line")); + assert_eq!( + ("a melon".to_string(), "\r\n".to_string()), + r.read_until(&ReadUntil::String("\r\n".to_string())) + .expect("cannot read line") + ); // check for EOF match r.read_until(&ReadUntil::NBytes(10)) { Ok(_) => assert!(false), @@ -302,21 +313,29 @@ mod tests { let f = io::Cursor::new("2014-03-15"); let mut r = NBReader::new(f, None); let re = Regex::new(r"-\d{2}-").unwrap(); - assert_eq!(("2014".to_string(), "-03-".to_string()), - r.read_until(&ReadUntil::Regex(re)) - .expect("regex doesn't match")); + assert_eq!( + ("2014".to_string(), "-03-".to_string()), + r.read_until(&ReadUntil::Regex(re)) + .expect("regex doesn't match") + ); } #[test] fn test_nbytes() { let f = io::Cursor::new("abcdef"); let mut r = NBReader::new(f, None); - assert_eq!(("".to_string(), "ab".to_string()), - r.read_until(&ReadUntil::NBytes(2)).expect("2 bytes")); - assert_eq!(("".to_string(), "cde".to_string()), - r.read_until(&ReadUntil::NBytes(3)).expect("3 bytes")); - assert_eq!(("".to_string(), "f".to_string()), - r.read_until(&ReadUntil::NBytes(4)).expect("4 bytes")); + assert_eq!( + ("".to_string(), "ab".to_string()), + r.read_until(&ReadUntil::NBytes(2)).expect("2 bytes") + ); + assert_eq!( + ("".to_string(), "cde".to_string()), + r.read_until(&ReadUntil::NBytes(3)).expect("3 bytes") + ); + assert_eq!( + ("".to_string(), "f".to_string()), + r.read_until(&ReadUntil::NBytes(4)).expect("4 bytes") + ); } #[test] @@ -324,8 +343,10 @@ mod tests { let f = io::Cursor::new("lorem ipsum dolor sit amet"); let mut r = NBReader::new(f, None); r.read_until(&ReadUntil::NBytes(2)).expect("2 bytes"); - assert_eq!(("".to_string(), "rem ipsum dolor sit amet".to_string()), - r.read_until(&ReadUntil::EOF).expect("reading until EOF")); + assert_eq!( + ("".to_string(), "rem ipsum dolor sit amet".to_string()), + r.read_until(&ReadUntil::EOF).expect("reading until EOF") + ); } #[test] @@ -339,5 +360,4 @@ mod tests { assert_eq!(None, r.try_read()); assert_eq!(None, r.try_read()); } - } diff --git a/src/session.rs b/src/session.rs index 379bf8f1..23c2304c 100644 --- a/src/session.rs +++ b/src/session.rs @@ -1,14 +1,14 @@ //! Main module of rexpect: start new process and interact with it +use crate::errors::*; // load error-chain use crate::process::PtyProcess; -use crate::reader::{NBReader, Regex}; pub use crate::reader::ReadUntil; +use crate::reader::{NBReader, Regex}; use std::fs::File; -use std::io::LineWriter; -use std::process::Command; use std::io::prelude::*; +use std::io::LineWriter; use std::ops::{Deref, DerefMut}; -use crate::errors::*; // load error-chain +use std::process::Command; use tempfile; pub struct StreamSession { @@ -30,13 +30,13 @@ impl StreamSession { /// returns number of written bytes pub fn send_line(&mut self, line: &str) -> Result { let mut len = self.send(line)?; - len += self.writer + len += self + .writer .write(&['\n' as u8]) .chain_err(|| "cannot write newline")?; Ok(len) } - /// Send string to process. As stdin of the process is most likely buffered, you'd /// need to call `flush()` after `send()` to make the process actually see your input. /// @@ -72,7 +72,6 @@ impl StreamSession { Ok(()) } - /// Make sure all bytes written via `send()` are sent to the process pub fn flush(&mut self) -> Result<()> { self.writer.flush().chain_err(|| "could not flush") @@ -117,7 +116,10 @@ impl StreamSession { /// Note that `exp_regex("^foo")` matches the start of the yet consumed output. /// For matching the start of the line use `exp_regex("\nfoo")` pub fn exp_regex(&mut self, regex: &str) -> Result<(String, String)> { - let res = self.exp(&ReadUntil::Regex(Regex::new(regex).chain_err(|| "invalid regex")?)) + let res = self + .exp(&ReadUntil::Regex( + Regex::new(regex).chain_err(|| "invalid regex")?, + )) .and_then(|s| Ok(s)); res } @@ -170,7 +172,6 @@ pub struct PtySession { pub commandname: String, // only for debugging purposes now } - // make StreamSession's methods available directly impl Deref for PtySession { type Target = StreamSession; @@ -206,7 +207,6 @@ impl DerefMut for PtySession { /// ``` impl PtySession { fn new(process: PtyProcess, timeout_ms: Option, commandname: String) -> Result { - let f = process.get_file_handle(); let reader = f.try_clone().chain_err(|| "couldn't open write stream")?; let stream = StreamSession::new(reader, f, timeout_ms); @@ -257,8 +257,7 @@ pub fn spawn(program: &str, timeout_ms: Option) -> Result { /// See `spawn` pub fn spawn_command(command: Command, timeout_ms: Option) -> Result { let commandname = format!("{:?}", &command); - let mut process = PtyProcess::new(command) - .chain_err(|| "couldn't start process")?; + let mut process = PtyProcess::new(command).chain_err(|| "couldn't start process")?; process.set_kill_timeout(timeout_ms); PtySession::new(process, timeout_ms, commandname) @@ -369,7 +368,6 @@ impl Drop for PtyReplSession { } } - /// Spawn bash in a pty session, run programs and expect output /// /// @@ -401,13 +399,23 @@ pub fn spawn_bash(timeout: Option) -> Result { // would set as PS1 and we cannot know when is the right time // to set the new PS1 let mut rcfile = tempfile::NamedTempFile::new().unwrap(); - rcfile.write(b"include () { [[ -f \"$1\" ]] && source \"$1\"; }\n\ + rcfile + .write( + b"include () { [[ -f \"$1\" ]] && source \"$1\"; }\n\ include /etc/bash.bashrc\n\ include ~/.bashrc\n\ PS1=\"~~~~\"\n\ - unset PROMPT_COMMAND\n").expect("cannot write to tmpfile"); + unset PROMPT_COMMAND\n", + ) + .expect("cannot write to tmpfile"); let mut c = Command::new("bash"); - c.args(&["--rcfile", rcfile.path().to_str().unwrap_or_else(|| return "temp file does not exist".into())]); + c.args(&[ + "--rcfile", + rcfile + .path() + .to_str() + .unwrap_or_else(|| return "temp file does not exist".into()), + ]); spawn_command(c, timeout).and_then(|p| { let new_prompt = "[REXPECT_PROMPT>"; let mut pb = PtyReplSession { @@ -417,7 +425,9 @@ pub fn spawn_bash(timeout: Option) -> Result { echo_on: false, }; pb.exp_string("~~~~")?; - rcfile.close().chain_err(|| "cannot delete temporary rcfile")?; + rcfile + .close() + .chain_err(|| "cannot delete temporary rcfile")?; pb.send_line(&("PS1='".to_string() + new_prompt + "'"))?; // wait until the new prompt appears pb.wait_for_prompt()?; @@ -440,7 +450,11 @@ pub fn spawn_python(timeout: Option) -> Result { } /// Spawn a REPL from a stream -pub fn spawn_stream(reader: R, writer: W, timeout_ms: Option) -> StreamSession { +pub fn spawn_stream( + reader: R, + writer: W, + timeout_ms: Option, +) -> StreamSession { StreamSession::new(reader, writer, timeout_ms) } @@ -454,16 +468,17 @@ mod tests { let mut s = spawn("cat", Some(1000))?; s.send_line("hans")?; assert_eq!("hans", s.read_line()?); - let should = crate::process::wait::WaitStatus::Signaled(s.process.child_pid, - crate::process::signal::Signal::SIGTERM, - false); + let should = crate::process::wait::WaitStatus::Signaled( + s.process.child_pid, + crate::process::signal::Signal::SIGTERM, + false, + ); assert_eq!(should, s.process.exit()?); Ok(()) }() - .unwrap_or_else(|e| panic!("test_read_line failed: {}", e)); + .unwrap_or_else(|e| panic!("test_read_line failed: {}", e)); } - #[test] fn test_expect_eof_timeout() { || -> Result<()> { @@ -472,11 +487,10 @@ mod tests { Ok(_) => assert!(false, "should raise Timeout"), Err(Error(ErrorKind::Timeout(_, _, _), _)) => {} Err(_) => assert!(false, "should raise TimeOut"), - } Ok(()) }() - .unwrap_or_else(|e| panic!("test_timeout failed: {}", e)); + .unwrap_or_else(|e| panic!("test_timeout failed: {}", e)); } #[test] @@ -495,7 +509,7 @@ mod tests { p.exp_string("hello heaven!")?; Ok(()) }() - .unwrap_or_else(|e| panic!("test_expect_string failed: {}", e)); + .unwrap_or_else(|e| panic!("test_expect_string failed: {}", e)); } #[test] @@ -506,7 +520,7 @@ mod tests { assert_eq!("lorem ipsum dolor sit ", p.exp_string("amet")?); Ok(()) }() - .unwrap_or_else(|e| panic!("test_read_string_before failed: {}", e)); + .unwrap_or_else(|e| panic!("test_read_string_before failed: {}", e)); } #[test] @@ -514,13 +528,16 @@ mod tests { || -> Result<()> { let mut p = spawn("cat", Some(1000)).expect("cannot run cat"); p.send_line("Hi")?; - match p.exp_any(vec![ReadUntil::NBytes(3), ReadUntil::String("Hi".to_string())]) { + match p.exp_any(vec![ + ReadUntil::NBytes(3), + ReadUntil::String("Hi".to_string()), + ]) { Ok(s) => assert_eq!(("".to_string(), "Hi\r".to_string()), s), Err(e) => assert!(false, format!("got error: {}", e)), } Ok(()) }() - .unwrap_or_else(|e| panic!("test_expect_any failed: {}", e)); + .unwrap_or_else(|e| panic!("test_expect_any failed: {}", e)); } #[test] @@ -539,7 +556,8 @@ mod tests { let mut p = spawn_bash(Some(1000))?; p.execute("cat <(echo ready) -", "ready")?; Ok(()) - }().unwrap_or_else(|e| panic!("test_kill_timeout failed: {}", e)); + }() + .unwrap_or_else(|e| panic!("test_kill_timeout failed: {}", e)); // p is dropped here and kill is sent immediately to bash // Since that is not enough to make bash exit, a kill -9 is sent within 1s (timeout) } @@ -554,7 +572,7 @@ mod tests { assert_eq!("/tmp\r\n", p.wait_for_prompt()?); Ok(()) }() - .unwrap_or_else(|e| panic!("test_bash failed: {}", e)); + .unwrap_or_else(|e| panic!("test_bash failed: {}", e)); } #[test] @@ -572,7 +590,7 @@ mod tests { p.send_control('c')?; Ok(()) }() - .unwrap_or_else(|e| panic!("test_bash_control_chars failed: {}", e)); + .unwrap_or_else(|e| panic!("test_bash_control_chars failed: {}", e)); } #[test]