Problem
I'm working with asynchronous code in Rust and trying to handle user input from stdin in a cancel-safe manner. My goal is to be able to detect when a user presses a key (or hits enter) to interrupt a long-running async operation, but I want to ensure that the async operation is cancellable without blocking the executor.
In the following example, I use select! to wait for either the completion of a future (fut) or a user input from stdin. However, the issue I'm running into is that if the user doesn't provide input, stdin seems to block the executor, preventing the future from completing when it otherwise should.
So far I have tried
- Using mspc channels to cancel the task
- Dropping stdin
- Similar variations of spawning blocking/non_blocking tasks
And considered writing the new line from another process
use std::future::Future;
use console::style;
use spinoff::{spinners, Spinner};
use tokio::{io::{self, AsyncBufReadExt, BufReader}, select};
pub async fn interact_with_cancel<R>(prompt: &str, fut: impl Future<Output = R>) -> Option<R> {
let mut spinner = Spinner::new(
spinners::Moon,
format!("{} {}", prompt, style("| hit enter to cancel").dim()),
None
);
let mut stdin = BufReader::new(io::stdin()).lines();
select! {
// next_line is not cleaned up once fut returns
// and instead prevents the function from
// returning until it receives input
_ = stdin.next_line() => {
spinner.clear();
return None;
},
result = fut => {
spinner.clear();
return Some(result)
}
};
}
#[cfg(test)]
mod test {
use std::time::Duration;
use super::*;
#[tokio::test]
async fn test_interact_with_cancel() {
let _ = interact_with_cancel("testing", tokio::time::sleep(Duration::from_secs(2)))
.await;
}
}