Skip to main content

coreshift_core/io/
buffer.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/
4
5//! Asynchronous I/O buffering.
6//!
7//! This module provides the [`BufferState`] structure which accumulates
8//! stdout and stderr data from monitored processes.
9
10use crate::CoreError;
11use crate::fd::Fd;
12use std::sync::Arc;
13
14const READ_CHUNK: usize = 65536;
15
16/// Outcome of delivering one chunk to a [`ChunkSink`].
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum SinkResult {
19    /// The chunk was consumed and draining may continue.
20    Accept,
21    /// The consumer's bounded queue is full. The stream pauses; the chunk is
22    /// retained and re-delivered when the consumer resumes the stream. Bytes
23    /// are never dropped on this path.
24    Pause,
25}
26
27/// Streaming drain consumer.
28///
29/// The sink is invoked for every retained output chunk while draining (per
30/// stream: `is_stdout` distinguishes stdout from stderr). Returning
31/// [`SinkResult::Pause`] stops the drain without losing the chunk — the drain
32/// holds it and re-delivers on resume, so the stream stays lossless and the
33/// read loop never blocks the reactor thread. The caller (e.g. a daemon
34/// exec backend) implements the sink against its own bounded per-session
35/// queue and drives resume when the queue drains.
36pub type ChunkSink = Arc<dyn Fn(bool, &[u8]) -> SinkResult + Send + Sync>;
37
38/// Accumulates output from process streams.
39///
40/// `BufferState` manages the collection of bytes from stdout and stderr pipes.
41/// It enforces a combined memory limit to prevent runaway memory usage by
42/// misbehaving processes. When a [`ChunkSink`] is attached the buffer stops
43/// accumulating: chunks are forwarded to the sink as they are read and the
44/// retained output stays empty (streaming mode).
45#[derive(Default)]
46#[repr(align(64))]
47pub(crate) struct BufferState {
48    stdout: Vec<u8>,
49    stderr: Vec<u8>,
50    limit: usize,
51    output_limit_exceeded: bool,
52    stdout_early_exited: bool,
53    sink: Option<ChunkSink>,
54    scratch: Vec<u8>,
55    stdout_pending: Option<Vec<u8>>,
56    stderr_pending: Option<Vec<u8>>,
57    stdout_paused: bool,
58    stderr_paused: bool,
59}
60
61/// Result of one non-blocking drain attempt.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub(crate) enum ReadState {
64    /// The descriptor would block before EOF.
65    Open,
66    /// Kernel EOF was reached.
67    Eof,
68    /// The caller-provided early-exit predicate requested stdout shutdown.
69    EarlyExit,
70    /// The chunk sink returned [`SinkResult::Pause`]; the stream is paused and
71    /// the un-delivered chunk is held for re-delivery on resume.
72    Paused,
73}
74
75impl BufferState {
76    /// Create a new buffer state with the specified memory limit and an
77    /// optional streaming sink.
78    pub(crate) fn new(limit: usize, sink: Option<ChunkSink>) -> Self {
79        Self {
80            stdout: Vec::with_capacity(1024),
81            stderr: Vec::with_capacity(1024),
82            limit,
83            output_limit_exceeded: false,
84            stdout_early_exited: false,
85            sink,
86            scratch: Vec::with_capacity(READ_CHUNK),
87            stdout_pending: None,
88            stderr_pending: None,
89            stdout_paused: false,
90            stderr_paused: false,
91        }
92    }
93
94    /// Drain available data from a file descriptor into internal storage (or
95    /// forward it to the chunk sink when one is attached).
96    ///
97    /// # Returns
98    /// * `Ok(ReadState::Eof)` if EOF was reached.
99    /// * `Ok(ReadState::EarlyExit)` if stdout matched the early-exit callback.
100    /// * `Ok(ReadState::Paused)` if the chunk sink's queue is full (streaming
101    ///   mode; the un-delivered chunk is retained for resume).
102    /// * `Ok(ReadState::Open)` if the operation would block (`EAGAIN`).
103    #[inline(always)]
104    pub(crate) fn read_from_fd(
105        &mut self,
106        fd: &Fd,
107        is_stdout: bool,
108        early_exit: &mut Option<impl FnMut(&[u8]) -> bool>,
109    ) -> Result<ReadState, CoreError> {
110        if self.sink.is_some() {
111            return self.read_from_fd_streaming(fd, is_stdout, early_exit);
112        }
113        loop {
114            let current_total = self.stdout.len().saturating_add(self.stderr.len());
115            let remaining_limit = self.limit.saturating_sub(current_total);
116
117            if remaining_limit == 0 {
118                let mut drop_buf = [0u8; 8192];
119                match fd.read_slice(&mut drop_buf) {
120                    Ok(Some(n)) if n > 0 => {
121                        self.output_limit_exceeded = true;
122                        continue;
123                    }
124                    Ok(Some(_)) => return Ok(ReadState::Eof),
125                    Ok(None) => return Ok(ReadState::Open),
126                    Err(e) => return Err(e),
127                }
128            }
129
130            let dest = if is_stdout {
131                &mut self.stdout
132            } else {
133                &mut self.stderr
134            };
135            let len = dest.len();
136
137            // Ensure space and read directly into the Vec.
138            // We resize with 0s to remain safe (no UB with uninitialized memory).
139            let to_read = remaining_limit.min(READ_CHUNK);
140            dest.resize(len + to_read, 0);
141
142            match fd.read_slice(&mut dest[len..len + to_read]) {
143                Ok(Some(n)) if n > 0 => {
144                    dest.truncate(len + n);
145
146                    if is_stdout
147                        && let Some(f) = early_exit
148                        && f(&dest[len..len + n])
149                    {
150                        self.stdout_early_exited = true;
151                        return Ok(ReadState::EarlyExit);
152                    }
153                }
154                Ok(Some(_)) => {
155                    dest.truncate(len);
156                    return Ok(ReadState::Eof);
157                }
158                Ok(None) => {
159                    dest.truncate(len);
160                    return Ok(ReadState::Open);
161                }
162                Err(e) => {
163                    dest.truncate(len);
164                    return Err(e);
165                }
166            }
167        }
168    }
169
170    /// Streaming-mode drain: read one chunk at a time into the scratch buffer
171    /// and forward it to the sink. Never accumulates into the retained output
172    /// and never drops bytes: when the sink reports the queue full, the chunk
173    /// is held (`stdout_pending`/`stderr_pending`) and the stream pauses.
174    fn read_from_fd_streaming(
175        &mut self,
176        fd: &Fd,
177        is_stdout: bool,
178        early_exit: &mut Option<impl FnMut(&[u8]) -> bool>,
179    ) -> Result<ReadState, CoreError> {
180        let Some(sink) = &self.sink else {
181            return Ok(ReadState::Open);
182        };
183        loop {
184            // Re-deliver a held chunk before reading new bytes, preserving
185            // stream order across a pause/resume cycle.
186            let pending = if is_stdout {
187                self.stdout_pending.take()
188            } else {
189                self.stderr_pending.take()
190            };
191            if let Some(pending) = pending {
192                match sink(is_stdout, &pending) {
193                    SinkResult::Accept => {
194                        if is_stdout {
195                            self.stdout_paused = false;
196                        } else {
197                            self.stderr_paused = false;
198                        }
199                    }
200                    SinkResult::Pause => {
201                        if is_stdout {
202                            self.stdout_pending = Some(pending);
203                            self.stdout_paused = true;
204                        } else {
205                            self.stderr_pending = Some(pending);
206                            self.stderr_paused = true;
207                        }
208                        return Ok(ReadState::Paused);
209                    }
210                }
211            }
212
213            self.scratch.resize(READ_CHUNK, 0);
214            match fd.read_slice(&mut self.scratch) {
215                Ok(Some(n)) if n > 0 => {
216                    self.scratch.truncate(n);
217
218                    if is_stdout
219                        && let Some(f) = early_exit
220                        && f(&self.scratch)
221                    {
222                        self.stdout_early_exited = true;
223                        return Ok(ReadState::EarlyExit);
224                    }
225
226                    match sink(is_stdout, &self.scratch) {
227                        SinkResult::Accept => continue,
228                        SinkResult::Pause => {
229                            if is_stdout {
230                                self.stdout_pending = Some(self.scratch.clone());
231                                self.stdout_paused = true;
232                            } else {
233                                self.stderr_pending = Some(self.scratch.clone());
234                                self.stderr_paused = true;
235                            }
236                            return Ok(ReadState::Paused);
237                        }
238                    }
239                }
240                Ok(Some(_)) => {
241                    self.scratch.clear();
242                    return Ok(ReadState::Eof);
243                }
244                Ok(None) => {
245                    self.scratch.clear();
246                    return Ok(ReadState::Open);
247                }
248                Err(e) => {
249                    self.scratch.clear();
250                    return Err(e);
251                }
252            }
253        }
254    }
255
256    /// Attempt to re-deliver the held stdout chunk. Returns `true` when the
257    /// stream is resumed (chunk delivered, or nothing held), `false` when the
258    /// sink is still full and the stream stays paused.
259    pub(crate) fn deliver_pending_stdout(&mut self) -> Result<bool, CoreError> {
260        if !self.stdout_paused {
261            return Ok(true);
262        }
263        let Some(sink) = &self.sink else {
264            self.stdout_paused = false;
265            self.stdout_pending = None;
266            return Ok(true);
267        };
268        match self.stdout_pending.take() {
269            None => {
270                self.stdout_paused = false;
271                Ok(true)
272            }
273            Some(pending) => match sink(true, &pending) {
274                SinkResult::Accept => {
275                    self.stdout_paused = false;
276                    Ok(true)
277                }
278                SinkResult::Pause => {
279                    self.stdout_pending = Some(pending);
280                    Ok(false)
281                }
282            },
283        }
284    }
285
286    /// Attempt to re-deliver the held stderr chunk. Returns `true` when the
287    /// stream is resumed, `false` when the sink is still full.
288    pub(crate) fn deliver_pending_stderr(&mut self) -> Result<bool, CoreError> {
289        if !self.stderr_paused {
290            return Ok(true);
291        }
292        let Some(sink) = &self.sink else {
293            self.stderr_paused = false;
294            self.stderr_pending = None;
295            return Ok(true);
296        };
297        match self.stderr_pending.take() {
298            None => {
299                self.stderr_paused = false;
300                Ok(true)
301            }
302            Some(pending) => match sink(false, &pending) {
303                SinkResult::Accept => {
304                    self.stderr_paused = false;
305                    Ok(true)
306                }
307                SinkResult::Pause => {
308                    self.stderr_pending = Some(pending);
309                    Ok(false)
310                }
311            },
312        }
313    }
314
315    /// Return whether the stdout stream is paused on a full sink queue.
316    #[inline(always)]
317    pub(crate) fn stdout_paused(&self) -> bool {
318        self.stdout_paused
319    }
320
321    /// Return whether the stderr stream is paused on a full sink queue.
322    #[inline(always)]
323    pub(crate) fn stderr_paused(&self) -> bool {
324        self.stderr_paused
325    }
326
327    /// Take the un-delivered stdout chunk (streaming mode), if any.
328    pub(crate) fn take_stdout_pending(&mut self) -> Option<Vec<u8>> {
329        self.stdout_pending.take()
330    }
331
332    /// Take the un-delivered stderr chunk (streaming mode), if any.
333    pub(crate) fn take_stderr_pending(&mut self) -> Option<Vec<u8>> {
334        self.stderr_pending.take()
335    }
336
337    /// Return whether the combined stdout+stderr output limit was exceeded.
338    #[inline(always)]
339    pub(crate) fn output_limit_exceeded(&self) -> bool {
340        self.output_limit_exceeded
341    }
342
343    /// Return whether stdout was closed by the early-exit predicate.
344    #[inline(always)]
345    pub(crate) fn stdout_early_exited(&self) -> bool {
346        self.stdout_early_exited
347    }
348
349    /// Consume the state and return the accumulated buffers.
350    pub(crate) fn into_parts(mut self) -> (Vec<u8>, Vec<u8>, bool, bool) {
351        (
352            std::mem::take(&mut self.stdout),
353            std::mem::take(&mut self.stderr),
354            self.output_limit_exceeded,
355            self.stdout_early_exited,
356        )
357    }
358}