use std::fs::File;
use std::os::unix::fs::FileExt;
use std::process::Stdio;
pub const STEP_OUTPUT_LIMIT: usize = 8 * 1024 * 1024;
pub struct Capture {
out: File,
err: File,
}
fn anonymous_file() -> std::io::Result<File> {
#[cfg(target_os = "linux")]
{
use std::os::unix::io::FromRawFd;
let descriptor: libc::c_int = unsafe { libc::memfd_create(c"cli-git-output".as_ptr(), libc::MFD_CLOEXEC) };
if descriptor < 0 {
return Err(std::io::Error::last_os_error());
}
return Ok(unsafe { File::from_raw_fd(descriptor) });
}
#[cfg(not(target_os = "linux"))]
{
return Err(std::io::Error::new(std::io::ErrorKind::Unsupported, "anonymous output files need memfd_create, which this system lacks"));
}
}
impl Capture {
pub fn new() -> std::io::Result<Self> {
return Ok(Self { out: anonymous_file()?, err: anonymous_file()? });
}
pub fn stdio(&self) -> std::io::Result<(Stdio, Stdio)> {
return Ok((Stdio::from(self.out.try_clone()?), Stdio::from(self.err.try_clone()?)));
}
pub fn snapshot(&self, limit: usize) -> Captured {
let mut notes: Vec<String> = Vec::new();
let stdout: Vec<u8> = snapshot_of(&self.out, "standard output", limit, &mut notes);
let stderr: Vec<u8> = snapshot_of(&self.err, "standard error", limit, &mut notes);
return Captured { stdout, stderr, notes };
}
pub fn discard(&self) {
let _out: std::io::Result<()> = self.out.set_len(0);
let _err: std::io::Result<()> = self.err.set_len(0);
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Captured {
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
pub notes: Vec<String>,
}
fn snapshot_of(file: &File, name: &str, limit: usize, notes: &mut Vec<String>) -> Vec<u8> {
let length: u64 = match file.metadata() {
Ok(found) => found.len(),
Err(error) => {
notes.push(format!("the {name} of the step could not be measured ({error}), so none of it is shown"));
return Vec::new();
}
};
let wanted: u64 = length.min(limit as u64);
let start: u64 = length - wanted;
if start > 0 {
notes.push(format!("{start} bytes at the start of the {name} of the step are left out; the last {wanted} bytes are shown"));
}
let mut collected: Vec<u8> = Vec::with_capacity(usize::try_from(wanted).unwrap_or(0));
let mut buffer: [u8; 8192] = [0; 8192];
let mut position: u64 = start;
while position < length {
let room: usize = usize::try_from((length - position).min(buffer.len() as u64)).unwrap_or(buffer.len());
match file.read_at(&mut buffer[..room], position) {
Ok(0) => break,
Ok(read) => {
collected.extend_from_slice(&buffer[..read]);
position += read as u64;
}
Err(error) => {
notes.push(format!("the {name} of the step could not be read whole ({error}); {} of {wanted} bytes are shown", collected.len()));
return collected;
}
}
}
if (collected.len() as u64) < wanted {
notes.push(format!("the {name} of the step ended early: {} of {wanted} bytes are shown", collected.len()));
}
return collected;
}
#[cfg(test)]
#[path = "output_capture_tests.rs"]
mod tests;