use std::io::{self, Read};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use crossbeam_channel::{Receiver, RecvTimeoutError, bounded};
use crate::error::AtomwriteError;
const STDIN_POLL_INTERVAL: Duration =
Duration::from_millis(crate::constants::STDIN_POLL_INTERVAL_MS);
const STDIN_CHANNEL_BLOCKS: usize = crate::constants::STDIN_CHANNEL_BLOCKS;
const STDIN_BLOCK_BYTES: usize = crate::constants::STDIN_BLOCK_BYTES;
type Block = io::Result<Vec<u8>>;
enum Source<R> {
Idle(R),
Running(Receiver<Block>),
Ended,
}
pub struct CancellableStdin<R> {
source: Source<R>,
cancel: Arc<AtomicBool>,
current: Vec<u8>,
offset: usize,
}
impl<R: Read + Send + 'static> CancellableStdin<R> {
#[must_use]
pub fn new(inner: R, cancel: Arc<AtomicBool>) -> Self {
Self {
source: Source::Idle(inner),
cancel,
current: Vec::new(),
offset: 0,
}
}
fn start(inner: R) -> io::Result<Receiver<Block>> {
let (tx, rx) = bounded::<Block>(STDIN_CHANNEL_BLOCKS);
std::thread::Builder::new()
.name(crate::constants::THREAD_NAME_STDIN.into())
.spawn(move || {
let mut inner = inner;
let mut block = vec![0u8; STDIN_BLOCK_BYTES];
loop {
let item = match inner.read(&mut block) {
Ok(0) => return,
Ok(n) => Ok(block[..n].to_vec()),
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => Err(e),
};
let failed = item.is_err();
if tx.send(item).is_err() || failed {
return;
}
}
})?;
Ok(rx)
}
fn cancelled() -> io::Error {
io::Error::other(crate::signal::cancelled_error(
crate::signal::current_cancel_reason(),
))
}
fn fill(&mut self) -> io::Result<bool> {
if let Source::Idle(_) = self.source {
let Source::Idle(inner) = std::mem::replace(&mut self.source, Source::Ended) else {
return Ok(false);
};
self.source = Source::Running(Self::start(inner)?);
}
let Source::Running(rx) = &self.source else {
return Ok(false);
};
let rx = rx.clone();
loop {
if self.cancel.load(Ordering::Acquire) {
return Err(Self::cancelled());
}
match rx.recv_timeout(STDIN_POLL_INTERVAL) {
Ok(Ok(block)) => {
self.current = block;
self.offset = 0;
return Ok(true);
}
Ok(Err(e)) => {
self.source = Source::Ended;
return Err(e);
}
Err(RecvTimeoutError::Timeout) => {}
Err(RecvTimeoutError::Disconnected) => {
self.source = Source::Ended;
return Ok(false);
}
}
}
}
}
impl<R: Read + Send + 'static> Read for CancellableStdin<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
if self.cancel.load(Ordering::Acquire) {
return Err(Self::cancelled());
}
if self.offset >= self.current.len() && !self.fill()? {
return Ok(0);
}
let n = buf.len().min(self.current.len() - self.offset);
buf[..n].copy_from_slice(&self.current[self.offset..self.offset + n]);
self.offset += n;
Ok(n)
}
}
#[must_use]
pub fn surface_cancel(err: anyhow::Error) -> anyhow::Error {
let cancelled = err.chain().find_map(|cause| {
let inner = cause.downcast_ref::<io::Error>()?.get_ref()?;
match inner.downcast_ref::<AtomwriteError>()? {
AtomwriteError::Cancelled { reason, exit } => Some((reason.clone(), *exit)),
_ => None,
}
});
match cancelled {
Some((reason, exit)) => AtomwriteError::Cancelled { reason, exit }.into(),
None => err,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn flag(on: bool) -> Arc<AtomicBool> {
Arc::new(AtomicBool::new(on))
}
#[test]
fn passes_bytes_through_and_ends_at_eof() {
let mut r = CancellableStdin::new(&b"hello world"[..], flag(false));
let mut out = String::new();
r.read_to_string(&mut out).expect("read");
assert_eq!(out, "hello world");
}
#[test]
fn empty_input_is_eof() {
let mut r = CancellableStdin::new(io::empty(), flag(false));
let mut out = Vec::new();
assert_eq!(r.read_to_end(&mut out).expect("read"), 0);
}
#[test]
fn cancelled_flag_yields_typed_error() {
let mut r = CancellableStdin::new(&b"data"[..], flag(true));
let err = r.read(&mut [0u8; 8]).expect_err("must be cancelled");
let surfaced = surface_cancel(anyhow::Error::new(err).context("failed to read stdin"));
assert!(matches!(
surfaced.downcast_ref::<AtomwriteError>(),
Some(AtomwriteError::Cancelled { .. })
));
}
#[test]
fn other_errors_are_left_alone() {
let surfaced = surface_cancel(anyhow::anyhow!("boom"));
assert_eq!(surfaced.to_string(), "boom");
}
}