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 self.read_next_async_inner(|| {}).await
234 }
235
236 #[cfg(all(test, feature = "async-process"))]
239 async fn read_next_async_after_empty<F>(&mut self, after_empty: F) -> CursorRead
240 where
241 F: FnMut(),
242 {
243 self.read_next_async_inner(after_empty).await
244 }
245
246 #[cfg(feature = "async-process")]
247 async fn read_next_async_inner<F>(&mut self, mut after_empty: F) -> CursorRead
248 where
249 F: FnMut(),
250 {
251 loop {
252 let state = Arc::clone(&self.inner);
253 let notified = state.notify.notified();
257 tokio::pin!(notified);
258 notified.as_mut().enable();
259 match self.read_next() {
260 CursorRead::Eof if self.inner.closed.load(Ordering::Acquire) => {
261 return CursorRead::Eof;
262 }
263 CursorRead::Eof => {
264 after_empty();
265 notified.as_mut().await;
266 }
267 result => return result,
268 }
269 }
270 }
271
272 pub fn position(&self) -> u64 {
274 self.cursor.position()
275 }
276
277 pub fn is_closed(&self) -> bool {
279 self.inner.closed.load(Ordering::Acquire)
280 }
281}
282
283impl OutputCursor {
284 pub fn position(&self) -> u64 {
286 self.next_sequence
287 }
288
289 pub fn read_next(&mut self, log: &OutputLog) -> CursorRead {
291 let first = log.first_sequence();
292 if self.next_sequence < first {
293 let from = self.next_sequence;
294 self.next_sequence = first;
295 return CursorRead::Gap {
296 from,
297 to: first.saturating_sub(1),
298 };
299 }
300 let Some(record) = log
301 .records
302 .iter()
303 .find(|record| record.sequence == self.next_sequence)
304 else {
305 return CursorRead::Eof;
306 };
307 self.next_sequence = self.next_sequence.saturating_add(1);
308 CursorRead::Record(record.clone())
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::{CursorRead, OutputLog, SharedOutputLog};
315 use crate::StreamKind;
316
317 #[test]
318 fn bounded_retention_reports_gaps_to_lagging_cursors() {
319 let mut log = OutputLog::new(4);
320 let mut cursor = log.cursor();
321 assert_eq!(log.append(StreamKind::Stdout, b"aa"), 0);
322 assert!(
323 matches!(cursor.read_next(&log), CursorRead::Record(record) if record.sequence == 0)
324 );
325 log.append(StreamKind::Stderr, b"bb");
326 log.append(StreamKind::Stdout, b"cc");
327 log.append(StreamKind::Stderr, b"dd");
328 assert_eq!(cursor.read_next(&log), CursorRead::Gap { from: 1, to: 1 });
329 assert!(
330 matches!(cursor.read_next(&log), CursorRead::Record(record) if record.sequence == 2)
331 );
332 }
333
334 #[test]
335 fn cursors_are_independent_and_oversized_records_are_explicitly_lost() {
336 let mut log = OutputLog::new(3);
337 log.append(StreamKind::Stdout, b"1234");
338 let mut first = log.cursor_from(0);
339 let mut second = log.cursor_from(0);
340 log.append(StreamKind::Stderr, b"ok");
341 assert!(matches!(
342 first.read_next(&log),
343 CursorRead::Gap { from: 0, to: 0 }
344 ));
345 assert!(matches!(
346 second.read_next(&log),
347 CursorRead::Gap { from: 0, to: 0 }
348 ));
349 assert!(
350 matches!(first.read_next(&log), CursorRead::Record(record) if record.bytes == b"ok")
351 );
352 assert!(
353 matches!(second.read_next(&log), CursorRead::Record(record) if record.bytes == b"ok")
354 );
355 assert_eq!(log.retained_bytes(), 2);
356 }
357
358 #[test]
359 fn shared_log_keeps_cursor_positions_independent() {
360 let log = SharedOutputLog::new(8);
361 let mut first = log.cursor();
362 let mut second = log.cursor();
363 log.append(StreamKind::Stdout, b"one");
364 assert!(matches!(first.read_next(), CursorRead::Record(_)));
365 assert!(matches!(second.read_next(), CursorRead::Record(_)));
366 assert_eq!(first.position(), second.position());
367 assert_eq!(log.retained_bytes(), 3);
368 }
369
370 #[cfg(feature = "async-process")]
371 #[tokio::test]
372 async fn async_cursor_returns_terminal_eof_after_close() {
373 let log = SharedOutputLog::new(8);
374 let mut cursor = log.cursor();
375 log.close();
376 assert_eq!(cursor.read_next_async().await, CursorRead::Eof);
377 assert!(cursor.is_closed());
378 }
379
380 #[cfg(feature = "async-process")]
384 #[tokio::test]
385 async fn async_cursor_close_between_empty_check_and_wait_is_not_lost() {
386 let log = SharedOutputLog::new(8);
387 let close_log = log.clone();
388 let mut cursor = log.cursor();
389
390 let result = tokio::time::timeout(
391 std::time::Duration::from_secs(1),
392 cursor.read_next_async_after_empty(move || close_log.close()),
393 )
394 .await
395 .expect("close wakes a cursor that has no record");
396
397 assert_eq!(result, CursorRead::Eof);
398 assert!(cursor.is_closed());
399 }
400}