Skip to content

Commit

Permalink
Implement InotifyReader for raw inotify event iteration (#1113)
Browse files Browse the repository at this point in the history
* Implement InotifyReader for raw inotify event iteration

Signed-off-by: Alex Saveau <[email protected]>

* Add inotify test

Signed-off-by: Alex Saveau <[email protected]>

---------

Signed-off-by: Alex Saveau <[email protected]>
  • Loading branch information
SUPERCILEX authored Aug 26, 2024
1 parent a01a716 commit 777fbc6
Show file tree
Hide file tree
Showing 5 changed files with 323 additions and 1 deletion.
46 changes: 46 additions & 0 deletions src/backend/libc/fs/inotify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,49 @@ bitflags! {
const _ = !0;
}
}

bitflags! {
/// `IN*` for use with [`InotifyReader`].
///
/// [`InotifyReader`]: crate::fs::inotify::InotifyReader
#[repr(transparent)]
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct ReadFlags: u32 {
/// `IN_ACCESS`
const ACCESS = c::IN_ACCESS;
/// `IN_ATTRIB`
const ATTRIB = c::IN_ATTRIB;
/// `IN_CLOSE_NOWRITE`
const CLOSE_NOWRITE = c::IN_CLOSE_NOWRITE;
/// `IN_CLOSE_WRITE`
const CLOSE_WRITE = c::IN_CLOSE_WRITE;
/// `IN_CREATE`
const CREATE = c::IN_CREATE;
/// `IN_DELETE`
const DELETE = c::IN_DELETE;
/// `IN_DELETE_SELF`
const DELETE_SELF = c::IN_DELETE_SELF;
/// `IN_MODIFY`
const MODIFY = c::IN_MODIFY;
/// `IN_MOVE_SELF`
const MOVE_SELF = c::IN_MOVE_SELF;
/// `IN_MOVED_FROM`
const MOVED_FROM = c::IN_MOVED_FROM;
/// `IN_MOVED_TO`
const MOVED_TO = c::IN_MOVED_TO;
/// `IN_OPEN`
const OPEN = c::IN_OPEN;

/// `IN_IGNORED`
const IGNORED = c::IN_IGNORED;
/// `IN_ISDIR`
const ISDIR = c::IN_ISDIR;
/// `IN_Q_OVERFLOW`
const QUEUE_OVERFLOW = c::IN_Q_OVERFLOW;
/// `IN_UNMOUNT`
const UNMOUNT = c::IN_UNMOUNT;

/// <https://docs.rs/bitflags/*/bitflags/#externally-defined-flags>
const _ = !0;
}
}
46 changes: 46 additions & 0 deletions src/backend/linux_raw/fs/inotify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,49 @@ bitflags! {
const _ = !0;
}
}

bitflags! {
/// `IN*` for use with [`InotifyReader`].
///
/// [`InotifyReader`]: crate::fs::inotify::InotifyReader
#[repr(transparent)]
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct ReadFlags: c::c_uint {
/// `IN_ACCESS`
const ACCESS = linux_raw_sys::general::IN_ACCESS;
/// `IN_ATTRIB`
const ATTRIB = linux_raw_sys::general::IN_ATTRIB;
/// `IN_CLOSE_NOWRITE`
const CLOSE_NOWRITE = linux_raw_sys::general::IN_CLOSE_NOWRITE;
/// `IN_CLOSE_WRITE`
const CLOSE_WRITE = linux_raw_sys::general::IN_CLOSE_WRITE;
/// `IN_CREATE`
const CREATE = linux_raw_sys::general::IN_CREATE;
/// `IN_DELETE`
const DELETE = linux_raw_sys::general::IN_DELETE;
/// `IN_DELETE_SELF`
const DELETE_SELF = linux_raw_sys::general::IN_DELETE_SELF;
/// `IN_MODIFY`
const MODIFY = linux_raw_sys::general::IN_MODIFY;
/// `IN_MOVE_SELF`
const MOVE_SELF = linux_raw_sys::general::IN_MOVE_SELF;
/// `IN_MOVED_FROM`
const MOVED_FROM = linux_raw_sys::general::IN_MOVED_FROM;
/// `IN_MOVED_TO`
const MOVED_TO = linux_raw_sys::general::IN_MOVED_TO;
/// `IN_OPEN`
const OPEN = linux_raw_sys::general::IN_OPEN;

/// `IN_IGNORED`
const IGNORED = linux_raw_sys::general::IN_IGNORED;
/// `IN_ISDIR`
const ISDIR = linux_raw_sys::general::IN_ISDIR;
/// `IN_Q_OVERFLOW`
const QUEUE_OVERFLOW = linux_raw_sys::general::IN_Q_OVERFLOW;
/// `IN_UNMOUNT`
const UNMOUNT = linux_raw_sys::general::IN_UNMOUNT;

/// <https://docs.rs/bitflags/*/bitflags/#externally-defined-flags>
const _ = !0;
}
}
121 changes: 120 additions & 1 deletion src/fs/inotify.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
//! inotify support for working with inotifies
pub use crate::backend::fs::inotify::{CreateFlags, WatchFlags};
pub use crate::backend::fs::inotify::{CreateFlags, ReadFlags, WatchFlags};
use crate::backend::fs::syscalls;
use crate::fd::{AsFd, OwnedFd};
use crate::ffi::CStr;
use crate::io;
use crate::io::{read_uninit, Errno};
use core::mem::{align_of, size_of, MaybeUninit};
use linux_raw_sys::general::inotify_event;

/// `inotify_init1(flags)`—Creates a new inotify object.
///
Expand Down Expand Up @@ -41,3 +45,118 @@ pub fn inotify_add_watch<P: crate::path::Arg>(
pub fn inotify_remove_watch(inot: impl AsFd, wd: i32) -> io::Result<()> {
syscalls::inotify_rm_watch(inot.as_fd(), wd)
}

/// An inotify event iterator implemented with the read syscall.
///
/// See the [`RawDir`] API for more details and usage examples as this API is
/// based on it.
///
/// [`RawDir`]: crate::fs::raw_dir::RawDir
pub struct InotifyReader<'buf, Fd: AsFd> {
fd: Fd,
buf: &'buf mut [MaybeUninit<u8>],
initialized: usize,
offset: usize,
}

impl<'buf, Fd: AsFd> InotifyReader<'buf, Fd> {
/// Create a new iterator from the given file descriptor and buffer.
pub fn new(fd: Fd, buf: &'buf mut [MaybeUninit<u8>]) -> Self {
Self {
fd,
buf: {
let offset = buf.as_ptr().align_offset(align_of::<inotify_event>());
if offset < buf.len() {
&mut buf[offset..]
} else {
&mut []
}
},
initialized: 0,
offset: 0,
}
}
}

/// An inotify event.
#[derive(Debug)]
pub struct InotifyEvent<'a> {
wd: i32,
events: ReadFlags,
cookie: u32,
file_name: Option<&'a CStr>,
}

impl<'a> InotifyEvent<'a> {
/// Returns the watch for which this event occurs.
#[inline]
pub fn wd(&self) -> i32 {
self.wd
}

/// Returns a description of the events.
#[inline]
#[doc(alias = "mask")]
pub fn events(&self) -> ReadFlags {
self.events
}

/// Returns the unique cookie associating related events.
#[inline]
pub fn cookie(&self) -> u32 {
self.cookie
}

/// Returns the file name of this event, if any.
#[inline]
pub fn file_name(&self) -> Option<&CStr> {
self.file_name
}
}

impl<'buf, Fd: AsFd> InotifyReader<'buf, Fd> {
/// Read the next inotify event.
#[allow(unsafe_code)]
pub fn next(&mut self) -> io::Result<InotifyEvent> {
if self.is_buffer_empty() {
match read_uninit(self.fd.as_fd(), self.buf).map(|(init, _)| init.len()) {
Ok(0) => return Err(Errno::INVAL),
Ok(bytes_read) => {
self.initialized = bytes_read;
self.offset = 0;
}
Err(e) => return Err(e),
}
}

let ptr = self.buf[self.offset..].as_ptr();
// SAFETY:
// - This data is initialized by the check above.
// - Assumption: the kernel will not give us partial structs.
// - Assumption: the kernel uses proper alignment between structs.
// - The starting pointer is aligned (performed in RawDir::new)
let event = unsafe { &*ptr.cast::<inotify_event>() };

self.offset += size_of::<inotify_event>() + usize::try_from(event.len).unwrap();

Ok(InotifyEvent {
wd: event.wd,
events: ReadFlags::from_bits_retain(event.mask),
cookie: event.cookie,
file_name: if event.len > 0 {
// SAFETY: The kernel guarantees a NUL-terminated string.
Some(unsafe { CStr::from_ptr(event.name.as_ptr().cast()) })
} else {
None
},
})
}

/// Returns true if the internal buffer is empty and will be refilled when
/// calling [`next`]. This is useful to avoid further blocking reads.
///
/// [`next`]: Self::next
pub fn is_buffer_empty(&self) -> bool {
self.offset >= self.initialized
}
}
109 changes: 109 additions & 0 deletions tests/fs/inotify.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use rustix::fs::inotify::{
inotify_add_watch, inotify_init, CreateFlags, InotifyReader, WatchFlags,
};
use rustix::io::Errno;
use std::fmt::Write;
use std::fs::{create_dir_all, remove_file, rename, File};
use std::mem::MaybeUninit;

#[test]
fn test_inotify_iter() {
let inotify = inotify_init(CreateFlags::NONBLOCK).unwrap();
create_dir_all("/tmp/.rustix-inotify-test").unwrap();
inotify_add_watch(
&inotify,
"/tmp/.rustix-inotify-test",
WatchFlags::ALL_EVENTS,
)
.unwrap();

File::create("/tmp/.rustix-inotify-test/foo").unwrap();
rename(
"/tmp/.rustix-inotify-test/foo",
"/tmp/.rustix-inotify-test/bar",
)
.unwrap();
remove_file("/tmp/.rustix-inotify-test/bar").unwrap();

let mut output = String::new();
let mut cookie = 0;

let mut buf = [MaybeUninit::uninit(); 512];
let mut iter = InotifyReader::new(inotify, &mut buf);
loop {
let e = match iter.next() {
Err(Errno::WOULDBLOCK) => break,
r => r.unwrap(),
};

writeln!(output, "{e:#?}").unwrap();
if e.cookie() != 0 {
cookie = e.cookie();
}
}

let expected = format!(
r#"InotifyEvent {{
wd: 1,
events: ReadFlags(
CREATE,
),
cookie: 0,
file_name: Some(
"foo",
),
}}
InotifyEvent {{
wd: 1,
events: ReadFlags(
OPEN,
),
cookie: 0,
file_name: Some(
"foo",
),
}}
InotifyEvent {{
wd: 1,
events: ReadFlags(
CLOSE_WRITE,
),
cookie: 0,
file_name: Some(
"foo",
),
}}
InotifyEvent {{
wd: 1,
events: ReadFlags(
MOVED_FROM,
),
cookie: {cookie},
file_name: Some(
"foo",
),
}}
InotifyEvent {{
wd: 1,
events: ReadFlags(
MOVED_TO,
),
cookie: {cookie},
file_name: Some(
"bar",
),
}}
InotifyEvent {{
wd: 1,
events: ReadFlags(
DELETE,
),
cookie: 0,
file_name: Some(
"bar",
),
}}
"#
);
assert_eq!(expected, output);
}
2 changes: 2 additions & 0 deletions tests/fs/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ mod file;
#[cfg(not(target_os = "wasi"))]
mod flock;
mod futimens;
#[cfg(linux_kernel)]
mod inotify;
mod invalid_offset;
#[cfg(not(target_os = "redox"))]
mod ioctl;
Expand Down

0 comments on commit 777fbc6

Please sign in to comment.