Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 39 additions & 13 deletions src/io/posix_event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,7 @@ where
/// Error for a kevent changelist entry that came back with `EV_ERROR` set:
/// the kernel stores the errno *value* in `data`. That is not the -1-sentinel
/// return-code convention `errno_sys` decodes — feeding `data` through it
/// yields `None` for every real errno. `EV_DELETE` reports EBADF/ENOENT here
/// when the knote is already gone (e.g. the fd was closed while the poll was
/// still registered, which removes its kevents); the deinit path tolerates
/// that race by discarding the returned error.
/// yields `None` for every real errno.
#[cfg(any(target_os = "macos", all(test, not(windows))))]
#[inline]
fn kevent_change_error(data: i64) -> sys::Result<()> {
Expand All @@ -68,6 +65,26 @@ fn kevent_change_error(data: i64) -> sys::Result<()> {
))
}

/// Is this errno from a failed deregistration just "the registration is
/// already gone"? Two routine producers:
/// - the fd was closed while the poll was still registered (close() removes
/// an fd's kevents; epoll drops closed fds automatically) → EBADF/ENOENT
/// - on macOS, closing a pty master marks the slave's knotes
/// `EV_EOF|EV_ONESHOT`, so the kernel deletes them when the hangup event is
/// delivered; the reader's teardown `EV_DELETE` then finds nothing → ENOENT.
/// This happens on every terminal window/tab close while a tty is polled.
///
/// Both mean the kernel-side state already matches what unregistration wants,
/// so they count as success — in particular the registration flags must still
/// be cleared, which an error return would skip, leaving the poll claiming to
/// be registered and re-issuing doomed deletes on later teardown calls. libuv
/// ignores the same errnos for its kqueue/epoll delete operations.
#[cfg(not(windows))]
#[inline]
fn deregistration_already_gone(errno: sys::E) -> bool {
matches!(errno, sys::E::ENOENT | sys::E::EBADF)
}

pub use crate::{EventLoopCtx, EventLoopCtxKind, EventLoopKind, OpaqueCallback};

unsafe extern "Rust" {
Expand Down Expand Up @@ -1024,8 +1041,10 @@ impl FilePoll {
linux::epoll_ctl(watcher_fd, EPOLL::CTL_DEL, fd.native(), ptr::null_mut())
};

if let Some(errno) = errno_sys(ctl, sys::Tag::epoll_ctl) {
return errno;
match sys::get_errno(ctl) {
sys::E::SUCCESS => {}
e if deregistration_already_gone(e) => {}
e => return sys::Result::Err(sys::Error::from_code(e, sys::Tag::epoll_ctl)),
}
}
#[cfg(target_os = "macos")]
Expand Down Expand Up @@ -1124,11 +1143,16 @@ impl FilePoll {
// such error events; they are packed from index 0 regardless of
// which change failed. xnu ORs EV_ERROR into the existing action
// bits (EV_DELETE|EV_ERROR = 0x4002), so test the bit, not equality.
if rc >= 1 && (changelist[0].flags & EV::ERROR) != 0 && changelist[0].data != 0 {
return kevent_change_error(changelist[0].data);
}
if rc >= 2 && (changelist[1].flags & EV::ERROR) != 0 && changelist[1].data != 0 {
return kevent_change_error(changelist[1].data);
for i in 0..usize::try_from(rc.min(2)).expect("int cast") {
if (changelist[i].flags & EV::ERROR) == 0 || changelist[i].data == 0 {
continue;
}
if sys::SystemErrno::init(changelist[i].data)
.is_some_and(deregistration_already_gone)
{
continue;
}
return kevent_change_error(changelist[i].data);
}
}
#[cfg(target_os = "freebsd")]
Expand Down Expand Up @@ -1171,8 +1195,10 @@ impl FilePoll {
ptr::null(),
)
};
if let Some(err) = errno_sys(rc, sys::Tag::kevent) {
return err;
match sys::get_errno(rc) {
sys::E::SUCCESS => {}
e if deregistration_already_gone(e) => {}
e => return sys::Result::Err(sys::Error::from_code(e, sys::Tag::kevent)),
}
}

Expand Down
Loading