33

Is there a way to check whether data is available on stdin in Rust, or to do a read that returns immediately with the currently available data?

My goal is to be able to read the input produced for instance by cursor keys in a shell that is setup to return all read data immediately. For instance with an equivalent to: stty -echo -echok -icanon min 1 time 0.

I suppose one solution would be to use ncurses or similar libraries, but I would like to avoid any kind of large dependencies.

So far, I got only blocking input, which is not what I want:

let mut reader = stdin(); let mut s = String::new(); match reader.read_to_string(&mut s) {...} // this blocks :( 

5 Answers 5

21

Converting OP's comment into an answer:

You can spawn a thread and send data over a channel. You can then poll that channel in the main thread using try_recv.

The downside of this simple method, that the spawned thread will be stuck in the blocking read_line call, making it impossible to close it gracefully without user interaction.

use std::io; use std::sync::mpsc; use std::sync::mpsc::Receiver; use std::sync::mpsc::TryRecvError; use std::{thread, time}; fn main() { let stdin_channel = spawn_stdin_channel(); loop { match stdin_channel.try_recv() { Ok(key) => println!("Received: {}", key), Err(TryRecvError::Empty) => println!("Channel empty"), Err(TryRecvError::Disconnected) => panic!("Channel disconnected"), } sleep(1000); } } fn spawn_stdin_channel() -> Receiver<String> { let (tx, rx) = mpsc::channel::<String>(); thread::spawn(move || loop { let mut buffer = String::new(); io::stdin().read_line(&mut buffer).unwrap(); tx.send(buffer).unwrap(); }); rx } fn sleep(millis: u64) { let duration = time::Duration::from_millis(millis); thread::sleep(duration); } 
Sign up to request clarification or add additional context in comments.

2 Comments

If the receiver were to be dropped, calling send will return Result::Err, which will cause a panic if unwrapped and is one way to get rid of the spawned thread. You could also use if let Err(_) = tx.send(buffer) {break;} to gracefully exit the thread.
The trick to solve this is to use a multi_select with a pipe(). select(stdin, pipe) And then when you want to exit the thread, you send some I/O on the pipe and voila - your thread can now be exited cleanly! Homage: Claude code, who came up with the approach (as people had used this as workaround before)
10

Most operating systems default to work with the standard input and output in a blocking way. No wonder then that the Rust library follows in stead.

To read from a blocking stream in a non-blocking way you might create a separate thread, so that the extra thread blocks instead of the main one. Checking whether a blocking file descriptor produced some input is similar: spawn a thread, make it read the data, check whether it produced any data so far.

Here's a piece of code that I use with a similar goal of processing a pipe output interactively and that can hopefully serve as an example. It sends the data over a channel, which supports the try_recv method - allowing you to check whether the data is available or not.

Someone has told me that mio might be used to read from a pipe in a non-blocking way, so you might want to check it out too. I suspect that passing the stdin file descriptor (0) to Receiver::from_raw_fd should just work.

4 Comments

pub fn init() -> Receiver<ControlKeys> { let (tx, rx) = channel::<ControlKeys>(); thread::spawn(move|| { while true { tx.send(read_stdin()); } }); rx } pub fn get(rx: &Receiver<ControlKeys>, old: ControlKeys) -> ControlKeys { match rx.try_recv() { Ok(key) => key, Err(_) => old } } fn read_stdin() -> ControlKeys { let mut reader = stdin(); let mut buf = &mut [0u8; 10]; match reader.read(buf) { Err(why) => panic!(...), Ok(size) => { // read buf return res; } } }
Threads or async/await and channels might be the right solution, perhaps most of the time. If just wanting to do O_NONBLOCK read()s without having to interface directly with libc, I found nonblock to be a useful and good enough crate.
How to asking the thread about received data while it's blocking waiting for potentionally more data?
@GünterZöchbauer a thread might forward the data to the channel, which would have the non-blocking try_recv method
0

You could also potentially look at using ncurses (also on crates.io) which would allow you read in raw mode. There are a few examples in the Github repository which show how to do this.

3 Comments

none of its examples is using non-blocking stdin read
The OP explicitly said they want to avoid a big dependency. I still believe there is value in an answer like this, but I downvoted because you add nothing to the OP. If you would show an example of how to do that this post would be worthy.
The ncurses library is also horribly unsafe, it should honestly be renamed ncurses-sys because that's what it actually is.
0

Unfortunately https://docs.rs/tokio/latest/tokio/io/fn.stdin.html says that:

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.

So we cannot use tokio::io::stdin and need to create our own, so that we can do:

 // Start stdin reader in dedicated thread let mut stdin_reader = StdinReader::start()?; // This uses async IO from the channel Some(input_data) = stdin_reader.receiver().recv() => {...} // Shutdown stdin reader gracefully if let Err(e) = stdin_reader.shutdown().await { tracing::warn!("Failed to shutdown stdin reader: {}", e); } 

Here is the code of the stdin reader as used within https://github.com/LionsAd/term-replay/tree/main:

// src/stdin.rs // Stdin handling module using dedicated thread with proper shutdown signaling use anyhow::Result; use std::os::unix::io::{BorrowedFd, OwnedFd}; use std::thread::JoinHandle; use tokio::sync::mpsc; use tracing; /// Handle for managing the stdin reader thread pub struct StdinReader { handle: JoinHandle<()>, shutdown_write_fd: OwnedFd, receiver: mpsc::UnboundedReceiver<Vec<u8>>, } impl StdinReader { /// Create a new stdin reader that runs in a dedicated thread /// Returns a handle and a receiver for stdin data pub fn start() -> Result<Self> { let (stdin_tx, stdin_rx) = mpsc::unbounded_channel::<Vec<u8>>(); // Create a pipe for signaling the thread to shutdown let (shutdown_read_fd, shutdown_write_fd) = nix::unistd::pipe()?; // Spawn blocking thread for stdin using select() to wait on multiple FDs let handle = std::thread::spawn(move || { use nix::sys::select::{FdSet, select}; use std::os::unix::io::AsRawFd; let stdin_fd = std::io::stdin().as_raw_fd(); let mut buf = [0u8; 1024]; tracing::debug!("Stdin reader thread started"); loop { // Set up file descriptor sets for select() let mut read_fds = FdSet::new(); let stdin_borrowed = unsafe { BorrowedFd::borrow_raw(stdin_fd) }; let shutdown_borrowed = unsafe { BorrowedFd::borrow_raw(shutdown_read_fd.as_raw_fd()) }; read_fds.insert(stdin_borrowed); read_fds.insert(shutdown_borrowed); // Wait for either stdin or shutdown signal match select(None, Some(&mut read_fds), None, None, None) { Ok(_) => { // Check if shutdown was signaled if read_fds.contains(shutdown_borrowed) { tracing::debug!("Stdin thread received shutdown signal via pipe"); break; } // Check if stdin is ready if read_fds.contains(stdin_borrowed) { match nix::unistd::read(stdin_fd, &mut buf) { Ok(0) => { tracing::debug!("Stdin EOF detected"); break; } Ok(n) => { let data = buf[..n].to_vec(); if stdin_tx.send(data).is_err() { tracing::debug!("Stdin channel closed, exiting thread"); break; } } Err(e) => { tracing::error!("Stdin read error: {}", e); break; } } } } Err(e) => { tracing::error!("Select error in stdin thread: {}", e); break; } } } tracing::debug!("Stdin reader thread exiting"); // shutdown_read_fd will be automatically closed when it goes out of scope }); Ok(StdinReader { handle, shutdown_write_fd, receiver: stdin_rx, }) } /// Get the receiver for stdin data pub fn receiver(&mut self) -> &mut mpsc::UnboundedReceiver<Vec<u8>> { &mut self.receiver } /// Shutdown the stdin reader thread gracefully pub async fn shutdown(self) -> Result<()> { tracing::debug!("Shutting down stdin reader"); // Signal the stdin thread to shutdown by writing to the pipe if let Err(e) = nix::unistd::write(&self.shutdown_write_fd, &[1u8]) { tracing::warn!("Failed to signal stdin thread shutdown: {}", e); } // Wait for the stdin thread to exit gracefully with a timeout match tokio::task::spawn_blocking(move || self.handle.join()).await { Ok(Ok(())) => { tracing::debug!("Stdin thread exited gracefully"); Ok(()) } Ok(Err(e)) => { tracing::warn!("Stdin thread panicked: {:?}", e); anyhow::bail!("Stdin thread panicked: {:?}", e); } Err(e) => { tracing::warn!("Failed to join stdin thread: {}", e); anyhow::bail!("Failed to join stdin thread: {}", e); } } } } 

Code has been co-authored by Claude Code.

Comments

-1

I had the same question and got an answer that worked for me here, which I will copy below for convenience. As was pointed out to me, "tokio already comes with impl AsyncRead for Stdin, so you should be able to do something like":

async fn read_from_stdin_timeout(timeout: Duration) -> io::Result<String> { let mut buf = String::new(); tokio::time::timeout(timeout, tokio::io::stdin().read_to_string(&mut buf)).await??; Ok(buf) } 

1 Comment

Quoting tokio's docs, "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.".

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.