diff --git a/Cargo.toml b/Cargo.toml index 6bd849e..3474455 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "fanotify-rs" -version = "0.3.2" +version = "0.4.0" authors = ["zhanglei ", "n01e0 ", "Philip Woolford "] -edition = "2021" +edition = "2024" license = "MIT OR Apache-2.0" description = "The high-level/low-level implementation of Linux Fanotify." repository = "https://github.com/ZhangLei-cn/fanotify-rs" @@ -13,6 +13,11 @@ name = "fanotify" path = "src/lib.rs" doc = true +[features] +default = ["high-level"] +high-level = ["bitflags"] + [dependencies] libc = "0.2" -enum-iterator = "1.5" \ No newline at end of file +enum-iterator = "2.3" +bitflags = {version = "2.10", optional = true} diff --git a/demo/with_poll/Cargo.toml b/demo/with_poll/Cargo.toml index 63b43cf..57bde79 100644 --- a/demo/with_poll/Cargo.toml +++ b/demo/with_poll/Cargo.toml @@ -10,4 +10,4 @@ description = "fanotify-rs with poll" [dependencies] clap = "4.4.18" fanotify-rs = { path = "../../" } -nix = {version = "0.27.1", features = ["poll"] } +nix = {version = "0.30.1", features = ["poll"] } diff --git a/demo/with_poll/src/main.rs b/demo/with_poll/src/main.rs index e131fdb..eb2b5ba 100644 --- a/demo/with_poll/src/main.rs +++ b/demo/with_poll/src/main.rs @@ -2,25 +2,24 @@ use fanotify::high_level::*; use nix::poll::{poll, PollFd, PollFlags}; use std::os::fd::AsFd; -fn main() { +fn main() -> Result<(), Box> { let app = clap::Command::new("with_poll") .arg(clap::Arg::new("path").index(1).required(true)) .get_matches(); - let fd = Fanotify::new_with_nonblocking(FanotifyMode::CONTENT); + let fd = Fanotify::new_nonblocking(FanotifyMode::CONTENT)?; fd.add_mountpoint( - FAN_OPEN_EXEC | FAN_CLOSE_WRITE, + MarkMode::OpenExec | MarkMode::CloseWrite, app.get_one::("path") .expect("We can unwrap here as clap enforces the existence of `path`"), - ) - .unwrap(); + )?; let fd_handle = fd.as_fd(); - let mut fds = [PollFd::new(&fd_handle, PollFlags::POLLIN)]; + let mut fds = [PollFd::new(fd_handle, PollFlags::POLLIN)]; loop { - let poll_num = poll(&mut fds, -1).unwrap(); + let poll_num = poll(&mut fds, None::).unwrap(); if poll_num > 0 { - for event in fd.read_event() { + for event in fd.read_event()? { println!("{:#?}", event); fd.send_response(event.fd, FanotifyResponse::Allow); } @@ -29,4 +28,6 @@ fn main() { break; } } + + Ok(()) } diff --git a/src/high_level.rs b/src/high_level.rs index 1fe92a6..ee9895e 100644 --- a/src/high_level.rs +++ b/src/high_level.rs @@ -1,21 +1,11 @@ -use crate::low_level::{ - close_fd, fanotify_init, fanotify_mark, fanotify_read, FanotifyEventMetadata, AT_FDCWD, - FAN_ALLOW, FAN_CLASS_CONTENT, FAN_CLASS_NOTIF, FAN_CLASS_PRE_CONTENT, FAN_CLOEXEC, FAN_DENY, - FAN_MARK_ADD, FAN_MARK_FLUSH, FAN_MARK_MOUNT, FAN_MARK_REMOVE, FAN_NONBLOCK, O_CLOEXEC, - O_RDONLY, -}; use crate::FanotifyPath; -use enum_iterator::{all, Sequence}; +use bitflags::bitflags; +use enum_iterator::{Sequence, all}; use std::fs::read_link; use std::io::Error; use std::os::fd::{AsFd, BorrowedFd}; -pub use crate::low_level::{ - FAN_ACCESS, FAN_ACCESS_PERM, FAN_ATTRIB, FAN_CLOSE, FAN_CLOSE_NOWRITE, FAN_CLOSE_WRITE, - FAN_CREATE, FAN_DELETE, FAN_DELETE_SELF, FAN_EVENT_ON_CHILD, FAN_MODIFY, FAN_MOVE, - FAN_MOVED_FROM, FAN_MOVED_TO, FAN_MOVE_SELF, FAN_ONDIR, FAN_OPEN, FAN_OPEN_EXEC, - FAN_OPEN_EXEC_PERM, FAN_OPEN_PERM, -}; +use crate::low_level::*; pub struct Fanotify { fd: i32, @@ -41,6 +31,61 @@ where } } +bitflags! { + pub struct MarkMode: u64 { + const Access = FAN_ACCESS; + const Modify = FAN_MODIFY; + const Attributes = FAN_ATTRIB; + const CloseWrite = FAN_CLOSE_WRITE; + const CloseNoWrite = FAN_CLOSE_NOWRITE; + const Open = FAN_OPEN; + const MovedFrom = FAN_MOVED_FROM; + const MovedTo = FAN_MOVED_TO; + const Moved = FAN_MOVE; + const Create = FAN_CREATE; + const Delete = FAN_DELETE; + const DeleteSelf = FAN_DELETE_SELF; + const MoveSelf = FAN_MOVE_SELF; + const OpenExec = FAN_OPEN_EXEC; + const OpenPerm = FAN_OPEN_PERM; + const OpenExecPerm = FAN_OPEN_EXEC_PERM; + const OnDirectory = FAN_ONDIR; + const EventOnChild = FAN_EVENT_ON_CHILD; + const Close = FAN_CLOSE; + } +} + +bitflags! { + pub struct Flags: u32 { + const CloseOnExec = FAN_CLOEXEC; + const NonBlocking = FAN_NONBLOCK; + const NotificationClass = FAN_CLASS_NOTIF; + const ContentClass = FAN_CLASS_CONTENT; + const PreContentClass = FAN_CLASS_PRE_CONTENT; + const UnlimitedEventQueue = FAN_UNLIMITED_QUEUE; + const UnlimitedMarks = FAN_UNLIMITED_MARKS; + const Audit = FAN_ENABLE_AUDIT; + const ReportThreadID = FAN_REPORT_TID; + const ReportDirectoryID = FAN_REPORT_DIR_FID; + const ReportName = FAN_REPORT_NAME; + } +} + +bitflags! { + pub struct EventFlags: u32 { + const CloseOnExec = FAN_CLOEXEC; + const ReadOnly = O_RDONLY.cast_unsigned(); + const ReadWrite = O_RDWR.cast_unsigned(); + const WriteOnly = O_WRONLY.cast_unsigned(); + const LargeFile = O_LARGEFILE.cast_unsigned(); + const Append = O_APPEND.cast_unsigned(); + const MetadataSync = O_DSYNC.cast_unsigned(); + const NoAccessTime = O_NOATIME.cast_unsigned(); + const NonBlock = O_NONBLOCK.cast_unsigned(); + const Sync = O_SYNC.cast_unsigned(); + } +} + #[derive(Debug, Clone, Copy, Sequence, PartialEq)] pub enum FanEvent { Access = FAN_ACCESS as isize, @@ -119,6 +164,7 @@ pub struct Event { pub path: String, pub events: Vec, pub pid: i32, + pub additional_records: Vec, } impl Event { @@ -134,6 +180,7 @@ impl Event { path: self.path.clone(), events: self.events.clone(), pid: self.pid, + additional_records: self.additional_records.clone(), }) } } @@ -152,6 +199,7 @@ impl From for Event { path: path.to_str().unwrap().to_string(), events: events_from_mask(metadata.mask), pid: metadata.pid, + additional_records: vec![], } } } @@ -193,34 +241,67 @@ impl Fanotify { }) } - pub fn add_path(&self, mode: u64, path: &P) -> Result<(), Error> { - fanotify_mark(self.fd, FAN_MARK_ADD, mode, AT_FDCWD, path)?; + pub fn add_path( + &self, + mode: MarkMode, + path: &P, + ) -> Result<(), Error> { + fanotify_mark(self.fd, FAN_MARK_ADD, mode.bits(), AT_FDCWD, path)?; Ok(()) } pub fn add_mountpoint( &self, - mode: u64, + mode: MarkMode, path: &P, ) -> Result<(), Error> { - fanotify_mark(self.fd, FAN_MARK_ADD | FAN_MARK_MOUNT, mode, AT_FDCWD, path)?; + fanotify_mark( + self.fd, + FAN_MARK_ADD | FAN_MARK_MOUNT, + mode.bits(), + AT_FDCWD, + path, + )?; Ok(()) } - pub fn remove_path(&self, mode: u64, path: &P) -> Result<(), Error> { - fanotify_mark(self.fd, FAN_MARK_REMOVE, mode, AT_FDCWD, path)?; + pub fn add_filesystem( + &self, + mode: MarkMode, + path: &P, + ) -> Result<(), Error> { + fanotify_mark( + self.fd, + FAN_MARK_ADD | FAN_MARK_FILESYSTEM, + mode.bits(), + AT_FDCWD, + path, + )?; Ok(()) } - pub fn flush_path(&self, mode: u64, path: &P) -> Result<(), Error> { - fanotify_mark(self.fd, FAN_MARK_FLUSH, mode, AT_FDCWD, path)?; + pub fn remove_path( + &self, + mode: MarkMode, + path: &P, + ) -> Result<(), Error> { + fanotify_mark(self.fd, FAN_MARK_REMOVE, mode.bits(), AT_FDCWD, path)?; Ok(()) } - pub fn read_event(&self) -> Vec { + pub fn flush_path( + &self, + mode: MarkMode, + path: &P, + ) -> Result<(), Error> { + fanotify_mark(self.fd, FAN_MARK_FLUSH, mode.bits(), AT_FDCWD, path)?; + Ok(()) + } + + pub fn read_event(&self) -> std::io::Result> { let mut result = Vec::new(); - let events = fanotify_read(self.fd); - for metadata in events { + let events = fanotify_read(self.fd)?; + for (metadata, additional_records) in events { let path = read_link(format!("/proc/self/fd/{}", metadata.fd)).unwrap_or_default(); let path = path.to_str().unwrap(); result.push(Event { @@ -228,9 +309,10 @@ impl Fanotify { path: String::from(path), events: events_from_mask(metadata.mask), pid: metadata.pid, + additional_records, }); } - result + Ok(result) } pub fn send_response>(&self, fd: T, resp: FanotifyResponse) { @@ -292,16 +374,16 @@ impl FanotifyBuilder { Self { class, ..self } } - pub fn with_flags(self, flags: u32) -> Self { + pub fn with_flags(self, flags: Flags) -> Self { Self { - flags: FAN_CLOEXEC | flags, + flags: FAN_CLOEXEC | flags.bits(), ..self } } - pub fn with_event_flags(self, event_flags: u32) -> Self { + pub fn with_event_flags(self, event_flags: EventFlags) -> Self { Self { - event_flags, + event_flags: event_flags.bits(), ..self } } @@ -312,3 +394,9 @@ impl FanotifyBuilder { }) } } + +impl Default for FanotifyBuilder { + fn default() -> Self { + Self::new() + } +} diff --git a/src/lib.rs b/src/lib.rs index 821cfb2..305b173 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "high-level")] pub mod high_level; pub mod low_level; @@ -21,4 +22,4 @@ impl> FanotifyPath for T { fn as_os_str(&self) -> &std::ffi::OsStr { self.as_ref() } -} \ No newline at end of file +} diff --git a/src/low_level.rs b/src/low_level.rs index 52959c2..2197f30 100644 --- a/src/low_level.rs +++ b/src/low_level.rs @@ -1,13 +1,16 @@ use crate::FanotifyPath; -use libc::{__s32, __u16, __u32, __u64, __u8}; -use std::io::Error; +use libc::{__s32, __u8, __u16, __u32, __u64}; +use std::ffi::{CStr, CString}; +use std::io::{Error, ErrorKind}; use std::mem; use std::os::unix::ffi::OsStrExt; -use std::slice; #[doc(hidden)] /// Re-export relevant libc constants -pub use libc::{O_RDONLY, O_WRONLY, O_RDWR, O_LARGEFILE, O_CLOEXEC, O_APPEND, O_DSYNC, O_NOATIME, O_NONBLOCK, O_SYNC}; +pub use libc::{ + O_APPEND, O_CLOEXEC, O_DSYNC, O_LARGEFILE, O_NOATIME, O_NONBLOCK, O_RDONLY, O_RDWR, O_SYNC, + O_WRONLY, +}; #[derive(Debug, Clone, Copy)] #[repr(C)] @@ -47,6 +50,255 @@ pub struct FanotifyEventMetadata { pub pid: __s32, } +impl TryFrom<&[u8]> for FanotifyEventMetadata { + type Error = std::io::Error; + fn try_from(slice: &[u8]) -> Result { + if slice.len() < size_of::() { + return Err(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + "The buffer is too small to contain a FanotifyEventMetadata", + )); + } + + // SAFETY: we know that all bit paterns of `FanotifyEventMetadata` are the valid, + // and there is enough space in the buffer to contain the `FanotifyEventMetadata` + // so a memory cast and read is safe. + Ok(unsafe { slice.as_ptr().cast::().read_unaligned() }) + } +} + +/// The different types of fanotify information records that can be returned with an event. +#[derive(Debug, Clone, Copy)] +#[repr(u8)] +pub enum FanoityEnventInfoType { + FID = 1, + DFIDName = 2, + DFID = 3, + PIDFD = 4, + Error = 5, +} + +/// The header indentifying what type of information event was generated by the fanotify subsystem, and the size of the struct (for the `fid` DST). +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct FanotifyEventInfoHeader { + /// The type of information event that was generated by the fanotify subsystem. + info_type: FanoityEnventInfoType, + /// Padding to align the u16 length field. + _pad: __u8, + /// The size of the additional information record, including this header. + len: __u16, +} + +/// Container class to encapsulate the different types of fanotify information records that can be returned with an event +#[derive(Debug, Clone)] +#[repr(C)] +pub enum FanAdditionalRecords { + FanotifyEventInfoFID(FanotifyEventInfoFID), + FanotifyEventInfoDFIDName { + fid: FanotifyEventInfoFID, + name: CString, + }, + FanotifyEventInfoPIDFD(FanotifyEventInfoPIDFD), + FanotifyEventInfoError(FanotifyEventInfoError), +} + +impl FanAdditionalRecords { + pub fn size(&self) -> usize { + match self { + Self::FanotifyEventInfoFID(e) => e.header.len as usize, + Self::FanotifyEventInfoDFIDName { fid, name } => { + fid.header.len as usize + name.as_bytes_with_nul().len() + } + Self::FanotifyEventInfoPIDFD(e) => e.header.len as usize, + Self::FanotifyEventInfoError(e) => e.header.len as usize, + } + } + + pub fn from_slice(slice: &[u8]) -> std::io::Result<(Self, usize)> { + if slice.len() < size_of::() { + return Err(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + "The buffer is too small to contain an event info header record", + )); + }; + // SAFETY: We definitely have enough bytes so we can safely read. + match unsafe { *(slice.as_ptr().cast()) } { + FanoityEnventInfoType::FID | FanoityEnventInfoType::DFID => { + let (fid, size) = FanotifyEventInfoFID::from_slice(slice)?; + Ok((Self::FanotifyEventInfoFID(fid), size)) + } + FanoityEnventInfoType::DFIDName => { + let (fid, size) = FanotifyEventInfoFID::from_slice(slice)?; + if slice.len() < size { + return Err(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + "The buffer is too small to contain an the expected DFIDName record", + )); + } + // SAFETY: We definitely have enough bytes so we can safely read. + let slice = &slice[size..]; + let name: CString = unsafe { CStr::from_ptr(slice.as_ptr() as _) }.to_owned(); + let consumed_bytes = size + name.as_bytes_with_nul().len(); + Ok(( + Self::FanotifyEventInfoDFIDName { fid, name }, + consumed_bytes, + )) + } + FanoityEnventInfoType::PIDFD => Ok(( + Self::FanotifyEventInfoPIDFD(unsafe { + slice + .as_ptr() + .cast::() + .read_unaligned() + }), + size_of::(), + )), + FanoityEnventInfoType::Error => Ok(( + Self::FanotifyEventInfoError(unsafe { + slice + .as_ptr() + .cast::() + .read_unaligned() + }), + size_of::(), + )), + } + } +} + +/// Reporting event for File System Errors when `FAN_REPORT_FSERROR` is reported in the event mask +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct FanotifyEventInfoError { + pub header: FanotifyEventInfoHeader, + pub error_code: __s32, + pub error_count: __u32, +} + +/// Additional information record containing a file descriptor for the target process. +#[derive(Debug)] +#[repr(C)] +pub struct FanotifyEventInfoPIDFD { + pub header: FanotifyEventInfoHeader, + pub pidfd: __s32, +} + +impl Clone for FanotifyEventInfoPIDFD { + fn clone(&self) -> Self { + unsafe { + Self { + header: self.header, + pidfd: libc::dup(self.pidfd), + } + } + } +} + +impl Drop for FanotifyEventInfoPIDFD { + fn drop(&mut self) { + unsafe { + libc::close(self.pidfd); + } + } +} + +#[derive(Debug, Clone)] +#[repr(C)] +struct OpaqueFileHandle { + handle_bytes: u32, + handle_type: i32, + file_handle: Vec, +} + +impl OpaqueFileHandle { + fn from_slice(mut slice: &[u8]) -> Result<(Self, usize), Error> { + if slice.len() < 8 { + return Err(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + "The buffer is too small to contain a file handle", + )); + } + + let handle_bytes = u32::from_ne_bytes( + slice[..4] + .try_into() + .expect("We already checked the length, so this should never fail."), + ); + let min_size = handle_bytes as usize + size_of::() + size_of::(); + if slice.len() < min_size { + return Err(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + format!( + "The buffer is too small to contain a file handle with the declared length {}", + handle_bytes + ), + )); + } + slice = &slice[4..]; + + let handle_type = i32::from_ne_bytes( + slice[..4] + .try_into() + .expect("We already checked the length, so this should never fail."), + ); + slice = &slice[4..]; + let file_handle = Vec::from(&slice[..handle_bytes as usize]); + + Ok(( + Self { + handle_bytes, + handle_type, + file_handle, + }, + handle_bytes as usize + size_of::() + size_of::(), + )) + } +} + +pub type KernelFSID = [libc::c_int; 2]; + +#[derive(Debug, Clone)] +#[repr(C)] +pub struct FanotifyEventInfoFID { + pub header: FanotifyEventInfoHeader, + pub fsid: KernelFSID, + handle: OpaqueFileHandle, +} + +impl FanotifyEventInfoFID { + fn from_slice(slice: &[u8]) -> Result<(Self, usize), Error> { + if slice.len() < size_of::() + size_of::() { + return Err(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + "The buffer is too small to contain a FID record", + )); + } + + let ptr = slice.as_ptr().cast::(); + // SAFETY: We have already checked that there are enough bytes in the buffer + let (header, fsid) = unsafe { ((*ptr).header, (*ptr).fsid) }; + + if slice.len() < header.len as usize { + return Err(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + "The buffer is too small to contain the full FID record", + )); + } + + let (handle, handle_size) = OpaqueFileHandle::from_slice(&slice[12..])?; + + Ok(( + Self { + header, + fsid, + handle, + }, + size_of::() + size_of::() + handle_size, + )) + } +} + #[derive(Debug)] #[repr(C)] /// It is used to control file access. @@ -265,14 +517,10 @@ pub const AT_EMPTY_PATH: i32 = 0x1000; pub fn fanotify_init(flags: u32, event_f_flags: u32) -> Result { unsafe { match libc::fanotify_init(flags, event_f_flags) { - -1 => { - Err(Error::last_os_error()) - } - fd => { - Ok(fd) - } + -1 => Err(Error::last_os_error()), + fd => Ok(fd), } -} + } } /// Adds, removes, or modifies an fanotify mark on a filesystem object. // The caller must have read permission on the filesystem object that is to be marked. @@ -344,43 +592,60 @@ pub fn fanotify_mark( raw_path.push(0u8); // data must be null terminated //make sure path is null terminated - match libc::fanotify_mark( - fanotify_fd, - flags, - mask, - dirfd, - raw_path.as_ptr().cast(), - ) { - 0 => { - Ok(()) - } - _ => { - Err(Error::last_os_error()) - } + match libc::fanotify_mark(fanotify_fd, flags, mask, dirfd, raw_path.as_ptr().cast()) { + 0 => Ok(()), + _ => Err(Error::last_os_error()), } } } -pub fn fanotify_read(fanotify_fd: i32) -> Vec { - let mut vec = Vec::new(); - let mut buffer = Box::new([0u8;FAN_EVENT_METADATA_LEN * 200]); - unsafe { - // Allocate a buffer to store up to 200 events - - let sizeof = libc::read(fanotify_fd, buffer.as_mut_ptr() as _, FAN_EVENT_METADATA_LEN * 200); - if sizeof != libc::EAGAIN as isize && sizeof > 0 { - let src = slice::from_raw_parts( - buffer.as_ptr().cast::(), - sizeof as usize / FAN_EVENT_METADATA_LEN, - ); - vec.extend_from_slice(src); +pub fn fanotify_read( + fanotify_fd: i32, +) -> std::io::Result)>> { + let mut events = Vec::new(); + let mut read_buffer = Box::new([0u8; FAN_EVENT_METADATA_LEN * 200]); + let read_len = unsafe { + libc::read( + fanotify_fd, + read_buffer.as_mut_ptr() as _, + FAN_EVENT_METADATA_LEN * 200, + ) + }; + + if read_len == -1 + && let errno = std::io::Error::last_os_error() + && !matches!(errno.kind(), ErrorKind::WouldBlock) + { + return Err(errno); + } + + let mut cursor = &read_buffer[..read_len as usize]; + while let Ok(event_metadata) = FanotifyEventMetadata::try_from(cursor) { + let mut extra_records = Vec::new(); + if event_metadata.event_len as usize > FAN_EVENT_METADATA_LEN { + // we have optional events, allocate a new vector to hold the additional info + let mut inner_cursor = + &cursor[FAN_EVENT_METADATA_LEN..event_metadata.event_len as usize]; + while inner_cursor.len() >= size_of::() { + // TODO: handle the path CStr sent after the info record for a `FAN_EVENT_INFO_TYPE_DFID_NAME` + if let Ok((additional_info, slice_consumed)) = + FanAdditionalRecords::from_slice(inner_cursor) + { + inner_cursor = &inner_cursor[slice_consumed..]; + extra_records.push(additional_info); + } else { + break; // this only happens when there wansn't enough space to read the events that should have been there, so we just stop here. + } + } } + events.push((event_metadata, extra_records)); + cursor = &cursor[event_metadata.event_len as usize..]; } - vec + Ok(events) } pub fn close_fd(fd: i32) { unsafe { libc::close(fd); } -} \ No newline at end of file +} diff --git a/tests/high_level.rs b/tests/high_level.rs index e1febe5..274e170 100644 --- a/tests/high_level.rs +++ b/tests/high_level.rs @@ -1,16 +1,20 @@ #[test] fn high_level_test() { - use fanotify::high_level::{ - Fanotify, FanotifyMode, FAN_ACCESS, FAN_CLOSE, FAN_EVENT_ON_CHILD, FAN_MODIFY, FAN_ONDIR, - FAN_OPEN, - }; + use fanotify::high_level::MarkMode; + use fanotify::high_level::{Fanotify, FanotifyMode}; use std::io::{Read, Write}; - let ft = Fanotify::new_blocking(FanotifyMode::NOTIF).expect("Error regitering fanotify listener"); + let ft = + Fanotify::new_blocking(FanotifyMode::NOTIF).expect("Error registering fanotify listener"); ft.add_path( - FAN_ACCESS | FAN_CLOSE | FAN_EVENT_ON_CHILD | FAN_MODIFY | FAN_ONDIR | FAN_OPEN, + MarkMode::Access + | MarkMode::Close + | MarkMode::EventOnChild + | MarkMode::Modify + | MarkMode::OnDirectory + | MarkMode::Open, "/tmp", ) - .unwrap(); + .unwrap(); let handler = std::thread::spawn(|| { let mut tmp = std::fs::File::create("/tmp/fanotify_test").unwrap(); tmp.write_all(b"xxx").unwrap(); @@ -20,4 +24,4 @@ fn high_level_test() { assert_eq!(res, "xxx".to_string()); }); handler.join().unwrap(); -} \ No newline at end of file +}