use std::collections::VecDeque;
use std::sync::{Arc, Mutex as StdMutex};
use tokio::io::{AsyncRead, AsyncReadExt as _};
use tokio::time::{Duration, sleep, timeout};
use tokio_util::sync::CancellationToken;
use tracing::{debug, warn};
use crate::error::StderrExcerpt;
use crate::redaction::Redactions;
use crate::util::lock_std;
const HEAD_BYTES: usize = 1024;
const TAIL_BYTES: usize = 3072;
const EOF_GRACE: Duration = Duration::from_millis(100);
const READ_RETRY_DELAY: Duration = Duration::from_secs(1);
const MAX_CONSECUTIVE_READ_ERRORS: u32 = 5;
const READ_CHUNK_BYTES: usize = 4096;
#[derive(Debug)]
struct Ring {
head: Vec<u8>,
tail: VecDeque<u8>,
elided: bool,
}
impl Ring {
fn new() -> Self {
Self {
head: Vec::with_capacity(HEAD_BYTES),
tail: VecDeque::with_capacity(TAIL_BYTES),
elided: false,
}
}
fn push(&mut self, chunk: &[u8]) {
let room = HEAD_BYTES.saturating_sub(self.head.len()).min(chunk.len());
let (head_part, tail_part) = chunk.split_at(room);
self.head.extend_from_slice(head_part);
self.tail.extend(tail_part);
if let Some(overflow) = self.tail.len().checked_sub(TAIL_BYTES)
&& overflow > 0
{
self.tail.drain(..overflow);
self.elided = true;
}
}
fn excerpt(&self, redactions: &Redactions) -> Option<StderrExcerpt> {
let tail = self.tail.iter().copied().collect::<Vec<u8>>();
if self.elided {
StderrExcerpt::elided(&self.head, &tail, redactions)
} else {
StderrExcerpt::complete(&[self.head.as_slice(), &tail].concat(), redactions)
}
}
}
#[derive(Debug)]
pub(super) struct StderrCapture {
ring: Arc<StdMutex<Ring>>,
eof: CancellationToken,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum EofWait {
Grace,
Skip,
}
impl StderrCapture {
pub(super) fn start<R: AsyncRead + Unpin + Send + 'static>(stderr: R) -> Self {
let ring = Arc::new(StdMutex::new(Ring::new()));
let eof = CancellationToken::new();
tokio::spawn(drain(stderr, Arc::clone(&ring), eof.clone()));
Self { ring, eof }
}
pub(super) async fn finish(
&self,
eof_wait: EofWait,
redactions: &Redactions,
) -> Option<StderrExcerpt> {
if eof_wait == EofWait::Grace && timeout(EOF_GRACE, self.eof.cancelled()).await.is_err() {
debug!("stderr still open after the server exited; using a partial snapshot");
}
lock_std(&self.ring).excerpt(redactions)
}
}
async fn drain<R: AsyncRead + Unpin>(
mut stderr: R,
ring: Arc<StdMutex<Ring>>,
eof: CancellationToken,
) {
let _signal_eof = eof.drop_guard();
let mut buffer = [0u8; READ_CHUNK_BYTES];
let mut consecutive_errors = 0_u32;
loop {
match stderr.read(&mut buffer).await {
Ok(0) => break,
Ok(read) => {
consecutive_errors = 0;
lock_std(&ring).push(buffer.get(..read).unwrap_or_default());
}
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(e) => {
consecutive_errors = consecutive_errors.saturating_add(1);
if consecutive_errors >= MAX_CONSECUTIVE_READ_ERRORS {
warn!("giving up on LSP server stderr after repeated read errors: {e}");
break;
}
debug!("LSP server stderr read failed, retrying: {e}");
sleep(READ_RETRY_DELAY).await;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ring_keeps_short_output_complete() {
let mut ring = Ring::new();
ring.push(b"hello ");
ring.push(b"world");
let excerpt = ring.excerpt(&Redactions::default()).unwrap();
assert_eq!(excerpt.head(), "hello world");
assert!(!excerpt.is_elided());
}
#[test]
fn test_ring_bounds_memory_and_keeps_head_and_tail() {
let mut ring = Ring::new();
ring.push(b"HEAD");
ring.push(&vec![b'x'; HEAD_BYTES + TAIL_BYTES + 500]);
ring.push(b"TAIL");
assert_eq!(ring.head.len(), HEAD_BYTES);
assert_eq!(ring.tail.len(), TAIL_BYTES);
let excerpt = ring.excerpt(&Redactions::default()).unwrap();
assert!(excerpt.is_elided());
assert!(excerpt.head().starts_with("HEAD"));
assert!(excerpt.tail().unwrap().ends_with("TAIL"));
}
#[test]
fn test_ring_exactly_filling_both_windows_is_not_elided() {
let mut ring = Ring::new();
ring.push(&vec![b'a'; HEAD_BYTES + TAIL_BYTES]);
assert!(!ring.elided);
}
#[tokio::test]
async fn test_a_dead_drain_releases_the_eof_signal() {
let (_kept_open, reader) = tokio::io::duplex(8);
let eof = CancellationToken::new();
let task = tokio::spawn(drain(
reader,
Arc::new(StdMutex::new(Ring::new())),
eof.clone(),
));
tokio::task::yield_now().await;
task.abort();
timeout(std::time::Duration::from_secs(5), eof.cancelled())
.await
.expect("a dead drain left the eof signal pending");
}
struct ScriptedReader {
script: VecDeque<std::io::Result<Vec<u8>>>,
}
impl AsyncRead for ScriptedReader {
fn poll_read(
mut self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match self.script.pop_front() {
Some(Ok(bytes)) => {
buf.put_slice(&bytes);
std::task::Poll::Ready(Ok(()))
}
Some(Err(e)) => std::task::Poll::Ready(Err(e)),
None => std::task::Poll::Ready(Ok(())),
}
}
}
fn failure() -> std::io::Result<Vec<u8>> {
Err(std::io::Error::other("pipe broke"))
}
async fn run_drain(
script: Vec<std::io::Result<Vec<u8>>>,
) -> (Arc<StdMutex<Ring>>, usize, bool) {
let ring = Arc::new(StdMutex::new(Ring::new()));
let eof = CancellationToken::new();
let mut reader = ScriptedReader {
script: script.into(),
};
drain(&mut reader, Arc::clone(&ring), eof.clone()).await;
(ring, reader.script.len(), eof.is_cancelled())
}
#[tokio::test(start_paused = true)]
async fn test_drain_gives_up_after_consecutive_read_errors() {
let mut script: Vec<_> = (0..MAX_CONSECUTIVE_READ_ERRORS)
.map(|_| failure())
.collect();
script.push(Ok(b"never read".to_vec()));
let (ring, unread, eof) = run_drain(script).await;
assert_eq!(unread, 1, "the drain must stop at the fifth error");
assert!(eof, "end-of-file must still be signalled");
assert_eq!(ring.lock().unwrap().head.len(), 0);
}
#[tokio::test(start_paused = true)]
async fn test_drain_resets_error_counter_after_a_successful_read() {
let failures = MAX_CONSECUTIVE_READ_ERRORS - 1;
let mut script: Vec<_> = (0..failures).map(|_| failure()).collect();
script.push(Ok(b"first ".to_vec()));
script.extend((0..failures).map(|_| failure()));
script.push(Ok(b"second".to_vec()));
let (ring, unread, eof) = run_drain(script).await;
assert_eq!(unread, 0, "the drain must read to end-of-file");
assert!(eof);
let excerpt = ring
.lock()
.unwrap()
.excerpt(&Redactions::default())
.unwrap();
assert_eq!(excerpt.head(), "first second");
}
#[tokio::test(start_paused = true)]
async fn test_drain_retries_interrupted_reads_immediately() {
let interrupted = || Err(std::io::Error::from(std::io::ErrorKind::Interrupted));
let script = (0..MAX_CONSECUTIVE_READ_ERRORS + 2)
.map(|_| interrupted())
.chain([Ok(b"after".to_vec())])
.collect();
let (ring, unread, _eof) = run_drain(script).await;
assert_eq!(unread, 0);
assert!(ring.lock().unwrap().head.starts_with(b"after"));
}
#[test]
fn test_ring_empty_has_no_excerpt() {
assert_eq!(Ring::new().excerpt(&Redactions::default()), None);
}
}