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, PartialEq, Eq)]
14pub struct BoundedRead {
15 pub text: String,
16 pub truncated: bool,
17 pub total_bytes: u64,
18}
19
20#[derive(Debug, Clone)]
21pub enum BgBuffer {
22 Pipes {
23 stdout_path: PathBuf,
24 stderr_path: PathBuf,
25 },
26 Pty {
27 combined_path: PathBuf,
28 },
29}
30
31impl BgBuffer {
32 pub fn new(stdout_path: PathBuf, stderr_path: PathBuf) -> Self {
33 Self::Pipes {
34 stdout_path,
35 stderr_path,
36 }
37 }
38
39 pub fn pty(combined_path: PathBuf) -> Self {
40 Self::Pty { combined_path }
41 }
42
43 pub fn stdout_path(&self) -> Option<&Path> {
44 match self {
45 Self::Pipes { stdout_path, .. } => Some(stdout_path),
46 Self::Pty { .. } => None,
47 }
48 }
49
50 pub fn stderr_path(&self) -> Option<&Path> {
51 match self {
52 Self::Pipes { stderr_path, .. } => Some(stderr_path),
53 Self::Pty { .. } => None,
54 }
55 }
56
57 pub fn combined_path(&self) -> Option<&Path> {
58 match self {
59 Self::Pipes { .. } => None,
60 Self::Pty { combined_path } => Some(combined_path),
61 }
62 }
63
64 pub fn read_tail(&self, max_bytes: usize) -> (String, bool) {
65 match self {
66 Self::Pipes {
67 stdout_path,
68 stderr_path,
69 } => {
70 let stdout = read_file_tail(stdout_path, max_bytes);
71 let stderr = read_file_tail(stderr_path, max_bytes);
72 match (stdout, stderr) {
73 (Ok((stdout, stdout_truncated)), Ok((stderr, stderr_truncated))) => {
74 let mut output =
75 Vec::with_capacity(stdout.len().saturating_add(stderr.len()));
76 output.extend_from_slice(&stdout);
77 output.extend_from_slice(&stderr);
78 let was_over_cap = output.len() > max_bytes;
79 if was_over_cap {
80 let keep_from = output.len().saturating_sub(max_bytes);
81 output.drain(..keep_from);
82 }
83 (
84 String::from_utf8_lossy(&output).into_owned(),
85 stdout_truncated || stderr_truncated || was_over_cap,
86 )
87 }
88 (Ok((stdout, stdout_truncated)), Err(_)) => (
89 String::from_utf8_lossy(&stdout).into_owned(),
90 stdout_truncated,
91 ),
92 (Err(_), Ok((stderr, stderr_truncated))) => (
93 String::from_utf8_lossy(&stderr).into_owned(),
94 stderr_truncated,
95 ),
96 (Err(_), Err(_)) => (String::new(), false),
97 }
98 }
99 Self::Pty { combined_path } => match read_file_tail(combined_path, max_bytes) {
100 Ok((bytes, truncated)) => (String::from_utf8_lossy(&bytes).into_owned(), truncated),
101 Err(_) => (String::new(), false),
102 },
103 }
104 }
105
106 pub fn read_combined_head_tail(
107 &self,
108 max_bytes: usize,
109 head_bytes: usize,
110 tail_bytes: usize,
111 ) -> BoundedRead {
112 match self {
113 Self::Pipes {
114 stdout_path,
115 stderr_path,
116 } => {
117 read_two_file_head_tail(stdout_path, stderr_path, max_bytes, head_bytes, tail_bytes)
118 }
119 Self::Pty { combined_path } => {
120 read_single_file_head_tail(combined_path, max_bytes, head_bytes, tail_bytes)
121 .unwrap_or_else(|_| BoundedRead {
122 text: String::new(),
123 truncated: false,
124 total_bytes: 0,
125 })
126 }
127 }
128 }
129
130 pub fn read_stream_bounded(&self, stream: StreamKind, max_bytes: usize) -> BoundedRead {
131 let path = match (self, stream) {
132 (Self::Pipes { stdout_path, .. }, StreamKind::Stdout) => Some(stdout_path),
133 (Self::Pipes { stderr_path, .. }, StreamKind::Stderr) => Some(stderr_path),
134 (Self::Pty { combined_path }, _) => Some(combined_path),
135 };
136 path.and_then(|path| read_file_bounded(path, max_bytes).ok())
137 .unwrap_or_else(|| BoundedRead {
138 text: String::new(),
139 truncated: false,
140 total_bytes: 0,
141 })
142 }
143
144 pub fn stream_len(&self, stream: StreamKind) -> u64 {
145 let path = match (self, stream) {
146 (Self::Pipes { stdout_path, .. }, StreamKind::Stdout) => Some(stdout_path),
147 (Self::Pipes { stderr_path, .. }, StreamKind::Stderr) => Some(stderr_path),
148 (Self::Pty { combined_path }, _) => Some(combined_path),
149 };
150 path.and_then(|path| path.metadata().ok())
151 .map(|metadata| metadata.len())
152 .unwrap_or(0)
153 }
154
155 pub fn read_for_token_count(&self, max_bytes_per_stream: usize) -> TokenCountInput {
156 match self {
157 Self::Pipes {
158 stdout_path,
159 stderr_path,
160 } => {
161 let stdout = read_file_tail(stdout_path, max_bytes_per_stream);
164 let stderr = read_file_tail(stderr_path, max_bytes_per_stream);
165 match (stdout, stderr) {
166 (Ok((stdout, _)), Ok((stderr, _))) => TokenCountInput::Text(combine_streams(
167 String::from_utf8_lossy(&stdout).as_ref(),
168 String::from_utf8_lossy(&stderr).as_ref(),
169 )),
170 (Ok((stdout, _)), Err(_)) => TokenCountInput::Text(combine_streams(
171 String::from_utf8_lossy(&stdout).as_ref(),
172 "",
173 )),
174 (Err(_), Ok((stderr, _))) => TokenCountInput::Text(combine_streams(
175 "",
176 String::from_utf8_lossy(&stderr).as_ref(),
177 )),
178 (Err(_), Err(_)) => TokenCountInput::Skipped,
179 }
180 }
181 Self::Pty { .. } => TokenCountInput::Skipped,
185 }
186 }
187
188 pub fn read_stream_tail(&self, stream: StreamKind, max_bytes: usize) -> (String, bool) {
189 let path = match (self, stream) {
190 (Self::Pipes { stdout_path, .. }, StreamKind::Stdout) => Some(stdout_path),
191 (Self::Pipes { stderr_path, .. }, StreamKind::Stderr) => Some(stderr_path),
192 (Self::Pty { combined_path }, _) => Some(combined_path),
193 };
194 match path.and_then(|path| read_file_tail(path, max_bytes).ok()) {
195 Some((bytes, truncated)) => (String::from_utf8_lossy(&bytes).into_owned(), truncated),
196 None => (String::new(), false),
197 }
198 }
199
200 pub fn output_path(&self) -> Option<PathBuf> {
202 match self {
203 Self::Pipes { stdout_path, .. } => Some(stdout_path.clone()),
204 Self::Pty { combined_path } => Some(combined_path.clone()),
205 }
206 }
207
208 pub fn enforce_terminal_cap(&mut self) {
209 match self {
210 Self::Pipes {
211 stdout_path,
212 stderr_path,
213 } => {
214 let _ = truncate_front(stdout_path, DISK_LIMIT_BYTES);
215 let _ = truncate_front(stderr_path, DISK_LIMIT_BYTES);
216 }
217 Self::Pty { combined_path } => {
218 let _ = truncate_front(combined_path, DISK_LIMIT_BYTES);
219 }
220 }
221 }
222
223 pub fn cleanup(&self) {
224 match self {
225 Self::Pipes {
226 stdout_path,
227 stderr_path,
228 } => {
229 let _ = fs::remove_file(stdout_path);
230 let _ = fs::remove_file(stderr_path);
231 }
232 Self::Pty { combined_path } => {
233 let _ = fs::remove_file(combined_path);
234 }
235 }
236 }
237}
238
239#[derive(Debug, Clone, PartialEq, Eq)]
240pub enum TokenCountInput {
241 Text(String),
242 Skipped,
243}
244
245pub fn combine_streams(stdout: &str, stderr: &str) -> String {
246 match (stdout.is_empty(), stderr.is_empty()) {
247 (true, true) => String::new(),
248 (false, true) => stdout.to_string(),
249 (true, false) => stderr.to_string(),
250 (false, false) => format!("{stdout}\n{stderr}"),
251 }
252}
253
254pub(crate) fn read_file_tail(path: &Path, max_bytes: usize) -> io::Result<(Vec<u8>, bool)> {
255 if max_bytes == 0 {
256 return Ok((
257 Vec::new(),
258 path.metadata()
259 .map(|metadata| metadata.len() > 0)
260 .unwrap_or(false),
261 ));
262 }
263
264 let mut file = File::open(path)?;
265 let len = file.metadata()?.len();
266 let read_len = len.min(max_bytes as u64);
267 if read_len > 0 {
268 file.seek(SeekFrom::End(-(read_len as i64)))?;
269 }
270 let mut bytes = Vec::with_capacity(read_len as usize);
271 file.read_to_end(&mut bytes)?;
272 Ok((bytes, len > max_bytes as u64))
273}
274
275fn read_file_bounded(path: &Path, max_bytes: usize) -> io::Result<BoundedRead> {
276 let metadata = path.metadata()?;
277 let total_bytes = metadata.len();
278 if total_bytes > max_bytes as u64 {
279 return Ok(BoundedRead {
280 text: String::new(),
281 truncated: true,
282 total_bytes,
283 });
284 }
285 let bytes = fs::read(path)?;
286 Ok(BoundedRead {
287 text: String::from_utf8_lossy(&bytes).into_owned(),
288 truncated: false,
289 total_bytes,
290 })
291}
292
293fn read_single_file_head_tail(
294 path: &Path,
295 max_bytes: usize,
296 head_bytes: usize,
297 tail_bytes: usize,
298) -> io::Result<BoundedRead> {
299 let total_bytes = path.metadata()?.len();
300 if total_bytes <= max_bytes as u64 {
301 let bytes = fs::read(path)?;
302 return Ok(BoundedRead {
303 text: String::from_utf8_lossy(&bytes).into_owned(),
304 truncated: false,
305 total_bytes,
306 });
307 }
308
309 let head_len = head_bytes.min(max_bytes) as u64;
310 let tail_len = tail_bytes.min(max_bytes.saturating_sub(head_len as usize)) as u64;
311 let head = read_file_range(path, 0, head_len)?;
312 let tail_start = total_bytes.saturating_sub(tail_len);
313 let tail = read_file_range(path, tail_start, tail_len)?;
314 Ok(BoundedRead {
315 text: join_head_tail_bytes(head, tail, total_bytes.saturating_sub(head_len + tail_len)),
316 truncated: true,
317 total_bytes,
318 })
319}
320
321fn read_two_file_head_tail(
322 first: &Path,
323 second: &Path,
324 max_bytes: usize,
325 head_bytes: usize,
326 tail_bytes: usize,
327) -> BoundedRead {
328 let first_len = first.metadata().map(|metadata| metadata.len()).unwrap_or(0);
329 let second_len = second
330 .metadata()
331 .map(|metadata| metadata.len())
332 .unwrap_or(0);
333 let total_bytes = first_len.saturating_add(second_len);
334
335 if total_bytes <= max_bytes as u64 {
336 let mut bytes = Vec::with_capacity(total_bytes as usize);
337 if let Ok(first_bytes) = fs::read(first) {
338 bytes.extend_from_slice(&first_bytes);
339 }
340 if let Ok(second_bytes) = fs::read(second) {
341 bytes.extend_from_slice(&second_bytes);
342 }
343 return BoundedRead {
344 text: String::from_utf8_lossy(&bytes).into_owned(),
345 truncated: false,
346 total_bytes,
347 };
348 }
349
350 let head_len = head_bytes.min(max_bytes) as u64;
351 let tail_len = tail_bytes.min(max_bytes.saturating_sub(head_len as usize)) as u64;
352 let head = read_virtual_range(first, first_len, second, 0, head_len).unwrap_or_default();
353 let tail_start = total_bytes.saturating_sub(tail_len);
354 let tail =
355 read_virtual_range(first, first_len, second, tail_start, tail_len).unwrap_or_default();
356 BoundedRead {
357 text: join_head_tail_bytes(head, tail, total_bytes.saturating_sub(head_len + tail_len)),
358 truncated: true,
359 total_bytes,
360 }
361}
362
363fn read_virtual_range(
364 first: &Path,
365 first_len: u64,
366 second: &Path,
367 start: u64,
368 len: u64,
369) -> io::Result<Vec<u8>> {
370 let mut output = Vec::with_capacity(len as usize);
371 if len == 0 {
372 return Ok(output);
373 }
374 let end = start.saturating_add(len);
375
376 if start < first_len {
377 let first_read_start = start;
378 let first_read_end = end.min(first_len);
379 output.extend(read_file_range(
380 first,
381 first_read_start,
382 first_read_end.saturating_sub(first_read_start),
383 )?);
384 }
385
386 if end > first_len {
387 let second_read_start = start.saturating_sub(first_len);
388 let second_read_end = end.saturating_sub(first_len);
389 output.extend(read_file_range(
390 second,
391 second_read_start,
392 second_read_end.saturating_sub(second_read_start),
393 )?);
394 }
395
396 Ok(output)
397}
398
399fn read_file_range(path: &Path, start: u64, len: u64) -> io::Result<Vec<u8>> {
400 if len == 0 {
401 return Ok(Vec::new());
402 }
403 let mut file = File::open(path)?;
404 file.seek(SeekFrom::Start(start))?;
405 let mut limited = file.take(len);
406 let mut bytes = Vec::with_capacity(len as usize);
407 limited.read_to_end(&mut bytes)?;
408 Ok(bytes)
409}
410
411fn join_head_tail_bytes(head: Vec<u8>, tail: Vec<u8>, truncated_bytes: u64) -> String {
412 let mut output = String::from_utf8_lossy(&head).into_owned();
413 if !output.ends_with('\n') {
414 output.push('\n');
415 }
416 output.push_str("...<truncated ");
417 output.push_str(&truncated_bytes.to_string());
418 output.push_str(" bytes>...\n");
419 output.push_str(&String::from_utf8_lossy(&tail));
420 output
421}
422
423fn truncate_front(path: &Path, retain_bytes: u64) -> io::Result<bool> {
424 let len = match path.metadata() {
425 Ok(metadata) => metadata.len(),
426 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
427 Err(error) => return Err(error),
428 };
429 if len <= retain_bytes {
430 return Ok(false);
431 }
432
433 let mut file = File::open(path)?;
434 file.seek(SeekFrom::End(-(retain_bytes as i64)))?;
435 let mut tail = Vec::with_capacity(retain_bytes as usize);
436 file.read_to_end(&mut tail)?;
437 let tmp = path.with_extension(format!(
438 "{}.tmp",
439 path.extension()
440 .and_then(|extension| extension.to_str())
441 .unwrap_or("out")
442 ));
443 fs::write(&tmp, tail)?;
444 fs::rename(&tmp, path)?;
445 Ok(true)
446}