use std::io;
use std::path::Path;
use std::process::Stdio;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
use tokio::process::{Child, Command};
pub struct Tailer {
process: Option<Child>,
reader: BufReader<tokio::process::ChildStdout>,
max_chunk_size: u64,
}
impl Drop for Tailer {
fn drop(&mut self) {
if let Some(mut process) = self.process.take() {
let _ = process.start_kill();
tokio::spawn(async move {
let _ = process.wait().await;
});
}
}
}
impl Tailer {
pub async fn next(&mut self) -> io::Result<Option<Vec<u8>>> {
let mut line = Vec::new();
match (&mut self.reader)
.take(self.max_chunk_size)
.read_until(b'\n', &mut line)
.await
{
Ok(0) => Ok(None), Ok(_) => Ok(Some(line)),
Err(e) => Err(e),
}
}
}
#[derive(Debug, Clone, bon::Builder)]
pub struct Options {
#[builder(required, default = None)]
num_lines: Option<usize>,
#[builder(default = 1024 * 8)]
max_chunk_size: u64,
#[builder(default = false)]
follow: bool,
}
impl Options {
pub fn tail(&self, filename: impl AsRef<Path>) -> io::Result<Tailer> {
let mut cmd = Command::new("tail");
if let Some(num_lines) = self.num_lines {
cmd.arg("-n").arg(num_lines.to_string());
}
if self.follow {
cmd.arg("-f");
}
cmd.arg(filename.as_ref());
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::null());
let mut child = cmd.spawn()?;
let stdout = child
.stdout
.take()
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "child stdout handle missing"))?;
let reader = BufReader::new(stdout);
Ok(Tailer {
process: Some(child),
reader,
max_chunk_size: self.max_chunk_size,
})
}
}
pub fn tailf(filename: impl AsRef<Path>, num_lines: Option<usize>) -> io::Result<Tailer> {
Options::builder()
.follow(true)
.num_lines(num_lines)
.build()
.tail(filename)
}