Ashish Singh
Ashish Singh

Reputation: 759

is there a way to accept non blocking command line input in rust?

I am trying to accept multiple command line inputs in rust. The user hits enter to and gets a response on the input. Along with this, I also want to close the client if user hits ctrl+c while entering the response. I am struggling to achieve this in my solution.

I am using tokio::select for this purpose with 2 branches responsible for accepting ctrl+c and user input. Using tokio::signal to capture the ctrl+c keystroke.

use mio::net::SocketAddr;
use tokio::net::TcpListener;
use tokio::signal;

#[tokio::main]
async fn main() {
    let shutdown = signal::ctrl_c();

    tokio::select! {
        _ = shutdown => {
            println!("after shutdown");
        }
        res = accept_input() => {
            println!(".."); // this will never run due to infinite loop in accept_input()
        }
    }
}

pub async fn accept_input() -> std::io::Result<()> {
    loop {
        let mut buffer = String::new();
        let word = std::io::stdin().read_line(&mut buffer)?;
    }
}

Unfortunately, read_line() is a blocking function and hence the shutdown branch listening to ctrl+c doesn't get the thread execution to capture tokio::signal::ctrl_c() signal.

An alternative implementation, where the responsibility of the accept_input() function was to listen on a TcpListener for new incoming socket connections with the help of await keyword, gives the desired results. Since await definition states it Suspend execution until the result of a [Future] is ready. Hence, the shutdown branch in tokio::select gets execution after the first .await on TcpListener.

Here is the code which gives desired result.

pub async fn accept_input() -> std::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:8000").await?;
    loop {
        let (client, _) = listener.accept().await?;
    }
}

I am struggling to come up with a way to achieve the same in case of accepting command line inputs. As I understand, new incoming socket connections can be thought of as new incoming command line message.

Upvotes: 0

Views: 1138

Answers (1)

Alice Ryhl
Alice Ryhl

Reputation: 4239

When you want to read commandline input from the user in an interactive manner, the async facilities in Tokio are not what you are looking for. The tokio::io::stdin method admits this in its documentation:

This handle is best used for non-interactive uses, such as when a file is piped into the application. For technical reasons, stdin is implemented by using an ordinary blocking read on a separate thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang until the user presses enter.

For interactive uses, it is recommended to spawn a thread dedicated to user input and use blocking IO directly in that thread.

To do this, you can use a utility like the one below:

use tokio::sync::mpsc;

struct InteractiveStdin {
    chan: mpsc::Receiver<std::io::Result<String>>,
}

impl InteractiveStdin {
    fn new() -> Self {
        let (send, recv) = mpsc::channel(16);
        std::thread::spawn(move || {
            for line in std::io::stdin().lines() {
                if send.blocking_send(line).is_err() {
                    return;
                }
            }
        });
        InteractiveStdin {
            chan: recv
        }
    }
    
    /// Get the next line from stdin.
    ///
    /// Returns `Ok(None)` if stdin has been closed.
    ///
    /// This method is cancel safe.
    async fn next_line(&mut self) -> std::io::Result<Option<String>> {
        self.chan.recv().await.transpose()
    }
}

By using std::thread::spawn rather than tokio::task::spawn_blocking, the Tokio runtime will not wait for the thread before shutting down. This is important as it may be waiting for user input indefinitely.

Using the above code with tokio::signal looks like this:

use tokio::signal;

#[tokio::main]
async fn main() {
    let shutdown = signal::ctrl_c();
    
    let mut stdin = InteractiveStdin::new();

    tokio::select! {
        _ = shutdown => {
            println!("after shutdown");
        }
        res = stdin.next_line() => {
            println!("got line: {:?}", res);
        }
    }
}

Upvotes: 2

Related Questions