457 lines
15 KiB
Rust
457 lines
15 KiB
Rust
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
use alloc::{sync::Arc, vec::Vec};
|
|
use core::{
|
|
any::Any,
|
|
sync::atomic::{AtomicBool, AtomicU32, Ordering},
|
|
};
|
|
|
|
use atomic_integer_wrapper::define_atomic_version_of_integer_like_type;
|
|
use bitflags::bitflags;
|
|
use ostd::sync::RwLock;
|
|
|
|
use crate::{
|
|
fs::{file_handle::FileLike, path::Path, utils::AccessMode},
|
|
prelude::*,
|
|
};
|
|
|
|
pub mod inotify;
|
|
|
|
use super::utils::{Inode, InodeExt, InodeType};
|
|
|
|
/// Publishes filesystem events to subscribers.
|
|
///
|
|
/// Each inode has an associated `FsEventPublisher` that maintains a list of
|
|
/// subscribers interested in filesystem events. When an event occurs, the publisher
|
|
/// notifies all subscribers whose interesting events match the event.
|
|
pub struct FsEventPublisher {
|
|
/// List of FS event subscribers.
|
|
subscribers: RwLock<Vec<Arc<dyn FsEventSubscriber>>>,
|
|
/// All interesting FS event types (aggregated from all subscribers).
|
|
all_interesting_events: AtomicFsEvents,
|
|
/// Whether this publisher still accepts new subscribers.
|
|
accepts_new_subscribers: AtomicBool,
|
|
}
|
|
|
|
impl Debug for FsEventPublisher {
|
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
f.debug_struct("FsEventPublisher")
|
|
.field("num_subscribers", &self.subscribers.read().len())
|
|
.finish_non_exhaustive()
|
|
}
|
|
}
|
|
|
|
impl Default for FsEventPublisher {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl FsEventPublisher {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
subscribers: RwLock::new(Vec::new()),
|
|
all_interesting_events: AtomicFsEvents::new(FsEvents::empty()),
|
|
accepts_new_subscribers: AtomicBool::new(true),
|
|
}
|
|
}
|
|
|
|
/// Adds a subscriber to this publisher.
|
|
pub fn add_subscriber(&self, subscriber: Arc<dyn FsEventSubscriber>) -> bool {
|
|
let mut subscribers = self.subscribers.write();
|
|
|
|
// This check must be done after locking `self.subscribers.write()` to avoid race
|
|
// conditions.
|
|
if !self.accepts_new_subscribers.load(Ordering::Relaxed) {
|
|
return false;
|
|
}
|
|
|
|
let current = self.all_interesting_events.load(Ordering::Relaxed);
|
|
self.all_interesting_events
|
|
.store(current | subscriber.interesting_events(), Ordering::Relaxed);
|
|
|
|
subscribers.push(subscriber);
|
|
|
|
true
|
|
}
|
|
|
|
/// Removes a subscriber from this publisher.
|
|
pub fn remove_subscriber(&self, subscriber: &Arc<dyn FsEventSubscriber>) -> bool {
|
|
let mut subscribers = self.subscribers.write();
|
|
|
|
let orig_len = subscribers.len();
|
|
subscribers.retain(|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
|
|
}
|
|
|
|
/// Removes all subscribers from this publisher.
|
|
pub fn remove_all_subscribers(&self) -> usize {
|
|
let mut subscribers = self.subscribers.write();
|
|
|
|
for subscriber in subscribers.iter() {
|
|
subscriber.deliver_event(FsEvents::IN_IGNORED, None);
|
|
}
|
|
|
|
let num_subscribers = subscribers.len();
|
|
subscribers.clear();
|
|
|
|
self.all_interesting_events
|
|
.store(FsEvents::empty(), Ordering::Relaxed);
|
|
|
|
num_subscribers
|
|
}
|
|
|
|
/// Forbids new subscribers from attaching to this publisher and removes all existing
|
|
/// subscribers.
|
|
pub fn disable_new_and_remove_subscribers(&self) -> usize {
|
|
// Do this before calling `remove_all_subscribers` so that the `self.subscribers.write()`
|
|
// lock will synchronize this variable.
|
|
self.accepts_new_subscribers.store(false, Ordering::Relaxed);
|
|
|
|
self.remove_all_subscribers()
|
|
}
|
|
|
|
/// Broadcasts an event to all the subscribers of this publisher.
|
|
pub fn publish_event(&self, events: FsEvents, name: Option<String>) {
|
|
let interesting = self.all_interesting_events.load(Ordering::Relaxed);
|
|
if !interesting.intersects(events) {
|
|
return;
|
|
}
|
|
|
|
let subscribers = self.subscribers.read();
|
|
for subscriber in subscribers.iter() {
|
|
subscriber.deliver_event(events, name.clone());
|
|
}
|
|
}
|
|
|
|
/// Updates the aggregated events when a subscriber's interesting events change.
|
|
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);
|
|
}
|
|
|
|
/// Recalculates the aggregated interesting events from all subscribers.
|
|
fn recalc_interesting_events(&self, subscribers: &[Arc<dyn FsEventSubscriber>]) {
|
|
let mut new_events = FsEvents::empty();
|
|
for subscriber in subscribers.iter() {
|
|
new_events |= subscriber.interesting_events();
|
|
}
|
|
self.all_interesting_events
|
|
.store(new_events, Ordering::Relaxed);
|
|
}
|
|
|
|
/// Finds a subscriber and applies an action if found.
|
|
///
|
|
/// The matcher should return `Some(T)` if the subscriber matches and processing
|
|
/// should stop, or `None` to continue searching.
|
|
#[expect(dead_code)]
|
|
pub fn find_subscriber_and_process<F, T>(&self, mut matcher: F) -> Option<T>
|
|
where
|
|
F: FnMut(&Arc<dyn FsEventSubscriber>) -> Option<T>,
|
|
{
|
|
let subscribers = self.subscribers.read();
|
|
for subscriber in subscribers.iter() {
|
|
if let Some(result) = matcher(subscriber) {
|
|
return Some(result);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Represents a subscriber to filesystem events on an `FsEventPublisher`.
|
|
///
|
|
/// A subscriber receives notifications from a publisher when filesystem events occur
|
|
/// that match the subscriber's interesting events. The subscriber specifies which events
|
|
/// it is interested in using `FsEvents`, which define the types of events (e.g.,
|
|
/// read, write, modify, delete) the subscriber wants to be notified about. When an event
|
|
/// occurs, the publisher (attached to an inode) broadcasts it to all subscribers whose
|
|
/// interesting events match the event type.
|
|
pub trait FsEventSubscriber: Any + Send + Sync {
|
|
/// Delivers a filesystem event notification to the subscriber.
|
|
///
|
|
/// 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>);
|
|
/// Returns the events that this subscriber is interested in.
|
|
fn interesting_events(&self) -> FsEvents;
|
|
}
|
|
|
|
bitflags! {
|
|
/// Represents filesystem events that have occurred.
|
|
///
|
|
/// These events are used to notify subscribers about specific filesystem actions.
|
|
/// Subscribers specify which events they are interested in to filter and receive
|
|
/// only the events they care about.
|
|
pub struct FsEvents: u32 {
|
|
const ACCESS = 0x00000001; // File was accessed
|
|
const MODIFY = 0x00000002; // File was modified
|
|
const ATTRIB = 0x00000004; // Metadata changed
|
|
const CLOSE_WRITE = 0x00000008; // Writable file was closed
|
|
const CLOSE_NOWRITE = 0x00000010; // Unwritable file closed
|
|
const OPEN = 0x00000020; // File was opened
|
|
const MOVED_FROM = 0x00000040; // File was moved from X
|
|
const MOVED_TO = 0x00000080; // File was moved to Y
|
|
const CREATE = 0x00000100; // Subfile was created
|
|
const DELETE = 0x00000200; // Subfile was deleted
|
|
const DELETE_SELF = 0x00000400; // Self was deleted
|
|
const MOVE_SELF = 0x00000800; // Self was moved
|
|
const OPEN_EXEC = 0x00001000; // File was opened for exec
|
|
const UNMOUNT = 0x00002000; // Inode on umount fs
|
|
const Q_OVERFLOW = 0x00004000; // Event queued overflowed
|
|
const ERROR = 0x00008000; // Filesystem Error (fanotify)
|
|
const IN_IGNORED = 0x00008000; // Last inotify event here (inotify)
|
|
const OPEN_PERM = 0x00010000; // Open event in a permission hook
|
|
const ACCESS_PERM = 0x00020000; // Access event in a permissions hook
|
|
const OPEN_EXEC_PERM = 0x00040000; // Open/exec event in a permission hook
|
|
const EVENT_ON_CHILD = 0x08000000; // Set on inode mark that cares about things that happen to its children.
|
|
const RENAME = 0x10000000; // File was renamed
|
|
const DN_MULTISHOT = 0x20000000; // dnotify multishot
|
|
const ISDIR = 0x40000000; // Event occurred against dir
|
|
}
|
|
}
|
|
|
|
impl From<u32> for FsEvents {
|
|
fn from(value: u32) -> Self {
|
|
Self::from_bits_truncate(value)
|
|
}
|
|
}
|
|
|
|
impl From<FsEvents> for u32 {
|
|
fn from(value: FsEvents) -> Self {
|
|
value.bits()
|
|
}
|
|
}
|
|
|
|
define_atomic_version_of_integer_like_type!(FsEvents, {
|
|
#[derive(Debug)]
|
|
pub(super) struct AtomicFsEvents(AtomicU32);
|
|
});
|
|
|
|
/// Notifies that a file was accessed.
|
|
pub fn on_access(file: &Arc<dyn FileLike>) {
|
|
// TODO: Check fmode flags (FMODE_NONOTIFY, FMODE_NONOTIFY_PERM).
|
|
let Some(path) = file.path() else {
|
|
return;
|
|
};
|
|
|
|
if !path
|
|
.inode()
|
|
.fs()
|
|
.fs_event_subscriber_stats()
|
|
.has_any_subscribers()
|
|
{
|
|
return;
|
|
}
|
|
notify_parent(path, FsEvents::ACCESS);
|
|
}
|
|
|
|
/// Notifies that a file was modified.
|
|
pub fn on_modify(file: &Arc<dyn FileLike>) {
|
|
// TODO: Check fmode flags (FMODE_NONOTIFY, FMODE_NONOTIFY_PERM).
|
|
let Some(path) = file.path() else {
|
|
return;
|
|
};
|
|
|
|
if !path
|
|
.inode()
|
|
.fs()
|
|
.fs_event_subscriber_stats()
|
|
.has_any_subscribers()
|
|
{
|
|
return;
|
|
}
|
|
notify_parent(path, FsEvents::MODIFY);
|
|
}
|
|
|
|
/// Notifies that a path's content was changed.
|
|
pub fn on_change(path: &Path) {
|
|
if !path
|
|
.inode()
|
|
.fs()
|
|
.fs_event_subscriber_stats()
|
|
.has_any_subscribers()
|
|
{
|
|
return;
|
|
}
|
|
notify_parent(path, FsEvents::MODIFY);
|
|
}
|
|
|
|
/// Notifies that a file was deleted from a directory.
|
|
pub fn on_delete(
|
|
dir_inode: &Arc<dyn Inode>,
|
|
inode: &Arc<dyn Inode>,
|
|
name: impl FnOnce() -> String,
|
|
) {
|
|
if !dir_inode
|
|
.fs()
|
|
.fs_event_subscriber_stats()
|
|
.has_any_subscribers()
|
|
{
|
|
return;
|
|
}
|
|
|
|
if inode.type_() == InodeType::Dir {
|
|
notify_inode_with_name(dir_inode, FsEvents::DELETE | FsEvents::ISDIR, name)
|
|
} else {
|
|
notify_inode_with_name(dir_inode, FsEvents::DELETE, name)
|
|
}
|
|
}
|
|
|
|
/// Notifies that an inode's link count changed.
|
|
pub fn on_link_count(inode: &Arc<dyn Inode>) {
|
|
if !inode.fs().fs_event_subscriber_stats().has_any_subscribers() {
|
|
return;
|
|
}
|
|
notify_inode(inode, FsEvents::ATTRIB);
|
|
}
|
|
|
|
/// Notifies that an inode was removed (link count reached 0).
|
|
pub fn on_inode_removed(inode: &Arc<dyn Inode>) {
|
|
if !inode.fs().fs_event_subscriber_stats().has_any_subscribers() {
|
|
return;
|
|
}
|
|
notify_inode(inode, FsEvents::DELETE_SELF);
|
|
}
|
|
|
|
/// Notifies that a file was linked to a directory.
|
|
pub fn on_link(dir_inode: &Arc<dyn Inode>, inode: &Arc<dyn Inode>, name: impl FnOnce() -> String) {
|
|
if !dir_inode
|
|
.fs()
|
|
.fs_event_subscriber_stats()
|
|
.has_any_subscribers()
|
|
{
|
|
return;
|
|
}
|
|
notify_inode(inode, FsEvents::ATTRIB);
|
|
notify_inode_with_name(dir_inode, FsEvents::CREATE, name);
|
|
}
|
|
|
|
/// Notifies that a directory was created.
|
|
pub fn on_mkdir(dir_path: &Path, name: impl FnOnce() -> String) {
|
|
if !dir_path
|
|
.inode()
|
|
.fs()
|
|
.fs_event_subscriber_stats()
|
|
.has_any_subscribers()
|
|
{
|
|
return;
|
|
}
|
|
notify_inode_with_name(dir_path.inode(), FsEvents::CREATE | FsEvents::ISDIR, name);
|
|
}
|
|
|
|
/// Notifies that a file was created.
|
|
pub fn on_create(file_path: &Path, name: impl FnOnce() -> String) {
|
|
if !file_path
|
|
.inode()
|
|
.fs()
|
|
.fs_event_subscriber_stats()
|
|
.has_any_subscribers()
|
|
{
|
|
return;
|
|
}
|
|
notify_inode_with_name(file_path.inode(), FsEvents::CREATE, name);
|
|
}
|
|
|
|
/// Notifies that a file was opened.
|
|
pub fn on_open(file: &Arc<dyn FileLike>) {
|
|
// TODO: Check fmode flags (FMODE_NONOTIFY, FMODE_NONOTIFY_PERM).
|
|
let Some(path) = file.path() else {
|
|
return;
|
|
};
|
|
|
|
if !path
|
|
.inode()
|
|
.fs()
|
|
.fs_event_subscriber_stats()
|
|
.has_any_subscribers()
|
|
{
|
|
return;
|
|
}
|
|
notify_parent(path, FsEvents::OPEN);
|
|
}
|
|
|
|
/// Notifies that a file was closed.
|
|
pub fn on_close(file: &Arc<dyn FileLike>) {
|
|
// TODO: Check fmode flags (FMODE_NONOTIFY, FMODE_NONOTIFY_PERM).
|
|
if let Some(path) = file.path() {
|
|
if !path
|
|
.inode()
|
|
.fs()
|
|
.fs_event_subscriber_stats()
|
|
.has_any_subscribers()
|
|
{
|
|
return;
|
|
}
|
|
let events = match file.access_mode() {
|
|
AccessMode::O_RDONLY => FsEvents::CLOSE_NOWRITE,
|
|
_ => FsEvents::CLOSE_WRITE,
|
|
};
|
|
notify_parent(path, events);
|
|
}
|
|
}
|
|
|
|
/// Notifies that a file's attributes changed.
|
|
pub fn on_attr_change(path: &Path) {
|
|
if !path
|
|
.inode()
|
|
.fs()
|
|
.fs_event_subscriber_stats()
|
|
.has_any_subscribers()
|
|
{
|
|
return;
|
|
}
|
|
notify_parent(path, FsEvents::ATTRIB);
|
|
}
|
|
|
|
/// Notifies a path's parent and the path itself about filesystem events.
|
|
///
|
|
/// If the parent is watching or if subscribers have registered interesting events with
|
|
/// parent and name information, notifies the parent with child name info.
|
|
/// Otherwise, notifies only the child without name information.
|
|
/// This function is already called after filesystem checking in the callers.
|
|
///
|
|
/// The child's real name (from `path.name()`) is used to notify the parent, since
|
|
/// FS events do not cross mount boundaries.
|
|
fn notify_parent(path: &Path, mut events: FsEvents) {
|
|
if path.inode().type_() == InodeType::Dir {
|
|
events |= FsEvents::ISDIR;
|
|
}
|
|
|
|
let parent = path.parent_within_mount();
|
|
if let Some(parent) = parent {
|
|
notify_inode_with_name(parent.inode(), events, || path.name());
|
|
}
|
|
notify_inode(path.inode(), events);
|
|
}
|
|
|
|
/// Sends a filesystem notification event to all subscribers of an inode.
|
|
///
|
|
/// This is the main entry point for FS event notification. The VFS layer calls hook-specific
|
|
/// functions in `fs/notify/`, which then call this function to broadcast events
|
|
/// to all registered subscribers through the inode's publisher.
|
|
fn notify_inode(inode: &Arc<dyn Inode>, events: FsEvents) {
|
|
if let Some(publisher) = inode.fs_event_publisher() {
|
|
publisher.publish_event(events, None);
|
|
}
|
|
}
|
|
|
|
/// Sends a filesystem notification event with a name to all subscribers of an inode.
|
|
///
|
|
/// Similar to `notify_inode`, but includes a name parameter for events that require
|
|
/// child name information (e.g., CREATE, DELETE).
|
|
fn notify_inode_with_name(inode: &Arc<dyn Inode>, events: FsEvents, name: impl FnOnce() -> String) {
|
|
if let Some(publisher) = inode.fs_event_publisher() {
|
|
publisher.publish_event(events, Some(name()));
|
|
}
|
|
}
|