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}