use parking_lot::Mutex;
use std::sync::Arc;
use tokio::io::AsyncReadExt;
use tokio::process::ChildStderr;
use tokio::task::JoinHandle;
const RETENTION: usize = 64 * 1024;
#[derive(Debug, Default)]
pub(super) struct StderrTail {
head: Vec<u8>,
dropped: usize,
}
impl StderrTail {
pub(super) fn retain(&mut self, chunk: &[u8]) {
let room = RETENTION.saturating_sub(self.head.len());
if room == 0 {
self.dropped += chunk.len();
return;
}
match chunk.len().checked_sub(room) {
None => self.head.extend_from_slice(chunk),
Some(overflow) => {
self.head.extend_from_slice(&chunk[..room]);
self.dropped += overflow;
}
}
}
pub(super) fn render(&self) -> Option<String> {
if self.head.is_empty() {
return None;
}
let mut text = String::from_utf8_lossy(&self.head).into_owned();
if self.dropped > 0 {
text.push_str(&format!(
"\n[{} further stderr bytes dropped past the retention cap]",
self.dropped
));
}
let trimmed = text.trim_end();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_owned())
}
}
}
pub(super) type SharedTail = Arc<Mutex<StderrTail>>;
pub(super) fn spawn_drain(pipe: ChildStderr, log: SharedTail) -> JoinHandle<()> {
tokio::task::spawn(async move {
let mut pipe = pipe;
let mut chunk = [0u8; 4096];
loop {
match pipe.read(&mut chunk).await {
Ok(0) => return,
Ok(count) => log.lock().retain(&chunk[..count]),
Err(_) => return,
}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn head_is_kept_and_overflow_counted() {
let mut tail = StderrTail::default();
let big: Vec<u8> = vec![b'x'; RETENTION + 100];
tail.retain(&big);
assert_eq!(tail.head.len(), RETENTION);
assert_eq!(tail.dropped, 100);
let text = tail.render().expect("retained text");
assert!(text.starts_with('x'));
assert!(text.contains("100 further stderr bytes dropped"));
}
#[test]
fn empty_tail_renders_nothing() {
assert!(StderrTail::default().render().is_none());
let whitespace = StderrTail {
head: b" \n".to_vec(),
dropped: 0,
};
assert!(whitespace.render().is_none());
}
#[test]
fn chunks_accumulate_within_the_cap() {
let mut tail = StderrTail::default();
tail.retain(b"Permission denied (publickey).\n");
tail.retain(b"second line\n");
assert_eq!(tail.dropped, 0);
assert_eq!(
tail.render().as_deref(),
Some("Permission denied (publickey).\nsecond line")
);
}
}