expectrl: Need non-blocking reads for interactive()
I can’t use expect() directly to read lines for line processing because I need non-blocking reads. Using non-blocking reads allows more control while waiting for data to be available. For example, I add an animated cursor if the data takes too long to appear.
Here’s the current code, roughly. It’s loosely based on the expectrl source code using try_read here.
The following code shows a number of useful expectrl features. Specifically, it:
- Catches
\rand\nas the command writes lines, allowing them to be rewritten. This is useful because some commands emit just\rto overwrite the previous line in a terminal rather than scrolling. - Allows lines to be read individually and processed.
- Allows timeouts to be checked.
- Allows other processing in the loop, such as starting an animated cursor or waiting message if the command is taking too long to emit any output.
use expectrl::{Session, Signal, WaitStatus};
use std::io::{stdout, Write};
use std::{process::Command, thread, time::Duration, time::SystemTime};
fn main() {
let mut cmd = Command::new("ping");
cmd.args(&["www.time.org"]);
let mut session = Session::spawn(cmd).expect("failed to execute process");
thread::sleep(Duration::from_millis(300)); // Give the process a moment to start.
let mut buf = vec![0; 512]; // Line buffer.
let mut b = [0; 1]; // Read buffer.
let sys_time = SystemTime::now();
loop {
match session.try_read(&mut b) {
Ok(0) => {
println!("==> EoF");
break; // EoF
}
Ok(_) => {
buf.push(b[0]);
if b[0] == 10 || b[0] == 13 {
stdout().write_all(&buf).unwrap();
stdout().flush().unwrap();
// (1) Further process the line as needed.
// ...
buf.clear();
}
}
Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {}
Err(err) => {
println!("err {}", err);
break;
}
}
// Check for timeout.
// Start/update animated cursor if there's no output
}
match session.wait() {
Ok(WaitStatus::Exited(pid, status)) => {
println!("child process {} exited with: {:?}", pid, status);
}
// Signaled() is expected if the process is terminated.
Ok(WaitStatus::Signaled(_, s, _)) => {
println!("child process terminated with signal: {}", s);
}
Err(e) => {
println!("error waiting for process: {:?}", e);
}
s => {
println!("unhandled WaitStatus: {:?}", s);
}
}
}
This code works because Session provides a try_read() that doesn’t block. Currently,expect() already provides many of these features, including timeouts and the Any() method to detect specific line endings. But expect() blocks. With expect() blocking, I would have to multithread cursor animation and synchronize terminal writes. Doable, but it adds complexity that the try_read() approach avoids.
The problem:
How to allow interactive typing into the terminal, with the line processing as above? There’s interactive()but it can’t be used in this context because it blocks.
Possible solutions:
- Could there be a version of
interactive()that emits lines for processing? - Could there be a version of
interactive()that accepts a passed-in function for line processing or other work? (eg, at (1) above). - Could there be a
try_interactive()allows interactivity, but checks for data written to stdout without blocking?
About this issue
- Original URL
- State: closed
- Created 3 years ago
- Comments: 59 (39 by maintainers)
I’m seeing some differences with the new macro and the original code I had that used
try_read(). I’m trying to create individual issue reproductions now.Meanwhile, here’s a program that demonstrates many of the kinds of outputs that I think
expectrlshould handle. Canexpectrlprocess each line (meaning apply functions like watching for keywords), and write each line to the screen, so that it appears the same as when the program is run directly? Note that this program includes user input in the middle, which ideally would just be entered by the user without having to also enter/exit a special interactive mode. Wdyt? Is this a good test for expectrl? There are likely other tests that could be added, such as multiline cursor movement and more ANSI codes.Heh. Was coding… 😃 I wanted to create some demo code that showed a couple of things:
on_output()handlers.on_output()handlers programmatically from a mapping of keywords to functions. This is necessary because in some cases we don’t know how many keywords we’ll need to recognize until we read a config or obtain it from the user.Here’s what I came up with. Notes:
state. Eg: thedcvariable.The reason was that the map ended up being mutably borrowed twice, once when passed into the state, next when use in the for loop. So that failed to compile. What worked was to pull the keys out separately and iterate over those. Note that the
forloop was actually over closures, not simple functions, so slightly more wordy, but it worked. If you know of a simpler way, please share it!BTW, note that the
on_output()closures could reuse the Found value given in the closure parameters, but that would involve more processing of them to utf8 decode them, slowing things down, and shouldn’t be necessary since the mapping of keyword to function was given in the map.I think the additions you made for this bug greatly increase the value of
expectrlbecause it means that it can be used in two ways: 1) the original way where the coder knows what to expect from given commands andexpectrlwill confirm them, and now 2) a new way where the coder doesn’t know exactly the commands or their order, such as if they’re given by the user or a user-written config file, but can watch the pty for keywords.I appreciate your help on this issue and enjoyed working with you on it. Thanks, Maxim!
Well, what’s interesting is that it only consumes that input that partially matches the the target string.
So to reproduce, use the example program I just added above, then
inputfunction (ans = input("Continue [y/n]:")) is missing the matched part:“You said: wq1q12q”
Oh, I was just wondering if
options.input.read()should be calledoptions.input.try_read()to match elsewhere wheretry_indicated non-blocking.Yes, that’s what I was thinking. Hmmm. I’m not sure about the options for handling buffers. I assume most pty output we’re watching is line-oriented, so buffering by lines would make sense, if possible. Ie, calling the callback function would happen on “\r”/“\n”. But I’m not sure about the efficiency and speed of how that would be done. As long as we don’t copy. 😃
Re, @dmgolembiowski’s comment, the suggestion would work for the situation where all of the input from the command was read, so that wouldn’t work for the use case where we’re watching, printing, and reacting to output lines as they are generated by the command being executed. In another comment, the suggestion was to use Python’s pty library, which would be simplifying so might be a workaround, but would incur all of the overhead of adding a Python layer. So perhaps slightly different conversations… 😃
@dmgolembiowski do you mean this reading from stdin? https://github.com/ProgrammingRust/async-chat/blob/abbd763289a1cf367b1c2183e7d432de83b1c650/src/bin/client.rs#L13-L14
Indeed If you do.
*I tend to think such a thing is provided in
expectrlas well.**the same must be possible with
asyncfeature.Alternatively, it may be healthier to directly interface with Python’s own pty module, except instead of using
subprocess.PIPEfor yourstdin,stdout, andstderrarguments you instead use something that has the same behavior assocket.socketand acquire its file descriptors from Rust to listen on and forward appropriately.@GaryBoone maybe you’ll find something useful here https://github.com/ProgrammingRust/async-chat/blob/master/src/bin/client.rs
Yes, please use the script as you like to create tests and demos in
expectrl.Re how
expectrlhandles ansi-sequences, it should read them into a buffer that can be repeated to stdout and work. For example, when I run the demo script by itself, I see the lines like[3/10]all print on the same line. I see colored output.On my screen, it looks like this:
So
expectrlshould be able to read the lines, process them if desired, then write them to stdout and the result should appear identical to directly running the script.is great for converting the data to strings for processing.
from_utf8_lossy()helpfully drops the ANSI codes. But how to print the line, including the ANSI codes? It should something like:… which I know works, except for the interactive parts of
terminal_example.py.expectrldoes read ANSI sequences just fine!We really have two cases: blocking and not blocking. In the above examples, you showed several
session.expect()examples. I’m working withcheck!()and previously,try_read()because I need do additional processing inside the read loop. So this bug is about the non-blocking case.Also, note that in real-world examples, we don’t always know if a program being run under
expectrlwill request a password or other input. Therefore, we need the above to work with interactive. Also, we don’t know how long a program will run or even whether it will exit, such as if it’s a server. These reasons are why my test includes all of these examples in one file. Is it possible to handle interactive input with this non-blocking case?Another reason to include idle processing: Without it, the system can only react as data is written to stdout. Except for a terminating timeout or adding additional threading, there’s no mechanism to check progress or react to delayed output.
Yes. Ideally, to me there would be no distinction between interactive and non-interactive modes. Ideally, user code would be able to watch for specific strings in the terminal whether the command emitted it, the user typed it, or it was an unpredictable output in a program caused by something the user typed.
+1 to the
interact!macro idea. It’s very concise and readable.Would it possible to have some kind of input for ‘idle’ when there’s no output to the pty?
+1 to the non-blocking
expectas well. It would allow the most flexibility. It would need to handle interactivity though…