Skip to content

Commit

Permalink
Merge #1016
Browse files Browse the repository at this point in the history
1016: Added inotify bindings. r=asomers a=vdagonneau

Hi !

I needed inotify bindings and noticed that nix did not have any so here is a PR to add it.

There was another PR from 2015 to add support for inotify that was never merged. I took some of the feedback and applied it here.

Co-authored-by: Vincent Dagonneau <vincentdagonneau@gmail.com>
  • Loading branch information
bors[bot] and vdagonneau committed Mar 6, 2019
2 parents 6e94c3c + 3966d0b commit 2d0d360
Show file tree
Hide file tree
Showing 5 changed files with 305 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Added
- Add IP_RECVIF & IP_RECVDSTADDR. Enable IP_PKTINFO and IP6_PKTINFO on netbsd/openbsd.
([#1002](https://github.com/nix-rust/nix/pull/1002))

- Added `inotify_init1`, `inotify_add_watch` and `inotify_rm_watch` wrappers for
Android and Linux. ([#1016](https://github.com/nix-rust/nix/pull/1016))

### Changed
- `PollFd` event flags renamed to `PollFlags` ([#1024](https://github.com/nix-rust/nix/pull/1024/))
- `recvmsg` now returns an Iterator over `ControlMessageOwned` objects rather
Expand Down
231 changes: 231 additions & 0 deletions src/sys/inotify.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
//! Monitoring API for filesystem events.
//!
//! Inotify is a Linux-only API to monitor filesystems events.
//!
//! For more documentation, please read [inotify(7)](http://man7.org/linux/man-pages/man7/inotify.7.html).
//!
//! # Examples
//!
//! Monitor all events happening in directory "test":
//! ```no_run
//! # use nix::sys::inotify::{AddWatchFlags,InitFlags,Inotify};
//! #
//! // We create a new inotify instance.
//! let instance = Inotify::init(InitFlags::empty()).unwrap();
//!
//! // We add a new watch on directory "test" for all events.
//! let wd = instance.add_watch("test", AddWatchFlags::IN_ALL_EVENTS).unwrap();
//!
//! loop {
//! // We read from our inotify instance for events.
//! let events = instance.read_events().unwrap();
//! println!("Events: {:?}", events);
//! }
//! ```

use libc;
use libc::{
c_char,
c_int,
uint32_t
};
use std::ffi::{OsString,OsStr,CStr};
use std::os::unix::ffi::OsStrExt;
use std::mem::size_of;
use std::os::unix::io::{RawFd,AsRawFd,FromRawFd};
use unistd::read;
use Result;
use NixPath;
use errno::Errno;

libc_bitflags! {
/// Configuration options for [`inotify_add_watch`](fn.inotify_add_watch.html).
pub struct AddWatchFlags: uint32_t {
IN_ACCESS;
IN_MODIFY;
IN_ATTRIB;
IN_CLOSE_WRITE;
IN_CLOSE_NOWRITE;
IN_OPEN;
IN_MOVED_FROM;
IN_MOVED_TO;
IN_CREATE;
IN_DELETE;
IN_DELETE_SELF;
IN_MOVE_SELF;

IN_UNMOUNT;
IN_Q_OVERFLOW;
IN_IGNORED;

IN_CLOSE;
IN_MOVE;

IN_ONLYDIR;
IN_DONT_FOLLOW;

IN_ISDIR;
IN_ONESHOT;
IN_ALL_EVENTS;
}
}

libc_bitflags! {
/// Configuration options for [`inotify_init1`](fn.inotify_init1.html).
pub struct InitFlags: c_int {
IN_CLOEXEC;
IN_NONBLOCK;
}
}

/// An inotify instance. This is also a file descriptor, you can feed it to
/// other interfaces consuming file descriptors, epoll for example.
#[derive(Debug, Clone, Copy)]
pub struct Inotify {
fd: RawFd
}

/// This object is returned when you create a new watch on an inotify instance.
/// It is then returned as part of an event once triggered. It allows you to
/// know which watch triggered which event.
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd)]
pub struct WatchDescriptor {
wd: i32
}

/// A single inotify event.
///
/// For more documentation see, [inotify(7)](http://man7.org/linux/man-pages/man7/inotify.7.html).
#[derive(Debug)]
pub struct InotifyEvent {
/// Watch descriptor. This field corresponds to the watch descriptor you
/// were issued when calling add_watch. It allows you to know which watch
/// this event comes from.
pub wd: WatchDescriptor,
/// Event mask. This field is a bitfield describing the exact event that
/// occured.
pub mask: AddWatchFlags,
/// This cookie is a number that allows you to connect related events. For
/// now only IN_MOVED_FROM and IN_MOVED_TO can be connected.
pub cookie: u32,
/// Filename. This field exists only if the event was triggered for a file
/// inside the watched directory.
pub name: Option<OsString>
}

impl Inotify {
/// Initialize a new inotify instance.
///
/// Returns a Result containing an inotify instance.
///
/// For more information see, [inotify_init(2)](http://man7.org/linux/man-pages/man2/inotify_init.2.html).
pub fn init(flags: InitFlags) -> Result<Inotify> {
let res = Errno::result(unsafe {
libc::inotify_init1(flags.bits())
});

res.map(|fd| Inotify { fd })
}

/// Adds a new watch on the target file or directory.
///
/// Returns a watch descriptor. This is not a File Descriptor!
///
/// For more information see, [inotify_add_watch(2)](http://man7.org/linux/man-pages/man2/inotify_add_watch.2.html).
pub fn add_watch<P: ?Sized + NixPath>(&self,
path: &P,
mask: AddWatchFlags)
-> Result<WatchDescriptor>
{
let res = path.with_nix_path(|cstr| {
unsafe {
libc::inotify_add_watch(self.fd, cstr.as_ptr(), mask.bits())
}
})?;

Errno::result(res).map(|wd| WatchDescriptor { wd })
}

/// Removes an existing watch using the watch descriptor returned by
/// inotify_add_watch.
///
/// Returns an EINVAL error if the watch descriptor is invalid.
///
/// For more information see, [inotify_rm_watch(2)](http://man7.org/linux/man-pages/man2/inotify_rm_watch.2.html).
#[cfg(target_os = "linux")]
pub fn rm_watch(&self, wd: WatchDescriptor) -> Result<()> {
let res = unsafe { libc::inotify_rm_watch(self.fd, wd.wd) };

Errno::result(res).map(drop)
}

#[cfg(target_os = "android")]
pub fn rm_watch(&self, wd: WatchDescriptor) -> Result<()> {
let res = unsafe { libc::inotify_rm_watch(self.fd, wd.wd as u32) };

Errno::result(res).map(drop)
}

/// Reads a collection of events from the inotify file descriptor. This call
/// can either be blocking or non blocking depending on whether IN_NONBLOCK
/// was set at initialization.
///
/// Returns as many events as available. If the call was non blocking and no
/// events could be read then the EAGAIN error is returned.
pub fn read_events(&self) -> Result<Vec<InotifyEvent>> {
let header_size = size_of::<libc::inotify_event>();
let mut buffer = [0u8; 4096];
let mut events = Vec::new();
let mut offset = 0;

let nread = read(self.fd, &mut buffer)?;

while (nread - offset) >= header_size {
let event = unsafe {
&*(
buffer
.as_ptr()
.offset(offset as isize) as *const libc::inotify_event
)
};

let name = match event.len {
0 => None,
_ => {
let ptr = unsafe {
buffer
.as_ptr()
.offset(offset as isize + header_size as isize)
as *const c_char
};
let cstr = unsafe { CStr::from_ptr(ptr) };

Some(OsStr::from_bytes(cstr.to_bytes()).to_owned())
}
};

events.push(InotifyEvent {
wd: WatchDescriptor { wd: event.wd },
mask: AddWatchFlags::from_bits_truncate(event.mask),
cookie: event.cookie,
name
});

offset += header_size + event.len as usize;
}

Ok(events)
}
}

impl AsRawFd for Inotify {
fn as_raw_fd(&self) -> RawFd {
self.fd
}
}

impl FromRawFd for Inotify {
unsafe fn from_raw_fd(fd: RawFd) -> Self {
Inotify { fd }
}
}
3 changes: 3 additions & 0 deletions src/sys/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,6 @@ pub mod uio;
pub mod utsname;

pub mod wait;

#[cfg(any(target_os = "android", target_os = "linux"))]
pub mod inotify;
2 changes: 2 additions & 0 deletions test/sys/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ mod test_uio;

#[cfg(target_os = "linux")]
mod test_epoll;
#[cfg(target_os = "linux")]
mod test_inotify;
mod test_pthread;
#[cfg(any(target_os = "android",
target_os = "dragonfly",
Expand Down
65 changes: 65 additions & 0 deletions test/sys/test_inotify.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use nix::sys::inotify::{AddWatchFlags,InitFlags,Inotify};
use nix::Error;
use nix::errno::Errno;
use tempfile;
use std::ffi::OsString;
use std::fs::{rename, File};

#[test]
pub fn test_inotify() {
let instance = Inotify::init(InitFlags::IN_NONBLOCK)
.unwrap();
let tempdir = tempfile::tempdir().unwrap();

instance.add_watch(tempdir.path(), AddWatchFlags::IN_ALL_EVENTS).unwrap();

let events = instance.read_events();
assert_eq!(events.unwrap_err(), Error::Sys(Errno::EAGAIN));

File::create(tempdir.path().join("test")).unwrap();

let events = instance.read_events().unwrap();
assert_eq!(events[0].name, Some(OsString::from("test")));
}

#[test]
pub fn test_inotify_multi_events() {
let instance = Inotify::init(InitFlags::IN_NONBLOCK)
.unwrap();
let tempdir = tempfile::tempdir().unwrap();

instance.add_watch(tempdir.path(), AddWatchFlags::IN_ALL_EVENTS).unwrap();

let events = instance.read_events();
assert_eq!(events.unwrap_err(), Error::Sys(Errno::EAGAIN));

File::create(tempdir.path().join("test")).unwrap();
rename(tempdir.path().join("test"), tempdir.path().join("test2")).unwrap();

// Now there should be 5 events in queue:
// - IN_CREATE on test
// - IN_OPEN on test
// - IN_CLOSE_WRITE on test
// - IN_MOVED_FROM on test with a cookie
// - IN_MOVED_TO on test2 with the same cookie

let events = instance.read_events().unwrap();
assert_eq!(events.len(), 5);

assert_eq!(events[0].mask, AddWatchFlags::IN_CREATE);
assert_eq!(events[0].name, Some(OsString::from("test")));

assert_eq!(events[1].mask, AddWatchFlags::IN_OPEN);
assert_eq!(events[1].name, Some(OsString::from("test")));

assert_eq!(events[2].mask, AddWatchFlags::IN_CLOSE_WRITE);
assert_eq!(events[2].name, Some(OsString::from("test")));

assert_eq!(events[3].mask, AddWatchFlags::IN_MOVED_FROM);
assert_eq!(events[3].name, Some(OsString::from("test")));

assert_eq!(events[4].mask, AddWatchFlags::IN_MOVED_TO);
assert_eq!(events[4].name, Some(OsString::from("test2")));

assert_eq!(events[3].cookie, events[4].cookie);
}

0 comments on commit 2d0d360

Please sign in to comment.