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/// Time bound (ms) for a [`DrainState::write_input`] `poll(POLLOUT)` wait. The
21/// pty master is `O_NONBLOCK`, so a write returns `EAGAIN` once the child's tty
22/// input buffer is full; the write then waits for writability. Bounded so a
23/// wedged child (never draining its stdin) cannot stall the caller forever.
24const WRITE_INPUT_POLL_TIMEOUT_MS: i32 = 2_000;
25
26#[inline(always)]
27fn errno() -> i32 {
28 std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
29}
30
31/// Associates a file descriptor with an optional reactor token and the current
32/// per-direction interest registered with the reactor.
33pub(crate) struct FdSlot {
34 /// Token assigned by the reactor for this descriptor. `None` while the fd
35 /// is not registered (never added, or removed wholesale on teardown).
36 pub token: Option<Token>,
37 /// The managed file descriptor.
38 pub fd: Fd,
39 /// Current readable interest (`EPOLLIN`) registered with the reactor.
40 pub readable: bool,
41 /// Current writable interest (`EPOLLOUT`) registered with the reactor.
42 pub writable: bool,
43}
44
45/// Orchestrates non-blocking process I/O.
46///
47/// `DrainState` tracks the state of stdin, stdout, and stderr pipes for a
48/// single process. It handles the multiplexing of data between these pipes
49/// and internal buffers.
50///
51/// # Example
52/// ```no_run
53/// # use coreshift_core::io::DrainState;
54/// # use coreshift_core::reactor::Reactor;
55/// # fn example(mut drain: DrainState<fn(&[u8]) -> bool>, mut reactor: Reactor) -> Result<(), Box<dyn std::error::Error>> {
56/// while !drain.is_done() {
57/// let mut events = Vec::new();
58/// reactor.wait(&mut events, 64, -1)?;
59/// for ev in events {
60/// // Map event tokens to drain calls...
61/// }
62/// }
63/// # Ok(())
64/// # }
65/// ```
66#[repr(align(64))]
67pub struct DrainState<F>
68where
69 F: FnMut(&[u8]) -> bool,
70{
71 pub(crate) stdout_slot: Option<FdSlot>,
72 pub(crate) stderr_slot: Option<FdSlot>,
73 pub(crate) stdin_slot: Option<FdSlot>,
74
75 pub(crate) buffer: BufferState,
76 pub(crate) writer: WriterState,
77
78 pub(crate) early_exit: Option<F>,
79
80 /// `true` when the stdout slot is a pty master rather than a pipe. A pty
81 /// master reports EOF as `EIO` (returned once the session leader and all
82 /// slave holders have closed), so the read path maps a stdout `EIO` to a
83 /// clean [`ReadState::Eof`] instead of surfacing it as an I/O error.
84 pub(crate) pty_master: bool,
85}
86
87impl<F> DrainState<F>
88where
89 F: FnMut(&[u8]) -> bool,
90{
91 /// Initialize a new drain state for the provided descriptors.
92 ///
93 /// This consumes the descriptors and sets them to non-blocking mode.
94 /// `chunk_sink` enables streaming mode: every retained output chunk is
95 /// forwarded to the sink instead of being accumulated into the internal
96 /// buffers.
97 ///
98 /// ### Errors
99 /// - `EBADF`: One of the provided file descriptors is invalid.
100 #[allow(clippy::too_many_arguments)] // published constructor; grouped params would break the API
101 pub fn new(
102 stdin_fd: Option<Fd>,
103 stdin_buf: Option<Box<[u8]>>,
104 stdout_fd: Option<Fd>,
105 stderr_fd: Option<Fd>,
106 limit: usize,
107 early_exit: Option<F>,
108 chunk_sink: Option<ChunkSink>,
109 pty_master: bool,
110 ) -> Result<Self, CoreError> {
111 let stdin_slot = if stdin_buf.is_some() {
112 if let Some(fd) = stdin_fd {
113 fd.set_nonblock()?;
114 Some(FdSlot {
115 token: None,
116 fd,
117 readable: false,
118 writable: false,
119 })
120 } else {
121 None
122 }
123 } else {
124 None
125 };
126
127 let stdout_slot = if let Some(fd) = stdout_fd {
128 fd.set_nonblock()?;
129 Some(FdSlot {
130 token: None,
131 fd,
132 readable: false,
133 writable: false,
134 })
135 } else {
136 None
137 };
138
139 let stderr_slot = if let Some(fd) = stderr_fd {
140 fd.set_nonblock()?;
141 Some(FdSlot {
142 token: None,
143 fd,
144 readable: false,
145 writable: false,
146 })
147 } else {
148 None
149 };
150
151 Ok(Self {
152 stdin_slot,
153 stdout_slot,
154 stderr_slot,
155 buffer: BufferState::new(limit, chunk_sink),
156 writer: WriterState::new(stdin_buf),
157 early_exit,
158 pty_master,
159 })
160 }
161
162 /// Returns `true` if all pipes have been closed or fully drained.
163 #[inline(always)]
164 pub fn is_done(&self) -> bool {
165 self.stdin_slot.is_none() && self.stdout_slot.is_none() && self.stderr_slot.is_none()
166 }
167
168 /// Apply a new window size (`TIOCSWINSZ`) to the pty master's terminal.
169 ///
170 /// Sending a signal to the foreground process group after a resize is the
171 /// caller's job (SIGWINCH); this only updates the kernel's `winsize` so a
172 /// subsequent `TIOCGWINSZ`/`SIGWINCH`-driven refresh reads the new size.
173 ///
174 /// ### Errors
175 /// - `EINVAL`: No pty master is present (non-pty spawn or stream already
176 /// closed), or `rows`/`cols` is zero.
177 /// - `ENOTTY`: The stdout descriptor is not a terminal.
178 pub(crate) fn resize_pty(&self, rows: u16, cols: u16) -> Result<(), CoreError> {
179 if rows == 0 || cols == 0 {
180 return Err(CoreError::sys(
181 libc::EINVAL,
182 "resize_pty: rows and cols must be non-zero",
183 ));
184 }
185 let Some(slot) = &self.stdout_slot else {
186 return Err(CoreError::sys(libc::EINVAL, "resize_pty: no pty master"));
187 };
188 let ws = libc::winsize {
189 ws_row: rows,
190 ws_col: cols,
191 ws_xpixel: 0,
192 ws_ypixel: 0,
193 };
194 let r = unsafe { libc::ioctl(slot.fd.raw(), libc::TIOCSWINSZ as libc::Ioctl, &ws) };
195 crate::error::syscall_ret(r, "TIOCSWINSZ")
196 }
197
198 /// Write bytes to the pty master — the child's stdin on a pty spawn.
199 ///
200 /// The master is `O_NONBLOCK`, so a full write waits for `POLLOUT` (bounded
201 /// by [`WRITE_INPUT_POLL_TIMEOUT_MS`]) when the tty input buffer is full,
202 /// and returns once every byte has been accepted by the line discipline.
203 ///
204 /// ### Errors
205 /// - `EINVAL`: Not a pty spawn, or the pty master is already closed.
206 /// - `EIO`: All slave holders have closed (master-side write failure).
207 /// - `ETIMEDOUT`: The child did not drain its input within the poll bound.
208 pub(crate) fn write_input(&self, bytes: &[u8]) -> Result<usize, CoreError> {
209 if !self.pty_master {
210 return Err(CoreError::sys(libc::EINVAL, "write_input: not a pty spawn"));
211 }
212 let Some(slot) = &self.stdout_slot else {
213 return Err(CoreError::sys(
214 libc::EINVAL,
215 "write_input: pty master closed",
216 ));
217 };
218 let fd = slot.fd.raw();
219 let mut written = 0usize;
220 while written < bytes.len() {
221 let n = unsafe {
222 libc::write(
223 fd,
224 bytes[written..].as_ptr() as *const libc::c_void,
225 bytes.len() - written,
226 )
227 };
228 if n < 0 {
229 let e = errno();
230 if e == libc::EINTR {
231 continue;
232 }
233 if e == libc::EAGAIN {
234 let mut pfd = libc::pollfd {
235 fd,
236 events: libc::POLLOUT,
237 revents: 0,
238 };
239 let rc = unsafe { libc::poll(&mut pfd, 1, WRITE_INPUT_POLL_TIMEOUT_MS) };
240 if rc < 0 {
241 let pe = errno();
242 if pe == libc::EINTR {
243 continue;
244 }
245 return Err(CoreError::sys(pe, "write_input:poll"));
246 }
247 if rc == 0 {
248 return Err(CoreError::sys(
249 libc::ETIMEDOUT,
250 "write_input: tty input buffer stayed full",
251 ));
252 }
253 continue;
254 }
255 return Err(CoreError::sys(e, "write_input"));
256 }
257 written += n as usize;
258 }
259 Ok(written)
260 }
261
262 /// Write bytes to the pty master without blocking.
263 ///
264 /// Returns `Ok(Some(n))` with the number of bytes written (which may be
265 /// less than `bytes.len()` when the tty input buffer fills mid-write), or
266 /// `Ok(None)` when `EAGAIN` on the first byte (buffer full). Never blocks
267 /// — the caller owns the input queue and re-arms `POLLOUT` interest on
268 /// `EAGAIN` (register writable), writing again on readiness.
269 ///
270 /// ### Errors
271 /// - `EINVAL`: Not a pty spawn, or the pty master is already closed.
272 /// - `EIO`: All slave holders have closed (master-side write failure).
273 pub(crate) fn write_input_nonblock(&self, bytes: &[u8]) -> Result<Option<usize>, CoreError> {
274 if !self.pty_master {
275 return Err(CoreError::sys(
276 libc::EINVAL,
277 "write_input_nonblock: not a pty spawn",
278 ));
279 }
280 let Some(slot) = &self.stdout_slot else {
281 return Err(CoreError::sys(
282 libc::EINVAL,
283 "write_input_nonblock: pty master closed",
284 ));
285 };
286 let fd = slot.fd.raw();
287 let mut written = 0usize;
288 while written < bytes.len() {
289 let n = unsafe {
290 libc::write(
291 fd,
292 bytes[written..].as_ptr() as *const libc::c_void,
293 bytes.len() - written,
294 )
295 };
296 if n < 0 {
297 let e = errno();
298 if e == libc::EINTR {
299 continue;
300 }
301 if e == libc::EAGAIN {
302 return Ok(if written == 0 { None } else { Some(written) });
303 }
304 return Err(CoreError::sys(e, "write_input_nonblock"));
305 }
306 written += n as usize;
307 }
308 Ok(Some(written))
309 }
310
311 /// Perform a non-blocking write to stdin if pending.
312 ///
313 /// Returns `Ok(true)` if the write buffer is empty or the descriptor is
314 /// closed.
315 ///
316 /// ### Errors
317 /// - `EPIPE`: The child process closed its reading end of the pipe.
318 /// - `EIO`: Low-level I/O error.
319 #[inline(always)]
320 pub fn write_stdin(&mut self) -> Result<bool, CoreError> {
321 let fd = if let Some(s) = &self.stdin_slot {
322 &s.fd
323 } else {
324 return Ok(true);
325 };
326
327 let done = self.writer.write_to_fd(fd)?;
328 if done {
329 self.stdin_slot.take();
330 return Ok(true);
331 }
332 Ok(false)
333 }
334
335 /// Read from a slot's descriptor, mapping a pty-master `EIO` EOF to a
336 /// clean [`ReadState::Eof`]. All other errors propagate.
337 ///
338 /// Associated fn (no `self` receiver) so callers can borrow `buffer` and
339 /// `early_exit` mutably while a `stdout_slot`/`stderr_slot` borrow is
340 /// still live — disjoint field borrows the compiler can see.
341 #[inline(always)]
342 fn read_from_slot(
343 buffer: &mut BufferState,
344 fd: &Fd,
345 is_stdout: bool,
346 early_exit: &mut Option<F>,
347 pty_master: bool,
348 ) -> Result<ReadState, CoreError> {
349 match buffer.read_from_fd(fd, is_stdout, early_exit) {
350 Err(e) if pty_master && is_stdout && e.raw_os_error() == Some(libc::EIO) => {
351 Ok(ReadState::Eof)
352 }
353 other => other,
354 }
355 }
356
357 /// Perform a non-blocking read from stdout or stderr.
358 ///
359 /// Returns `Ok(true)` if the stream reached EOF, the early-exit condition
360 /// was met, or the stream is paused on a full sink queue (the caller
361 /// resumes it later). In the paused case the slot is retained.
362 ///
363 /// ### Errors
364 /// - `EOVERFLOW`: The captured output exceeded the specified limit.
365 /// - `EIO`: Low-level I/O error.
366 #[inline(always)]
367 pub fn read_fd(&mut self, is_stdout: bool) -> Result<bool, CoreError> {
368 let pty_master = self.pty_master;
369 let read_state = {
370 let slot = if is_stdout {
371 &self.stdout_slot
372 } else {
373 &self.stderr_slot
374 };
375 let fd = if let Some(s) = slot {
376 &s.fd
377 } else {
378 return Ok(true);
379 };
380 Self::read_from_slot(
381 &mut self.buffer,
382 fd,
383 is_stdout,
384 &mut self.early_exit,
385 pty_master,
386 )?
387 };
388
389 match read_state {
390 ReadState::Open => Ok(false),
391 ReadState::Paused => Ok(false),
392 ReadState::Eof | ReadState::EarlyExit => {
393 if is_stdout {
394 self.stdout_slot.take();
395 } else {
396 self.stderr_slot.take();
397 }
398 Ok(true)
399 }
400 }
401 }
402
403 /// Extract all active slots for cleanup or reactor removal.
404 pub(crate) fn take_all_slots(&mut self) -> Vec<FdSlot> {
405 let mut slots = Vec::new();
406 if let Some(slot) = self.stdin_slot.take() {
407 slots.push(slot);
408 }
409 if let Some(slot) = self.stdout_slot.take() {
410 slots.push(slot);
411 }
412 if let Some(slot) = self.stderr_slot.take() {
413 slots.push(slot);
414 }
415 slots
416 }
417
418 pub(crate) fn register_with_reactor(
419 &mut self,
420 reactor: &mut crate::reactor::Reactor,
421 ) -> Result<(), CoreError> {
422 register_slot(reactor, &mut self.stdin_slot, false, true)?;
423 register_slot(reactor, &mut self.stdout_slot, true, false)?;
424 register_slot(reactor, &mut self.stderr_slot, true, false)?;
425 Ok(())
426 }
427
428 pub(crate) fn stdout_matches(&self, token: Token) -> bool {
429 self.stdout_slot
430 .as_ref()
431 .is_some_and(|slot| slot.token == Some(token))
432 }
433
434 pub(crate) fn stderr_matches(&self, token: Token) -> bool {
435 self.stderr_slot
436 .as_ref()
437 .is_some_and(|slot| slot.token == Some(token))
438 }
439
440 pub(crate) fn stdin_matches(&self, token: Token) -> bool {
441 self.stdin_slot
442 .as_ref()
443 .is_some_and(|slot| slot.token == Some(token))
444 }
445
446 pub(crate) fn drop_stdout(
447 &mut self,
448 reactor: &mut crate::reactor::Reactor,
449 ) -> Result<(), CoreError> {
450 if let Some(slot) = self.stdout_slot.take() {
451 del_slot(reactor, &slot)?;
452 }
453 Ok(())
454 }
455
456 pub(crate) fn drop_stderr(
457 &mut self,
458 reactor: &mut crate::reactor::Reactor,
459 ) -> Result<(), CoreError> {
460 if let Some(slot) = self.stderr_slot.take() {
461 del_slot(reactor, &slot)?;
462 }
463 Ok(())
464 }
465
466 pub(crate) fn drop_stdin(
467 &mut self,
468 reactor: &mut crate::reactor::Reactor,
469 ) -> Result<(), CoreError> {
470 if let Some(slot) = self.stdin_slot.take() {
471 del_slot(reactor, &slot)?;
472 }
473 self.writer.buf = None;
474 Ok(())
475 }
476
477 pub(crate) fn handle_stdout_ready(
478 &mut self,
479 reactor: &mut crate::reactor::Reactor,
480 ) -> Result<(), CoreError> {
481 if let Some(slot) = &self.stdout_slot {
482 let read_state = Self::read_from_slot(
483 &mut self.buffer,
484 &slot.fd,
485 true,
486 &mut self.early_exit,
487 self.pty_master,
488 )?;
489 match read_state {
490 ReadState::Open => {}
491 ReadState::Paused => {
492 // Sink queue full: remove the fd from the reactor so the
493 // edge-triggered readiness does not spin the loop; the
494 // caller re-registers via `resume_stdout` when it has
495 // drained the queue.
496 self.pause_stdout(reactor)?;
497 }
498 ReadState::Eof | ReadState::EarlyExit => {
499 self.drop_stdout(reactor)?;
500 }
501 }
502 }
503 Ok(())
504 }
505
506 pub(crate) fn handle_stderr_ready(
507 &mut self,
508 reactor: &mut crate::reactor::Reactor,
509 ) -> Result<(), CoreError> {
510 if let Some(slot) = &self.stderr_slot {
511 let read_state = Self::read_from_slot(
512 &mut self.buffer,
513 &slot.fd,
514 false,
515 &mut self.early_exit,
516 self.pty_master,
517 )?;
518 match read_state {
519 ReadState::Open => {}
520 ReadState::Paused => {
521 self.pause_stderr(reactor)?;
522 }
523 ReadState::Eof | ReadState::EarlyExit => {
524 self.drop_stderr(reactor)?;
525 }
526 }
527 }
528 Ok(())
529 }
530
531 /// Remove the stdout fd from the reactor while its sink queue is full.
532 /// The slot is retained (token cleared) so the stream can be resumed.
533 pub(crate) fn pause_stdout(
534 &mut self,
535 reactor: &mut crate::reactor::Reactor,
536 ) -> Result<(), CoreError> {
537 pause_slot(reactor, &mut self.stdout_slot)
538 }
539
540 /// Remove the stderr fd from the reactor while its sink queue is full.
541 pub(crate) fn pause_stderr(
542 &mut self,
543 reactor: &mut crate::reactor::Reactor,
544 ) -> Result<(), CoreError> {
545 pause_slot(reactor, &mut self.stderr_slot)
546 }
547
548 /// Return whether the stdout stream is paused on a full sink queue.
549 pub fn stdout_paused(&self) -> bool {
550 self.buffer.stdout_paused()
551 }
552
553 /// Return whether the stderr stream is paused on a full sink queue.
554 pub fn stderr_paused(&self) -> bool {
555 self.buffer.stderr_paused()
556 }
557
558 /// Re-deliver the held stdout chunk (if any) and re-register the fd when
559 /// the sink has room again. Returns `true` when the stream is resumed,
560 /// `false` when the sink is still full and the stream stays paused.
561 pub fn resume_stdout(
562 &mut self,
563 reactor: &mut crate::reactor::Reactor,
564 ) -> Result<bool, CoreError> {
565 if !self.buffer.deliver_pending_stdout()? {
566 return Ok(false);
567 }
568 register_slot(reactor, &mut self.stdout_slot, true, false)?;
569 Ok(true)
570 }
571
572 /// Re-deliver the held stderr chunk (if any) and re-register the fd when
573 /// the sink has room again. Returns `true` when the stream is resumed,
574 /// `false` when the sink is still full and the stream stays paused.
575 pub fn resume_stderr(
576 &mut self,
577 reactor: &mut crate::reactor::Reactor,
578 ) -> Result<bool, CoreError> {
579 if !self.buffer.deliver_pending_stderr()? {
580 return Ok(false);
581 }
582 register_slot(reactor, &mut self.stderr_slot, true, false)?;
583 Ok(true)
584 }
585
586 /// Arm or disarm the pty master's WRITABLE interest (the input route).
587 ///
588 /// Only valid when the stdout slot is a pty master (pty mode); the
589 /// readable interest is preserved — this is a direction-preserving
590 /// `EPOLL_CTL_MOD` on the existing registration, so arming writable never
591 /// disables output delivery and disarming never unregisters the fd. The
592 /// daemon arms this when its bounded input queue fills (EAGAIN on
593 /// `write_input_nonblock`) and flushes the queue on each writable event,
594 /// disarming when the queue drains (plan §5.2.2(2)/§5.2.3).
595 ///
596 /// ### Errors
597 /// - `EINVAL`: The spawn was not a pty spawn, or the stream is already
598 /// closed.
599 pub fn set_pty_writable(
600 &mut self,
601 reactor: &mut crate::reactor::Reactor,
602 writable: bool,
603 ) -> Result<(), CoreError> {
604 if !self.pty_master {
605 return Err(CoreError::sys(libc::EINVAL, "set_pty_writable: not a pty"));
606 }
607 register_slot(reactor, &mut self.stdout_slot, true, writable)
608 }
609
610 /// The pty master's input-route reactor token, when this drain owns a pty
611 /// master stdout (the input write target). `None` for pipe mode — the
612 /// daemon uses this to recognize pty-master writable events before routing
613 /// them to `handle_reactor_event`.
614 pub fn pty_input_token(&self) -> Option<Token> {
615 if !self.pty_master {
616 return None;
617 }
618 self.stdout_slot.as_ref().and_then(|s| s.token)
619 }
620
621 /// Take the un-delivered stdout chunk (streaming mode), if any.
622 pub(crate) fn take_stdout_pending(&mut self) -> Option<Vec<u8>> {
623 self.buffer.take_stdout_pending()
624 }
625
626 /// Take the un-delivered stderr chunk (streaming mode), if any.
627 pub(crate) fn take_stderr_pending(&mut self) -> Option<Vec<u8>> {
628 self.buffer.take_stderr_pending()
629 }
630
631 pub(crate) fn handle_stdin_writable(
632 &mut self,
633 reactor: &mut crate::reactor::Reactor,
634 ) -> Result<(), CoreError> {
635 if let Some(slot) = &self.stdin_slot {
636 let done = self.writer.write_to_fd(&slot.fd)?;
637 if done {
638 self.drop_stdin(reactor)?;
639 }
640 }
641 Ok(())
642 }
643
644 /// Consume the state and return (stdout, stderr) buffers.
645 pub fn into_parts(mut self) -> (Vec<u8>, Vec<u8>) {
646 let (stdout, stderr, _, _) = std::mem::take(&mut self.buffer).into_parts();
647 (stdout, stderr)
648 }
649
650 /// Return whether the combined stdout+stderr output limit was exceeded.
651 #[inline(always)]
652 pub fn output_limit_exceeded(&self) -> bool {
653 self.buffer.output_limit_exceeded()
654 }
655
656 /// Return whether stdout was explicitly stopped by the early-exit predicate.
657 #[inline(always)]
658 pub fn stdout_early_exited(&self) -> bool {
659 self.buffer.stdout_early_exited()
660 }
661
662 /// Consume the state and return buffers plus drain flags.
663 pub(crate) fn into_parts_with_state(mut self) -> (Vec<u8>, Vec<u8>, bool, bool) {
664 std::mem::take(&mut self.buffer).into_parts()
665 }
666}
667
668/// Register one slot with the given per-direction interest, leaving it in
669/// place on failure. When the fd is already registered the interest is updated
670/// with `EPOLL_CTL_MOD` (direction-preserving: pause/resume toggle ONE
671/// registration instead of del + add), which re-arms edge-triggered
672/// readiness for the same token.
673fn register_slot(
674 reactor: &mut crate::reactor::Reactor,
675 slot: &mut Option<FdSlot>,
676 readable: bool,
677 writable: bool,
678) -> Result<(), CoreError> {
679 let Some(s) = slot.as_mut() else {
680 return Ok(());
681 };
682 if let Some(token) = s.token {
683 if s.readable == readable && s.writable == writable {
684 return Ok(());
685 }
686 reactor.mod_(&s.fd, token, readable, writable)?;
687 s.readable = readable;
688 s.writable = writable;
689 return Ok(());
690 }
691 s.token = Some(reactor.add(&s.fd, readable, writable)?);
692 s.readable = readable;
693 s.writable = writable;
694 Ok(())
695}
696
697/// Remove a slot's fd from the reactor, skipping an already-deregistered
698/// (tokenless) slot. `ENOENT` is tolerated: the fd may already have been
699/// removed by reactor teardown.
700fn del_slot(reactor: &crate::reactor::Reactor, slot: &FdSlot) -> Result<(), CoreError> {
701 if slot.token.is_none() {
702 return Ok(());
703 }
704 match reactor.del(&slot.fd) {
705 Ok(()) => Ok(()),
706 Err(e) if e.raw_os_error() == Some(libc::ENOENT) => Ok(()),
707 Err(e) => Err(e),
708 }
709}
710
711/// Pause a slot's READABLE interest while keeping any writable interest and
712/// the registration itself (direction-preserving `EPOLL_CTL_MOD`). The token
713/// is retained so a later resume re-arms the same registration. `ENOENT` is
714/// tolerated for the teardown race.
715fn pause_slot(
716 reactor: &mut crate::reactor::Reactor,
717 slot: &mut Option<FdSlot>,
718) -> Result<(), CoreError> {
719 let Some(s) = slot.as_mut() else {
720 return Ok(());
721 };
722 let Some(token) = s.token else {
723 return Ok(());
724 };
725 if !s.readable {
726 return Ok(());
727 }
728 match reactor.mod_(&s.fd, token, false, s.writable) {
729 Ok(()) => {
730 s.readable = false;
731 Ok(())
732 }
733 Err(e) if e.raw_os_error() == Some(libc::ENOENT) => {
734 // Registration already gone (teardown race); drop the stale token.
735 s.token = None;
736 s.readable = false;
737 Ok(())
738 }
739 Err(e) => Err(e),
740 }
741}