/**
|
|
* @file Inotify.h
|
|
* @author Erik Zenker
|
|
* @date 02.11.2012
|
|
* @copyright Gnu Public License
|
|
**/
|
|
#pragma once
|
|
#include <sys/inotify.h>
|
|
#include <string>
|
|
#include <queue>
|
|
#include <map>
|
|
#include <vector>
|
|
#include <assert.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
#include <time.h>
|
|
#include <poll.h>
|
|
#include <errno.h>
|
|
#include <string>
|
|
#include <exception>
|
|
#include <sstream>
|
|
#include <experimental/filesystem>
|
|
#include <experimental/optional>
|
|
|
|
#include <FileSystemEvent.h>
|
|
|
|
#define MAX_EVENTS 4096
|
|
#define EVENT_SIZE (sizeof (inotify_event))
|
|
#define EVENT_BUF_LEN (MAX_EVENTS * (EVENT_SIZE + 16))
|
|
|
|
namespace fs = std::experimental::filesystem;
|
|
|
|
/**
|
|
* @brief C++ wrapper for linux inotify interface
|
|
* @class Inotify
|
|
* Inotify.h
|
|
* "include/Inotify.h"
|
|
*
|
|
* folders will be watched by watchFolderRecursively or
|
|
* files by watchFile. If there are changes inside this
|
|
* folder or files events will be raised. This events
|
|
* can be get by getNextEvent.
|
|
*
|
|
* @eventMask
|
|
*
|
|
* IN_ACCESS File was accessed (read) (*).
|
|
* IN_ATTRIB Metadata changed—for example, permissions,
|
|
* timestamps, extended attributes, link count
|
|
* (since Linux 2.6.25), UID, or GID. (*).
|
|
* IN_CLOSE_WRITE File opened for writing was closed (*).
|
|
* IN_CLOSE_NOWRITE File not opened for writing was closed (*).
|
|
* IN_CREATE File/directory created in watched directory(*).
|
|
* IN_DELETE File/directory deleted from watched directory(*).
|
|
* IN_DELETE_SELF Watched file/directory was itself deleted.
|
|
* IN_MODIFY File was modified (*).
|
|
* IN_MOVE_SELF Watched file/directory was itself moved.
|
|
* IN_MOVED_FROM Generated for the directory containing the old
|
|
* filename when a file is renamed (*).
|
|
* IN_MOVED_TO Generated for the directory containing the new
|
|
* filename when a file is renamed (*).
|
|
* IN_OPEN File was opened (*).
|
|
* IN_ALL_EVENTS macro is defined as a bit mask of all of the above
|
|
* events
|
|
* IN_MOVE IN_MOVED_FROM|IN_MOVED_TO
|
|
* IN_CLOSE IN_CLOSE_WRITE | IN_CLOSE_NOWRITE
|
|
*
|
|
* See inotify manpage for more event details
|
|
*
|
|
*/
|
|
class Inotify {
|
|
public:
|
|
Inotify();
|
|
Inotify(uint32_t eventMask);
|
|
Inotify(std::vector< std::string> ignoredDirectories, unsigned eventTimeout, uint32_t eventMask);
|
|
~Inotify();
|
|
void watchDirectoryRecursively(fs::path path);
|
|
void watchFile(fs::path file, std::optional<fs::path> recurseRoot = std::nullopt);
|
|
void ignoreFileOnce(fs::path file);
|
|
FileSystemEvent getNextEvent();
|
|
std::optional<FileSystemEvent> nb_getNextEvent();
|
|
int getLastErrno();
|
|
|
|
private:
|
|
fs::path wdToPath(int wd);
|
|
bool isIgnored(std::string file);
|
|
bool onTimeout(time_t eventTime);
|
|
void removeWatch(int wd); // TODO
|
|
void init();
|
|
|
|
// Member
|
|
int mError;
|
|
time_t mEventTimeout;
|
|
time_t mLastEventTime;
|
|
uint32_t mEventMask;
|
|
std::vector<std::string> mIgnoredDirectories;
|
|
std::vector<std::string> mOnceIgnoredDirectories;
|
|
std::queue<FileSystemEvent> mEventQueue;
|
|
std::map<int, fs::path> mDirectorieMap;
|
|
std::map<int, fs::path> mRecurseMap;
|
|
int mInotifyFd;
|
|
|
|
|
|
};
|
|
|
|
|
|
inline Inotify::Inotify() :
|
|
mError(0),
|
|
mEventTimeout(0),
|
|
mLastEventTime(0),
|
|
mEventMask(IN_ALL_EVENTS),
|
|
mIgnoredDirectories(std::vector<std::string>()),
|
|
mInotifyFd(0){
|
|
|
|
// Initialize inotify
|
|
init();
|
|
}
|
|
|
|
inline Inotify::Inotify(uint32_t eventMask) :
|
|
mError(0),
|
|
mEventTimeout(0),
|
|
mLastEventTime(0),
|
|
mEventMask(eventMask),
|
|
mIgnoredDirectories(std::vector<std::string>()),
|
|
mInotifyFd(0){
|
|
|
|
// Initialize inotify
|
|
init();
|
|
}
|
|
|
|
inline Inotify::Inotify(std::vector<std::string> ignoredDirectories, unsigned eventTimeout, uint32_t eventMask) :
|
|
mError(0),
|
|
mEventTimeout(eventTimeout),
|
|
mLastEventTime(0),
|
|
mEventMask(eventMask),
|
|
mIgnoredDirectories(ignoredDirectories),
|
|
mInotifyFd(0){
|
|
|
|
// Initialize inotify
|
|
init();
|
|
}
|
|
|
|
inline Inotify::~Inotify(){
|
|
if(!close(mInotifyFd)){
|
|
mError = errno;
|
|
}
|
|
|
|
}
|
|
|
|
inline void Inotify::init(){
|
|
mInotifyFd = inotify_init();
|
|
if(mInotifyFd == -1){
|
|
mError = errno;
|
|
std::stringstream errorStream;
|
|
errorStream << "Can't initialize inotify ! " << strerror(mError) << ".";
|
|
throw std::runtime_error(errorStream.str());
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* @brief Adds the given path and all files and subdirectories
|
|
* to the set of watched files/directories.
|
|
* Symlinks will be followed!
|
|
*
|
|
* @param path that will be watched recursively
|
|
*
|
|
*/
|
|
inline void Inotify::watchDirectoryRecursively(fs::path path){
|
|
if(fs::exists(path)){
|
|
if(fs::is_directory(path)){
|
|
fs::recursive_directory_iterator it(path, fs::directory_options::follow_directory_symlink);
|
|
fs::recursive_directory_iterator end;
|
|
|
|
while(it != end){
|
|
fs::path currentPath = *it;
|
|
|
|
if(fs::is_directory(currentPath)){
|
|
watchFile(currentPath, path);
|
|
}
|
|
if(fs::is_symlink(currentPath)){
|
|
watchFile(currentPath, path);
|
|
}
|
|
++it;
|
|
|
|
}
|
|
|
|
}
|
|
watchFile(path);
|
|
}
|
|
else {
|
|
throw std::invalid_argument("Can´t watch Path! Path does not exist. Path: " + path.string());
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* @brief Adds a single file/directorie to the list of
|
|
* watches. Path and corresponding watchdescriptor
|
|
* will be stored in the directorieMap. This is done
|
|
* because events on watches just return this
|
|
* watchdescriptor.
|
|
*
|
|
* @param path that will be watched
|
|
*
|
|
*/
|
|
inline void Inotify::watchFile(fs::path filePath, std::optional<fs::path> recurseRoot) {
|
|
if(fs::exists(filePath)){
|
|
mError = 0;
|
|
int wd = 0;
|
|
if(!isIgnored(filePath.string())){
|
|
wd = inotify_add_watch(mInotifyFd, filePath.string().c_str(), mEventMask);
|
|
}
|
|
|
|
if(wd == -1){
|
|
mError = errno;
|
|
std::stringstream errorStream;
|
|
if(mError == 28){
|
|
errorStream << "Failed to watch! " << strerror(mError) << ". Please increase number of watches in \"/proc/sys/fs/inotify/max_user_watches\".";
|
|
throw std::runtime_error(errorStream.str());
|
|
}
|
|
|
|
errorStream << "Failed to watch! " << strerror(mError) << ". Path: " << filePath.string();
|
|
throw std::runtime_error(errorStream.str());
|
|
}
|
|
mDirectorieMap[wd] = filePath;
|
|
if (recurseRoot)
|
|
mRecurseMap[wd] = *recurseRoot;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
inline void Inotify::ignoreFileOnce(fs::path file){
|
|
mOnceIgnoredDirectories.push_back(file.string());
|
|
|
|
}
|
|
|
|
/**
|
|
* @brief Removes watch from set of watches. This
|
|
* is not done recursively!
|
|
*
|
|
* @param wd watchdescriptor
|
|
*
|
|
*/
|
|
inline void Inotify::removeWatch(int wd){
|
|
int result = inotify_rm_watch(mInotifyFd, wd);
|
|
if(result == -1){
|
|
mError = errno;
|
|
std::stringstream errorStream;
|
|
errorStream << "Failed to remove watch! " << strerror(mError) << ".";
|
|
throw std::runtime_error(errorStream.str());
|
|
}
|
|
mDirectorieMap.erase(wd);
|
|
}
|
|
|
|
|
|
inline fs::path Inotify::wdToPath(int wd){
|
|
return mDirectorieMap[wd];
|
|
|
|
}
|
|
|
|
/**
|
|
* @brief Blocking wait on new events of watched files/directories
|
|
* specified on the eventmask. FileSystemEvents
|
|
* will be returned one by one. Thus this
|
|
* function can be called in some while(true)
|
|
* loop.
|
|
*
|
|
* @return A new FileSystemEvent
|
|
*
|
|
*/
|
|
inline FileSystemEvent Inotify::getNextEvent(){
|
|
int length = 0;
|
|
char buffer[EVENT_BUF_LEN];
|
|
time_t currentEventTime = time(NULL);
|
|
std::vector<FileSystemEvent> events;
|
|
|
|
// Read Events from fd into buffer
|
|
while(mEventQueue.empty()){
|
|
length = 0;
|
|
memset(&buffer, 0, EVENT_BUF_LEN);
|
|
while(length <= 0 ){
|
|
length = read(mInotifyFd, buffer, EVENT_BUF_LEN);
|
|
currentEventTime = time(NULL);
|
|
if(length == -1){
|
|
mError = errno;
|
|
if(mError != EINTR){
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Read events from buffer into queue
|
|
currentEventTime = time(NULL);
|
|
int i = 0;
|
|
while(i < length){
|
|
inotify_event *event = ((struct inotify_event*) &buffer[i]);
|
|
fs::path path(wdToPath(event->wd) / std::string(event->name));
|
|
if(fs::is_directory(path)){
|
|
event->mask |= IN_ISDIR;
|
|
}
|
|
FileSystemEvent fsEvent(event->wd, event->mask, path);
|
|
|
|
if(!fsEvent.path.empty()){
|
|
events.push_back(fsEvent);
|
|
|
|
}
|
|
else{
|
|
// Event is not complete --> ignore
|
|
}
|
|
|
|
i += EVENT_SIZE + event->len;
|
|
|
|
}
|
|
|
|
|
|
// Filter events
|
|
for(auto eventIt = events.begin(); eventIt < events.end(); ++eventIt){
|
|
FileSystemEvent currentEvent = *eventIt;
|
|
if(onTimeout(currentEventTime)){
|
|
events.erase(eventIt);
|
|
|
|
}
|
|
else if(isIgnored(currentEvent.path.string())){
|
|
events.erase(eventIt);
|
|
}
|
|
else{
|
|
mLastEventTime = currentEventTime;
|
|
mEventQueue.push(currentEvent);
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Return next event
|
|
FileSystemEvent event = mEventQueue.front();
|
|
mEventQueue.pop();
|
|
return event;
|
|
}
|
|
|
|
std::optional<FileSystemEvent> Inotify::nb_getNextEvent() {
|
|
char buffer[EVENT_BUF_LEN]
|
|
__attribute__ ((aligned(__alignof__(struct inotify_event))));
|
|
std::vector<FileSystemEvent> events;
|
|
|
|
struct pollfd fd = {.fd = mInotifyFd, .events = POLLIN, 0};
|
|
int pn = poll(&fd, 1, 0); // nonblocking poll(2)
|
|
if (pn == -1 && errno != EINTR) {
|
|
perror("poll()");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
if (pn > 0) {
|
|
ssize_t len = read(mInotifyFd, buffer, sizeof buffer);
|
|
if (len <= 0)
|
|
return std::nullopt;
|
|
|
|
struct inotify_event *event;
|
|
for (char *ptr = buffer; ptr < buffer + len;
|
|
ptr += sizeof(struct inotify_event) + event->len) {
|
|
event = (struct inotify_event*) ptr;
|
|
|
|
fs::path path(wdToPath(event->wd) / std::string(event->name));
|
|
if (fs::is_directory(path))
|
|
event->mask |= IN_ISDIR;
|
|
|
|
FileSystemEvent fsEvent(event->wd, event->mask, path);
|
|
|
|
if (mRecurseMap.find(event->wd) != mRecurseMap.end()) {
|
|
fsEvent.recursive_root_path = mRecurseMap[event->wd];
|
|
}
|
|
|
|
if (!fsEvent.path.empty())
|
|
events.push_back(fsEvent);
|
|
}
|
|
|
|
for (auto eventIt = events.begin(); eventIt < events.end(); ++eventIt){
|
|
FileSystemEvent currentEvent = *eventIt;
|
|
if (isIgnored(currentEvent.path.string())) {
|
|
events.erase(eventIt);
|
|
} else {
|
|
mEventQueue.push(currentEvent);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mEventQueue.empty())
|
|
return std::nullopt;
|
|
|
|
auto event = mEventQueue.front(); mEventQueue.pop();
|
|
return event;
|
|
}
|
|
|
|
inline int Inotify::getLastErrno(){
|
|
return mError;
|
|
|
|
}
|
|
|
|
inline bool Inotify::isIgnored(std::string file){
|
|
if(mIgnoredDirectories.empty() and mOnceIgnoredDirectories.empty()){
|
|
return false;
|
|
}
|
|
|
|
for(unsigned i = 0; i < mOnceIgnoredDirectories.size(); ++i){
|
|
size_t pos = file.find(mOnceIgnoredDirectories[i]);
|
|
if(pos!= std::string::npos){
|
|
mOnceIgnoredDirectories.erase(mOnceIgnoredDirectories.begin() + i);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
for(unsigned i = 0; i < mIgnoredDirectories.size(); ++i){
|
|
size_t pos = file.find(mIgnoredDirectories[i]);
|
|
if(pos!= std::string::npos){
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
inline bool Inotify::onTimeout(time_t eventTime){
|
|
return (mLastEventTime + mEventTimeout) > eventTime;
|
|
}
|
|
|