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    /// Token for the signalfd (if initialized).
66    sigchld_token: Option<Token>,
67    /// Token for the inotify fd (if initialized).
68    inotify_token: Option<Token>,
69}
70
71impl Reactor {
72    /// Create a new epoll reactor.
73    ///
74    /// ### Errors
75    /// - `EMFILE`: Process limit on open file descriptors hit.
76    /// - `ENFILE`: System-wide limit on open files hit.
77    /// - `ENOMEM`: Insufficient kernel memory.
78    pub fn new() -> Result<Self, CoreError> {
79        let epfd = unsafe { libc::epoll_create1(libc::EPOLL_CLOEXEC) };
80        syscall_ret(epfd, "epoll_create1")?;
81        Ok(Self {
82            epfd,
83            next_token: 1,
84            events_buf: Vec::with_capacity(64),
85            signalfd: None,
86            signalfd_previous_mask: None,
87            sigchld_token: None,
88            inotify_token: None,
89        })
90    }
91
92    /// Initialize inotify and add it to the reactor.
93    ///
94    /// ### Errors
95    /// - `EMFILE`: Process limit on open file descriptors hit.
96    /// - `ENFILE`: System-wide limit on open files hit.
97    /// - `ENOMEM`: Insufficient kernel memory.
98    /// - `EPERM`: Permission denied to create inotify instance.
99    pub fn setup_inotify(&mut self) -> Result<(Fd, Token), CoreError> {
100        let fd = unsafe { libc::inotify_init1(libc::IN_CLOEXEC | libc::IN_NONBLOCK) };
101        syscall_ret(fd, "inotify_init1")?;
102
103        let fd_obj = Fd::new(fd, "inotify")?;
104        let token = self.add(&fd_obj, true, false)?;
105        self.inotify_token = Some(token);
106
107        Ok((fd_obj, token))
108    }
109
110    /// Initialize signalfd for SIGCHLD and add it to the reactor.
111    ///
112    /// The previous current-thread signal mask is restored when the reactor is
113    /// dropped.
114    ///
115    /// ### Errors
116    /// - `EBADF`: The provided file descriptor is invalid.
117    /// - `EINVAL`: Signal mask is invalid or already set up.
118    /// - `EMFILE`: Process limit on open file descriptors hit.
119    pub fn setup_signalfd(&mut self) -> Result<Token, CoreError> {
120        if self.signalfd.is_some() {
121            return Err(CoreError::sys(
122                libc::EINVAL,
123                "setup_signalfd already initialized",
124            ));
125        }
126
127        let mut mask: libc::sigset_t = unsafe { std::mem::zeroed() };
128        unsafe { libc::sigemptyset(&mut mask) };
129        unsafe { libc::sigaddset(&mut mask, libc::SIGCHLD) };
130
131        let mut previous_mask: libc::sigset_t = unsafe { std::mem::zeroed() };
132        let r = unsafe { libc::pthread_sigmask(libc::SIG_BLOCK, &mask, &mut previous_mask) };
133        if r != 0 {
134            return Err(CoreError::sys(r, "pthread_sigmask(SIG_BLOCK)"));
135        }
136
137        let sfd = unsafe { libc::signalfd(-1, &mask, libc::SFD_NONBLOCK | libc::SFD_CLOEXEC) };
138        if let Err(err) = syscall_ret(sfd, "signalfd") {
139            let _ = unsafe {
140                libc::pthread_sigmask(libc::SIG_SETMASK, &previous_mask, std::ptr::null_mut())
141            };
142            return Err(err);
143        }
144
145        let fd = Fd::new(sfd, "signalfd")?;
146        let token = match self.add(&fd, true, false) {
147            Ok(token) => token,
148            Err(err) => {
149                let _ = unsafe {
150                    libc::pthread_sigmask(libc::SIG_SETMASK, &previous_mask, std::ptr::null_mut())
151                };
152                return Err(err);
153            }
154        };
155
156        self.signalfd = Some(fd);
157        self.signalfd_previous_mask = Some(previous_mask);
158        self.sigchld_token = Some(token);
159
160        Ok(token)
161    }
162
163    /// Drain the internal signalfd buffer.
164    pub fn drain_signalfd(&self) -> Result<(), CoreError> {
165        if let Some(fd) = &self.signalfd {
166            let mut buf = [0u8; std::mem::size_of::<libc::signalfd_siginfo>()];
167            loop {
168                match fd.read_slice(&mut buf) {
169                    Ok(Some(n)) if n < buf.len() => break,
170                    Ok(Some(_)) => continue,
171                    Ok(None) => break,
172                    Err(e) => return Err(e),
173                }
174            }
175        }
176        Ok(())
177    }
178
179    /// Register a file descriptor with the reactor.
180    ///
181    /// This assigns a new unique token for the descriptor and enables
182    /// edge-triggered monitoring.
183    #[inline(always)]
184    pub fn add(&mut self, fd: &Fd, readable: bool, writable: bool) -> Result<Token, CoreError> {
185        let token = Token(self.next_token);
186        self.next_token += 1;
187        self.add_with_token(fd.raw(), token, readable, writable, false)?;
188        Ok(token)
189    }
190
191    /// Register a file descriptor for priority readiness (EPOLLPRI).
192    #[inline(always)]
193    pub fn add_priority(&mut self, fd: &Fd) -> Result<Token, CoreError> {
194        let token = Token(self.next_token);
195        self.next_token += 1;
196        self.add_with_token(fd.raw(), token, false, false, true)?;
197        Ok(token)
198    }
199
200    /// Register a file descriptor with custom epoll flags.
201    ///
202    /// This allows registration with flags like `EPOLLONESHOT` or explicit
203    /// control over `EPOLLET`.
204    ///
205    /// # Example
206    /// ```no_run
207    /// # use coreshift_core::reactor::{Reactor, Fd};
208    /// let mut reactor = Reactor::new().unwrap();
209    /// let fd = Fd::eventfd(0).unwrap();
210    /// reactor.add_with_flags(&fd, (libc::EPOLLIN | libc::EPOLLONESHOT) as u32).unwrap();
211    /// ```
212    #[inline(always)]
213    pub fn add_with_flags(&mut self, fd: &Fd, flags: u32) -> Result<Token, CoreError> {
214        let token = Token(self.next_token);
215        self.next_token += 1;
216        let mut ev = libc::epoll_event {
217            events: flags,
218            u64: token.0,
219        };
220        let r = unsafe { libc::epoll_ctl(self.epfd, libc::EPOLL_CTL_ADD, fd.raw(), &mut ev) };
221        syscall_ret(r, "epoll_ctl_add")?;
222        Ok(token)
223    }
224
225    #[inline(always)]
226    pub(crate) fn add_with_token(
227        &mut self,
228        raw_fd: RawFd,
229        token: Token,
230        readable: bool,
231        writable: bool,
232        priority: bool,
233    ) -> Result<(), CoreError> {
234        let mut events = libc::EPOLLET as u32;
235        if readable {
236            events |= libc::EPOLLIN as u32;
237        }
238        if writable {
239            events |= libc::EPOLLOUT as u32;
240        }
241        if priority {
242            events |= libc::EPOLLPRI as u32;
243        }
244        let mut ev = libc::epoll_event {
245            events,
246            u64: token.0,
247        };
248        let r = unsafe { libc::epoll_ctl(self.epfd, libc::EPOLL_CTL_ADD, raw_fd, &mut ev) };
249        syscall_ret(r, "epoll_ctl_add")?;
250        Ok(())
251    }
252
253    /// Remove a file descriptor from the reactor.
254    #[inline(always)]
255    pub fn del(&self, fd: &Fd) -> Result<(), CoreError> {
256        self.del_raw(fd.raw())
257    }
258
259    /// Remove a raw descriptor from the reactor.
260    ///
261    /// NOTE: This is an escape hatch for low-level interactions. Prefer using
262    /// [`del`](Self::del).
263    #[inline(always)]
264    pub(crate) fn del_raw(&self, raw: RawFd) -> Result<(), CoreError> {
265        loop {
266            let ret = unsafe {
267                libc::epoll_ctl(self.epfd, libc::EPOLL_CTL_DEL, raw, std::ptr::null_mut())
268            };
269            if ret == -1 {
270                let e = errno();
271                if e == libc::EINTR {
272                    continue;
273                }
274                return Err(CoreError::sys(e, "epoll_ctl_del"));
275            }
276            return Ok(());
277        }
278    }
279
280    /// Wait for events.
281    ///
282    /// This function blocks until at least one event is ready or the timeout
283    /// expires. Ready events are appended to the `buffer`.
284    ///
285    /// ### Timeout Contract
286    /// - `-1`: Block indefinitely until an event occurs or a signal interrupts.
287    /// - `0`: Return immediately, even if no events are ready.
288    /// - `> 0`: Wait for up to the specified number of milliseconds.
289    ///
290    /// Returns the number of events received.
291    #[inline(always)]
292    pub fn wait(
293        &mut self,
294        buffer: &mut Vec<Event>,
295        max_events: usize,
296        timeout: i32,
297    ) -> Result<usize, CoreError> {
298        buffer.clear();
299
300        if max_events == 0 {
301            return Ok(0);
302        }
303
304        // Ensure buffer has enough capacity
305        if buffer.capacity() < max_events {
306            buffer.reserve(max_events.saturating_sub(buffer.len()));
307        }
308
309        if self.events_buf.capacity() < max_events {
310            self.events_buf
311                .reserve(max_events.saturating_sub(self.events_buf.len()));
312        }
313
314        let n = unsafe {
315            libc::epoll_wait(
316                self.epfd,
317                self.events_buf.as_mut_ptr(),
318                max_events as i32,
319                timeout,
320            )
321        };
322
323        if n > 0 {
324            unsafe {
325                self.events_buf.set_len(n as usize);
326            }
327            for i in 0..n as usize {
328                let ev = self.events_buf[i];
329                let is_read = (ev.events & libc::EPOLLIN as u32) != 0;
330                let is_priority = (ev.events & libc::EPOLLPRI as u32) != 0;
331                let is_write = (ev.events & libc::EPOLLOUT as u32) != 0;
332                let is_err = (ev.events & libc::EPOLLERR as u32) != 0;
333                let is_hup = (ev.events & libc::EPOLLHUP as u32) != 0;
334
335                buffer.push(Event {
336                    token: Token(ev.u64),
337                    readable: is_read || is_err,
338                    priority: is_priority || is_err,
339                    writable: is_write || is_err,
340                    error: is_err,
341                    hangup: is_hup,
342                });
343            }
344            return Ok(n as usize);
345        }
346
347        if n < 0 {
348            let e = errno();
349            if e == libc::EINTR {
350                return Ok(0);
351            }
352            return Err(CoreError::sys(e, "epoll_wait"));
353        }
354        Ok(0)
355    }
356
357    /// Return the raw epoll file descriptor.
358    ///
359    /// NOTE: This is an escape hatch for low-level interactions.
360    #[allow(dead_code)]
361    pub(crate) fn fd(&self) -> RawFd {
362        self.epfd
363    }
364}
365
366impl Drop for Reactor {
367    fn drop(&mut self) {
368        if let Some(mask) = self.signalfd_previous_mask.take() {
369            let _ =
370                unsafe { libc::pthread_sigmask(libc::SIG_SETMASK, &mask, std::ptr::null_mut()) };
371        }
372        if self.epfd >= 0 {
373            unsafe {
374                libc::close(self.epfd);
375            }
376        }
377    }
378}