1
0
Fork 0
mirror of https://github.com/beefytech/Beef.git synced 2025-06-08 11:38:21 +02:00
Beef/BeefySysLib/platform/linux/LinuxCommon.cpp
Fusioon 1c2af5467a Linux filewatch improvements
Improve handling of newly created directories and watcher removal
2022-10-11 04:31:03 +02:00

466 lines
16 KiB
C++

#define BFP_HAS_EXECINFO
#define BFP_HAS_PTHREAD_TIMEDJOIN_NP
#define BFP_HAS_PTHREAD_GETATTR_NP
#define BFP_HAS_DLINFO
#define BFP_HAS_FILEWATCHER
#include "../posix/PosixCommon.cpp"
#ifdef BFP_HAS_FILEWATCHER
#include <sys/inotify.h>
struct BfpFileWatcher
{
String mPath;
BfpDirectoryChangeFunc mDirectoryChangeFunc;
int mHandle;
BfpFileWatcherFlags mFlags;
void* mUserData;
};
class InotifyFileWatchManager : public FileWatchManager
{
struct SubdirInfo
{
BfpFileWatcher* mWatcher;
int mHandle;
String mRelativePath;
};
static constexpr size_t MAX_NOTIFY_EVENTS = 1024;
static constexpr size_t NOTIFY_BUFFER_SIZE = (sizeof(inotify_event) * (MAX_NOTIFY_EVENTS + PATH_MAX));
bool mIsClosing;
int mInotifyHandle;
pthread_t mWorkerThread;
Dictionary<int, BfpFileWatcher*> mWatchers;
Dictionary<int, SubdirInfo> mSubdirs;
CritSect mCritSect;
char mEventBuffer[NOTIFY_BUFFER_SIZE];
private:
void WorkerProc()
{
char pathBuffer[PATH_MAX];
Array<inotify_event*> unhandledEvents;
while (!mIsClosing)
{
int length = read(mInotifyHandle, &mEventBuffer, NOTIFY_BUFFER_SIZE);
if (mIsClosing)
break;
if (length < 0)
{
BFP_ERRPRINTF("Failed to read inotify event data!\n");
return;
}
int i = 0;
while(i < length)
{
inotify_event* event = (inotify_event*) &mEventBuffer[i];
if(event->len != 0)
{
BfpFileWatcher* w;
SubdirInfo* subdir;
{
AutoCrit autoCrit(mCritSect);
if (!mWatchers.TryGetValue(event->wd, &w))
continue;
if (!mSubdirs.TryGetValue(event->wd, &subdir))
subdir = NULL;
}
bool handleDir = (event->mask & IN_ISDIR) && (w->mFlags & BfpFileWatcherFlag_IncludeSubdirectories);
if (GetRelativePath(pathBuffer, sizeof(pathBuffer), event->name, event->len, w, subdir) == 0)
{
// our buffer was too small, we can't handle this event
i += sizeof(inotify_event) + event->len;
continue;
}
if (event->mask & IN_MOVED_FROM)
{
unhandledEvents.Add(event);
}
if ((event->mask & IN_MOVED_TO))
{
bool handled = false;
for (int i = 0; i < unhandledEvents.size(); i++)
{
// Only handle as rename if src and dst directory is the same
if ((event->cookie == unhandledEvents[i]->cookie) && (event->wd == unhandledEvents[i]->wd))
{
char renameBuffer[PATH_MAX];
if (GetRelativePath(renameBuffer, sizeof(renameBuffer), unhandledEvents[i]->name, unhandledEvents[i]->len, w, subdir) == 0)
{
break;
}
w->mDirectoryChangeFunc(w, w->mUserData, BfpFileChangeKind_Renamed, w->mPath.c_str(), renameBuffer, pathBuffer);
unhandledEvents.RemoveAtFast(i);
handled = true;
break;
}
}
if (!handled)
{
unhandledEvents.Add(event);
}
}
if (event->mask & IN_CREATE)
{
w->mDirectoryChangeFunc(w, w->mUserData, BfpFileChangeKind_Added, w->mPath.c_str(), pathBuffer, NULL);
HandleDirAdd(event, w, subdir, false);
}
if (event->mask & IN_DELETE)
{
w->mDirectoryChangeFunc(w, w->mUserData, BfpFileChangeKind_Removed, w->mPath.c_str(), pathBuffer, NULL);
HandleDirRemove(event, w, subdir);
}
if ((event->mask & IN_CLOSE_WRITE) || (event->mask & IN_ATTRIB))
{
w->mDirectoryChangeFunc(w, w->mUserData, BfpFileChangeKind_Modified, w->mPath.c_str(), pathBuffer, NULL);
}
}
i += sizeof(inotify_event) + event->len;
}
for (auto event : unhandledEvents)
{
BfpFileWatcher* w;
SubdirInfo* subdir;
{
AutoCrit autoCrit(mCritSect);
if (!mWatchers.TryGetValue(event->wd, &w))
continue;
if (!mSubdirs.TryGetValue(event->wd, &subdir))
subdir = NULL;
}
if (GetRelativePath(pathBuffer, sizeof(pathBuffer), event->name, event->len, w, subdir) == 0)
{
continue;
}
if (event->mask & IN_MOVED_FROM)
{
w->mDirectoryChangeFunc(w, w->mUserData, BfpFileChangeKind_Removed, w->mPath.c_str(), pathBuffer, NULL);
HandleDirRemove(event, w, subdir);
}
if (event->mask & IN_MOVED_TO)
{
w->mDirectoryChangeFunc(w, w->mUserData, BfpFileChangeKind_Added, w->mPath.c_str(), pathBuffer, NULL);
HandleDirAdd(event, w, subdir, true);
}
}
unhandledEvents.Clear();
}
}
static void* WorkerProcThunk(void* _this)
{
BfpThread_SetName(NULL, "InotifyFileWatcher", NULL);
((InotifyFileWatchManager*)_this)->WorkerProc();
return NULL;
}
void HandleDirRemove(const inotify_event* event, const BfpFileWatcher* fileWatch, const SubdirInfo* subdir)
{
const bool shouldHandle = (event->mask & IN_ISDIR) && (fileWatch->mFlags & BfpFileWatcherFlag_IncludeSubdirectories);
if (!shouldHandle)
return;
AutoCrit autoCrit(mCritSect);
Array<int> toRemove;
String removedDir;
if (subdir != NULL)
{
removedDir = subdir->mRelativePath;
removedDir.Append('/');
removedDir.Append(event->name);
}
for (const auto& kv : mSubdirs)
{
if (subdir == NULL)
{
if (kv.mValue.mWatcher == fileWatch)
{
toRemove.Add(kv.mKey);
if (kv.mValue.mRelativePath == event->name)
mWatchers.Remove(kv.mKey); // Watch is already destroyed by OS
}
}
else
{
if ((kv.mValue.mRelativePath.StartsWith(removedDir)))
{
//BFP_ERRPRINTF("REMOVING: %s %s\n", kv.mValue.mRelativePath.c_str(), subdir->mRelativePath.c_str());
toRemove.Add(kv.mKey);
}
}
}
for (const auto val : toRemove)
{
mSubdirs.Remove(val);
if (mWatchers.Remove(val))
InotifyRemoveWatch(val);
}
}
void HandleDirAdd(const inotify_event* event, BfpFileWatcher* fileWatch, const SubdirInfo* subdir, bool wasMoved)
{
const bool shouldHandle = (event->mask & IN_ISDIR) && (fileWatch->mFlags & BfpFileWatcherFlag_IncludeSubdirectories);
if (!shouldHandle)
return;
String dirPath = fileWatch->mPath;
if (subdir != NULL)
{
dirPath.Append('/');
dirPath.Append(subdir->mRelativePath);
}
dirPath.Append('/');
dirPath.Append(event->name);
int watchHandle = InotifyWatchPath(dirPath.c_str());
if (watchHandle == -1)
{
BFP_ERRPRINTF("Failed to add watch for subdirectory '%s' (%d)\n", dirPath.c_str(), errno);
return;
}
AddWatchEntry(watchHandle, fileWatch);
AddSubdirEntry(watchHandle, dirPath, fileWatch);
WatchSubdirectories(dirPath.c_str(), fileWatch, !wasMoved);
}
void AddWatchEntry(int handle, BfpFileWatcher* fileWatcher)
{
AutoCrit autoCrit(mCritSect);
mWatchers[handle] = fileWatcher;
}
void AddSubdirEntry(int handle, const String& currentPath, BfpFileWatcher* fileWatcher)
{
AutoCrit autoCrit(mCritSect);
SubdirInfo info;
info.mHandle = handle;
info.mWatcher = fileWatcher;
auto substringLength = std::min(currentPath.length(), fileWatcher->mPath.length() + 1);
info.mRelativePath = currentPath.Substring(substringLength);
mSubdirs[handle] = info;
}
int InotifyWatchPath(const char* path)
{
return inotify_add_watch(mInotifyHandle, path, IN_CREATE | IN_DELETE | IN_CLOSE_WRITE | IN_ATTRIB | IN_MOVE);
}
void InotifyRemoveWatch(int handle)
{
if (inotify_rm_watch(mInotifyHandle, handle) == -1)
{
BFP_ERRPRINTF("Failed to remove watch handle(%d) err(%d)\n", handle, errno);
}
}
void HandleDirectory(DIR* dirp, String& o_path, Array<DIR*>& o_workList, BfpFileWatcher* fileWatcher, bool sendEvents)
{
struct dirent* dp;
while ((dp = readdir(dirp)) != NULL)
{
if (!strcmp(dp->d_name, ".") || !strcmp(dp->d_name, ".."))
continue;
// Send events for files/dirs inside the directory as don't receive events in newly created directories
if (sendEvents)
{
String localPath = o_path.Substring(std::min(o_path.length(), fileWatcher->mPath.length()+1));
localPath.Append('/');
localPath.Append(dp->d_name);
fileWatcher->mDirectoryChangeFunc(fileWatcher, fileWatcher->mUserData, BfpFileChangeKind_Added, fileWatcher->mPath.c_str(), localPath.c_str(), NULL);
fileWatcher->mDirectoryChangeFunc(fileWatcher, fileWatcher->mUserData, BfpFileChangeKind_Modified, fileWatcher->mPath.c_str(), localPath.c_str(), NULL);
}
if (dp->d_type != DT_DIR)
continue;
const auto length = o_path.length();
o_path.Append('/');
o_path.Append(dp->d_name);
int watchHandle = InotifyWatchPath(o_path.c_str());
if (watchHandle == -1)
{
o_path.RemoveToEnd(length);
BFP_ERRPRINTF("Failed to add watch for subdirectory '%s' (%d)\n", o_path.c_str(), errno);
continue;
}
AddWatchEntry(watchHandle, fileWatcher);
AddSubdirEntry(watchHandle, o_path, fileWatcher);
DIR* todo = opendir(o_path.c_str());
if (todo == NULL)
{
o_path.RemoveToEnd(length);
continue;
}
o_workList.Add(dirp);
o_workList.Add(todo);
return;
}
o_workList.Add(NULL);
closedir(dirp);
}
void WatchSubdirectories(const char* path, BfpFileWatcher* fileWatcher, bool sendEvents)
{
DIR* dirp = opendir(path);
if (dirp == NULL)
return;
Array<DIR*> workList;
String currentPath(path);
HandleDirectory(dirp, currentPath, workList, fileWatcher, sendEvents);
while (workList.size() > 0)
{
dirp = workList.back();
workList.pop_back();
if (dirp == NULL)
{
auto dirSeparator = currentPath.LastIndexOf('/');
if (dirSeparator == -1)
{
BF_ASSERT(workList.IsEmpty());
break;
}
currentPath.RemoveToEnd(dirSeparator);
continue;
}
HandleDirectory(dirp, currentPath, workList, fileWatcher, sendEvents);
}
}
int GetRelativePath(char* buffer, int bufferSize, const char* fileName, int fileNameLength, const BfpFileWatcher* fileWatcher, const SubdirInfo* subdir)
{
if (subdir == NULL)
{
memcpy(buffer, fileName, fileNameLength);
return fileNameLength;
}
const auto subdirLength = subdir->mRelativePath.length();
if (bufferSize < (subdirLength + fileNameLength + 2))
return 0;
memcpy(buffer, subdir->mRelativePath.GetPtr(), subdirLength);
buffer[subdirLength] = '/';
buffer += subdirLength + 1;
memcpy(buffer, fileName, fileNameLength);
buffer[fileNameLength] = '\0';
return subdirLength + fileNameLength + 1;
}
public:
virtual bool Init() override
{
mIsClosing = false;
mInotifyHandle = inotify_init();
if (mInotifyHandle == -1)
{
BFP_ERRPRINTF("Failed to initialize inotify (%d)\n", errno);
return false;
}
int err = pthread_create(&mWorkerThread, NULL, &WorkerProcThunk, this);
if (err != 0)
{
BFP_ERRPRINTF("Failed to create worker thread for inotify FileWatcher!\n");
return false;
}
return true;
}
virtual void Shutdown() override
{
mIsClosing = true;
close(mInotifyHandle);
}
virtual BfpFileWatcher* WatchDirectory(const char* path, BfpDirectoryChangeFunc callback, BfpFileWatcherFlags flags, void* userData, BfpFileResult* outResult) override
{
int watchHandle = InotifyWatchPath(path);
if (watchHandle == -1)
{
BFP_ERRPRINTF("Failed to add watch for directory '%s' (%d)\n", path, errno);
OUTRESULT(BfpFileResult_UnknownError);
return NULL;
}
BfpFileWatcher* fileWatcher = new BfpFileWatcher();
fileWatcher->mPath = path;
fileWatcher->mDirectoryChangeFunc = callback;
fileWatcher->mHandle = watchHandle;
fileWatcher->mFlags = flags;
fileWatcher->mUserData = userData;
AddWatchEntry(watchHandle, fileWatcher);
if (flags & BfpFileWatcherFlag_IncludeSubdirectories)
{
WatchSubdirectories(path, fileWatcher, false);
}
return fileWatcher;
}
virtual void Remove(BfpFileWatcher* watcher) override
{
AutoCrit autoCrit(mCritSect);
if ((watcher->mFlags & BfpFileWatcherFlag_IncludeSubdirectories))
{
Array<int> toRemove;
for (const auto& subdir : mSubdirs)
{
if (subdir.mValue.mWatcher == watcher)
{
toRemove.Add(subdir.mKey);
}
}
for (auto handle : toRemove)
{
mSubdirs.Remove(handle);
if (mWatchers.Remove(handle))
InotifyRemoveWatch(handle);
}
}
// Check if watched directory exists so we don't error/remove other watch
if ((DirectoryExists(watcher->mPath)) && (mWatchers.Remove(watcher->mHandle)))
{
InotifyRemoveWatch(watcher->mHandle);
}
delete watcher;
}
};
FileWatchManager* FileWatchManager::Get()
{
if (gFileWatchManager == NULL)
{
gFileWatchManager = new InotifyFileWatchManager();
gFileWatchManager->Init();
}
return gFileWatchManager;
}
#endif // BFP_HAS_FILEWATCHER