Skip to main content

coreshift_core/io/
drain.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//! High-level process I/O management.
6//!
7//! This module provides the [`DrainState`] structure, which coordinates the
8//! simultaneous reading from process output pipes and writing to process
9//! input pipes.
10//!
11//! This is an advanced helper for callers that already own child-process file
12//! descriptors and want non-blocking drain semantics without reimplementing
13//! the bookkeeping.
14
15use crate::CoreError;
16use crate::fd::{Fd, Token};
17use crate::io::buffer::{BufferState, ChunkSink, ReadState};
18use crate::io::writer::WriterState;
19
20/// Associates a file descriptor with an optional reactor token.
21pub(crate) struct FdSlot {
22    /// Token assigned by the reactor for this descriptor. `None` while the fd
23    /// is paused (removed from the reactor) or not yet registered.
24    pub token: Option<Token>,
25    /// The managed file descriptor.
26    pub fd: Fd,
27}
28
29/// Orchestrates non-blocking process I/O.
30///
31/// `DrainState` tracks the state of stdin, stdout, and stderr pipes for a
32/// single process. It handles the multiplexing of data between these pipes
33/// and internal buffers.
34///
35/// # Example
36/// ```no_run
37/// # use coreshift_core::io::DrainState;
38/// # use coreshift_core::reactor::Reactor;
39/// # fn example(mut drain: DrainState<fn(&[u8]) -> bool>, mut reactor: Reactor) -> Result<(), Box<dyn std::error::Error>> {
40/// while !drain.is_done() {
41///     let mut events = Vec::new();
42///     reactor.wait(&mut events, 64, -1)?;
43///     for ev in events {
44///         // Map event tokens to drain calls...
45///     }
46/// }
47/// # Ok(())
48/// # }
49/// ```
50#[repr(align(64))]
51pub struct DrainState<F>
52where
53    F: FnMut(&[u8]) -> bool,
54{
55    pub(crate) stdout_slot: Option<FdSlot>,
56    pub(crate) stderr_slot: Option<FdSlot>,
57    pub(crate) stdin_slot: Option<FdSlot>,
58
59    pub(crate) buffer: BufferState,
60    pub(crate) writer: WriterState,
61
62    pub(crate) early_exit: Option<F>,
63}
64
65impl<F> DrainState<F>
66where
67    F: FnMut(&[u8]) -> bool,
68{
69    /// Initialize a new drain state for the provided descriptors.
70    ///
71    /// This consumes the descriptors and sets them to non-blocking mode.
72    /// `chunk_sink` enables streaming mode: every retained output chunk is
73    /// forwarded to the sink instead of being accumulated into the internal
74    /// buffers.
75    ///
76    /// ### Errors
77    /// - `EBADF`: One of the provided file descriptors is invalid.
78    pub fn new(
79        stdin_fd: Option<Fd>,
80        stdin_buf: Option<Box<[u8]>>,
81        stdout_fd: Option<Fd>,
82        stderr_fd: Option<Fd>,
83        limit: usize,
84        early_exit: Option<F>,
85        chunk_sink: Option<ChunkSink>,
86    ) -> Result<Self, CoreError> {
87        let stdin_slot = if stdin_buf.is_some() {
88            if let Some(fd) = stdin_fd {
89                fd.set_nonblock()?;
90                Some(FdSlot { token: None, fd })
91            } else {
92                None
93            }
94        } else {
95            None
96        };
97
98        let stdout_slot = if let Some(fd) = stdout_fd {
99            fd.set_nonblock()?;
100            Some(FdSlot { token: None, fd })
101        } else {
102            None
103        };
104
105        let stderr_slot = if let Some(fd) = stderr_fd {
106            fd.set_nonblock()?;
107            Some(FdSlot { token: None, fd })
108        } else {
109            None
110        };
111
112        Ok(Self {
113            stdin_slot,
114            stdout_slot,
115            stderr_slot,
116            buffer: BufferState::new(limit, chunk_sink),
117            writer: WriterState::new(stdin_buf),
118            early_exit,
119        })
120    }
121
122    /// Returns `true` if all pipes have been closed or fully drained.
123    #[inline(always)]
124    pub fn is_done(&self) -> bool {
125        self.stdin_slot.is_none() && self.stdout_slot.is_none() && self.stderr_slot.is_none()
126    }
127
128    /// Perform a non-blocking write to stdin if pending.
129    ///
130    /// Returns `Ok(true)` if the write buffer is empty or the descriptor is
131    /// closed.
132    ///
133    /// ### Errors
134    /// - `EPIPE`: The child process closed its reading end of the pipe.
135    /// - `EIO`: Low-level I/O error.
136    #[inline(always)]
137    pub fn write_stdin(&mut self) -> Result<bool, CoreError> {
138        let fd = if let Some(s) = &self.stdin_slot {
139            &s.fd
140        } else {
141            return Ok(true);
142        };
143
144        let done = self.writer.write_to_fd(fd)?;
145        if done {
146            self.stdin_slot.take();
147            return Ok(true);
148        }
149        Ok(false)
150    }
151
152    /// Perform a non-blocking read from stdout or stderr.
153    ///
154    /// Returns `Ok(true)` if the stream reached EOF, the early-exit condition
155    /// was met, or the stream is paused on a full sink queue (the caller
156    /// resumes it later). In the paused case the slot is retained.
157    ///
158    /// ### Errors
159    /// - `EOVERFLOW`: The captured output exceeded the specified limit.
160    /// - `EIO`: Low-level I/O error.
161    #[inline(always)]
162    pub fn read_fd(&mut self, is_stdout: bool) -> Result<bool, CoreError> {
163        let read_state = {
164            let slot = if is_stdout {
165                &self.stdout_slot
166            } else {
167                &self.stderr_slot
168            };
169            let fd = if let Some(s) = slot {
170                &s.fd
171            } else {
172                return Ok(true);
173            };
174            self.buffer
175                .read_from_fd(fd, is_stdout, &mut self.early_exit)?
176        };
177
178        match read_state {
179            ReadState::Open => Ok(false),
180            ReadState::Paused => Ok(false),
181            ReadState::Eof | ReadState::EarlyExit => {
182                if is_stdout {
183                    self.stdout_slot.take();
184                } else {
185                    self.stderr_slot.take();
186                }
187                Ok(true)
188            }
189        }
190    }
191
192    /// Extract all active slots for cleanup or reactor removal.
193    pub(crate) fn take_all_slots(&mut self) -> Vec<FdSlot> {
194        let mut slots = Vec::new();
195        if let Some(slot) = self.stdin_slot.take() {
196            slots.push(slot);
197        }
198        if let Some(slot) = self.stdout_slot.take() {
199            slots.push(slot);
200        }
201        if let Some(slot) = self.stderr_slot.take() {
202            slots.push(slot);
203        }
204        slots
205    }
206
207    pub(crate) fn register_with_reactor(
208        &mut self,
209        reactor: &mut crate::reactor::Reactor,
210    ) -> Result<(), CoreError> {
211        register_slot(reactor, &mut self.stdin_slot, false, true)?;
212        register_slot(reactor, &mut self.stdout_slot, true, false)?;
213        register_slot(reactor, &mut self.stderr_slot, true, false)?;
214        Ok(())
215    }
216
217    pub(crate) fn stdout_matches(&self, token: Token) -> bool {
218        self.stdout_slot
219            .as_ref()
220            .is_some_and(|slot| slot.token == Some(token))
221    }
222
223    pub(crate) fn stderr_matches(&self, token: Token) -> bool {
224        self.stderr_slot
225            .as_ref()
226            .is_some_and(|slot| slot.token == Some(token))
227    }
228
229    pub(crate) fn stdin_matches(&self, token: Token) -> bool {
230        self.stdin_slot
231            .as_ref()
232            .is_some_and(|slot| slot.token == Some(token))
233    }
234
235    pub(crate) fn drop_stdout(
236        &mut self,
237        reactor: &mut crate::reactor::Reactor,
238    ) -> Result<(), CoreError> {
239        if let Some(slot) = self.stdout_slot.take() {
240            del_slot(reactor, &slot)?;
241        }
242        Ok(())
243    }
244
245    pub(crate) fn drop_stderr(
246        &mut self,
247        reactor: &mut crate::reactor::Reactor,
248    ) -> Result<(), CoreError> {
249        if let Some(slot) = self.stderr_slot.take() {
250            del_slot(reactor, &slot)?;
251        }
252        Ok(())
253    }
254
255    pub(crate) fn drop_stdin(
256        &mut self,
257        reactor: &mut crate::reactor::Reactor,
258    ) -> Result<(), CoreError> {
259        if let Some(slot) = self.stdin_slot.take() {
260            del_slot(reactor, &slot)?;
261        }
262        self.writer.buf = None;
263        Ok(())
264    }
265
266    pub(crate) fn handle_stdout_ready(
267        &mut self,
268        reactor: &mut crate::reactor::Reactor,
269    ) -> Result<(), CoreError> {
270        if let Some(slot) = &self.stdout_slot {
271            let read_state = self
272                .buffer
273                .read_from_fd(&slot.fd, true, &mut self.early_exit)?;
274            match read_state {
275                ReadState::Open => {}
276                ReadState::Paused => {
277                    // Sink queue full: remove the fd from the reactor so the
278                    // edge-triggered readiness does not spin the loop; the
279                    // caller re-registers via `resume_stdout` when it has
280                    // drained the queue.
281                    self.pause_stdout(reactor)?;
282                }
283                ReadState::Eof | ReadState::EarlyExit => {
284                    self.drop_stdout(reactor)?;
285                }
286            }
287        }
288        Ok(())
289    }
290
291    pub(crate) fn handle_stderr_ready(
292        &mut self,
293        reactor: &mut crate::reactor::Reactor,
294    ) -> Result<(), CoreError> {
295        if let Some(slot) = &self.stderr_slot {
296            let read_state = self
297                .buffer
298                .read_from_fd(&slot.fd, false, &mut self.early_exit)?;
299            match read_state {
300                ReadState::Open => {}
301                ReadState::Paused => {
302                    self.pause_stderr(reactor)?;
303                }
304                ReadState::Eof | ReadState::EarlyExit => {
305                    self.drop_stderr(reactor)?;
306                }
307            }
308        }
309        Ok(())
310    }
311
312    /// Remove the stdout fd from the reactor while its sink queue is full.
313    /// The slot is retained (token cleared) so the stream can be resumed.
314    pub(crate) fn pause_stdout(
315        &mut self,
316        reactor: &mut crate::reactor::Reactor,
317    ) -> Result<(), CoreError> {
318        pause_slot(reactor, &mut self.stdout_slot)
319    }
320
321    /// Remove the stderr fd from the reactor while its sink queue is full.
322    pub(crate) fn pause_stderr(
323        &mut self,
324        reactor: &mut crate::reactor::Reactor,
325    ) -> Result<(), CoreError> {
326        pause_slot(reactor, &mut self.stderr_slot)
327    }
328
329    /// Return whether the stdout stream is paused on a full sink queue.
330    pub fn stdout_paused(&self) -> bool {
331        self.buffer.stdout_paused()
332    }
333
334    /// Return whether the stderr stream is paused on a full sink queue.
335    pub fn stderr_paused(&self) -> bool {
336        self.buffer.stderr_paused()
337    }
338
339    /// Re-deliver the held stdout chunk (if any) and re-register the fd when
340    /// the sink has room again. Returns `true` when the stream is resumed,
341    /// `false` when the sink is still full and the stream stays paused.
342    pub fn resume_stdout(
343        &mut self,
344        reactor: &mut crate::reactor::Reactor,
345    ) -> Result<bool, CoreError> {
346        if !self.buffer.deliver_pending_stdout()? {
347            return Ok(false);
348        }
349        register_slot(reactor, &mut self.stdout_slot, true, false)?;
350        Ok(true)
351    }
352
353    /// Re-deliver the held stderr chunk (if any) and re-register the fd when
354    /// the sink has room again. Returns `true` when the stream is resumed,
355    /// `false` when the sink is still full and the stream stays paused.
356    pub fn resume_stderr(
357        &mut self,
358        reactor: &mut crate::reactor::Reactor,
359    ) -> Result<bool, CoreError> {
360        if !self.buffer.deliver_pending_stderr()? {
361            return Ok(false);
362        }
363        register_slot(reactor, &mut self.stderr_slot, true, false)?;
364        Ok(true)
365    }
366
367    /// Take the un-delivered stdout chunk (streaming mode), if any.
368    pub(crate) fn take_stdout_pending(&mut self) -> Option<Vec<u8>> {
369        self.buffer.take_stdout_pending()
370    }
371
372    /// Take the un-delivered stderr chunk (streaming mode), if any.
373    pub(crate) fn take_stderr_pending(&mut self) -> Option<Vec<u8>> {
374        self.buffer.take_stderr_pending()
375    }
376
377    pub(crate) fn handle_stdin_writable(
378        &mut self,
379        reactor: &mut crate::reactor::Reactor,
380    ) -> Result<(), CoreError> {
381        if let Some(slot) = &self.stdin_slot {
382            let done = self.writer.write_to_fd(&slot.fd)?;
383            if done {
384                self.drop_stdin(reactor)?;
385            }
386        }
387        Ok(())
388    }
389
390    /// Consume the state and return (stdout, stderr) buffers.
391    pub fn into_parts(mut self) -> (Vec<u8>, Vec<u8>) {
392        let (stdout, stderr, _, _) = std::mem::take(&mut self.buffer).into_parts();
393        (stdout, stderr)
394    }
395
396    /// Return whether the combined stdout+stderr output limit was exceeded.
397    #[inline(always)]
398    pub fn output_limit_exceeded(&self) -> bool {
399        self.buffer.output_limit_exceeded()
400    }
401
402    /// Return whether stdout was explicitly stopped by the early-exit predicate.
403    #[inline(always)]
404    pub fn stdout_early_exited(&self) -> bool {
405        self.buffer.stdout_early_exited()
406    }
407
408    /// Consume the state and return buffers plus drain flags.
409    pub(crate) fn into_parts_with_state(mut self) -> (Vec<u8>, Vec<u8>, bool, bool) {
410        std::mem::take(&mut self.buffer).into_parts()
411    }
412}
413
414/// Register one slot, leaving it in place on failure and treating a second
415/// registration as a no-op so the fd and stream are never lost.
416fn register_slot(
417    reactor: &mut crate::reactor::Reactor,
418    slot: &mut Option<FdSlot>,
419    readable: bool,
420    writable: bool,
421) -> Result<(), CoreError> {
422    let Some(s) = slot.as_mut() else {
423        return Ok(());
424    };
425    if s.token.is_some() {
426        return Ok(());
427    }
428    s.token = Some(reactor.add(&s.fd, readable, writable)?);
429    Ok(())
430}
431
432/// Remove a slot's fd from the reactor, skipping an already-paused (tokenless)
433/// slot. `ENOENT` is tolerated: the fd may already have been removed by a
434/// pause or by reactor teardown.
435fn del_slot(
436    reactor: &crate::reactor::Reactor,
437    slot: &FdSlot,
438) -> Result<(), CoreError> {
439    if slot.token.is_none() {
440        return Ok(());
441    }
442    match reactor.del(&slot.fd) {
443        Ok(()) => Ok(()),
444        Err(e) if e.raw_os_error() == Some(libc::ENOENT) => Ok(()),
445        Err(e) => Err(e),
446    }
447}
448
449/// Remove a slot's fd from the reactor and clear its token, keeping the slot
450/// so the stream can be resumed later.
451fn pause_slot(
452    reactor: &mut crate::reactor::Reactor,
453    slot: &mut Option<FdSlot>,
454) -> Result<(), CoreError> {
455    let Some(s) = slot.as_mut() else {
456        return Ok(());
457    };
458    if s.token.is_none() {
459        return Ok(());
460    }
461    s.token = None;
462    match reactor.del(&s.fd) {
463        Ok(()) => Ok(()),
464        Err(e) if e.raw_os_error() == Some(libc::ENOENT) => Ok(()),
465        Err(e) => Err(e),
466    }
467}