Skip to content

Commit 777fbc6

Browse files
authored
Implement InotifyReader for raw inotify event iteration (#1113)
* 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]>
1 parent a01a716 commit 777fbc6

File tree

5 files changed

+323
-1
lines changed

5 files changed

+323
-1
lines changed

src/backend/libc/fs/inotify.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,49 @@ bitflags! {
7676
const _ = !0;
7777
}
7878
}
79+
80+
bitflags! {
81+
/// `IN*` for use with [`InotifyReader`].
82+
///
83+
/// [`InotifyReader`]: crate::fs::inotify::InotifyReader
84+
#[repr(transparent)]
85+
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash, Debug)]
86+
pub struct ReadFlags: u32 {
87+
/// `IN_ACCESS`
88+
const ACCESS = c::IN_ACCESS;
89+
/// `IN_ATTRIB`
90+
const ATTRIB = c::IN_ATTRIB;
91+
/// `IN_CLOSE_NOWRITE`
92+
const CLOSE_NOWRITE = c::IN_CLOSE_NOWRITE;
93+
/// `IN_CLOSE_WRITE`
94+
const CLOSE_WRITE = c::IN_CLOSE_WRITE;
95+
/// `IN_CREATE`
96+
const CREATE = c::IN_CREATE;
97+
/// `IN_DELETE`
98+
const DELETE = c::IN_DELETE;
99+
/// `IN_DELETE_SELF`
100+
const DELETE_SELF = c::IN_DELETE_SELF;
101+
/// `IN_MODIFY`
102+
const MODIFY = c::IN_MODIFY;
103+
/// `IN_MOVE_SELF`
104+
const MOVE_SELF = c::IN_MOVE_SELF;
105+
/// `IN_MOVED_FROM`
106+
const MOVED_FROM = c::IN_MOVED_FROM;
107+
/// `IN_MOVED_TO`
108+
const MOVED_TO = c::IN_MOVED_TO;
109+
/// `IN_OPEN`
110+
const OPEN = c::IN_OPEN;
111+
112+
/// `IN_IGNORED`
113+
const IGNORED = c::IN_IGNORED;
114+
/// `IN_ISDIR`
115+
const ISDIR = c::IN_ISDIR;
116+
/// `IN_Q_OVERFLOW`
117+
const QUEUE_OVERFLOW = c::IN_Q_OVERFLOW;
118+
/// `IN_UNMOUNT`
119+
const UNMOUNT = c::IN_UNMOUNT;
120+
121+
/// <https://docs.rs/bitflags/*/bitflags/#externally-defined-flags>
122+
const _ = !0;
123+
}
124+
}

src/backend/linux_raw/fs/inotify.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,49 @@ bitflags! {
7676
const _ = !0;
7777
}
7878
}
79+
80+
bitflags! {
81+
/// `IN*` for use with [`InotifyReader`].
82+
///
83+
/// [`InotifyReader`]: crate::fs::inotify::InotifyReader
84+
#[repr(transparent)]
85+
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash, Debug)]
86+
pub struct ReadFlags: c::c_uint {
87+
/// `IN_ACCESS`
88+
const ACCESS = linux_raw_sys::general::IN_ACCESS;
89+
/// `IN_ATTRIB`
90+
const ATTRIB = linux_raw_sys::general::IN_ATTRIB;
91+
/// `IN_CLOSE_NOWRITE`
92+
const CLOSE_NOWRITE = linux_raw_sys::general::IN_CLOSE_NOWRITE;
93+
/// `IN_CLOSE_WRITE`
94+
const CLOSE_WRITE = linux_raw_sys::general::IN_CLOSE_WRITE;
95+
/// `IN_CREATE`
96+
const CREATE = linux_raw_sys::general::IN_CREATE;
97+
/// `IN_DELETE`
98+
const DELETE = linux_raw_sys::general::IN_DELETE;
99+
/// `IN_DELETE_SELF`
100+
const DELETE_SELF = linux_raw_sys::general::IN_DELETE_SELF;
101+
/// `IN_MODIFY`
102+
const MODIFY = linux_raw_sys::general::IN_MODIFY;
103+
/// `IN_MOVE_SELF`
104+
const MOVE_SELF = linux_raw_sys::general::IN_MOVE_SELF;
105+
/// `IN_MOVED_FROM`
106+
const MOVED_FROM = linux_raw_sys::general::IN_MOVED_FROM;
107+
/// `IN_MOVED_TO`
108+
const MOVED_TO = linux_raw_sys::general::IN_MOVED_TO;
109+
/// `IN_OPEN`
110+
const OPEN = linux_raw_sys::general::IN_OPEN;
111+
112+
/// `IN_IGNORED`
113+
const IGNORED = linux_raw_sys::general::IN_IGNORED;
114+
/// `IN_ISDIR`
115+
const ISDIR = linux_raw_sys::general::IN_ISDIR;
116+
/// `IN_Q_OVERFLOW`
117+
const QUEUE_OVERFLOW = linux_raw_sys::general::IN_Q_OVERFLOW;
118+
/// `IN_UNMOUNT`
119+
const UNMOUNT = linux_raw_sys::general::IN_UNMOUNT;
120+
121+
/// <https://docs.rs/bitflags/*/bitflags/#externally-defined-flags>
122+
const _ = !0;
123+
}
124+
}

src/fs/inotify.rs

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
//! inotify support for working with inotifies
22
3-
pub use crate::backend::fs::inotify::{CreateFlags, WatchFlags};
3+
pub use crate::backend::fs::inotify::{CreateFlags, ReadFlags, WatchFlags};
44
use crate::backend::fs::syscalls;
55
use crate::fd::{AsFd, OwnedFd};
6+
use crate::ffi::CStr;
67
use crate::io;
8+
use crate::io::{read_uninit, Errno};
9+
use core::mem::{align_of, size_of, MaybeUninit};
10+
use linux_raw_sys::general::inotify_event;
711

812
/// `inotify_init1(flags)`—Creates a new inotify object.
913
///
@@ -41,3 +45,118 @@ pub fn inotify_add_watch<P: crate::path::Arg>(
4145
pub fn inotify_remove_watch(inot: impl AsFd, wd: i32) -> io::Result<()> {
4246
syscalls::inotify_rm_watch(inot.as_fd(), wd)
4347
}
48+
49+
/// An inotify event iterator implemented with the read syscall.
50+
///
51+
/// See the [`RawDir`] API for more details and usage examples as this API is
52+
/// based on it.
53+
///
54+
/// [`RawDir`]: crate::fs::raw_dir::RawDir
55+
pub struct InotifyReader<'buf, Fd: AsFd> {
56+
fd: Fd,
57+
buf: &'buf mut [MaybeUninit<u8>],
58+
initialized: usize,
59+
offset: usize,
60+
}
61+
62+
impl<'buf, Fd: AsFd> InotifyReader<'buf, Fd> {
63+
/// Create a new iterator from the given file descriptor and buffer.
64+
pub fn new(fd: Fd, buf: &'buf mut [MaybeUninit<u8>]) -> Self {
65+
Self {
66+
fd,
67+
buf: {
68+
let offset = buf.as_ptr().align_offset(align_of::<inotify_event>());
69+
if offset < buf.len() {
70+
&mut buf[offset..]
71+
} else {
72+
&mut []
73+
}
74+
},
75+
initialized: 0,
76+
offset: 0,
77+
}
78+
}
79+
}
80+
81+
/// An inotify event.
82+
#[derive(Debug)]
83+
pub struct InotifyEvent<'a> {
84+
wd: i32,
85+
events: ReadFlags,
86+
cookie: u32,
87+
file_name: Option<&'a CStr>,
88+
}
89+
90+
impl<'a> InotifyEvent<'a> {
91+
/// Returns the watch for which this event occurs.
92+
#[inline]
93+
pub fn wd(&self) -> i32 {
94+
self.wd
95+
}
96+
97+
/// Returns a description of the events.
98+
#[inline]
99+
#[doc(alias = "mask")]
100+
pub fn events(&self) -> ReadFlags {
101+
self.events
102+
}
103+
104+
/// Returns the unique cookie associating related events.
105+
#[inline]
106+
pub fn cookie(&self) -> u32 {
107+
self.cookie
108+
}
109+
110+
/// Returns the file name of this event, if any.
111+
#[inline]
112+
pub fn file_name(&self) -> Option<&CStr> {
113+
self.file_name
114+
}
115+
}
116+
117+
impl<'buf, Fd: AsFd> InotifyReader<'buf, Fd> {
118+
/// Read the next inotify event.
119+
#[allow(unsafe_code)]
120+
pub fn next(&mut self) -> io::Result<InotifyEvent> {
121+
if self.is_buffer_empty() {
122+
match read_uninit(self.fd.as_fd(), self.buf).map(|(init, _)| init.len()) {
123+
Ok(0) => return Err(Errno::INVAL),
124+
Ok(bytes_read) => {
125+
self.initialized = bytes_read;
126+
self.offset = 0;
127+
}
128+
Err(e) => return Err(e),
129+
}
130+
}
131+
132+
let ptr = self.buf[self.offset..].as_ptr();
133+
// SAFETY:
134+
// - This data is initialized by the check above.
135+
// - Assumption: the kernel will not give us partial structs.
136+
// - Assumption: the kernel uses proper alignment between structs.
137+
// - The starting pointer is aligned (performed in RawDir::new)
138+
let event = unsafe { &*ptr.cast::<inotify_event>() };
139+
140+
self.offset += size_of::<inotify_event>() + usize::try_from(event.len).unwrap();
141+
142+
Ok(InotifyEvent {
143+
wd: event.wd,
144+
events: ReadFlags::from_bits_retain(event.mask),
145+
cookie: event.cookie,
146+
file_name: if event.len > 0 {
147+
// SAFETY: The kernel guarantees a NUL-terminated string.
148+
Some(unsafe { CStr::from_ptr(event.name.as_ptr().cast()) })
149+
} else {
150+
None
151+
},
152+
})
153+
}
154+
155+
/// Returns true if the internal buffer is empty and will be refilled when
156+
/// calling [`next`]. This is useful to avoid further blocking reads.
157+
///
158+
/// [`next`]: Self::next
159+
pub fn is_buffer_empty(&self) -> bool {
160+
self.offset >= self.initialized
161+
}
162+
}

tests/fs/inotify.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use rustix::fs::inotify::{
2+
inotify_add_watch, inotify_init, CreateFlags, InotifyReader, WatchFlags,
3+
};
4+
use rustix::io::Errno;
5+
use std::fmt::Write;
6+
use std::fs::{create_dir_all, remove_file, rename, File};
7+
use std::mem::MaybeUninit;
8+
9+
#[test]
10+
fn test_inotify_iter() {
11+
let inotify = inotify_init(CreateFlags::NONBLOCK).unwrap();
12+
create_dir_all("/tmp/.rustix-inotify-test").unwrap();
13+
inotify_add_watch(
14+
&inotify,
15+
"/tmp/.rustix-inotify-test",
16+
WatchFlags::ALL_EVENTS,
17+
)
18+
.unwrap();
19+
20+
File::create("/tmp/.rustix-inotify-test/foo").unwrap();
21+
rename(
22+
"/tmp/.rustix-inotify-test/foo",
23+
"/tmp/.rustix-inotify-test/bar",
24+
)
25+
.unwrap();
26+
remove_file("/tmp/.rustix-inotify-test/bar").unwrap();
27+
28+
let mut output = String::new();
29+
let mut cookie = 0;
30+
31+
let mut buf = [MaybeUninit::uninit(); 512];
32+
let mut iter = InotifyReader::new(inotify, &mut buf);
33+
loop {
34+
let e = match iter.next() {
35+
Err(Errno::WOULDBLOCK) => break,
36+
r => r.unwrap(),
37+
};
38+
39+
writeln!(output, "{e:#?}").unwrap();
40+
if e.cookie() != 0 {
41+
cookie = e.cookie();
42+
}
43+
}
44+
45+
let expected = format!(
46+
r#"InotifyEvent {{
47+
wd: 1,
48+
events: ReadFlags(
49+
CREATE,
50+
),
51+
cookie: 0,
52+
file_name: Some(
53+
"foo",
54+
),
55+
}}
56+
InotifyEvent {{
57+
wd: 1,
58+
events: ReadFlags(
59+
OPEN,
60+
),
61+
cookie: 0,
62+
file_name: Some(
63+
"foo",
64+
),
65+
}}
66+
InotifyEvent {{
67+
wd: 1,
68+
events: ReadFlags(
69+
CLOSE_WRITE,
70+
),
71+
cookie: 0,
72+
file_name: Some(
73+
"foo",
74+
),
75+
}}
76+
InotifyEvent {{
77+
wd: 1,
78+
events: ReadFlags(
79+
MOVED_FROM,
80+
),
81+
cookie: {cookie},
82+
file_name: Some(
83+
"foo",
84+
),
85+
}}
86+
InotifyEvent {{
87+
wd: 1,
88+
events: ReadFlags(
89+
MOVED_TO,
90+
),
91+
cookie: {cookie},
92+
file_name: Some(
93+
"bar",
94+
),
95+
}}
96+
InotifyEvent {{
97+
wd: 1,
98+
events: ReadFlags(
99+
DELETE,
100+
),
101+
cookie: 0,
102+
file_name: Some(
103+
"bar",
104+
),
105+
}}
106+
"#
107+
);
108+
assert_eq!(expected, output);
109+
}

tests/fs/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ mod file;
2020
#[cfg(not(target_os = "wasi"))]
2121
mod flock;
2222
mod futimens;
23+
#[cfg(linux_kernel)]
24+
mod inotify;
2325
mod invalid_offset;
2426
#[cfg(not(target_os = "redox"))]
2527
mod ioctl;

0 commit comments

Comments
 (0)