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 enum BgBuffer {
15 Pipes {
16 stdout_path: PathBuf,
17 stderr_path: PathBuf,
18 },
19 Pty {
20 combined_path: PathBuf,
21 },
22}
23
24impl BgBuffer {
25 pub fn new(stdout_path: PathBuf, stderr_path: PathBuf) -> Self {
26 Self::Pipes {
27 stdout_path,
28 stderr_path,
29 }
30 }
31
32 pub fn pty(combined_path: PathBuf) -> Self {
33 Self::Pty { combined_path }
34 }
35
36 pub fn stdout_path(&self) -> Option<&Path> {
37 match self {
38 Self::Pipes { stdout_path, .. } => Some(stdout_path),
39 Self::Pty { .. } => None,
40 }
41 }
42
43 pub fn stderr_path(&self) -> Option<&Path> {
44 match self {
45 Self::Pipes { stderr_path, .. } => Some(stderr_path),
46 Self::Pty { .. } => None,
47 }
48 }
49
50 pub fn combined_path(&self) -> Option<&Path> {
51 match self {
52 Self::Pipes { .. } => None,
53 Self::Pty { combined_path } => Some(combined_path),
54 }
55 }
56
57 pub fn read_tail(&self, max_bytes: usize) -> (String, bool) {
58 match self {
59 Self::Pipes {
60 stdout_path,
61 stderr_path,
62 } => {
63 let stdout = read_file_tail(stdout_path, max_bytes);
64 let stderr = read_file_tail(stderr_path, max_bytes);
65 match (stdout, stderr) {
66 (Ok((stdout, stdout_truncated)), Ok((stderr, stderr_truncated))) => {
67 let mut output =
68 Vec::with_capacity(stdout.len().saturating_add(stderr.len()));
69 output.extend_from_slice(&stdout);
70 output.extend_from_slice(&stderr);
71 let was_over_cap = output.len() > max_bytes;
72 if was_over_cap {
73 let keep_from = output.len().saturating_sub(max_bytes);
74 output.drain(..keep_from);
75 }
76 (
77 String::from_utf8_lossy(&output).into_owned(),
78 stdout_truncated || stderr_truncated || was_over_cap,
79 )
80 }
81 (Ok((stdout, stdout_truncated)), Err(_)) => (
82 String::from_utf8_lossy(&stdout).into_owned(),
83 stdout_truncated,
84 ),
85 (Err(_), Ok((stderr, stderr_truncated))) => (
86 String::from_utf8_lossy(&stderr).into_owned(),
87 stderr_truncated,
88 ),
89 (Err(_), Err(_)) => (String::new(), false),
90 }
91 }
92 Self::Pty { combined_path } => match read_file_tail(combined_path, max_bytes) {
93 Ok((bytes, truncated)) => (String::from_utf8_lossy(&bytes).into_owned(), truncated),
94 Err(_) => (String::new(), false),
95 },
96 }
97 }
98
99 pub fn read_for_token_count(&self, max_bytes_per_stream: usize) -> TokenCountInput {
100 match self {
101 Self::Pipes {
102 stdout_path,
103 stderr_path,
104 } => {
105 let stdout = read_file_tail(stdout_path, max_bytes_per_stream);
108 let stderr = read_file_tail(stderr_path, max_bytes_per_stream);
109 match (stdout, stderr) {
110 (Ok((stdout, _)), Ok((stderr, _))) => TokenCountInput::Text(combine_streams(
111 String::from_utf8_lossy(&stdout).as_ref(),
112 String::from_utf8_lossy(&stderr).as_ref(),
113 )),
114 (Ok((stdout, _)), Err(_)) => TokenCountInput::Text(combine_streams(
115 String::from_utf8_lossy(&stdout).as_ref(),
116 "",
117 )),
118 (Err(_), Ok((stderr, _))) => TokenCountInput::Text(combine_streams(
119 "",
120 String::from_utf8_lossy(&stderr).as_ref(),
121 )),
122 (Err(_), Err(_)) => TokenCountInput::Skipped,
123 }
124 }
125 Self::Pty { .. } => TokenCountInput::Skipped,
129 }
130 }
131
132 pub fn read_stream_tail(&self, stream: StreamKind, max_bytes: usize) -> (String, bool) {
133 let path = match (self, stream) {
134 (Self::Pipes { stdout_path, .. }, StreamKind::Stdout) => Some(stdout_path),
135 (Self::Pipes { stderr_path, .. }, StreamKind::Stderr) => Some(stderr_path),
136 (Self::Pty { combined_path }, _) => Some(combined_path),
137 };
138 match path.and_then(|path| read_file_tail(path, max_bytes).ok()) {
139 Some((bytes, truncated)) => (String::from_utf8_lossy(&bytes).into_owned(), truncated),
140 None => (String::new(), false),
141 }
142 }
143
144 pub fn output_path(&self) -> Option<PathBuf> {
146 match self {
147 Self::Pipes { stdout_path, .. } => Some(stdout_path.clone()),
148 Self::Pty { combined_path } => Some(combined_path.clone()),
149 }
150 }
151
152 pub fn enforce_terminal_cap(&mut self) {
153 match self {
154 Self::Pipes {
155 stdout_path,
156 stderr_path,
157 } => {
158 let _ = truncate_front(stdout_path, DISK_LIMIT_BYTES);
159 let _ = truncate_front(stderr_path, DISK_LIMIT_BYTES);
160 }
161 Self::Pty { combined_path } => {
162 let _ = truncate_front(combined_path, DISK_LIMIT_BYTES);
163 }
164 }
165 }
166
167 pub fn cleanup(&self) {
168 match self {
169 Self::Pipes {
170 stdout_path,
171 stderr_path,
172 } => {
173 let _ = fs::remove_file(stdout_path);
174 let _ = fs::remove_file(stderr_path);
175 }
176 Self::Pty { combined_path } => {
177 let _ = fs::remove_file(combined_path);
178 }
179 }
180 }
181}
182
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub enum TokenCountInput {
185 Text(String),
186 Skipped,
187}
188
189pub fn combine_streams(stdout: &str, stderr: &str) -> String {
190 match (stdout.is_empty(), stderr.is_empty()) {
191 (true, true) => String::new(),
192 (false, true) => stdout.to_string(),
193 (true, false) => stderr.to_string(),
194 (false, false) => format!("{stdout}\n{stderr}"),
195 }
196}
197
198pub(crate) fn read_file_tail(path: &Path, max_bytes: usize) -> io::Result<(Vec<u8>, bool)> {
199 if max_bytes == 0 {
200 return Ok((
201 Vec::new(),
202 path.metadata()
203 .map(|metadata| metadata.len() > 0)
204 .unwrap_or(false),
205 ));
206 }
207
208 let mut file = File::open(path)?;
209 let len = file.metadata()?.len();
210 let read_len = len.min(max_bytes as u64);
211 if read_len > 0 {
212 file.seek(SeekFrom::End(-(read_len as i64)))?;
213 }
214 let mut bytes = Vec::with_capacity(read_len as usize);
215 file.read_to_end(&mut bytes)?;
216 Ok((bytes, len > max_bytes as u64))
217}
218
219fn truncate_front(path: &Path, retain_bytes: u64) -> io::Result<bool> {
220 let len = match path.metadata() {
221 Ok(metadata) => metadata.len(),
222 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
223 Err(error) => return Err(error),
224 };
225 if len <= retain_bytes {
226 return Ok(false);
227 }
228
229 let mut file = File::open(path)?;
230 file.seek(SeekFrom::End(-(retain_bytes as i64)))?;
231 let mut tail = Vec::with_capacity(retain_bytes as usize);
232 file.read_to_end(&mut tail)?;
233 let tmp = path.with_extension(format!(
234 "{}.tmp",
235 path.extension()
236 .and_then(|extension| extension.to_str())
237 .unwrap_or("out")
238 ));
239 fs::write(&tmp, tail)?;
240 fs::rename(&tmp, path)?;
241 Ok(true)
242}