159 lines
6.2 KiB
C++
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;
|
|
}
|
|
|