How to handle Stdin hanging when there is no input

Update/Edit

Just found this after posting...

TL;DR

Using Stdin to read input from standard in, how to do I program a check, so that if a user has provided no input to standard in, the program exits? Without it, the program just hangs while Stdin's read methods block on waiting for the end of the non-existent input.

Background

I have written a program that imports data for testing and demo purposes, optionally importing from a file when provided a PATH argument from CLI or from standard input.

Issue

However, if a user runs the program with the standard input arguments but provides nothing from standard input, my program hangs when it reads from Stdin.

Code

    info!("Reading import data from stdin");
    let mut buffer = Vec::new();
    let mut stdin = io::stdin();
    
    // Program hangs here if there is no input from standard input
    stdin.read_to_end(&mut buffer)?;
    if buffer.is_empty() {
        panic!("No input provided from stdin");
    }
    buffer

Failed Attempts

I started out using read_to_string (I'm reading a single JSON array as input), which seemed obviously wrong. But read_to_end also produces the same behavior.

I've tried elaborate solutions such as

use std::io::{self, Read};
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;

struct ReadFromStdin<'a> {
    buffer: &'a mut String,
    stdin: &'a mut io::Stdin,
}

impl<'a> Future for ReadFromStdin<'a> {
    type Output = io::Result<usize>;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let mut buffer = String::new();
        match self.stdin.read_to_string(&mut buffer) {
            Ok(bytes) => {
                self.buffer.push_str(&buffer);
                Poll::Ready(Ok(bytes))
            },
            Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
                cx.waker().wake_by_ref();
                Poll::Pending
            },
            Err(e) => Poll::Ready(Err(e)),
        }
    }
}

async fn read_from_stdin_timeout(buffer: &mut String, stdin: &mut io::Stdin, timeout: Duration) -> io::Result<usize> {
    tokio::time::timeout(timeout, ReadFromStdin { buffer, stdin }).await?
}

let mut buffer = String::new();
let mut stdin = io::stdin();

// Set a timeout of 5 seconds on stdin read
let bytes = read_from_stdin_timeout(&mut buffer, &mut stdin, Duration::from_secs(5)).await?;

// Handle the result of the read operation
if bytes > 0 {
    info!("Loaded {} bytes of import data from stdin", bytes);
    buffer
} else {
    panic!("No input from stdin");
}

And while that compiles (trying with both read_to_end and read_to_string) it produces the same behavior while seeming way too complicated for my purposes.

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)
}

Unfortunately, the Playground seems to close stdin immediately, so I couldn't actually test it – I get back an empty string successfully instead of a timeout error.

2 Likes

Thank you! Great solution!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.