Skip to main content

coreshift_core/reactor/
mod.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 event reactor.
6//!
7//! This module provides a lightweight wrapper around Linux `epoll` for
8//! multiplexing I/O events. It is optimized for edge-triggered monitoring.
9//! It is intentionally explicit about Linux readiness semantics rather than
10//! hiding them behind a higher-level async runtime abstraction.
11
12pub use crate::fd::{Event, Fd, Token};
13
14use crate::CoreError;
15use crate::error::syscall_ret;
16use std::io::Error as IoError;
17use std::os::unix::io::RawFd;
18
19#[inline(always)]
20fn errno() -> i32 {
21    IoError::last_os_error().raw_os_error().unwrap_or(0)
22}
23
24/// A lightweight epoll reactor using edge-triggered monitoring (EPOLLET).
25///
26/// ### Edge-Triggered Contract
27/// Because this reactor uses EPOLLET, all handlers MUST drain their respective
28/// read or write sources until they receive an `EAGAIN` / `EWOULDBLOCK` error
29/// (represented as `Ok(None)` in the `Fd` helpers).
30///
31/// Failure to drain a source will result in missing future readiness events
32/// for that file descriptor until it is re-registered or another event occurs.
33///
34/// ### Fork Safety
35/// The `Reactor` owns an `epoll` descriptor which is `O_CLOEXEC`. After an
36/// `exec` call in a child process, the reactor and all its registrations are
37/// lost. If the child continues without `exec`, it shares the same epoll
38/// instance, which is generally unsafe and requires careful coordination.
39///
40/// # Example
41/// ```no_run
42/// # use coreshift_core::reactor::{Reactor, Fd, Event};
43/// # fn example(fd: Fd) -> Result<(), Box<dyn std::error::Error>> {
44/// let mut reactor = Reactor::new()?;
45/// let token = reactor.add(&fd, true, false)?;
46///
47/// let mut events = Vec::new();
48/// loop {
49///     reactor.wait(&mut events, 64, -1)?;
50///     for ev in &events {
51///         if ev.token == token {
52///             // Drain fd...
53///         }
54///     }
55/// }
56/// # Ok(())
57/// # }
58/// ```
59pub struct Reactor {
60    epfd: RawFd,
61    next_token: u64,
62    events_buf: Vec<libc::epoll_event>,
63    signalfd: Option<Fd>,
64    signalfd_previous_mask: Option<libc::sigset_t>,
65    /// Thread that called [`Self::setup_signalfd`]; its signal mask is only
66    /// restored in `Drop` when the drop runs on the same thread.
67    signalfd_setup_thread: Option<libc::pthread_t>,
68    /// Token for the signalfd (if initialized).
69    sigchld_token: Option<Token>,
70    /// Token for the inotify fd (if initialized).
71    inotify_token: Option<Token>,
72}
73
74impl Reactor {
75    /// Create a new epoll reactor.
76    ///
77    /// ### Errors
78    /// - `EMFILE`: Process limit on open file descriptors hit.
79    /// - `ENFILE`: System-wide limit on open files hit.
80    /// - `ENOMEM`: Insufficient kernel memory.
81    pub fn new() -> Result<Self, CoreError> {
82        let epfd = unsafe { libc::epoll_create1(libc::EPOLL_CLOEXEC) };
83        syscall_ret(epfd, "epoll_create1")?;
84        Ok(Self {
85            epfd,
86            next_token: 1,
87            events_buf: Vec::with_capacity(64),
88            signalfd: None,
89            signalfd_previous_mask: None,
90            signalfd_setup_thread: None,
91            sigchld_token: None,
92            inotify_token: None,
93        })
94    }
95
96    /// Initialize inotify and add it to the reactor.
97    ///
98    /// ### Errors
99    /// - `EMFILE`: Process limit on open file descriptors hit.
100    /// - `ENFILE`: System-wide limit on open files hit.
101    /// - `ENOMEM`: Insufficient kernel memory.
102    /// - `EPERM`: Permission denied to create inotify instance.
103    pub fn setup_inotify(&mut self) -> Result<(Fd, Token), CoreError> {
104        let fd = unsafe { libc::inotify_init1(libc::IN_CLOEXEC | libc::IN_NONBLOCK) };
105        syscall_ret(fd, "inotify_init1")?;
106
107        let fd_obj = Fd::new(fd, "inotify")?;
108        let token = self.add(&fd_obj, true, false)?;
109        self.inotify_token = Some(token);
110
111        Ok((fd_obj, token))
112    }
113
114    /// Initialize signalfd for SIGCHLD and add it to the reactor.
115    ///
116    /// SIGCHLD is blocked on the calling thread (the thread that will read the
117    /// signalfd) so child exits arrive via the signalfd instead of the default
118    /// disposition. The previous mask of the calling thread is restored when
119    /// the reactor is dropped **on the same thread** that called this method;
120    /// dropping from another thread leaves the block in place rather than
121    /// clobbering that thread's mask.
122    ///
123    /// ### Multi-threaded delivery
124    /// `pthread_sigmask` affects only the calling thread. In a multi-threaded
125    /// process every thread that must not consume SIGCHLD — at minimum the
126    /// thread driving this reactor — needs SIGCHLD blocked. Block it early
127    /// (e.g. in `main` before spawning threads) so worker threads inherit the
128    /// mask and the signalfd never loses a child-exit notification to a
129    /// sibling thread.
130    ///
131    /// ### Errors
132    /// - `EBADF`: The provided file descriptor is invalid.
133    /// - `EINVAL`: Signal mask is invalid or already set up.
134    /// - `EMFILE`: Process limit on open file descriptors hit.
135    pub fn setup_signalfd(&mut self) -> Result<Token, CoreError> {
136        if self.signalfd.is_some() {
137            return Err(CoreError::sys(
138                libc::EINVAL,
139                "setup_signalfd already initialized",
140            ));
141        }
142
143        let mut mask: libc::sigset_t = unsafe { std::mem::zeroed() };
144        unsafe { libc::sigemptyset(&mut mask) };
145        unsafe { libc::sigaddset(&mut mask, libc::SIGCHLD) };
146
147        let mut previous_mask: libc::sigset_t = unsafe { std::mem::zeroed() };
148        let r = unsafe { libc::pthread_sigmask(libc::SIG_BLOCK, &mask, &mut previous_mask) };
149        if r != 0 {
150            return Err(CoreError::sys(r, "pthread_sigmask(SIG_BLOCK)"));
151        }
152
153        let sfd = unsafe { libc::signalfd(-1, &mask, libc::SFD_NONBLOCK | libc::SFD_CLOEXEC) };
154        if let Err(err) = syscall_ret(sfd, "signalfd") {
155            let _ = unsafe {
156                libc::pthread_sigmask(libc::SIG_SETMASK, &previous_mask, std::ptr::null_mut())
157            };
158            return Err(err);
159        }
160
161        let fd = Fd::new(sfd, "signalfd")?;
162        let token = match self.add(&fd, true, false) {
163            Ok(token) => token,
164            Err(err) => {
165                let _ = unsafe {
166                    libc::pthread_sigmask(libc::SIG_SETMASK, &previous_mask, std::ptr::null_mut())
167                };
168                return Err(err);
169            }
170        };
171
172        self.signalfd = Some(fd);
173        self.signalfd_previous_mask = Some(previous_mask);
174        self.signalfd_setup_thread = Some(unsafe { libc::pthread_self() });
175        self.sigchld_token = Some(token);
176
177        Ok(token)
178    }
179
180    /// Drain the internal signalfd buffer.
181    pub fn drain_signalfd(&self) -> Result<(), CoreError> {
182        if let Some(fd) = &self.signalfd {
183            let mut buf = [0u8; std::mem::size_of::<libc::signalfd_siginfo>()];
184            loop {
185                match fd.read_slice(&mut buf) {
186                    Ok(Some(n)) if n < buf.len() => break,
187                    Ok(Some(_)) => continue,
188                    Ok(None) => break,
189                    Err(e) => return Err(e),
190                }
191            }
192        }
193        Ok(())
194    }
195
196    /// Register a file descriptor with the reactor.
197    ///
198    /// This assigns a new unique token for the descriptor and enables
199    /// edge-triggered monitoring.
200    #[inline(always)]
201    pub fn add(&mut self, fd: &Fd, readable: bool, writable: bool) -> Result<Token, CoreError> {
202        let token = Token(self.next_token);
203        self.next_token += 1;
204        self.add_with_token(fd.raw(), token, readable, writable, false)?;
205        Ok(token)
206    }
207
208    /// Register a file descriptor for priority readiness (EPOLLPRI).
209    #[inline(always)]
210    pub fn add_priority(&mut self, fd: &Fd) -> Result<Token, CoreError> {
211        let token = Token(self.next_token);
212        self.next_token += 1;
213        self.add_with_token(fd.raw(), token, false, false, true)?;
214        Ok(token)
215    }
216
217    /// Register a file descriptor with custom epoll flags.
218    ///
219    /// This allows registration with flags like `EPOLLONESHOT` or explicit
220    /// control over `EPOLLET`.
221    ///
222    /// # Example
223    /// ```no_run
224    /// # use coreshift_core::reactor::{Reactor, Fd};
225    /// let mut reactor = Reactor::new().unwrap();
226    /// let fd = Fd::eventfd(0).unwrap();
227    /// reactor.add_with_flags(&fd, (libc::EPOLLIN | libc::EPOLLONESHOT) as u32).unwrap();
228    /// ```
229    #[inline(always)]
230    pub fn add_with_flags(&mut self, fd: &Fd, flags: u32) -> Result<Token, CoreError> {
231        let token = Token(self.next_token);
232        self.next_token += 1;
233        let mut ev = libc::epoll_event {
234            events: flags,
235            u64: token.0,
236        };
237        let r = unsafe { libc::epoll_ctl(self.epfd, libc::EPOLL_CTL_ADD, fd.raw(), &mut ev) };
238        syscall_ret(r, "epoll_ctl_add")?;
239        Ok(token)
240    }
241
242    #[inline(always)]
243    pub(crate) fn add_with_token(
244        &mut self,
245        raw_fd: RawFd,
246        token: Token,
247        readable: bool,
248        writable: bool,
249        priority: bool,
250    ) -> Result<(), CoreError> {
251        let mut events = libc::EPOLLET as u32;
252        if readable {
253            events |= libc::EPOLLIN as u32;
254        }
255        if writable {
256            events |= libc::EPOLLOUT as u32;
257        }
258        if priority {
259            events |= libc::EPOLLPRI as u32;
260        }
261        let mut ev = libc::epoll_event {
262            events,
263            u64: token.0,
264        };
265        let r = unsafe { libc::epoll_ctl(self.epfd, libc::EPOLL_CTL_ADD, raw_fd, &mut ev) };
266        syscall_ret(r, "epoll_ctl_add")?;
267        Ok(())
268    }
269
270    /// Remove a file descriptor from the reactor.
271    #[inline(always)]
272    pub fn del(&self, fd: &Fd) -> Result<(), CoreError> {
273        self.del_raw(fd.raw())
274    }
275
276    /// Remove a raw descriptor from the reactor.
277    ///
278    /// NOTE: This is an escape hatch for low-level interactions. Prefer using
279    /// [`del`](Self::del).
280    #[inline(always)]
281    pub(crate) fn del_raw(&self, raw: RawFd) -> Result<(), CoreError> {
282        loop {
283            let ret = unsafe {
284                libc::epoll_ctl(self.epfd, libc::EPOLL_CTL_DEL, raw, std::ptr::null_mut())
285            };
286            if ret == -1 {
287                let e = errno();
288                if e == libc::EINTR {
289                    continue;
290                }
291                return Err(CoreError::sys(e, "epoll_ctl_del"));
292            }
293            return Ok(());
294        }
295    }
296
297    /// Wait for events.
298    ///
299    /// This function blocks until at least one event is ready or the timeout
300    /// expires. Ready events are appended to the `buffer`.
301    ///
302    /// ### Timeout Contract
303    /// - `-1`: Block indefinitely until an event occurs or a signal interrupts.
304    /// - `0`: Return immediately, even if no events are ready.
305    /// - `> 0`: Wait for up to the specified number of milliseconds.
306    ///
307    /// Returns the number of events received.
308    #[inline(always)]
309    pub fn wait(
310        &mut self,
311        buffer: &mut Vec<Event>,
312        max_events: usize,
313        timeout: i32,
314    ) -> Result<usize, CoreError> {
315        buffer.clear();
316
317        if max_events == 0 {
318            return Ok(0);
319        }
320
321        // Ensure buffer has enough capacity
322        if buffer.capacity() < max_events {
323            buffer.reserve(max_events.saturating_sub(buffer.len()));
324        }
325
326        if self.events_buf.capacity() < max_events {
327            self.events_buf
328                .reserve(max_events.saturating_sub(self.events_buf.len()));
329        }
330
331        let n = unsafe {
332            libc::epoll_wait(
333                self.epfd,
334                self.events_buf.as_mut_ptr(),
335                max_events as i32,
336                timeout,
337            )
338        };
339
340        if n > 0 {
341            unsafe {
342                self.events_buf.set_len(n as usize);
343            }
344            for i in 0..n as usize {
345                let ev = self.events_buf[i];
346                let is_read = (ev.events & libc::EPOLLIN as u32) != 0;
347                let is_priority = (ev.events & libc::EPOLLPRI as u32) != 0;
348                let is_write = (ev.events & libc::EPOLLOUT as u32) != 0;
349                let is_err = (ev.events & libc::EPOLLERR as u32) != 0;
350                let is_hup = (ev.events & libc::EPOLLHUP as u32) != 0;
351
352                buffer.push(Event {
353                    token: Token(ev.u64),
354                    readable: is_read || is_err,
355                    priority: is_priority || is_err,
356                    writable: is_write || is_err,
357                    error: is_err,
358                    hangup: is_hup,
359                });
360            }
361            return Ok(n as usize);
362        }
363
364        if n < 0 {
365            let e = errno();
366            if e == libc::EINTR {
367                return Ok(0);
368            }
369            return Err(CoreError::sys(e, "epoll_wait"));
370        }
371        Ok(0)
372    }
373
374    /// Return the raw epoll file descriptor.
375    ///
376    /// NOTE: This is an escape hatch for low-level interactions.
377    #[allow(dead_code)]
378    pub(crate) fn fd(&self) -> RawFd {
379        self.epfd
380    }
381}
382
383impl Drop for Reactor {
384    fn drop(&mut self) {
385        // Only restore the calling thread's mask when the drop happens on the
386        // thread that originally blocked SIGCHLD. Restoring on a different
387        // thread would clobber that thread's mask; leaving the block in place
388        // on the setup thread is harmless (SIGCHLD is silently ignored while
389        // blocked and the signalfd fd is closed with the reactor).
390        let same_thread = self
391            .signalfd_setup_thread
392            .is_some_and(|t| unsafe { libc::pthread_equal(t, libc::pthread_self()) } != 0);
393        if same_thread && let Some(mask) = self.signalfd_previous_mask.take() {
394            let _ =
395                unsafe { libc::pthread_sigmask(libc::SIG_SETMASK, &mask, std::ptr::null_mut()) };
396        }
397        if self.epfd >= 0 {
398            unsafe {
399                libc::close(self.epfd);
400            }
401        }
402    }
403}