use std::io;
use std::sync::mpsc as blocking;
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
const THREAD_NAME: &str = "clawless-input";
type ReadLine = Box<dyn FnMut(&mut String) -> io::Result<usize> + Send>;
type Read = io::Result<Option<String>>;
pub(super) struct LineReader {
state: State,
}
enum State {
Idle {
read_line: ReadLine,
},
Started {
requests: blocking::Sender<()>,
reads: UnboundedReceiver<Read>,
open: OpenRead,
},
Stopped,
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
enum OpenRead {
No,
Yes,
}
impl LineReader {
pub(super) fn new(
read_line: impl FnMut(&mut String) -> io::Result<usize> + Send + 'static,
) -> Self {
Self {
state: State::Idle {
read_line: Box::new(read_line),
},
}
}
pub(super) fn stdin() -> Self {
Self::new(|buffer| io::stdin().read_line(buffer))
}
pub(super) async fn next_line(&mut self) -> Read {
self.start()?;
let State::Started {
requests,
reads,
open,
} = &mut self.state
else {
return Err(stopped());
};
match open {
OpenRead::Yes => {}
OpenRead::No => {
requests.send(()).map_err(|_closed| stopped())?;
*open = OpenRead::Yes;
}
}
let Some(read) = reads.recv().await else {
self.state = State::Stopped;
return Err(stopped());
};
*open = OpenRead::No;
read
}
fn start(&mut self) -> io::Result<()> {
let read_line = match std::mem::replace(&mut self.state, State::Stopped) {
State::Idle { read_line } => read_line,
state @ (State::Started { .. } | State::Stopped) => {
self.state = state;
return Ok(());
}
};
let (requests, requested) = blocking::channel();
let (sender, reads) = unbounded_channel();
std::thread::Builder::new()
.name(THREAD_NAME.to_owned())
.spawn(move || serve(read_line, &requested, &sender))?;
self.state = State::Started {
requests,
reads,
open: OpenRead::No,
};
Ok(())
}
}
fn serve(
mut read_line: ReadLine,
requested: &blocking::Receiver<()>,
reads: &UnboundedSender<Read>,
) {
while requested.recv().is_ok() {
let mut line = String::new();
let read = match read_line(&mut line) {
Ok(0) => Ok(None),
Ok(_count) => {
let end = line.trim_end_matches(['\r', '\n']).len();
line.truncate(end);
Ok(Some(line))
}
Err(error) => Err(error),
};
if reads.send(read).is_err() {
return;
}
}
}
fn stopped() -> io::Error {
io::Error::new(
io::ErrorKind::BrokenPipe,
"the reader of the input has stopped",
)
}
#[cfg(test)]
mod tests {
#![allow(clippy::missing_panics_doc)]
use std::collections::VecDeque;
use std::future::Future;
use std::io;
use std::pin::pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc;
use std::task::Poll;
use std::time::Duration;
use super::*;
fn scripted(lines: &[&str]) -> (LineReader, Arc<AtomicUsize>) {
let mut lines = lines
.iter()
.map(|line| (*line).to_owned())
.collect::<VecDeque<_>>();
let reads = Arc::new(AtomicUsize::new(0));
let counter = Arc::clone(&reads);
let reader = LineReader::new(move |buffer| {
counter.fetch_add(1, Ordering::SeqCst);
let line = lines.pop_front().unwrap_or_default();
buffer.push_str(&line);
Ok(line.len())
});
(reader, reads)
}
fn settle() {
std::thread::sleep(Duration::from_millis(50));
}
#[tokio::test]
async fn new_starts_no_read() {
let (_reader, reads) = scripted(&["y\n"]);
settle();
assert_eq!(reads.load(Ordering::SeqCst), 0);
}
#[tokio::test]
async fn next_line_after_an_abandoned_read_returns_the_line_of_that_read() {
let (feed, source) = mpsc::channel::<String>();
let mut reader = LineReader::new(move |buffer| {
let line = source.recv().unwrap_or_default();
buffer.push_str(&line);
Ok(line.len())
});
{
let mut abandoned = pin!(reader.next_line());
std::future::poll_fn(|context| {
Poll::Ready(abandoned.as_mut().poll(context).is_pending())
})
.await;
}
feed.send("late\n".to_owned()).expect("should feed");
let line = reader.next_line().await.expect("should read");
assert_eq!(line, Some("late".to_owned()));
}
#[tokio::test]
async fn next_line_at_the_end_of_the_input_returns_none() {
let (mut reader, _reads) = scripted(&[]);
let line = reader.next_line().await.expect("should read");
assert_eq!(line, None);
}
#[tokio::test]
async fn next_line_reads_no_further_than_the_line_it_returns() {
let (mut reader, reads) = scripted(&["y\n", "n\n"]);
drop(reader.next_line().await);
settle();
assert_eq!(reads.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn next_line_returns_the_lines_in_order() {
let (mut reader, _reads) = scripted(&["y\n", "n\n"]);
drop(reader.next_line().await);
let line = reader.next_line().await.expect("should read");
assert_eq!(line, Some("n".to_owned()));
}
#[tokio::test]
async fn next_line_with_a_failing_input_returns_the_error() {
let mut reader = LineReader::new(|_buffer| Err(io::Error::other("unplugged")));
let error = reader.next_line().await.expect_err("should fail");
assert_eq!(error.to_string(), "unplugged");
}
#[tokio::test]
async fn next_line_with_a_last_line_without_an_end_returns_the_line() {
let (mut reader, _reads) = scripted(&["y"]);
let line = reader.next_line().await.expect("should read");
assert_eq!(line, Some("y".to_owned()));
}
#[tokio::test]
async fn next_line_with_a_windows_line_end_returns_the_line_without_it() {
let (mut reader, _reads) = scripted(&["y\r\n"]);
let line = reader.next_line().await.expect("should read");
assert_eq!(line, Some("y".to_owned()));
}
}