/* * Copyright (C) by Klaas Freitag * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. */ // event masks #include "folderwatcher.h" #include #include #include #if defined(Q_OS_WIN) #include "folderwatcher_win.h" #elif defined(Q_OS_MAC) #include "folderwatcher_mac.h" #elif defined(Q_OS_UNIX) #include "folderwatcher_linux.h" #endif #include "folder.h" #include "filesystem.h" using namespace std::chrono_literals; namespace { constexpr auto notificationTimeoutC = 10s; } namespace OCC { Q_LOGGING_CATEGORY(lcFolderWatcher, "gui.folderwatcher", QtInfoMsg) FolderWatcher::FolderWatcher(Folder *folder) : QObject(folder) , _folder(folder) { _timer.setInterval(notificationTimeoutC); _timer.setSingleShot(true); connect(&_timer, &QTimer::timeout, this, [this] { auto paths = popChangeSet(); Q_ASSERT(!paths.empty()); if (!paths.isEmpty()) { qCInfo(lcFolderWatcher) << u"Detected changes in paths:" << paths; Q_EMIT pathChanged(paths); } }); } FolderWatcher::~FolderWatcher() { } void FolderWatcher::init(const QString &root) { _d.reset(new FolderWatcherPrivate(this, root)); } bool FolderWatcher::pathIsIgnored(const QString &path) const { Q_ASSERT(!path.isEmpty()); Q_ASSERT(_folder); if (!QFileInfo::exists(path)) { std::error_code ec; const auto relPath = FileSystem::fromFilesystemPath(std::filesystem::proximate(FileSystem::toFilesystemPath(path), FileSystem::toFilesystemPath(_folder->path()), ec)); Q_ASSERT(!ec); if (ec) { qCDebug(lcFolderWatcher) << u"* Failed to lookup record for path:" << path << u"error:" << ec.message(); return false; } if (auto record = _folder->journalDb()->getFileRecord(relPath); record.isValid()) { // we know about the file, it got removed, we should not ignore that qCDebug(lcFolderWatcher) << u"* Not ignoring removed file" << path; return false; } // probably a temporary file that no longer exists qCDebug(lcFolderWatcher) << u"* Ignoring file" << path << u"It no longer exists and we don't have it in the database."; return true; } if (_folder->isFileExcludedAbsolute(path) && !Utility::isConflictFile(path)) { qCDebug(lcFolderWatcher) << u"* Ignoring file" << path; return true; } return false; } bool FolderWatcher::isReliable() const { return _isReliable; } void FolderWatcher::startNotificatonTest(const QString &path) { #ifdef Q_OS_MAC // Testing the folder watcher on OSX is harder because the watcher // automatically discards changes that were performed by our process. // It would still be useful to test but the OSX implementation // is deferred until later. return; #endif Q_ASSERT(_testNotificationPath.isEmpty()); Q_ASSERT(!path.isEmpty()); _testNotificationPath = path; // Don't do the local file modification immediately: // wait for FolderWatchPrivate::_ready startNotificationTestWhenReady(); } void FolderWatcher::startNotificationTestWhenReady() { if (!_testNotificationPath.isEmpty()) { // we already received the notification return; } if (!_d->isReady()) { QTimer::singleShot(1s, this, &FolderWatcher::startNotificationTestWhenReady); return; } if (OC_ENSURE(QFile::exists(_testNotificationPath))) { const auto mtime = FileSystem::getModTime(_testNotificationPath); FileSystem::setModTime(_testNotificationPath, mtime + 1); } else { QFile f(_testNotificationPath); f.open(QIODevice::WriteOnly | QIODevice::Append); } QTimer::singleShot(notificationTimeoutC + 5s, this, [this]() { if (!_testNotificationPath.isEmpty()) Q_EMIT becameUnreliable(tr("The watcher did not receive a test notification.")); _testNotificationPath.clear(); }); } int FolderWatcher::testLinuxWatchCount() const { #ifdef Q_OS_LINUX return _d->testWatchCount(); #else return -1; #endif } void FolderWatcher::addChanges(QSet &&paths) { Q_ASSERT(thread() == QThread::currentThread()); // the timer must be inactive if we haven't received changes yet Q_ASSERT((!_timer.isActive() && _changeSet.isEmpty()) || !_changeSet.isEmpty()); Q_ASSERT(!paths.isEmpty()); // ------- handle ignores: auto it = paths.cbegin(); while (it != paths.cend()) { // we cause a file change from time to time to check whether the folder watcher works as expected if (!_testNotificationPath.isEmpty() && Utility::fileNamesEqual(*it, _testNotificationPath)) { _testNotificationPath.clear(); } if (pathIsIgnored(*it)) { it = paths.erase(it); } else { ++it; } } if (!paths.isEmpty()) { _changeSet.unite(paths); if (!_timer.isActive()) { _timer.start(); // promote that we will report changes once _timer times out Q_EMIT changesDetected(); } } } QSet FolderWatcher::popChangeSet() { // stop the timer as we pop all queued changes _timer.stop(); return std::move(_changeSet); } } // namespace OCC