diff --git a/api/src/file/inotify.rs b/api/src/file/inotify.rs new file mode 100644 index 00000000..9b16d655 --- /dev/null +++ b/api/src/file/inotify.rs @@ -0,0 +1,327 @@ +use alloc::{ + borrow::Cow, + collections::{BTreeMap, VecDeque}, + sync::{Arc, Weak}, + vec::Vec, +}; +use core::{ + any::Any, + mem::size_of, + sync::atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering}, +}; + +use axerrno::{AxError, AxResult}; +use axfs::ROOT_FS_CONTEXT; +use axfs_ng_vfs::Location; +use axio::{BufMut, Write}; +use axpoll::{IoEvents, PollSet, Pollable}; +use axsync::Mutex; +use axtask::future::{block_on, poll_io}; +use bitflags::bitflags; +use lazy_static::lazy_static; + +use crate::file::{FileLike, Kstat, SealedBuf, SealedBufMut}; + +/// ========== Inotify event flags ========== +pub const IN_IGNORED: u32 = 0x00008000; // File was ignored +/// ========== Flags for inotify_init1()========== +pub const IN_CLOEXEC: u32 = 0o2000000; // 02000000, Set FD_CLOEXEC +pub const IN_NONBLOCK: u32 = 0o0004000; // 00004000, Set O_NONBLOCK + +// flags for inotify_syscalls +bitflags! { + #[derive(Debug, Clone, Copy, Default)] + pub struct InotifyFlags: u32 { + /// Create a file descriptor that is closed on `exec`. + const CLOEXEC = IN_CLOEXEC; + /// Create a non-blocking inotify instance. + const NONBLOCK = IN_NONBLOCK; + } +} + +/// inotifyEvent(Memory layout fully compatible with Linux) +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct InotifyEvent { + pub wd: i32, // Watch descriptor + pub mask: u32, // Mask describing event + pub cookie: u32, // Unique cookie associating related events + pub len: u32, /* Size of name field (including null terminator) + * note: the name field is a variable-length array and is not contained in this struct */ +} + +/// Monitoring data of each inode(stored in Location::user_data()) +#[derive(Default)] +struct InodeWatchData { + // key: watch descriptor, value: (instance_id, event mask) + watches: Mutex>, // Using Mutex to wrap +} +impl InodeWatchData { + fn add_watch(&self, wd: i32, instance_id: u64, mask: u32) { + self.watches.lock().insert(wd, (instance_id, mask)); + } + + fn remove_watch(&self, wd: i32) -> bool { + self.watches.lock().remove(&wd).is_some() + } + + fn is_empty(&self) -> bool { + self.watches.lock().is_empty() + } +} + +/// inotify instance +pub struct InotifyInstance { + // event_queue:save serialized event data + event_queue: Mutex>>, + + // Added: weak reference from wd to Location (for quick lookup and path retrieval) + wd_to_location: Mutex>>, + + next_wd: AtomicI32, + + // Instance ID (unique identifier) + instance_id: u64, + + // blocking/non-blocking mode + non_blocking: AtomicBool, + + // poll support + poll_set: PollSet, +} + +impl InotifyInstance { + /// create new instance + pub fn new(flags: i32) -> AxResult> { + static NEXT_INSTANCE_ID: AtomicU64 = AtomicU64::new(1); + + let flags = flags as u32; + // verify flags + let valid_flags = IN_NONBLOCK | IN_CLOEXEC; + if flags & !valid_flags != 0 { + return Err(AxError::InvalidInput); + } + + let non_blocking = (flags & IN_NONBLOCK) != 0; + let instance_id = NEXT_INSTANCE_ID.fetch_add(1, Ordering::Relaxed); + + let instance = Arc::new(Self { + event_queue: Mutex::new(VecDeque::new()), + wd_to_location: Mutex::new(BTreeMap::new()), + next_wd: AtomicI32::new(1), + instance_id, + non_blocking: AtomicBool::new(non_blocking), + poll_set: PollSet::new(), + }); + + // Registered to global manager + INOTIFY_MANAGER.register(instance_id, Arc::clone(&instance)); + + Ok(instance) + } + + /// Serialized events are in binary format for users to read with char[] + fn serialize_event(event: &InotifyEvent, name: Option<&str>) -> Vec { + // +1 for null terminator + let name_len = name.map_or(0, |s| s.len() + 1); + let total_len = size_of::() + name_len; + + // Linux requires events to be 4-byte aligned + let aligned_len = (total_len + 3) & !3; + + let mut buf = Vec::with_capacity(aligned_len); + + // Write event header (native byte order, matching architecture) + buf.extend_from_slice(&event.wd.to_ne_bytes()); + buf.extend_from_slice(&event.mask.to_ne_bytes()); + buf.extend_from_slice(&event.cookie.to_ne_bytes()); + buf.extend_from_slice(&(name_len as u32).to_ne_bytes()); + + // Write filename (if any) + if let Some(name) = name { + buf.extend_from_slice(name.as_bytes()); + buf.push(0); // null terminator + + // Padding for alignment (using null bytes) + let padding = aligned_len - total_len; + buf.resize(buf.len() + padding, 0); + } + + buf + } + + /// add watch for a path + /// Returns watch descriptor (wd) + pub fn add_watch(&self, path: &str, mask: u32) -> AxResult { + // Convert path to Location + let location = self.resolve_path(path)?; + // Generate a new watch descriptor + let wd = self.next_wd.fetch_add(1, Ordering::Relaxed); + if wd == i32::MAX { + return Err(AxError::StorageFull); + } + + // Get user_data of location + let mut user_data = location.user_data(); + + // Get or create InodeWatchData + // Use get_or_insert_with to get Arc + let inode_data = user_data.get_or_insert_with(InodeWatchData::default); + + // Store watch info: wd -> (instance_id, mask) + inode_data.add_watch(wd, self.instance_id, mask); + + // Store reverse mapping: wd -> location + self.wd_to_location + .lock() + .insert(wd, Arc::downgrade(&location)); + + Ok(wd) + } + + /// remove watch (generate IN_IGNORED event) + pub fn remove_watch(&self, wd: i32) -> AxResult<()> { + // Get location from wd_to_location + let location_weak = { + let mut wd_map = self.wd_to_location.lock(); + wd_map.remove(&wd).ok_or(AxError::InvalidInput)? + }; + + // Generate IN_IGNORED event + let event = InotifyEvent { + wd, + mask: IN_IGNORED, + cookie: 0, + len: 0, + }; + let event_data = Self::serialize_event(&event, None); + self.push_event(event_data); + + // If location exists, remove watch from its user_data + if let Some(location) = location_weak.upgrade() { + let user_data = location.user_data(); + + if let Some(inode_data) = user_data.get::() { + // Remove watch + inode_data.remove_watch(wd); + // If no more watches, remove InodeWatchData + if inode_data.is_empty() { + // Actually TypeMap has no remove, can only leave empty + } + } + } + + Ok(()) + } + + /// Push event to queue + fn push_event(&self, event_data: Vec) { + let mut queue = self.event_queue.lock(); + queue.push_back(event_data); + self.poll_set.wake(); + } + + fn resolve_path(&self, path: &str) -> AxResult> { + let fs_ctx = ROOT_FS_CONTEXT.get().ok_or(AxError::NotFound)?; + let loc = fs_ctx.resolve(path).map_err(|_| AxError::NotFound)?; + Ok(Arc::new(loc)) + } +} + +impl FileLike for InotifyInstance { + fn read(&self, dst: &mut SealedBufMut) -> axio::Result { + block_on(poll_io(self, IoEvents::IN, self.nonblocking(), || { + let mut queue = self.event_queue.lock(); + + if queue.is_empty() { + return Err(AxError::WouldBlock); + } + + let mut bytes_written = 0; + + // Write as many events as possible without exceeding the buffer + while let Some(event_data) = queue.front() { + if dst.remaining_mut() < event_data.len() { + break; + } + + dst.write(event_data)?; + bytes_written += event_data.len(); + queue.pop_front(); + } + + if bytes_written > 0 { + Ok(bytes_written) + } else { + // Buffer too small to write a complete event + Err(AxError::InvalidInput) + } + })) + } + + fn write(&self, _src: &mut SealedBuf) -> axio::Result { + Err(AxError::BadFileDescriptor) + } + + fn stat(&self) -> axio::Result { + Ok(Kstat::default()) + } + + fn nonblocking(&self) -> bool { + self.non_blocking.load(Ordering::Acquire) + } + + fn set_nonblocking(&self, non_blocking: bool) -> axio::Result { + self.non_blocking.store(non_blocking, Ordering::Release); + Ok(()) + } + + fn path(&self) -> Cow { + "anon_inode:[inotify]".into() + } + + fn into_any(self: Arc) -> Arc { + self + } +} + +impl Pollable for InotifyInstance { + fn poll(&self) -> IoEvents { + let mut events = IoEvents::empty(); + let queue = self.event_queue.lock(); + + // Events available to read + events.set(IoEvents::IN, !queue.is_empty()); + // inotify file is not writable + events.set(IoEvents::OUT, false); + + events + } + + fn register(&self, context: &mut core::task::Context<'_>, events: IoEvents) { + if events.contains(IoEvents::IN) { + self.poll_set.register(context.waker()); + } + } +} + +// Global manager (singleton) +struct InotifyManager { + instances: Mutex>>, +} + +impl InotifyManager { + fn new() -> Self { + Self { + instances: Mutex::new(BTreeMap::new()), + } + } + + fn register(&self, instance_id: u64, instance: Arc) { + self.instances.lock().insert(instance_id, instance); + } +} + +lazy_static! { + static ref INOTIFY_MANAGER: InotifyManager = InotifyManager::new(); +} diff --git a/api/src/file/mod.rs b/api/src/file/mod.rs index f6d6e4e5..48d4e4c2 100644 --- a/api/src/file/mod.rs +++ b/api/src/file/mod.rs @@ -1,6 +1,7 @@ pub mod epoll; pub mod event; mod fs; +mod inotify; mod net; mod pidfd; mod pipe; @@ -23,6 +24,7 @@ use starry_core::{resources::AX_FILE_LIMIT, task::AsThread}; pub use self::{ fs::{Directory, File, ResolveAtResult, metadata_to_kstat, resolve_at, with_fs}, + inotify::{InotifyFlags, InotifyInstance}, net::Socket, pidfd::PidFd, pipe::Pipe, diff --git a/api/src/syscall/fs/inotify.rs b/api/src/syscall/fs/inotify.rs new file mode 100644 index 00000000..9f73a18c --- /dev/null +++ b/api/src/syscall/fs/inotify.rs @@ -0,0 +1,62 @@ +use core::ffi::c_char; + +use axerrno::{AxError, AxResult}; + +use crate::{ + file::{InotifyFlags, InotifyInstance, add_file_like, get_file_like}, + mm::vm_load_string, +}; +pub fn sys_inotify_init1(flags: i32) -> AxResult { + let instance = InotifyInstance::new(flags)?; + + // Use add_file_like to add file descriptor + let cloexec = (flags as u32) & InotifyFlags::CLOEXEC.bits() != 0; + add_file_like(instance, cloexec).map(|fd| { + debug!("sys_inotify_init1: allocated fd {fd}"); + fd as isize + }) +} + +pub fn sys_inotify_add_watch(fd: i32, pathname: *const c_char, mask: u32) -> AxResult { + debug!("inotify_add_watch called: fd={fd}, mask={mask:#x}"); + + // Load pathname (using vm_load_string, same as sys_open) + let path = vm_load_string(pathname)?; + // Get file corresponding to file descriptor + let file = get_file_like(fd)?; + // Convert to inotify instance + let inotify = match file.clone().into_any().downcast::() { + Ok(inst) => inst, + Err(_) => { + warn!("inotify_add_watch: fd {fd} is not an inotify instance"); + return Err(AxError::InvalidInput); + } + }; + + // Add watch + let wd = inotify.add_watch(&path, mask)?; + + info!("inotify watch added: fd={fd}, path={path}, wd={wd}"); + Ok(wd as isize) +} + +pub fn sys_inotify_rm_watch(fd: i32, wd: i32) -> AxResult { + debug!("sys_inotify_rm_watch: fd={fd}, wd={wd}"); + + // Get file + let file = get_file_like(fd)?; + + // Convert to inotify instance + let inotify = match file.clone().into_any().downcast::() { + Ok(inst) => inst, + Err(_) => { + warn!("inotify_rm_watch: fd {fd} is not an inotify instance"); + return Err(AxError::InvalidInput); + } + }; + + // Remove watch + inotify.remove_watch(wd)?; + info!("inotify_rm_watch: removed watch wd={wd} from fd={fd}"); + Ok(0) +} diff --git a/api/src/syscall/fs/mod.rs b/api/src/syscall/fs/mod.rs index e54069d4..e63d17b7 100644 --- a/api/src/syscall/fs/mod.rs +++ b/api/src/syscall/fs/mod.rs @@ -1,6 +1,7 @@ mod ctl; mod event; mod fd_ops; +mod inotify; mod io; mod memfd; mod mount; @@ -10,5 +11,6 @@ mod signalfd; mod stat; pub use self::{ - ctl::*, event::*, fd_ops::*, io::*, memfd::*, mount::*, pidfd::*, pipe::*, signalfd::*, stat::*, + ctl::*, event::*, fd_ops::*, inotify::*, io::*, memfd::*, mount::*, pidfd::*, pipe::*, + signalfd::*, stat::*, }; diff --git a/api/src/syscall/mod.rs b/api/src/syscall/mod.rs index aea95737..10338ca9 100644 --- a/api/src/syscall/mod.rs +++ b/api/src/syscall/mod.rs @@ -284,6 +284,13 @@ pub fn handle_syscall(uctx: &mut UserContext) { // event Sysno::eventfd2 => sys_eventfd2(uctx.arg0() as _, uctx.arg1() as _), + // inotify + Sysno::inotify_init1 => sys_inotify_init1(uctx.arg0() as _), + Sysno::inotify_add_watch => { + sys_inotify_add_watch(uctx.arg0() as _, uctx.arg1() as _, uctx.arg2() as _) + } + Sysno::inotify_rm_watch => sys_inotify_rm_watch(uctx.arg0() as _, uctx.arg1() as _), + // pidfd Sysno::pidfd_open => sys_pidfd_open(uctx.arg0() as _, uctx.arg1() as _), Sysno::pidfd_getfd => sys_pidfd_getfd(uctx.arg0() as _, uctx.arg1() as _, uctx.arg2() as _), @@ -593,7 +600,6 @@ pub fn handle_syscall(uctx: &mut UserContext) { // dummy fds Sysno::timerfd_create | Sysno::fanotify_init - | Sysno::inotify_init1 | Sysno::userfaultfd | Sysno::perf_event_open | Sysno::io_uring_setup