rust-cli / rexpect

.github/workflows/ci.yml
https://docs.rs/rexpect
MIT License
328 stars 56 forks source link

Generalizes PtySession to work with any Stream type #16

Closed thomasantony closed 4 years ago

thomasantony commented 4 years ago

I wanted to use rexpect with a REPL that is presented over a raw TcpStream (and possibly a serial link in the future). The changes I have made creates a generic StreamSession struct that can take something that implements Read and something that implements Write instead of being forced to use a command-line program.

I have also updated PtySession to use this. One drawback is that "exp()" no longer gives extra context on error. I am open to suggestions. Thanks!

philippkeller commented 4 years ago

I'm currently sick in bed with stomach pains, so I'll look later into this. I didn't quite understand your use case. I don't understand "repl that is presented over a raw TcpStream". Can you explain this?

thomasantony commented 4 years ago

I have been using this to interact with a device that presents a console over a serial port. We send commands and read responses/prompts from the device over this interface. In our particular use case, we have a separate TCP bridge program that passes through data from a TCP port to the serial port and back. We connect to it using a TCPStream and I use StreamSession to send commands and read responses from the device. We still use the send_line and exp_string methods like your examples, but the data goes over TCP and then a serial port instead of a shell command.

StreamSession would also work with a direct serial port connection. A rust library like serialport implements the Read and Write traits and StreamSession could be used with that.

Basically, StreamSession makes it so you can use rexpect with any system that implements the Read and Write traits regardless of the underlying implementation.

Here is an example:

use rexpect::spawn_stream;
use std::net::TcpStream;

fn main() -> Result<(), Box<dyn Error>>
{
    let tcp = TcpStream::connect("127.0.0.1:1234")?;
    let tcp_w = tcp.try_clone()?;
    let mut session = spawn_stream(tcp, tcp_w, Some(2000));
    loop {
        // This loop waits until we get a command prompt from the device
        session.send_line(" ")?; // get command prompt
        match session.exp_string(">") {
            Ok(_) => break, // Got prompt! break the loop
            Err(_) => println!("Device is not responding, trying again."), // Keep trying
        };
    }
    // Do other stuff with session
    Ok(())
}
philippkeller commented 4 years ago

I just checked about the "less context on error", and the loss of information is minimal. When giving a wrong argument to ftp. Before PR:

thread 'main' panicked at 'ftp job failed with EOF (End of File): Expected Regex: "Name \(.*\):" but got EOF after reading "ftp: invalid option -- 'l'
Try 'ftp --help' or 'ftp --usage' for more information.
", process terminated with "Exited(Pid(24427), 64)"', examples/ftp.rs:24:33

after PR:

thread 'main' panicked at 'ftp job failed with EOF (End of File): Expected Regex: "Name \(.*\):" but got EOF after reading "ftp: invalid option -- 'l'
Try 'ftp --help' or 'ftp --usage' for more information.
", process terminated with "unknown"', examples/ftp.rs:24:33

=> seems ok to me, as pid and exit code are usually not crucial for debugging

philippkeller commented 4 years ago

thanks @thomasantony for this pull PR. Elegantly solved and now allows also sessions over TCP, I'll add an example to master branch (didn't see any way I can add a file in your PR, I only could edit the changes…)