1use std::collections::VecDeque;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::{Arc, Mutex};
11
12#[cfg(feature = "async-process")]
13use tokio::sync::Notify;
14
15use crate::StreamKind;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct OutputRecord {
20 pub sequence: u64,
22 pub stream: StreamKind,
24 pub bytes: Vec<u8>,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum CursorRead {
31 Gap {
33 from: u64,
35 to: u64,
37 },
38 Record(OutputRecord),
40 Eof,
42}
43
44#[derive(Debug)]
46pub struct OutputLog {
47 capacity_bytes: usize,
48 retained_bytes: usize,
49 next_sequence: u64,
50 records: VecDeque<OutputRecord>,
51}
52
53impl OutputLog {
54 pub fn new(capacity_bytes: usize) -> Self {
56 Self {
57 capacity_bytes,
58 retained_bytes: 0,
59 next_sequence: 0,
60 records: VecDeque::new(),
61 }
62 }
63
64 pub fn append(&mut self, stream: StreamKind, bytes: impl Into<Vec<u8>>) -> u64 {
69 let sequence = self.next_sequence;
70 self.next_sequence = self.next_sequence.saturating_add(1);
71 let bytes = bytes.into();
72 if bytes.len() > self.capacity_bytes {
73 self.records.clear();
74 self.retained_bytes = 0;
75 return sequence;
76 }
77
78 self.retained_bytes = self.retained_bytes.saturating_add(bytes.len());
79 self.records.push_back(OutputRecord {
80 sequence,
81 stream,
82 bytes,
83 });
84 while self.retained_bytes > self.capacity_bytes {
85 if let Some(record) = self.records.pop_front() {
86 self.retained_bytes = self.retained_bytes.saturating_sub(record.bytes.len());
87 }
88 }
89 sequence
90 }
91
92 pub fn first_sequence(&self) -> u64 {
94 self.records
95 .front()
96 .map_or(self.next_sequence, |record| record.sequence)
97 }
98
99 pub fn next_sequence(&self) -> u64 {
101 self.next_sequence
102 }
103
104 pub fn retained_bytes(&self) -> usize {
106 self.retained_bytes
107 }
108
109 pub fn len(&self) -> usize {
111 self.records.len()
112 }
113
114 pub fn is_empty(&self) -> bool {
116 self.records.is_empty()
117 }
118
119 pub fn cursor(&self) -> OutputCursor {
121 OutputCursor {
122 next_sequence: self.first_sequence(),
123 }
124 }
125
126 pub fn cursor_from(&self, sequence: u64) -> OutputCursor {
128 OutputCursor {
129 next_sequence: sequence,
130 }
131 }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub struct OutputCursor {
137 next_sequence: u64,
138}
139
140#[derive(Debug, Clone)]
142pub struct SharedOutputLog {
143 inner: Arc<SharedOutputState>,
144}
145
146#[derive(Debug)]
147struct SharedOutputState {
148 log: Mutex<OutputLog>,
149 #[cfg(feature = "async-process")]
150 notify: Notify,
151 closed: AtomicBool,
152}
153
154impl SharedOutputLog {
155 pub fn new(capacity_bytes: usize) -> Self {
157 Self {
158 inner: Arc::new(SharedOutputState {
159 log: Mutex::new(OutputLog::new(capacity_bytes)),
160 #[cfg(feature = "async-process")]
161 notify: Notify::new(),
162 closed: AtomicBool::new(false),
163 }),
164 }
165 }
166
167 pub fn append(&self, stream: StreamKind, bytes: impl Into<Vec<u8>>) -> u64 {
169 let sequence = self
170 .inner
171 .log
172 .lock()
173 .expect("output log lock is not poisoned")
174 .append(stream, bytes);
175 #[cfg(feature = "async-process")]
176 self.inner.notify.notify_waiters();
177 sequence
178 }
179
180 pub fn close(&self) {
182 self.inner.closed.store(true, Ordering::Release);
183 #[cfg(feature = "async-process")]
184 self.inner.notify.notify_waiters();
185 }
186
187 pub fn cursor(&self) -> SharedOutputCursor {
189 let cursor = self
190 .inner
191 .log
192 .lock()
193 .expect("output log lock is not poisoned")
194 .cursor();
195 SharedOutputCursor {
196 inner: Arc::clone(&self.inner),
197 cursor,
198 }
199 }
200
201 pub fn retained_bytes(&self) -> usize {
203 self.inner
204 .log
205 .lock()
206 .expect("output log lock is not poisoned")
207 .retained_bytes()
208 }
209}
210
211#[derive(Debug, Clone)]
213pub struct SharedOutputCursor {
214 inner: Arc<SharedOutputState>,
215 cursor: OutputCursor,
216}
217
218impl SharedOutputCursor {
219 pub fn read_next(&mut self) -> CursorRead {
221 self.cursor.read_next(
222 &self
223 .inner
224 .log
225 .lock()
226 .expect("output log lock is not poisoned"),
227 )
228 }
229
230 #[cfg(feature = "async-process")]
232 pub async fn read_next_async(&mut self) -> CursorRead {
233 loop {
234 let state = Arc::clone(&self.inner);
235 let notified = state.notify.notified();
236 match self.read_next() {
237 CursorRead::Eof if self.inner.closed.load(Ordering::Acquire) => {
238 return CursorRead::Eof;
239 }
240 CursorRead::Eof => notified.await,
241 result => return result,
242 }
243 }
244 }
245
246 pub fn position(&self) -> u64 {
248 self.cursor.position()
249 }
250
251 pub fn is_closed(&self) -> bool {
253 self.inner.closed.load(Ordering::Acquire)
254 }
255}
256
257impl OutputCursor {
258 pub fn position(&self) -> u64 {
260 self.next_sequence
261 }
262
263 pub fn read_next(&mut self, log: &OutputLog) -> CursorRead {
265 let first = log.first_sequence();
266 if self.next_sequence < first {
267 let from = self.next_sequence;
268 self.next_sequence = first;
269 return CursorRead::Gap {
270 from,
271 to: first.saturating_sub(1),
272 };
273 }
274 let Some(record) = log
275 .records
276 .iter()
277 .find(|record| record.sequence == self.next_sequence)
278 else {
279 return CursorRead::Eof;
280 };
281 self.next_sequence = self.next_sequence.saturating_add(1);
282 CursorRead::Record(record.clone())
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::{CursorRead, OutputLog, SharedOutputLog};
289 use crate::StreamKind;
290
291 #[test]
292 fn bounded_retention_reports_gaps_to_lagging_cursors() {
293 let mut log = OutputLog::new(4);
294 let mut cursor = log.cursor();
295 assert_eq!(log.append(StreamKind::Stdout, b"aa"), 0);
296 assert!(
297 matches!(cursor.read_next(&log), CursorRead::Record(record) if record.sequence == 0)
298 );
299 log.append(StreamKind::Stderr, b"bb");
300 log.append(StreamKind::Stdout, b"cc");
301 log.append(StreamKind::Stderr, b"dd");
302 assert_eq!(cursor.read_next(&log), CursorRead::Gap { from: 1, to: 1 });
303 assert!(
304 matches!(cursor.read_next(&log), CursorRead::Record(record) if record.sequence == 2)
305 );
306 }
307
308 #[test]
309 fn cursors_are_independent_and_oversized_records_are_explicitly_lost() {
310 let mut log = OutputLog::new(3);
311 log.append(StreamKind::Stdout, b"1234");
312 let mut first = log.cursor_from(0);
313 let mut second = log.cursor_from(0);
314 log.append(StreamKind::Stderr, b"ok");
315 assert!(matches!(
316 first.read_next(&log),
317 CursorRead::Gap { from: 0, to: 0 }
318 ));
319 assert!(matches!(
320 second.read_next(&log),
321 CursorRead::Gap { from: 0, to: 0 }
322 ));
323 assert!(
324 matches!(first.read_next(&log), CursorRead::Record(record) if record.bytes == b"ok")
325 );
326 assert!(
327 matches!(second.read_next(&log), CursorRead::Record(record) if record.bytes == b"ok")
328 );
329 assert_eq!(log.retained_bytes(), 2);
330 }
331
332 #[test]
333 fn shared_log_keeps_cursor_positions_independent() {
334 let log = SharedOutputLog::new(8);
335 let mut first = log.cursor();
336 let mut second = log.cursor();
337 log.append(StreamKind::Stdout, b"one");
338 assert!(matches!(first.read_next(), CursorRead::Record(_)));
339 assert!(matches!(second.read_next(), CursorRead::Record(_)));
340 assert_eq!(first.position(), second.position());
341 assert_eq!(log.retained_bytes(), 3);
342 }
343
344 #[cfg(feature = "async-process")]
345 #[tokio::test]
346 async fn async_cursor_returns_terminal_eof_after_close() {
347 let log = SharedOutputLog::new(8);
348 let mut cursor = log.cursor();
349 log.close();
350 assert_eq!(cursor.read_next_async().await, CursorRead::Eof);
351 assert!(cursor.is_closed());
352 }
353}