Files
2025-12-10 14:38:26 -08:00

159 lines
6.2 KiB
C++

// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************
#include "dir_watcher.h"
#include "thread.h"
#include "scope_guard.h"
#include "file_access.h"
#include <map>
#include <sys/inotify.h>
#include <fcntl.h> //fcntl
#include <unistd.h> //close
#include <limits.h> //NAME_MAX
#include "file_traverser.h"
using namespace zen;
struct DirWatcher::Impl
{
int notifDescr = 0;
std::unordered_map<int, Zstring> watchedPaths; //watch descriptor and (sub-)directory paths -> owned by "notifDescr"
};
DirWatcher::DirWatcher(const Zstring& dirPath) : //throw FileError
baseDirPath_(dirPath),
pimpl_(std::make_unique<Impl>())
{
//get all subdirectories
std::vector<Zstring> fullFolderList {baseDirPath_};
{
std::function<void(const Zstring& path)> traverse;
traverse = [&traverse, &fullFolderList](const Zstring& path) //throw FileError
{
traverseFolder(path, nullptr,
[&](const FolderInfo& fi )
{
fullFolderList.push_back(fi.fullPath);
traverse(fi.fullPath); //throw FileError
},
nullptr /*don't traverse into symlinks (analog to Windows)*/); //throw FileError
};
traverse(baseDirPath_); //throw FileError
}
//init
pimpl_->notifDescr = ::inotify_init();
if (pimpl_->notifDescr == -1)
THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot monitor directory %x."), L"%x", fmtPath(baseDirPath_)), "inotify_init");
ZEN_ON_SCOPE_FAIL( ::close(pimpl_->notifDescr); );
//set non-blocking mode
const int flags = ::fcntl(pimpl_->notifDescr, F_GETFL);
if (flags == -1)
THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot monitor directory %x."), L"%x", fmtPath(baseDirPath_)), "fcntl(F_GETFL)");
if (::fcntl(pimpl_->notifDescr, F_SETFL, flags | O_NONBLOCK) != 0)
THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot monitor directory %x."), L"%x", fmtPath(baseDirPath_)), "fcntl(F_SETFL, O_NONBLOCK)");
//add watches
for (const Zstring& subDirPath : fullFolderList)
{
int wd = ::inotify_add_watch(pimpl_->notifDescr, subDirPath.c_str(),
IN_ONLYDIR | //"Only watch pathname if it is a directory."
IN_DONT_FOLLOW | //don't follow symbolic links
IN_CREATE |
IN_MODIFY |
IN_CLOSE_WRITE |
IN_DELETE |
IN_DELETE_SELF |
IN_MOVED_FROM |
IN_MOVED_TO |
IN_MOVE_SELF);
if (wd == -1)
{
const ErrorCode ec = getLastError(); //copy before directly/indirectly making other system calls!
if (ec == ENOSPC) //fix misleading system message "No space left on device"
throw FileError(replaceCpy(_("Cannot monitor directory %x."), L"%x", fmtPath(subDirPath)),
formatSystemError("inotify_add_watch", L"ENOSPC",
L"The user limit on the total number of inotify watches was reached or the kernel failed to allocate a needed resource."));
throw FileError(replaceCpy(_("Cannot monitor directory %x."), L"%x", fmtPath(subDirPath)), formatSystemError("inotify_add_watch", ec));
}
pimpl_->watchedPaths.emplace(wd, subDirPath);
}
}
DirWatcher::~DirWatcher()
{
::close(pimpl_->notifDescr); //associated watches are removed automatically!
}
std::vector<DirWatcher::Change> DirWatcher::fetchChanges(const std::function<void()>& requestUiUpdate, std::chrono::milliseconds cbInterval) //throw FileError
{
std::vector<std::byte> buf(512 * (sizeof(inotify_event) + NAME_MAX + 1));
ssize_t bytesRead = 0;
do
{
//non-blocking call, see O_NONBLOCK
bytesRead = ::read(pimpl_->notifDescr, buf.data(), buf.size());
}
while (bytesRead < 0 && errno == EINTR); //"Interrupted function call; When this happens, you should try the call again."
if (bytesRead < 0)
{
if (errno == EAGAIN) //this error is ignored in all inotify wrappers I found
return std::vector<Change>();
THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot monitor directory %x."), L"%x", fmtPath(baseDirPath_)), "read");
}
std::vector<Change> output;
ssize_t bytePos = 0;
while (bytePos < bytesRead)
{
inotify_event& evt = reinterpret_cast<inotify_event&>(buf[bytePos]);
if (evt.len != 0) //exclude case: deletion of "self", already reported by parent directory watch
{
auto it = pimpl_->watchedPaths.find(evt.wd);
if (it != pimpl_->watchedPaths.end())
{
//Note: evt.len is NOT the size of the evt.name c-string, but the array size including all padding 0 characters!
//It may be even 0 in which case evt.name must not be used!
const Zstring itemPath = appendPath(it->second, evt.name);
if ((evt.mask & IN_CREATE) ||
(evt.mask & IN_MOVED_TO))
output.push_back({ChangeType::create, itemPath});
else if ((evt.mask & IN_MODIFY) ||
(evt.mask & IN_CLOSE_WRITE))
output.push_back({ChangeType::update, itemPath});
else if ((evt.mask & IN_DELETE ) ||
(evt.mask & IN_DELETE_SELF) ||
(evt.mask & IN_MOVE_SELF ) ||
(evt.mask & IN_MOVED_FROM))
output.push_back({ChangeType::remove, itemPath});
}
}
bytePos += sizeof(inotify_event) + evt.len;
}
return output;
}