Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added inotify bindings. #1016

Merged
merged 1 commit into from
Mar 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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).
vdagonneau marked this conversation as resolved.
Show resolved Hide resolved
#[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,
vdagonneau marked this conversation as resolved.
Show resolved Hide resolved
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).
vdagonneau marked this conversation as resolved.
Show resolved Hide resolved
#[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
vdagonneau marked this conversation as resolved.
Show resolved Hide resolved
.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);
}