Respect `InotifyControls::ONESHOT`

This commit is contained in:
Ruihan Li 2026-01-05 11:00:02 +08:00 committed by Tate, Hongliang Tian
parent b5518ccd64
commit e082d4eaa6
5 changed files with 106 additions and 46 deletions

View File

@ -90,7 +90,6 @@ Unsupported event flags:
Unsupported control flags:
* `IN_EXCL_UNLINK` - Events on unlinked files are not excluded
* `IN_ONESHOT` - Watches are not automatically removed after the first event
For more information,
see [the man page](https://man7.org/linux/man-pages/man7/inotify.7.html).

View File

@ -3,7 +3,7 @@ inotify_events = IN_ACCESS | IN_MODIFY | IN_ATTRIB | IN_CLOSE_WRITE |
IN_DELETE_SELF | IN_CLOSE;
inotify_controls = IN_ONLYDIR | IN_DONT_FOLLOW | IN_MASK_CREATE |
IN_MASK_ADD | IN_ISDIR;
IN_MASK_ADD | IN_ISDIR | IN_ONESHOT;
// Add a watch to an initialized inotify instance
inotify_add_watch(fd, pathname, mask = <inotify_events> | <inotify_controls>);

View File

@ -139,15 +139,19 @@ impl InotifyFile {
// Try to find and update the existing subscriber first.
let inode_weak = Arc::downgrade(path.inode());
for (wd, entry) in watch_map.iter() {
let mut watch_iter = watch_map.iter();
while let Some((wd, entry)) = watch_iter.next() {
if !Weak::ptr_eq(&entry.inode, &inode_weak) {
continue;
}
// The inode has been unlinked and the subscriber is dead. We shouldn't need to update
// since no new events can occur.
// The subscriber is dead because it's a one-shot subscriber or the inode is dead.
// The watch is considered removed.
let Some(subscriber) = entry.subscriber.upgrade() else {
return Ok(*wd);
let wd = *wd;
watch_map.remove(&wd);
watch_iter = watch_map.iter();
continue;
};
subscriber.update(interesting, options)?;
@ -200,8 +204,8 @@ impl InotifyFile {
let (inode, subscriber) = match (entry.inode.upgrade(), entry.subscriber.upgrade()) {
(Some(inode), Some(subscriber)) => (inode, subscriber),
// The inode has been unlinked and the subscriber is dead. The watch is considered
// removed, so we return an error.
// The subscriber is dead because it's a one-shot subscriber or the inode is dead.
// The watch is considered removed.
_ => return_errno_with_message!(Errno::EINVAL, "the inotify watch does not exist"),
};
@ -221,12 +225,7 @@ impl InotifyFile {
/// The event will be queued and can be read by users.
/// If the event can be merged with the last event in the queue, it will be merged.
/// The event is only queued if it matches one of the subscriber's interesting events.
fn receive_event(&self, subscriber: &InotifySubscriber, event: FsEvents, name: Option<String>) {
if !event.contains(FsEvents::IN_IGNORED) && !subscriber.is_interesting(event) {
return;
}
let wd = subscriber.wd();
fn receive_event(&self, wd: u32, event: FsEvents, name: Option<String>) {
let new_event = InotifyEvent::new(wd, event, 0, name);
'notify: {
@ -445,6 +444,8 @@ pub struct InotifySubscriber {
// This field is packed into a `u64`: the high 32 bits store options,
// and the low 32 bits store interesting events.
interesting_and_controls: AtomicU64,
// Whether the subscriber is one-shot and has been dead.
is_dead: AtomicBool,
// Watch descriptor.
wd: u32,
// Reference to the owning inotify file.
@ -461,6 +462,7 @@ impl InotifySubscriber {
let wd = inotify_file.alloc_wd()?;
let this = Arc::new(Self {
interesting_and_controls: AtomicU64::new(0),
is_dead: AtomicBool::new(false),
wd,
inotify_file,
});
@ -473,14 +475,13 @@ impl InotifySubscriber {
self.wd
}
fn interesting(&self) -> InotifyEvents {
/// Loads the interesting events and options atomically.
fn interesting_and_controls(&self) -> (InotifyEvents, InotifyControls) {
let flags = self.interesting_and_controls.load(Ordering::Relaxed);
InotifyEvents::from_bits_truncate((flags & 0xFFFFFFFF) as u32)
}
fn options(&self) -> InotifyControls {
let flags = self.interesting_and_controls.load(Ordering::Relaxed);
InotifyControls::from_bits_truncate((flags >> 32) as u32)
(
InotifyEvents::from_bits_truncate((flags & 0xFFFFFFFF) as u32),
InotifyControls::from_bits_truncate((flags >> 32) as u32),
)
}
pub fn inotify_file(&self) -> &Arc<InotifyFile> {
@ -497,8 +498,10 @@ impl InotifySubscriber {
let mut merged_options = options;
if options.contains(InotifyControls::MASK_ADD) {
merged_interesting |= self.interesting();
merged_options |= self.options();
// There are no races because `update()` is only called under the `watch_map` lock.
let (old_interesting, old_options) = self.interesting_and_controls();
merged_interesting |= old_interesting;
merged_options |= old_options;
}
merged_options.remove(InotifyControls::MASK_ADD);
@ -513,25 +516,53 @@ impl InotifySubscriber {
self.interesting_and_controls
.store(new_flags, Ordering::Relaxed);
}
/// Checks if the event matches the subscriber's interesting events.
fn is_interesting(&self, event: FsEvents) -> bool {
self.interesting().bits() & event.bits() != 0
}
}
impl FsEventSubscriber for InotifySubscriber {
/// Sends FS events to the inotify file.
fn deliver_event(&self, event: FsEvents, name: Option<String>) {
fn deliver_event(&self, event: FsEvents, name: Option<String>) -> bool {
let (interesting, options) = self.interesting_and_controls();
if !event.contains(FsEvents::IN_IGNORED) && !is_interesting(interesting, event) {
return false;
}
let (is_dead, is_oneshot) = if options.contains(InotifyControls::ONESHOT) {
(self.is_dead.swap(true, Ordering::Relaxed), true)
} else {
(self.is_dead.load(Ordering::Relaxed), false)
};
if is_dead {
return false;
}
let inotify_file = self.inotify_file();
inotify_file.receive_event(self, event, name);
let wd = self.wd();
inotify_file.receive_event(wd, event, name);
if !event.contains(FsEvents::IN_IGNORED) && is_oneshot {
inotify_file.receive_event(wd, FsEvents::IN_IGNORED, None);
}
is_oneshot
}
/// Returns the events that this subscriber is interested in.
fn interesting_events(&self) -> FsEvents {
let inotify_events = self.interesting();
let inotify_events = self.interesting_and_controls().0;
FsEvents::from_bits_truncate(inotify_events.bits())
}
/// Returns whether the subscriber is one-shot and dead.
fn is_oneshot_and_dead(&self) -> bool {
self.is_dead.load(Ordering::Relaxed)
}
}
/// Checks if the event matches the subscriber's interesting events.
fn is_interesting(interesting: InotifyEvents, event: FsEvents) -> bool {
interesting.bits() & event.bits() != 0
}
/// Represents an inotify event that can be read by users.

View File

@ -80,12 +80,11 @@ impl FsEventPublisher {
let mut subscribers = self.subscribers.write();
let orig_len = subscribers.len();
subscribers.retain(|m| !Arc::ptr_eq(m, subscriber));
self.retain_and_recalc_events(&mut subscribers, |m| !Arc::ptr_eq(m, subscriber));
let removed = subscribers.len() != orig_len;
if removed {
subscriber.deliver_event(FsEvents::IN_IGNORED, None);
self.recalc_interesting_events(&subscribers);
}
removed
@ -126,8 +125,17 @@ impl FsEventPublisher {
}
let subscribers = self.subscribers.read();
let mut has_oneshot = false;
for subscriber in subscribers.iter() {
subscriber.deliver_event(events, name.clone());
has_oneshot |= subscriber.deliver_event(events, name.clone());
}
drop(subscribers);
if has_oneshot {
let mut subscribers = self.subscribers.write();
// The `deliver_event()` method should already deliver the `FsEvents::IN_IGNORED`
// events for one-shot subscribers. Here, we simply remove them.
self.retain_and_recalc_events(&mut subscribers, |m| !m.is_oneshot_and_dead());
}
}
@ -135,16 +143,28 @@ impl FsEventPublisher {
pub fn update_subscriber_events(&self) {
// Take a write lock to avoid race conditions that may change `all_interesting_events` to
// an outdated value.
let subscribers = self.subscribers.write();
self.recalc_interesting_events(&subscribers);
let mut subscribers = self.subscribers.write();
self.retain_and_recalc_events(&mut subscribers, |_| true);
}
/// Recalculates the aggregated interesting events from all subscribers.
fn recalc_interesting_events(&self, subscribers: &[Arc<dyn FsEventSubscriber>]) {
/// Retains only the subscribers specified by the predicate and recalculates the aggregated
/// interesting events.
fn retain_and_recalc_events<F>(
&self,
subscribers: &mut Vec<Arc<dyn FsEventSubscriber>>,
mut pred: F,
) where
F: FnMut(&Arc<dyn FsEventSubscriber>) -> bool,
{
let mut new_events = FsEvents::empty();
for subscriber in subscribers.iter() {
new_events |= subscriber.interesting_events();
}
subscribers.retain(|subscriber| {
if pred(subscriber) {
new_events |= subscriber.interesting_events();
true
} else {
false
}
});
self.all_interesting_events
.store(new_events, Ordering::Relaxed);
}
@ -179,11 +199,25 @@ impl FsEventPublisher {
pub trait FsEventSubscriber: Any + Send + Sync {
/// Delivers a filesystem event notification to the subscriber.
///
/// Returns whether the subscriber is a one-shot subscriber and the event has been
/// delivered. If there are no one-shot subscribers, simply return `false` here.
/// Otherwise, [`Self::is_oneshot_and_dead`] should be implemented correspondingly.
///
/// Invariant: This method must not sleep or perform blocking operations. The publisher
/// may hold a spin lock when calling this method.
fn deliver_event(&self, events: FsEvents, name: Option<String>);
fn deliver_event(&self, events: FsEvents, name: Option<String>) -> bool;
/// Returns the events that this subscriber is interested in.
fn interesting_events(&self) -> FsEvents;
/// Returns whether the subscriber is a one-shot subscriber and an event has been
/// delivered.
///
/// This method should return `true` if and only if a previous call to
/// [`Self::deliver_event`] has already returned `true`.
fn is_oneshot_and_dead(&self) -> bool {
false
}
}
bitflags! {
@ -280,7 +314,6 @@ pub fn on_delete(
{
return;
}
if inode.type_() == InodeType::Dir {
notify_inode_with_name(dir_inode, FsEvents::DELETE | FsEvents::ISDIR, name)
} else {

View File

@ -21,9 +21,6 @@ Inotify.ExcludeUnlinkMultipleChildren_NoRandomSave
Inotify.MoveGeneratesEvents
Inotify.MoveWatchedTargetGeneratesEvents
# TODO: Support the `ONESHOT` flag.
Inotify.OneShot
# TODO: Support the `splice()` system call.
Inotify.SpliceOnInotifyFD
Inotify.SpliceOnWatchTarget