use std::collections::VecDeque;
use std::io::{self, BufRead, BufReader, Read};
use std::sync::{Arc, Condvar, Mutex};
use std::thread::{self, JoinHandle};
type Retained = (VecDeque<String>, usize);
pub struct StderrTail {
lines: Arc<Mutex<Retained>>,
handle: Option<JoinHandle<()>>,
done: Arc<Done>,
}
type Done = (Mutex<bool>, Condvar);
struct DoneGuard(Arc<Done>);
impl Drop for DoneGuard {
fn drop(&mut self) {
*self.0 .0.lock().unwrap_or_else(|e| e.into_inner()) = true;
self.0 .1.notify_all();
}
}
impl StderrTail {
pub fn capture<R: Read + Send + 'static>(
stderr: R,
max_bytes: usize,
map: impl Fn(&str) -> Option<String> + Send + 'static,
on_line: impl Fn(&str) + Send + 'static,
) -> Self {
Self::try_capture(stderr, max_bytes, map, on_line).unwrap_or_else(|_| Self {
lines: Arc::default(),
handle: None,
done: Arc::new((Mutex::new(true), Condvar::new())),
})
}
pub fn try_capture<R: Read + Send + 'static>(
stderr: R,
max_bytes: usize,
map: impl Fn(&str) -> Option<String> + Send + 'static,
on_line: impl Fn(&str) + Send + 'static,
) -> io::Result<Self> {
Self::try_capture_with(
|name, body| thread::Builder::new().name(name).spawn(body),
stderr,
max_bytes,
map,
on_line,
)
}
fn try_capture_with<R: Read + Send + 'static>(
spawn: impl FnOnce(String, Box<dyn FnOnce() + Send + 'static>) -> io::Result<JoinHandle<()>>,
stderr: R,
max_bytes: usize,
map: impl Fn(&str) -> Option<String> + Send + 'static,
on_line: impl Fn(&str) + Send + 'static,
) -> io::Result<Self> {
let lines: Arc<Mutex<Retained>> = Arc::default();
let shared = Arc::clone(&lines);
let done: Arc<Done> = Arc::new((Mutex::new(false), Condvar::new()));
let guard = DoneGuard(Arc::clone(&done));
let handle = spawn(
"rightkit-stderr-tail".into(),
Box::new(move || {
let _guard = guard;
let line_cap = max_bytes.max(1).saturating_mul(4);
let mut reader = BufReader::new(stderr);
let mut raw = Vec::new();
let mut skip_lf = false;
loop {
raw.clear();
let (terminated, stream_ended) =
read_line_bounded(&mut reader, &mut raw, line_cap, &mut skip_lf);
if raw.is_empty() && !terminated {
return; }
let oversized = !terminated && !stream_ended && raw.len() > line_cap;
let (kept, ended) = if oversized {
let ended = discard_rest_of_line(&mut reader, &mut skip_lf);
(
Some(format!("[stderr line over {line_cap} bytes dropped]")),
ended,
)
} else {
let text = String::from_utf8_lossy(&raw);
(map(text.trim_end_matches(['\r', '\n'])), stream_ended)
};
if let Some(kept) = kept {
on_line(&kept);
let mut g = shared.lock().unwrap_or_else(|e| e.into_inner());
retain(&mut g, kept, max_bytes);
}
if ended {
return;
}
}
}),
)?;
Ok(Self {
lines,
handle: Some(handle),
done,
})
}
pub fn plain<R: Read + Send + 'static>(stderr: R, max_bytes: usize) -> Self {
Self::capture(stderr, max_bytes, |l| Some(l.to_string()), |_| {})
}
pub fn snapshot(&self) -> String {
let g = self.lines.lock().unwrap_or_else(|e| e.into_inner());
g.0.iter()
.map(String::as_str)
.collect::<Vec<_>>()
.join("\n")
}
pub fn finish_within(&mut self, timeout: std::time::Duration) -> String {
{
let guard = self.done.0.lock().unwrap_or_else(|e| e.into_inner());
let _ = self
.done
.1
.wait_timeout_while(guard, timeout, |over| !*over)
.unwrap_or_else(|e| e.into_inner());
}
self.snapshot()
}
pub fn finish(&mut self) -> String {
if let Some(h) = self.handle.take() {
let _ = h.join();
}
self.snapshot()
}
}
fn read_line_bounded(
reader: &mut impl BufRead,
raw: &mut Vec<u8>,
line_cap: usize,
skip_lf: &mut bool,
) -> (bool, bool) {
loop {
let buffer = match reader.fill_buf() {
Ok([]) => return (false, true),
Ok(buffer) => buffer,
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
Err(_) => return (false, true),
};
if std::mem::take(skip_lf) && buffer[0] == b'\n' {
reader.consume(1);
continue;
}
let room = line_cap.saturating_add(1).saturating_sub(raw.len());
let scan = &buffer[..buffer.len().min(room)];
match scan.iter().position(|&byte| byte == b'\n' || byte == b'\r') {
Some(index) => {
let carriage = scan[index] == b'\r';
raw.extend_from_slice(&scan[..=index]);
reader.consume(index + 1);
*skip_lf = carriage;
return (true, false);
}
None => {
let used = scan.len();
raw.extend_from_slice(scan);
reader.consume(used);
if raw.len() > line_cap {
return (false, false);
}
}
}
}
}
fn discard_rest_of_line(reader: &mut impl BufRead, skip_lf: &mut bool) -> bool {
loop {
let (used, terminator) = match reader.fill_buf() {
Ok([]) => return true,
Ok(buffer) => match buffer
.iter()
.position(|&byte| byte == b'\n' || byte == b'\r')
{
Some(index) => (index + 1, Some(buffer[index])),
None => (buffer.len(), None),
},
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
Err(_) => return true,
};
reader.consume(used);
match terminator {
Some(b'\r') => {
*skip_lf = true;
return false;
}
Some(_) => return false,
None => {}
}
}
}
fn retain(state: &mut Retained, kept: String, max_bytes: usize) {
let room = max_bytes.saturating_sub(1);
let kept = if kept.len() > room {
let mut start = kept.len() - room;
while !kept.is_char_boundary(start) {
start += 1;
}
kept[start..].to_string()
} else {
kept
};
let cost = kept.len() + 1;
if cost > max_bytes {
return;
}
state.1 += cost;
state.0.push_back(kept);
while state.1 > max_bytes {
match state.0.pop_front() {
Some(old) => state.1 -= old.len() + 1,
None => break,
}
}
}