blob: b96e125db7466b2791a7ae33ae70c444a14ade54 [file] [log] [blame]
use kqueue_sys::{kevent, kqueue};
use libc::{pid_t, uintptr_t};
use std::convert::{AsRef, Into, TryInto};
use std::default::Default;
use std::fs::File;
use std::io::{self, Error, Result};
use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd};
use std::path::Path;
use std::ptr;
use std::time::Duration;
pub use kqueue_sys::constants::*;
mod os;
use crate::os::vnode;
mod time;
use crate::time::duration_to_timespec;
/// The watched object that fired the event
#[derive(Debug, Eq, Clone)]
pub enum Ident {
Filename(RawFd, String),
Fd(RawFd),
Pid(pid_t),
Signal(i32),
Timer(i32),
}
#[doc(hidden)]
#[derive(Debug, PartialEq, Clone)]
pub struct Watched {
filter: EventFilter,
flags: FilterFlag,
ident: Ident,
}
/// Watches one or more resources
///
/// These can be created with `Watcher::new()`. You can create as many
/// `Watcher`s as you want, and they can watch as many objects as you wish.
/// The objects do not need to be the same type.
///
/// Each `Watcher` is backed by a `kqueue(2)` queue. These resources are freed
/// on the `Watcher`s destruction. If the destructor cannot run for whatever
/// reason, the underlying kernel object will be leaked.
#[derive(Debug)]
pub struct Watcher {
watched: Vec<Watched>,
queue: RawFd,
started: bool,
opts: KqueueOpts,
}
/// Vnode events
///
/// These are OS-specific, and may not all be supported on your platform. Check
/// `kqueue(2)` for more information.
#[derive(Debug)]
#[non_exhaustive]
pub enum Vnode {
/// The file was deleted
Delete,
/// The file received a write
Write,
/// The file was extended with `truncate(2)`
Extend,
/// The file was shrunk with `truncate(2)`
Truncate,
/// The attributes of the file were changed
Attrib,
/// The link count of the file was changed
Link,
/// The file was renamed
Rename,
/// Access to the file was revoked with `revoke(2)` or the fs was unmounted
Revoke,
/// File was opened by a process (FreeBSD-specific)
Open,
/// File was closed and the descriptor had write access (FreeBSD-specific)
CloseWrite,
/// File was closed and the descriptor had read access (FreeBSD-specific)
Close,
}
/// Process events
///
/// These are OS-specific, and may not all be supported on your platform. Check
/// `kqueue(2)` for more information.
#[derive(Debug)]
pub enum Proc {
/// The watched process exited with the returned exit code
Exit(usize),
/// The process called `fork(2)`
Fork,
/// The process called `exec(2)`
Exec,
/// The process called `fork(2)`, and returned the child pid.
Track(libc::pid_t),
/// The process called `fork(2)`, but we were not able to track the child
Trackerr,
/// The process called `fork(2)`, and returned the child pid.
// TODO: this is FreeBSD-specific. We can probably convert this to `Track`.
Child(libc::pid_t),
}
/// Event-specific data returned with the event.
///
/// Like much of this library, this is OS-specific. Check `kqueue(2)` for more
/// details on your target OS.
#[derive(Debug)]
pub enum EventData {
/// Data relating to `Vnode` events
Vnode(Vnode),
/// Data relating to process events
Proc(Proc),
/// The returned number of bytes are ready for reading from the watched
/// descriptor
ReadReady(usize),
/// The file is ready for writing. On some files (like sockets, pipes, etc),
/// the number of bytes in the write buffer will be returned.
WriteReady(usize),
/// One of the watched signals fired. The number of times this signal was received
/// is returned.
Signal(usize),
/// One of the watched timers fired. The number of times this timer fired
/// is returned.
Timer(usize),
/// Some error was received
Error(Error),
}
/// An event from a `Watcher` object.
///
/// An event contains both the a signifier of the watched object that triggered
/// the event, as well as any event-specific. See the `EventData` enum for info
/// on what event-specific data is returned for each event.
#[derive(Debug)]
pub struct Event {
/// The watched resource that triggered the event
pub ident: Ident,
/// Any event-specific data returned with the event.
pub data: EventData,
}
pub struct EventIter<'a> {
watcher: &'a Watcher,
}
/// Options for a `Watcher`
#[derive(Debug)]
pub struct KqueueOpts {
/// Clear state on watched objects
clear: bool,
}
impl Default for KqueueOpts {
/// Returns the default options for a `Watcher`
///
/// `clear` is set to `true`
fn default() -> KqueueOpts {
KqueueOpts { clear: true }
}
}
// We don't have enough information to turn a `usize` into
// an `Ident`, so we only implement `Into<usize>` here.
#[allow(clippy::from_over_into)]
impl Into<usize> for Ident {
fn into(self) -> usize {
match self {
Ident::Filename(fd, _) => fd as usize,
Ident::Fd(fd) => fd as usize,
Ident::Pid(pid) => pid as usize,
Ident::Signal(sig) => sig as usize,
Ident::Timer(timer) => timer as usize,
}
}
}
impl PartialEq<Ident> for Ident {
fn eq(&self, other: &Ident) -> bool {
match *self {
Ident::Filename(_, ref name) => {
if let Ident::Filename(_, ref othername) = *other {
name == othername
} else {
false
}
}
_ => self.as_usize() == other.as_usize(),
}
}
}
impl Ident {
fn as_usize(&self) -> usize {
match *self {
Ident::Filename(fd, _) => fd as usize,
Ident::Fd(fd) => fd as usize,
Ident::Pid(pid) => pid as usize,
Ident::Signal(sig) => sig as usize,
Ident::Timer(timer) => timer as usize,
}
}
}
impl Watcher {
/// Creates a new `Watcher`
///
/// Creates a brand new `Watcher` with `KqueueOpts::default()`. Will return
/// an `io::Error` if creation fails.
pub fn new() -> Result<Watcher> {
let queue = unsafe { kqueue() };
if queue == -1 {
Err(Error::last_os_error())
} else {
Ok(Watcher {
watched: Vec::new(),
queue,
started: false,
opts: Default::default(),
})
}
}
/// Disables the `clear` flag on a `Watcher`. New events will no longer
/// be added with the `EV_CLEAR` flag on `watch`.
pub fn disable_clears(&mut self) -> &mut Self {
self.opts.clear = false;
self
}
/// Adds a `pid` to the `Watcher` to be watched
pub fn add_pid(
&mut self,
pid: libc::pid_t,
filter: EventFilter,
flags: FilterFlag,
) -> Result<()> {
let watch = Watched {
filter,
flags,
ident: Ident::Pid(pid),
};
if !self.watched.contains(&watch) {
self.watched.push(watch);
}
Ok(())
}
/// Adds a file by filename to be watched
///
/// **NB**: `kqueue(2)` is an `fd`-based API. If you add a filename with
/// `add_filename`, internally we open it and pass the file descriptor to
/// `kqueue(2)`. If the file is moved or deleted, and a new file is created
/// with the same name, you will not receive new events for it without
/// calling `add_filename` again.
///
/// TODO: Adding new files requires calling `Watcher.watch` again
pub fn add_filename<P: AsRef<Path>>(
&mut self,
filename: P,
filter: EventFilter,
flags: FilterFlag,
) -> Result<()> {
let file = File::open(filename.as_ref())?;
let watch = Watched {
filter,
flags,
ident: Ident::Filename(
file.into_raw_fd(),
filename.as_ref().to_string_lossy().into_owned(),
),
};
if !self.watched.contains(&watch) {
self.watched.push(watch);
}
Ok(())
}
/// Adds a descriptor to a `Watcher`. This or `add_file` is the preferred
/// way to watch a file
///
/// TODO: Adding new files requires calling `Watcher.watch` again
pub fn add_fd(&mut self, fd: RawFd, filter: EventFilter, flags: FilterFlag) -> Result<()> {
let watch = Watched {
filter,
flags,
ident: Ident::Fd(fd),
};
if !self.watched.contains(&watch) {
self.watched.push(watch);
}
Ok(())
}
/// Adds a `File` to a `Watcher`. This, or `add_fd` is the preferred way
/// to watch a file
///
/// TODO: Adding new files requires calling `Watcher.watch` again
pub fn add_file(&mut self, file: &File, filter: EventFilter, flags: FilterFlag) -> Result<()> {
self.add_fd(file.as_raw_fd(), filter, flags)
}
fn delete_kevents(&self, ident: Ident, filter: EventFilter) -> Result<()> {
let kev = vec![kevent::new(
ident.as_usize(),
filter,
EventFlag::EV_DELETE,
FilterFlag::empty(),
)];
let ret = unsafe {
kevent(
self.queue,
kev.as_ptr(),
// On NetBSD, this is passed as a usize, not i32
#[allow(clippy::useless_conversion)]
(kev.len() as i32).try_into().unwrap(),
ptr::null_mut(),
0,
ptr::null(),
)
};
match ret {
-1 => Err(Error::last_os_error()),
_ => Ok(()),
}
}
/// Removes a pid from a `Watcher`
pub fn remove_pid(&mut self, pid: libc::pid_t, filter: EventFilter) -> Result<()> {
let new_watched = self
.watched
.drain(..)
.filter(|x| {
if let Ident::Pid(iterpid) = x.ident {
iterpid != pid
} else {
true
}
})
.collect();
self.watched = new_watched;
self.delete_kevents(Ident::Pid(pid), filter)
}
/// Removes a filename from a `Watcher`.
///
/// *NB*: This matches the `filename` that this item was initially added under.
/// If a file has been moved, it will not be removable by the new name.
pub fn remove_filename<P: AsRef<Path>>(
&mut self,
filename: P,
filter: EventFilter,
) -> Result<()> {
let mut fd: RawFd = 0;
let new_watched = self
.watched
.drain(..)
.filter(|x| {
if let Ident::Filename(iterfd, ref iterfile) = x.ident {
if iterfile == filename.as_ref().to_str().unwrap() {
fd = iterfd;
false
} else {
true
}
} else {
true
}
})
.collect();
self.watched = new_watched;
self.delete_kevents(Ident::Fd(fd), filter)
}
/// Removes an fd from a `Watcher`
pub fn remove_fd(&mut self, fd: RawFd, filter: EventFilter) -> Result<()> {
let new_watched = self
.watched
.drain(..)
.filter(|x| {
if let Ident::Fd(iterfd) = x.ident {
iterfd != fd
} else {
true
}
})
.collect();
self.watched = new_watched;
self.delete_kevents(Ident::Fd(fd), filter)
}
/// Removes a `File` from a `Watcher`
pub fn remove_file(&mut self, file: &File, filter: EventFilter) -> Result<()> {
self.remove_fd(file.as_raw_fd(), filter)
}
/// Starts watching for events from `kqueue(2)`. This function needs to
/// be called before `Watcher.iter()` or `Watcher.poll()` to actually
/// start listening for events.
pub fn watch(&mut self) -> Result<()> {
let mut kevs: Vec<kevent> = Vec::new();
for watched in &self.watched {
let raw_ident = match watched.ident {
Ident::Fd(fd) => fd as uintptr_t,
Ident::Filename(fd, _) => fd as uintptr_t,
Ident::Pid(pid) => pid as uintptr_t,
Ident::Signal(sig) => sig as uintptr_t,
Ident::Timer(ident) => ident as uintptr_t,
};
kevs.push(kevent::new(
raw_ident,
watched.filter,
if self.opts.clear {
EventFlag::EV_ADD | EventFlag::EV_CLEAR
} else {
EventFlag::EV_ADD
},
watched.flags,
));
}
let ret = unsafe {
kevent(
self.queue,
kevs.as_ptr(),
// On NetBSD, this is passed as a usize, not i32
#[allow(clippy::useless_conversion)]
(kevs.len() as i32).try_into().unwrap(),
ptr::null_mut(),
0,
ptr::null(),
)
};
self.started = true;
match ret {
-1 => Err(Error::last_os_error()),
_ => Ok(()),
}
}
/// Polls for a new event, with an optional timeout. If no `timeout`
/// is passed, then it will return immediately.
pub fn poll(&self, timeout: Option<Duration>) -> Option<Event> {
// poll will not block indefinitely
// None -> return immediately
match timeout {
Some(timeout) => get_event(self, Some(timeout)),
None => get_event(self, Some(Duration::new(0, 0))),
}
}
/// Polls for a new event, with an optional timeout. If no `timeout`
/// is passed, then it will block until an event is received.
pub fn poll_forever(&self, timeout: Option<Duration>) -> Option<Event> {
if timeout.is_some() {
self.poll(timeout)
} else {
get_event(self, None)
}
}
/// Creates an iterator that iterates over the queue. This iterator will block
/// until a new event is received.
pub fn iter(&self) -> EventIter {
EventIter { watcher: self }
}
}
impl AsRawFd for Watcher {
fn as_raw_fd(&self) -> RawFd {
self.queue
}
}
impl Drop for Watcher {
fn drop(&mut self) {
unsafe { libc::close(self.queue) };
for watched in &self.watched {
match watched.ident {
Ident::Fd(fd) => unsafe { libc::close(fd) },
Ident::Filename(fd, _) => unsafe { libc::close(fd) },
_ => continue,
};
}
}
}
fn find_file_ident(watcher: &Watcher, fd: RawFd) -> Option<Ident> {
for watched in &watcher.watched {
match watched.ident.clone() {
Ident::Fd(ident_fd) => {
if fd == ident_fd {
return Some(Ident::Fd(fd));
} else {
continue;
}
}
Ident::Filename(ident_fd, ident_str) => {
if fd == ident_fd {
return Some(Ident::Filename(ident_fd, ident_str));
} else {
continue;
}
}
_ => continue,
}
}
None
}
fn get_event(watcher: &Watcher, timeout: Option<Duration>) -> Option<Event> {
let mut kev = kevent::new(
0,
EventFilter::EVFILT_SYSCOUNT,
EventFlag::empty(),
FilterFlag::empty(),
);
let ret = if let Some(ts) = timeout {
unsafe {
kevent(
watcher.queue,
ptr::null(),
0,
&mut kev,
1,
&duration_to_timespec(ts),
)
}
} else {
unsafe { kevent(watcher.queue, ptr::null(), 0, &mut kev, 1, ptr::null()) }
};
match ret {
-1 => Some(Event::from_error(kev, watcher)),
0 => None, // timeout expired
_ => Some(Event::new(kev, watcher)),
}
}
// OS specific
// TODO: Events can have more than one filter flag
impl Event {
#[doc(hidden)]
pub fn new(ev: kevent, watcher: &Watcher) -> Event {
let data = match ev.filter {
EventFilter::EVFILT_READ => EventData::ReadReady(ev.data as usize),
EventFilter::EVFILT_WRITE => EventData::WriteReady(ev.data as usize),
EventFilter::EVFILT_SIGNAL => EventData::Signal(ev.data as usize),
EventFilter::EVFILT_TIMER => EventData::Timer(ev.data as usize),
EventFilter::EVFILT_PROC => {
let inner = if ev.fflags.contains(FilterFlag::NOTE_EXIT) {
Proc::Exit(ev.data as usize)
} else if ev.fflags.contains(FilterFlag::NOTE_FORK) {
Proc::Fork
} else if ev.fflags.contains(FilterFlag::NOTE_EXEC) {
Proc::Exec
} else if ev.fflags.contains(FilterFlag::NOTE_TRACK) {
Proc::Track(ev.data as libc::pid_t)
} else if ev.fflags.contains(FilterFlag::NOTE_CHILD) {
Proc::Child(ev.data as libc::pid_t)
} else {
panic!("not supported: {:?}", ev.fflags)
};
EventData::Proc(inner)
}
EventFilter::EVFILT_VNODE => {
let inner = if ev.fflags.contains(FilterFlag::NOTE_DELETE) {
Vnode::Delete
} else if ev.fflags.contains(FilterFlag::NOTE_WRITE) {
Vnode::Write
} else if ev.fflags.contains(FilterFlag::NOTE_EXTEND) {
Vnode::Extend
} else if ev.fflags.contains(FilterFlag::NOTE_ATTRIB) {
Vnode::Attrib
} else if ev.fflags.contains(FilterFlag::NOTE_LINK) {
Vnode::Link
} else if ev.fflags.contains(FilterFlag::NOTE_RENAME) {
Vnode::Rename
} else if ev.fflags.contains(FilterFlag::NOTE_REVOKE) {
Vnode::Revoke
} else {
// This handles any filter flags that are OS-specific
vnode::handle_vnode_extras(ev.fflags)
};
EventData::Vnode(inner)
}
_ => panic!("not supported"),
};
let ident = match ev.filter {
EventFilter::EVFILT_READ => find_file_ident(watcher, ev.ident as RawFd).unwrap(),
EventFilter::EVFILT_WRITE => find_file_ident(watcher, ev.ident as RawFd).unwrap(),
EventFilter::EVFILT_VNODE => find_file_ident(watcher, ev.ident as RawFd).unwrap(),
EventFilter::EVFILT_SIGNAL => Ident::Signal(ev.ident as i32),
EventFilter::EVFILT_TIMER => Ident::Timer(ev.ident as i32),
EventFilter::EVFILT_PROC => Ident::Pid(ev.ident as pid_t),
_ => panic!("not supported"),
};
Event { ident, data }
}
#[doc(hidden)]
pub fn from_error(ev: kevent, watcher: &Watcher) -> Event {
let ident = match ev.filter {
EventFilter::EVFILT_READ => find_file_ident(watcher, ev.ident as RawFd).unwrap(),
EventFilter::EVFILT_WRITE => find_file_ident(watcher, ev.ident as RawFd).unwrap(),
EventFilter::EVFILT_VNODE => find_file_ident(watcher, ev.ident as RawFd).unwrap(),
EventFilter::EVFILT_SIGNAL => Ident::Signal(ev.ident as i32),
EventFilter::EVFILT_TIMER => Ident::Timer(ev.ident as i32),
EventFilter::EVFILT_PROC => Ident::Pid(ev.ident as pid_t),
_ => panic!("not supported"),
};
Event {
data: EventData::Error(io::Error::last_os_error()),
ident,
}
}
#[doc(hidden)]
pub fn is_err(&self) -> bool {
matches!(self.data, EventData::Error(_))
}
}
impl<'a> Iterator for EventIter<'a> {
type Item = Event;
// rather than call kevent(2) each time, we can likely optimize and
// call it once for like 100 items
fn next(&mut self) -> Option<Self::Item> {
if !self.watcher.started {
return None;
}
get_event(self.watcher, None)
}
}
#[cfg(test)]
mod tests {
use super::{EventData, EventFilter, FilterFlag, Ident, Vnode, Watcher};
use std::fs;
use std::io::Write;
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::path::Path;
use std::thread;
use std::time;
#[cfg(target_os = "freebsd")]
use std::process;
#[test]
fn test_new_watcher() {
let mut watcher = Watcher::new().expect("new failed");
let file = tempfile::tempfile().expect("Couldn't create tempfile");
watcher
.add_file(&file, EventFilter::EVFILT_VNODE, FilterFlag::NOTE_WRITE)
.expect("add failed");
watcher.watch().expect("watch failed");
}
#[test]
fn test_filename() {
let mut watcher = Watcher::new().expect("new failed");
let file = tempfile::NamedTempFile::new().expect("Couldn't create tempfile");
watcher
.add_filename(
file.path(),
EventFilter::EVFILT_VNODE,
FilterFlag::NOTE_WRITE,
)
.expect("add failed");
watcher.watch().expect("watch failed");
let mut new_file = fs::OpenOptions::new()
.write(true)
.open(file.path())
.expect("open failed");
new_file.write_all(b"foo").expect("write failed");
thread::sleep(time::Duration::from_secs(1));
let ev = watcher.iter().next().expect("Could not get a watch");
assert!(matches!(ev.data, EventData::Vnode(Vnode::Write)));
match ev.ident {
Ident::Filename(_, name) => assert!(Path::new(&name) == file.path()),
_ => panic!(),
};
}
#[test]
fn test_file() {
let mut watcher = Watcher::new().expect("new failed");
let mut file = tempfile::tempfile().expect("Could not create tempfile");
watcher
.add_file(&file, EventFilter::EVFILT_VNODE, FilterFlag::NOTE_WRITE)
.expect("add failed");
watcher.watch().expect("watch failed");
file.write_all(b"foo").expect("write failed");
thread::sleep(time::Duration::from_secs(1));
let ev = watcher.iter().next().expect("Didn't get an event");
assert!(matches!(ev.data, EventData::Vnode(Vnode::Write)));
assert!(matches!(ev.ident, Ident::Fd(_)));
}
#[test]
fn test_delete_filename() {
let mut watcher = Watcher::new().expect("new failed");
let file = tempfile::NamedTempFile::new().expect("Could not create tempfile");
let filename = file.path();
watcher
.add_filename(filename, EventFilter::EVFILT_VNODE, FilterFlag::NOTE_WRITE)
.expect("add failed");
watcher.watch().expect("watch failed");
watcher
.remove_filename(filename, EventFilter::EVFILT_VNODE)
.expect("delete failed");
}
#[test]
fn test_dupe() {
let mut watcher = Watcher::new().expect("new failed");
let file = tempfile::NamedTempFile::new().expect("Couldn't create tempfile");
let filename = file.path();
watcher
.add_filename(filename, EventFilter::EVFILT_VNODE, FilterFlag::NOTE_WRITE)
.expect("add failed");
watcher
.add_filename(filename, EventFilter::EVFILT_VNODE, FilterFlag::NOTE_WRITE)
.expect("second add failed");
assert_eq!(
watcher.watched.len(),
1,
"Did not get an expected number of events"
);
}
#[test]
fn test_two_files() {
let mut watcher = Watcher::new().expect("new failed");
let mut first_file = tempfile::tempfile().expect("Unable to create first temporary file");
let mut second_file = tempfile::tempfile().expect("Unable to create second temporary file");
watcher
.add_file(
&first_file,
EventFilter::EVFILT_VNODE,
FilterFlag::NOTE_WRITE,
)
.expect("add failed");
watcher
.add_file(
&second_file,
EventFilter::EVFILT_VNODE,
FilterFlag::NOTE_WRITE,
)
.expect("add failed");
watcher.watch().expect("watch failed");
first_file.write_all(b"foo").expect("first write failed");
second_file.write_all(b"foo").expect("second write failed");
thread::sleep(time::Duration::from_secs(1));
watcher.iter().next().expect("didn't get any events");
watcher.iter().next().expect("didn't get any events");
}
#[test]
fn test_nested_kqueue() {
let mut watcher = Watcher::new().expect("Failed to create main watcher");
let mut nested_watcher = Watcher::new().expect("Failed to create nested watcher");
let kqueue_file = unsafe { fs::File::from_raw_fd(nested_watcher.as_raw_fd()) };
watcher
.add_file(&kqueue_file, EventFilter::EVFILT_READ, FilterFlag::empty())
.expect("add_file failed for main watcher");
let mut file = tempfile::tempfile().expect("Couldn't create tempfile");
nested_watcher
.add_file(&file, EventFilter::EVFILT_VNODE, FilterFlag::NOTE_WRITE)
.expect("add_file failed for nested watcher");
watcher.watch().expect("watch failed on main watcher");
nested_watcher
.watch()
.expect("watch failed on nested watcher");
file.write_all(b"foo").expect("write failed");
thread::sleep(time::Duration::from_secs(1));
watcher.iter().next().expect("didn't get any events");
nested_watcher.iter().next().expect("didn't get any events");
}
#[test]
#[cfg(target_os = "freebsd")]
fn test_close_read() {
let mut watcher = Watcher::new().expect("new failed");
{
let file = tempfile::NamedTempFile::new().expect("temporary file failed to create");
watcher
.add_filename(
file.path(),
EventFilter::EVFILT_VNODE,
FilterFlag::NOTE_CLOSE,
)
.expect("add failed");
watcher.watch().expect("watch failed");
// we launch this in a separate process since it appears that FreeBSD does not fire
// off a NOTE_CLOSE(_WRITE)? event for the same process closing a file descriptor.
process::Command::new("cat")
.arg(file.path())
.spawn()
.expect("should spawn a file");
thread::sleep(time::Duration::from_secs(1));
}
let ev = watcher.iter().next().expect("did not receive event");
assert!(matches!(ev.data, EventData::Vnode(Vnode::Close)));
}
#[test]
#[cfg(target_os = "freebsd")]
fn test_close_write() {
let mut watcher = match Watcher::new() {
Ok(wat) => wat,
Err(_) => panic!("new failed"),
};
{
let file = tempfile::NamedTempFile::new().expect("couldn't create tempfile");
watcher
.add_filename(
file.path(),
EventFilter::EVFILT_VNODE,
FilterFlag::NOTE_CLOSE_WRITE,
)
.expect("add failed");
watcher.watch().expect("watch failed");
// See above for rationale as to why we use a separate process here
process::Command::new("cat")
.arg(file.path())
.spawn()
.expect("should spawn a file");
thread::sleep(time::Duration::from_secs(1));
}
let ev = watcher.iter().next().expect("didn't get an event");
assert!(matches!(ev.data, EventData::Vnode(Vnode::CloseWrite)));
}
}