blob: c265d865139b60231241e65143ee8937a9f5f77e [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncReadExt};
use tokio_util::sync::CancellationToken;
const TAIL_BYTES: usize = 65_536;
pub(crate) const DRAIN: Duration = Duration::from_secs(2);
#[derive(Default)]
pub(crate) struct Tail {
bytes: Vec<u8>,
pub truncated: bool,
}
impl Tail {
fn push(&mut self, bytes: &[u8]) {
let overflow = (self.bytes.len() + bytes.len()).saturating_sub(TAIL_BYTES);
if overflow > 0 {
self.truncated = true;
if overflow >= self.bytes.len() {
self.bytes.clear();
self.bytes
.extend_from_slice(&bytes[bytes.len().saturating_sub(TAIL_BYTES)..]);
return;
}
self.bytes.drain(..overflow);
}
self.bytes.extend_from_slice(bytes);
}
pub fn text(&mut self) -> String {
let mut start = 0;
// A byte tail can begin inside a valid UTF-8 code point.
if self.truncated {
while start < self.bytes.len() && self.bytes[start] & 0xc0 == 0x80 {
start += 1;
}
}
let text = String::from_utf8_lossy(&self.bytes[start..]);
// Invalid source bytes can expand to replacement characters.
let mut start = text.len().saturating_sub(TAIL_BYTES);
while !text.is_char_boundary(start) {
start += 1;
}
self.truncated |= start > 0;
text[start..].to_owned()
}
}
pub(crate) async fn capture(
mut pipe: impl AsyncRead + Unpin,
exited: CancellationToken,
observer: Option<(
tokio::sync::mpsc::Sender<crate::PipeEvent>,
maka_agent_facts::shell_run::PipeStream,
)>,
) -> Tail {
let mut tail = Tail::default();
let deadline = async {
exited.cancelled().await;
tokio::time::sleep(DRAIN).await
};
tokio::pin!(deadline);
let mut buffer = [0; 8192];
loop {
tokio::select! {
biased;
_ = &mut deadline => { tail.truncated = true; break; }
read = pipe.read(&mut buffer) => match read {
Ok(0) => break,
Ok(n) => {
tail.push(&buffer[..n]);
if let Some((observer, stream)) = &observer {
// The bounded observer may apply backpressure, but root
// exit still bounds drain even if its consumer stops.
let event = crate::PipeEvent::Output { stream: *stream, bytes: buffer[..n].to_vec() };
tokio::select! {
biased;
_ = &mut deadline => { tail.truncated = true; break; }
_ = observer.send(event) => {}
}
}
},
Err(_) => { tail.truncated = true; break; }
}
}
}
tail
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn replacement_expansion_is_bounded_and_marked_truncated() {
let mut tail = Tail::default();
tail.push(&vec![0xff; TAIL_BYTES]);
let text = tail.text();
assert!(text.len() <= TAIL_BYTES);
assert!(tail.truncated);
assert!(text.chars().all(|character| character == '\u{fffd}'));
}
}