1use std::fs::{self, File};
2use std::io::{self, Read, Seek, SeekFrom};
3use std::path::{Path, PathBuf};
4
5pub const DISK_LIMIT_BYTES: u64 = 100 * 1024 * 1024;
6
7#[derive(Debug, Clone, Copy)]
8pub enum StreamKind {
9 Stdout,
10 Stderr,
11}
12
13#[derive(Debug, Clone)]
14pub struct BgBuffer {
15 stdout_path: PathBuf,
16 stderr_path: PathBuf,
17 rotated: bool,
18}
19
20impl BgBuffer {
21 pub fn new(stdout_path: PathBuf, stderr_path: PathBuf) -> Self {
22 Self {
23 stdout_path,
24 stderr_path,
25 rotated: false,
26 }
27 }
28
29 pub fn stdout_path(&self) -> &Path {
30 &self.stdout_path
31 }
32
33 pub fn stderr_path(&self) -> &Path {
34 &self.stderr_path
35 }
36
37 pub fn read_tail(&self, max_bytes: usize) -> (String, bool) {
38 let stdout = read_file_tail(&self.stdout_path, max_bytes);
39 let stderr = read_file_tail(&self.stderr_path, max_bytes);
40 match (stdout, stderr) {
41 (Ok((stdout, stdout_truncated)), Ok((stderr, stderr_truncated))) => {
42 let mut output = Vec::with_capacity(stdout.len().saturating_add(stderr.len()));
43 output.extend_from_slice(&stdout);
44 output.extend_from_slice(&stderr);
45 if output.len() > max_bytes {
46 let keep_from = output.len().saturating_sub(max_bytes);
47 output.drain(..keep_from);
48 }
49 (
50 String::from_utf8_lossy(&output).into_owned(),
51 self.rotated
52 || stdout_truncated
53 || stderr_truncated
54 || output.len() >= max_bytes && (stdout.len() + stderr.len()) > max_bytes,
55 )
56 }
57 (Ok((stdout, stdout_truncated)), Err(_)) => (
58 String::from_utf8_lossy(&stdout).into_owned(),
59 self.rotated || stdout_truncated,
60 ),
61 (Err(_), Ok((stderr, stderr_truncated))) => (
62 String::from_utf8_lossy(&stderr).into_owned(),
63 self.rotated || stderr_truncated,
64 ),
65 (Err(_), Err(_)) => (String::new(), self.rotated),
66 }
67 }
68
69 pub fn read_for_token_count(&self, max_bytes_per_stream: usize) -> TokenCountInput {
70 let stdout = read_file_tail(&self.stdout_path, max_bytes_per_stream);
81 let stderr = read_file_tail(&self.stderr_path, max_bytes_per_stream);
82 match (stdout, stderr) {
83 (Ok((stdout, _)), Ok((stderr, _))) => TokenCountInput::Text(combine_streams(
84 String::from_utf8_lossy(&stdout).as_ref(),
85 String::from_utf8_lossy(&stderr).as_ref(),
86 )),
87 (Ok((stdout, _)), Err(_)) => TokenCountInput::Text(combine_streams(
90 String::from_utf8_lossy(&stdout).as_ref(),
91 "",
92 )),
93 (Err(_), Ok((stderr, _))) => TokenCountInput::Text(combine_streams(
94 "",
95 String::from_utf8_lossy(&stderr).as_ref(),
96 )),
97 (Err(_), Err(_)) => TokenCountInput::Skipped,
98 }
99 }
100
101 pub fn read_stream_tail(&self, stream: StreamKind, max_bytes: usize) -> (String, bool) {
102 let path = match stream {
103 StreamKind::Stdout => &self.stdout_path,
104 StreamKind::Stderr => &self.stderr_path,
105 };
106 match read_file_tail(path, max_bytes) {
107 Ok((bytes, truncated)) => (
108 String::from_utf8_lossy(&bytes).into_owned(),
109 self.rotated || truncated,
110 ),
111 Err(_) => (String::new(), self.rotated),
112 }
113 }
114
115 pub fn output_path(&self) -> Option<PathBuf> {
117 Some(self.stdout_path.clone())
118 }
119
120 pub fn enforce_terminal_cap(&mut self) {
123 if truncate_front(&self.stdout_path, DISK_LIMIT_BYTES).unwrap_or(false) {
124 self.rotated = true;
125 }
126 if truncate_front(&self.stderr_path, DISK_LIMIT_BYTES).unwrap_or(false) {
127 self.rotated = true;
128 }
129 }
130
131 pub fn cleanup(&self) {
132 let _ = fs::remove_file(&self.stdout_path);
133 let _ = fs::remove_file(&self.stderr_path);
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub enum TokenCountInput {
139 Text(String),
140 Skipped,
141}
142
143pub fn combine_streams(stdout: &str, stderr: &str) -> String {
144 match (stdout.is_empty(), stderr.is_empty()) {
145 (true, true) => String::new(),
146 (false, true) => stdout.to_string(),
147 (true, false) => stderr.to_string(),
148 (false, false) => format!("{stdout}\n{stderr}"),
149 }
150}
151
152fn read_file_tail(path: &Path, max_bytes: usize) -> io::Result<(Vec<u8>, bool)> {
153 if max_bytes == 0 {
154 return Ok((
155 Vec::new(),
156 path.metadata()
157 .map(|metadata| metadata.len() > 0)
158 .unwrap_or(false),
159 ));
160 }
161
162 let mut file = File::open(path)?;
163 let len = file.metadata()?.len();
164 let read_len = len.min(max_bytes as u64);
165 if read_len > 0 {
166 file.seek(SeekFrom::End(-(read_len as i64)))?;
167 }
168 let mut bytes = Vec::with_capacity(read_len as usize);
169 file.read_to_end(&mut bytes)?;
170 Ok((bytes, len > max_bytes as u64))
171}
172
173fn truncate_front(path: &Path, retain_bytes: u64) -> io::Result<bool> {
174 let len = match path.metadata() {
175 Ok(metadata) => metadata.len(),
176 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
177 Err(error) => return Err(error),
178 };
179 if len <= retain_bytes {
180 return Ok(false);
181 }
182
183 let mut file = File::open(path)?;
184 file.seek(SeekFrom::End(-(retain_bytes as i64)))?;
185 let mut tail = Vec::with_capacity(retain_bytes as usize);
186 file.read_to_end(&mut tail)?;
187 let tmp = path.with_extension(format!(
188 "{}.tmp",
189 path.extension()
190 .and_then(|extension| extension.to_str())
191 .unwrap_or("out")
192 ));
193 fs::write(&tmp, tail)?;
194 fs::rename(&tmp, path)?;
195 Ok(true)
196}