use std::collections::{HashMap, VecDeque};
use std::fmt::Debug;
use std::fs::File;
use std::io::{self, Error, Result};
use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd, RawFd};
use std::path::Path;
use std::ptr;
use std::sync::Mutex;
use std::time::Duration;
use kqueue_sys::constants::{EventFilter, EventFlag, FilterFlag};
use kqueue_sys::{kevent, kqueue};
use libc::close;
use crate::event::{get_event, Event, EventIter};
use crate::types::{Ident, KqueueOpts};
#[doc(hidden)]
#[derive(Debug)]
pub struct Watched {
flags: FilterFlag,
}
#[derive(Debug)]
pub struct Watcher {
pub(crate) watched: HashMap<(Ident, EventFilter), Watched>,
pub(crate) decoalesced_events: Mutex<VecDeque<Event>>,
pub(crate) queue: OwnedFd,
pub(crate) started: bool,
pub(crate) opts: KqueueOpts,
}
impl Watcher {
pub fn new() -> Result<Watcher> {
let queue = unsafe { kqueue() };
if queue == -1 {
Err(Error::last_os_error())
} else {
Ok(Watcher {
watched: HashMap::new(),
decoalesced_events: Mutex::new(VecDeque::new()),
queue: unsafe { OwnedFd::from_raw_fd(queue) },
started: false,
opts: KqueueOpts::default(),
})
}
}
pub fn disable_clears(&mut self) -> &mut Self {
self.opts.clear = false;
self
}
fn prune(&mut self, ident: &Ident) {
if let Ident::Filename(_, filename) = ident {
self.decoalesced_events
.get_mut()
.unwrap()
.retain(|ev| !matches!(&ev.ident, Ident::Filename(_, ev_filename) if filename == ev_filename));
} else {
self.decoalesced_events
.get_mut()
.unwrap()
.retain(|ev| &ev.ident != ident);
}
}
#[allow(clippy::missing_errors_doc)]
pub fn add_pid(
&mut self,
pid: libc::pid_t,
filter: EventFilter,
flags: FilterFlag,
) -> Result<()> {
let ident = Ident::Pid(pid);
let watch = Watched { flags };
self.prune(&ident);
self.watched.insert((ident, filter), watch);
Ok(())
}
pub fn add_filename<P: AsRef<Path>>(
&mut self,
filename: P,
filter: EventFilter,
flags: FilterFlag,
) -> Result<()> {
fn inner(
watcher: &mut Watcher,
filename: &Path,
filter: EventFilter,
flags: FilterFlag,
) -> Result<()> {
let file = File::open(filename)?;
let filename = filename.to_string_lossy().into_owned();
let ident = Ident::Filename(file.into_raw_fd(), filename);
let watch = Watched { flags };
let key = (ident, filter);
if let Some((key, _)) = watcher.watched.remove_entry(&key) {
unsafe { close(key.0.into()) };
}
watcher.prune(&key.0);
watcher.watched.insert(key, watch);
Ok(())
}
inner(self, filename.as_ref(), filter, flags)
}
#[allow(clippy::missing_errors_doc)]
pub fn add_fd(&mut self, fd: RawFd, filter: EventFilter, flags: FilterFlag) -> Result<()> {
let ident = Ident::Fd(fd);
let watch = Watched { flags };
self.prune(&ident);
self.watched.insert((ident, filter), watch);
Ok(())
}
#[allow(clippy::missing_errors_doc)]
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 = &[kevent::new(
ident.try_into()?,
filter,
EventFlag::EV_DELETE,
FilterFlag::empty(),
)];
let ret = unsafe {
kevent(
self.queue.as_raw_fd(),
kev.as_ptr(),
1,
ptr::null_mut(),
0,
ptr::null(),
)
};
match ret {
-1 => Err(Error::last_os_error()),
_ => Ok(()),
}
}
#[allow(clippy::missing_errors_doc)]
pub fn remove_pid(&mut self, pid: libc::pid_t, filter: EventFilter) -> Result<()> {
self.watched.remove(&(Ident::Pid(pid), filter));
let ret = self.delete_kevents(&Ident::Pid(pid), filter);
self.prune(&(Ident::Pid(pid)));
ret
}
pub fn remove_filename<P: AsRef<Path> + Debug>(
&mut self,
filename: P,
filter: EventFilter,
) -> Result<()> {
fn inner(watcher: &mut Watcher, filename: &Path, filter: EventFilter) -> Result<()> {
let filename = filename.to_string_lossy().into_owned();
let ident = Ident::Filename(0, filename);
let key = (ident, filter);
let Some((key, _)) = watcher.watched.remove_entry(&key) else {
let Ident::Filename(_, ref name) = key.0 else {
unreachable!()
};
return Err(Error::new(
io::ErrorKind::NotFound,
format!("{name:?} was not being watched"),
));
};
let fd: i32 = (&(key.0)).into();
let ret = watcher.delete_kevents(&key.0, filter);
watcher.prune(&key.0);
unsafe { close(fd) };
ret
}
inner(self, filename.as_ref(), filter)
}
#[allow(clippy::missing_errors_doc)]
pub fn remove_fd(&mut self, fd: RawFd, filter: EventFilter) -> Result<()> {
let ident = Ident::Fd(fd);
self.watched.remove(&(ident.clone(), filter));
let ret = self.delete_kevents(&ident, filter);
self.prune(&ident);
ret
}
#[allow(clippy::missing_errors_doc)]
pub fn remove_file(&mut self, file: &File, filter: EventFilter) -> Result<()> {
self.remove_fd(file.as_raw_fd(), filter)
}
pub fn watch(&mut self) -> Result<()> {
let kevs: Vec<kevent> = self.watched.iter().try_fold(
Vec::with_capacity(self.watched.len()),
|mut v, ((ident, filter), watched)| {
v.push(kevent::new(
ident.try_into()?,
*filter,
if self.opts.clear {
EventFlag::EV_ADD | EventFlag::EV_CLEAR
} else {
EventFlag::EV_ADD
},
watched.flags,
));
Ok::<Vec<kevent>, Error>(v)
},
)?;
#[cfg(target_os = "netbsd")]
let Ok(len) = i32::try_from(kevs.len()).and_then(|i| i.try_into()) else {
return Err(Error::new(
io::ErrorKind::InvalidData,
format!("invalid number of events: {:?}", kevs.len()),
));
};
#[cfg(not(target_os = "netbsd"))]
let Ok(len) = i32::try_from(kevs.len()) else {
return Err(Error::new(
io::ErrorKind::InvalidData,
format!("invalid number of events: {:?}", kevs.len()),
));
};
let ret = unsafe {
kevent(
self.queue.as_raw_fd(),
kevs.as_ptr(),
len,
ptr::null_mut(),
0,
ptr::null(),
)
};
if ret == -1 {
Err(Error::last_os_error())
} else {
self.started = true;
Ok(())
}
}
#[must_use]
pub fn poll(&self, timeout: Option<Duration>) -> Option<Event> {
match timeout {
Some(timeout) => get_event(self, Some(timeout)),
None => get_event(self, Some(Duration::new(0, 0))),
}
}
#[must_use]
pub fn poll_forever(&self, timeout: Option<Duration>) -> Option<Event> {
if timeout.is_some() {
self.poll(timeout)
} else {
get_event(self, None)
}
}
#[must_use]
pub fn iter(&self) -> EventIter<'_> {
EventIter { watcher: self }
}
}
impl<'a> IntoIterator for &'a Watcher {
type Item = crate::event::Event;
type IntoIter = crate::event::EventIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl AsRawFd for Watcher {
fn as_raw_fd(&self) -> RawFd {
self.queue.as_raw_fd()
}
}
impl Drop for Watcher {
fn drop(&mut self) {
for (ident, _) in self.watched.keys() {
match *ident {
Ident::Filename(fd, _) => unsafe { libc::close(fd) },
_ => continue,
};
}
}
}
#[cfg(test)]
mod tests {
use std::fs::{self, File, Permissions};
use std::io::{ErrorKind, Seek, SeekFrom, Write};
use std::os::unix::fs::PermissionsExt;
use std::os::unix::io::AsRawFd;
use std::os::unix::process::CommandExt;
use std::path::Path;
use std::process::Command;
use std::thread;
use std::time;
use kqueue_sys::constants::*;
use libc::pid_t;
use crate::{Event, EventData, Ident, Proc, Vnode, Watcher};
#[cfg(target_os = "freebsd")]
use std::io::Read;
#[cfg(target_os = "freebsd")]
use std::process;
#[test]
fn test_add_filename_duplicate_no_fd_leak() {
let file = tempfile::NamedTempFile::new().expect("couldn't create tempfile");
let mut watcher = Watcher::new().expect("couldn't create watcher");
watcher
.add_filename(
file.path(),
EventFilter::EVFILT_VNODE,
FilterFlag::NOTE_WRITE,
)
.expect("first add failed");
let probe_fd = unsafe { libc::dup(0) };
let test_fd = probe_fd - 1;
assert!(probe_fd >= 0, "dup failed");
unsafe { libc::close(probe_fd) };
watcher
.add_filename(
file.path(),
EventFilter::EVFILT_VNODE,
FilterFlag::NOTE_WRITE,
)
.expect("second add failed");
drop(watcher);
let leaked = unsafe { libc::fcntl(test_fd, libc::F_GETFD) } != -1;
assert!(
!leaked,
"fd {test_fd} opened by the first add_filename was not closed on drop"
);
let leaked = unsafe { libc::fcntl(probe_fd, libc::F_GETFD) } != -1;
assert!(
!leaked,
"fd {probe_fd} opened by the second add_filename was not closed on drop"
);
}
#[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_fd = nested_watcher.as_raw_fd();
watcher
.add_fd(kqueue_fd, 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");
file.flush().expect("flush failed");
watcher
.poll(Some(time::Duration::from_secs(2)))
.expect("didn't get any events");
nested_watcher
.poll(Some(time::Duration::from_secs(2)))
.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");
process::Command::new("cat")
.arg(file.path())
.spawn()
.expect("should spawn a file")
.wait()
.expect("should exit successfully");
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");
process::Command::new("cat")
.arg(file.path())
.spawn()
.expect("should spawn a file")
.wait()
.expect("should exit successfully");
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)));
}
#[test]
fn test_not_found_remove_watch() {
let mut watcher = Watcher::new().unwrap();
let ret = watcher.remove_filename("foo", EventFilter::EVFILT_VNODE);
assert!(ret.is_err());
let err = ret.unwrap_err();
assert_eq!(err.kind(), ErrorKind::NotFound);
assert_eq!(err.to_string(), "\"foo\" was not being watched");
}
#[test]
fn test_double_close_file() {
let mut watcher = Watcher::new().expect("couldn't create watcher");
{
let fil = File::open("/").expect("failed to open /");
watcher
.add_file(&fil, EventFilter::EVFILT_VNODE, FilterFlag::NOTE_RENAME)
.expect("couldn't add test file");
watcher.watch().expect("failed to watch");
watcher
.remove_file(&fil, EventFilter::EVFILT_VNODE)
.expect("couldn't remove file");
watcher.watch().expect("failed to watch");
}
}
#[test]
fn test_double_close_file_drop() {
let fil = File::open("/").expect("failed to open /");
{
let mut watcher = Watcher::new().expect("couldn't create watcher");
watcher
.add_file(&fil, EventFilter::EVFILT_VNODE, FilterFlag::NOTE_RENAME)
.expect("couldn't add test file");
watcher.watch().expect("failed to watch");
}
}
#[test]
#[cfg(target_os = "freebsd")]
fn test_read() {
let mut contents = Vec::<u8>::new();
let mut fil = tempfile::tempfile().expect("Unable to first temporary file");
fil.write_all(b"foo bar baz").expect("Couldn't write");
let mut watcher = Watcher::new().expect("couldn't create watcher");
watcher
.add_file(&fil, EventFilter::EVFILT_VNODE, FilterFlag::NOTE_READ)
.expect("couldn't add test file");
watcher.watch().expect("failed to watch");
let _ = fil.read_to_end(&mut contents);
let _expected_text: Vec<u8> = b"foo bar baz".into();
assert!(matches!(contents, _expected_text));
let ev = watcher.iter().next().expect("didn't get an event");
assert!(matches!(ev.data, EventData::Vnode(Vnode::Read)));
}
#[test]
fn test_same_fd_different_filters() {
let mut fil = tempfile::tempfile().expect("Unable to write temporary file");
let mut watcher = Watcher::new().expect("couldn't create watcher");
watcher
.add_file(&fil, EventFilter::EVFILT_READ, FilterFlag::empty())
.expect("couldn't add test file");
watcher
.add_file(&fil, EventFilter::EVFILT_VNODE, FilterFlag::NOTE_WRITE)
.expect("couldn't add test file another time");
watcher.watch().expect("failed to watch");
let expected_written = 3;
let actual_written = fil.write("foo".as_bytes()).expect("couldn't write");
assert_eq!(
actual_written, expected_written,
"expected to write {expected_written} bytes, wrote: {actual_written}"
);
fil.seek(SeekFrom::Start(0)).expect("couldn't seek");
let (mut rr_happened, mut write_happened) = (false, false);
for _ in 0..3 {
if let Some(ev) = watcher.poll(Some(time::Duration::from_secs(1))) {
match ev.data {
EventData::ReadReady(bytes) => {
assert_eq!(bytes, expected_written, "more than 3 bytes ready");
if rr_happened {
panic!("received {:?} more than once", ev);
}
rr_happened = true;
}
EventData::Vnode(Vnode::Write) => {
if write_happened {
panic!("received {:?} more than once", ev);
}
write_happened = true;
}
_ => continue,
};
}
}
assert!(rr_happened, "did not get EventData::ReadReady");
assert!(write_happened, "did not get EventData::Vnode(Vnode::Write)");
}
#[test]
fn test_same_filename_different_filters() {
let mut fil = tempfile::NamedTempFile::new().expect("Unable to write temporary file");
let mut watcher = Watcher::new().expect("couldn't create watcher");
watcher
.add_filename(fil.path(), EventFilter::EVFILT_READ, FilterFlag::empty())
.expect("couldn't add test file");
watcher
.add_filename(
fil.path(),
EventFilter::EVFILT_VNODE,
FilterFlag::NOTE_WRITE,
)
.expect("couldn't add test file another time");
watcher.watch().expect("failed to watch");
let expected_written = 3;
let actual_written = fil.write("foo".as_bytes()).expect("couldn't write");
assert_eq!(
actual_written, expected_written,
"expected to write {expected_written} bytes, wrote: {actual_written}"
);
let (mut rr_happened, mut write_happened) = (false, false);
for _ in 0..3 {
if let Some(ev) = watcher.poll(Some(time::Duration::from_secs(1))) {
match ev.data {
EventData::ReadReady(bytes) => {
assert_eq!(bytes, expected_written, "more than 3 bytes ready");
if rr_happened {
panic!("received {:?} more than once", ev);
}
rr_happened = true;
}
EventData::Vnode(Vnode::Write) => {
if write_happened {
panic!("received {:?} more than once", ev);
}
write_happened = true;
}
_ => continue,
};
}
}
assert!(rr_happened, "did not get EventData::ReadReady");
assert!(write_happened, "did not get EventData::Vnode(Vnode::Write)");
}
#[test]
#[cfg(not(any(target_os = "macos", target_os = "ios")))]
fn test_remove_non_utf8_filename() {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
let dir = tempfile::tempdir().expect("couldn't create tempdir");
let path = dir.path().join(OsStr::from_bytes(b"bad\xff.txt"));
File::create(&path).expect("could not file");
let mut watcher = Watcher::new().expect("couldn't create watcher");
watcher
.add_filename(&path, EventFilter::EVFILT_VNODE, FilterFlag::NOTE_WRITE)
.expect("add_filename should accept non-UTF-8 paths");
watcher.watch().expect("watch failed");
let result = watcher.remove_filename(&path, EventFilter::EVFILT_VNODE);
assert!(
result.is_ok(),
"unexpected result from remove_filename: {result:?}",
);
}
#[test]
fn test_into_iter() {
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_millis(200));
let ev = watcher.into_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_coalesced_vnode_events() {
let mut fil = tempfile::NamedTempFile::new().expect("Unable to write temporary file");
let mut watcher = Watcher::new().expect("couldn't create watcher");
watcher
.add_filename(
fil.path(),
EventFilter::EVFILT_VNODE,
FilterFlag::NOTE_WRITE | FilterFlag::NOTE_ATTRIB,
)
.expect("couldn't add test file another time");
watcher.watch().expect("failed to watch");
let expected_written = 3;
let actual_written = fil.write("foo".as_bytes()).expect("couldn't write");
assert_eq!(
actual_written, expected_written,
"expected to write {expected_written} bytes, wrote: {actual_written}"
);
fs::set_permissions(fil.path(), Permissions::from_mode(0o200))
.expect("could not set permissions on tempfile");
let (mut attrib_happened, mut write_happened) = (false, false);
for _ in 0..3 {
if let Some(ev) = watcher.poll(Some(time::Duration::from_secs(1))) {
match ev.data {
EventData::Vnode(Vnode::Attrib) => {
if attrib_happened {
panic!("received {:?} more than once", ev);
}
attrib_happened = true;
}
EventData::Vnode(Vnode::Write) => {
if write_happened {
panic!("received {:?} more than once", ev);
}
write_happened = true;
}
_ => continue,
};
}
}
assert!(
attrib_happened,
"did not get EventData::Vnode(Vnode::Attrib)"
);
assert!(write_happened, "did not get EventData::Vnode(Vnode::Write)");
}
#[test]
fn test_coalesced_proc_events() {
let mut watcher = Watcher::new().expect("could not create Watcher");
let mut child = unsafe {
Command::new("/bin/sh")
.pre_exec(|| {
let mut ts = libc::timespec {
tv_sec: 0,
tv_nsec: 200_000_000,
};
let mut ret = -1;
while ret == -1 && ts.tv_nsec != 0 {
ret = libc::nanosleep(&ts, &mut ts);
}
Ok(())
})
.arg("-c")
.arg("/usr/bin/true")
.spawn()
.expect("could not spawn process")
};
#[cfg(not(target_os = "macos"))]
let flags = FilterFlag::NOTE_EXIT | FilterFlag::NOTE_EXEC | FilterFlag::NOTE_TRACK;
#[cfg(target_os = "macos")]
let flags = FilterFlag::NOTE_EXIT | FilterFlag::NOTE_EXEC;
watcher
.add_pid(child.id() as pid_t, EventFilter::EVFILT_PROC, flags)
.expect("could not add pid");
watcher.watch().expect("could not start");
child.wait().expect("could not wait on child");
let (mut exec_happened, mut exit_happened) = (false, false);
for _ in 0..3 {
if let Some(ev) = watcher.poll(Some(time::Duration::from_secs(1))) {
match ev.data {
EventData::Proc(Proc::Exec) => {
if exec_happened {
panic!("received {:?} more than once", ev);
}
exec_happened = true;
}
EventData::Proc(Proc::Exit(_)) => {
if exit_happened {
panic!("received {:?} more than once", ev);
}
exit_happened = true;
}
_ => continue,
};
}
}
assert!(exec_happened, "did not get EventData::Proc(Proc::Exec)");
assert!(exit_happened, "did not get EventData::Proc(Proc::Exit(_))");
}
#[test]
fn test_remove_coalesced_filename_events() {
let mut fil = tempfile::NamedTempFile::new().expect("Unable to write temporary file");
let mut watcher = Watcher::new().expect("couldn't create watcher");
watcher
.add_filename(
fil.path(),
EventFilter::EVFILT_VNODE,
FilterFlag::NOTE_WRITE | FilterFlag::NOTE_ATTRIB,
)
.expect("couldn't add test file another time");
watcher.watch().expect("failed to watch");
let expected_written = 3;
let actual_written = fil.write("foo".as_bytes()).expect("couldn't write");
assert_eq!(
actual_written, expected_written,
"expected to write {expected_written} bytes, wrote: {actual_written}"
);
fs::set_permissions(fil.path(), Permissions::from_mode(0o200))
.expect("could not set permissions on tempfile");
let evt = watcher.poll(Some(time::Duration::from_millis(200)));
assert!(evt.is_some(), "did not get event");
watcher
.remove_filename(fil.path(), EventFilter::EVFILT_VNODE)
.expect("could not remove filename from watch");
let evt = watcher.poll(None);
assert!(evt.is_none(), "got unexpected event: {:?}", evt);
}
#[test]
fn remove_only_relevant_events() {
let mut watcher = Watcher::new().expect("couldn't create watcher");
watcher.decoalesced_events.lock().unwrap().push_back(Event {
ident: Ident::Pid(10),
data: EventData::Proc(Proc::Exit(1)),
});
watcher
.remove_fd(10, EventFilter::EVFILT_READ)
.expect_err("should error, since fd 10 isn't watched");
assert_eq!(
watcher.decoalesced_events.lock().unwrap().len(),
1,
"decoalesced_events should still contain one item"
);
}
#[test]
fn remove_stale_events_pid() {
let mut watcher = Watcher::new().expect("couldn't create watcher");
watcher.decoalesced_events.lock().unwrap().push_back(Event {
ident: Ident::Pid(10),
data: EventData::Proc(Proc::Exit(1)),
});
watcher.decoalesced_events.lock().unwrap().push_back(Event {
ident: Ident::Pid(100),
data: EventData::Proc(Proc::Exit(1)),
});
let _ = watcher.add_pid(10, EventFilter::EVFILT_PROC, FilterFlag::NOTE_EXEC);
assert_eq!(
watcher.decoalesced_events.lock().unwrap().len(),
1,
"we should have deleted previous entries from the queue"
);
}
#[test]
fn remove_stale_events_fd() {
let mut watcher = Watcher::new().expect("couldn't create watcher");
watcher.decoalesced_events.lock().unwrap().push_back(Event {
ident: Ident::Fd(200),
data: EventData::Vnode(Vnode::Attrib),
});
watcher.decoalesced_events.lock().unwrap().push_back(Event {
ident: Ident::Fd(201),
data: EventData::Vnode(Vnode::Attrib),
});
let _ = watcher.add_fd(200, EventFilter::EVFILT_VNODE, FilterFlag::NOTE_ATTRIB);
assert_eq!(
watcher.decoalesced_events.lock().unwrap().len(),
1,
"we should have deleted previous entries from the queue"
);
}
#[test]
fn remove_stale_events_filename() {
let mut watcher = Watcher::new().expect("couldn't create watcher");
watcher.decoalesced_events.lock().unwrap().push_back(Event {
ident: Ident::Filename(200, "Cargo.toml".to_string()),
data: EventData::Vnode(Vnode::Attrib),
});
watcher.decoalesced_events.lock().unwrap().push_back(Event {
ident: Ident::Filename(200, ".gitignore".to_string()),
data: EventData::Vnode(Vnode::Attrib),
});
let _ = watcher.add_filename(
"Cargo.toml",
EventFilter::EVFILT_VNODE,
FilterFlag::NOTE_ATTRIB,
);
assert_eq!(
watcher.decoalesced_events.lock().unwrap().len(),
1,
"we should have deleted previous entries from the queue"
);
}
}