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 /// Change the interest of an existing registration (`EPOLL_CTL_MOD`),
271 /// preserving its token. This is the direction-preserving pause/resume
272 /// primitive: a readable-only registration can be paused (readable off)
273 /// while a writable interest on the same fd survives, and resume re-arms
274 /// the same token.
275 #[inline(always)]
276 pub fn mod_(
277 &self,
278 fd: &Fd,
279 token: Token,
280 readable: bool,
281 writable: bool,
282 ) -> Result<(), CoreError> {
283 let mut events = libc::EPOLLET as u32;
284 if readable {
285 events |= libc::EPOLLIN as u32;
286 }
287 if writable {
288 events |= libc::EPOLLOUT as u32;
289 }
290 let mut ev = libc::epoll_event {
291 events,
292 u64: token.0,
293 };
294 loop {
295 let r = unsafe { libc::epoll_ctl(self.epfd, libc::EPOLL_CTL_MOD, fd.raw(), &mut ev) };
296 if r == -1 {
297 let e = errno();
298 if e == libc::EINTR {
299 continue;
300 }
301 return Err(CoreError::sys(e, "epoll_ctl_mod"));
302 }
303 return Ok(());
304 }
305 }
306
307 /// Remove a file descriptor from the reactor.
308 #[inline(always)]
309 pub fn del(&self, fd: &Fd) -> Result<(), CoreError> {
310 self.del_raw(fd.raw())
311 }
312
313 /// Remove a raw descriptor from the reactor.
314 ///
315 /// NOTE: This is an escape hatch for low-level interactions. Prefer using
316 /// [`del`](Self::del).
317 #[inline(always)]
318 pub(crate) fn del_raw(&self, raw: RawFd) -> Result<(), CoreError> {
319 loop {
320 let ret = unsafe {
321 libc::epoll_ctl(self.epfd, libc::EPOLL_CTL_DEL, raw, std::ptr::null_mut())
322 };
323 if ret == -1 {
324 let e = errno();
325 if e == libc::EINTR {
326 continue;
327 }
328 return Err(CoreError::sys(e, "epoll_ctl_del"));
329 }
330 return Ok(());
331 }
332 }
333
334 /// Wait for events.
335 ///
336 /// This function blocks until at least one event is ready or the timeout
337 /// expires. Ready events are appended to the `buffer`.
338 ///
339 /// ### Timeout Contract
340 /// - `-1`: Block indefinitely until an event occurs or a signal interrupts.
341 /// - `0`: Return immediately, even if no events are ready.
342 /// - `> 0`: Wait for up to the specified number of milliseconds.
343 ///
344 /// Returns the number of events received.
345 #[inline(always)]
346 pub fn wait(
347 &mut self,
348 buffer: &mut Vec<Event>,
349 max_events: usize,
350 timeout: i32,
351 ) -> Result<usize, CoreError> {
352 buffer.clear();
353
354 if max_events == 0 {
355 return Ok(0);
356 }
357
358 // Ensure buffer has enough capacity
359 if buffer.capacity() < max_events {
360 buffer.reserve(max_events.saturating_sub(buffer.len()));
361 }
362
363 if self.events_buf.capacity() < max_events {
364 self.events_buf
365 .reserve(max_events.saturating_sub(self.events_buf.len()));
366 }
367
368 let n = unsafe {
369 libc::epoll_wait(
370 self.epfd,
371 self.events_buf.as_mut_ptr(),
372 max_events as i32,
373 timeout,
374 )
375 };
376
377 if n > 0 {
378 unsafe {
379 self.events_buf.set_len(n as usize);
380 }
381 for i in 0..n as usize {
382 let ev = self.events_buf[i];
383 let is_read = (ev.events & libc::EPOLLIN as u32) != 0;
384 let is_priority = (ev.events & libc::EPOLLPRI as u32) != 0;
385 let is_write = (ev.events & libc::EPOLLOUT as u32) != 0;
386 let is_err = (ev.events & libc::EPOLLERR as u32) != 0;
387 let is_hup = (ev.events & libc::EPOLLHUP as u32) != 0;
388
389 buffer.push(Event {
390 token: Token(ev.u64),
391 readable: is_read || is_err,
392 priority: is_priority || is_err,
393 writable: is_write || is_err,
394 error: is_err,
395 hangup: is_hup,
396 });
397 }
398 return Ok(n as usize);
399 }
400
401 if n < 0 {
402 let e = errno();
403 if e == libc::EINTR {
404 return Ok(0);
405 }
406 return Err(CoreError::sys(e, "epoll_wait"));
407 }
408 Ok(0)
409 }
410
411 /// Return the raw epoll file descriptor.
412 ///
413 /// NOTE: This is an escape hatch for low-level interactions.
414 #[allow(dead_code)]
415 pub(crate) fn fd(&self) -> RawFd {
416 self.epfd
417 }
418}
419
420impl Drop for Reactor {
421 fn drop(&mut self) {
422 // Only restore the calling thread's mask when the drop happens on the
423 // thread that originally blocked SIGCHLD. Restoring on a different
424 // thread would clobber that thread's mask; leaving the block in place
425 // on the setup thread is harmless (SIGCHLD is silently ignored while
426 // blocked and the signalfd fd is closed with the reactor).
427 let same_thread = self
428 .signalfd_setup_thread
429 .is_some_and(|t| unsafe { libc::pthread_equal(t, libc::pthread_self()) } != 0);
430 if same_thread && let Some(mask) = self.signalfd_previous_mask.take() {
431 let _ =
432 unsafe { libc::pthread_sigmask(libc::SIG_SETMASK, &mask, std::ptr::null_mut()) };
433 }
434 if self.epfd >= 0 {
435 unsafe {
436 libc::close(self.epfd);
437 }
438 }
439 }
440}