diff --git a/autotests/unit/file/filewatchtest.cpp b/autotests/unit/file/filewatchtest.cpp index 717091cd..e288741d 100644 --- a/autotests/unit/file/filewatchtest.cpp +++ b/autotests/unit/file/filewatchtest.cpp @@ -1,234 +1,232 @@ /* * * Copyright (C) 2014 Vishesh Handa * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ #include "filewatch.h" #include "fileindexerconfigutils.h" #include "database.h" #include "fileindexerconfig.h" #include "pendingfilequeue.h" #include #include #include #include namespace Baloo { class FileWatchTest : public QObject { Q_OBJECT private Q_SLOTS: void testFileCreation(); void testConfigChange(); }; } using namespace Baloo; namespace { bool createFile(const QString& fileUrl) { QFile f1(fileUrl); f1.open(QIODevice::WriteOnly); f1.close(); return QFile::exists(fileUrl); } void modifyFile(const QString& fileUrl) { QFile f1(fileUrl); f1.open(QIODevice::Append | QIODevice::Text); QTextStream stream(&f1); stream << "1"; } } void FileWatchTest::testFileCreation() { QTemporaryDir includeDir; QStringList includeFolders; includeFolders << includeDir.path(); QStringList excludeFolders; Test::writeIndexerConfig(includeFolders, excludeFolders); QTemporaryDir dbDir; Database db(dbDir.path()); db.open(Baloo::Database::CreateDatabase); FileIndexerConfig config; FileWatch fileWatch(&db, &config); fileWatch.m_pendingFileQueue->setMaximumTimeout(0); fileWatch.m_pendingFileQueue->setMinimumTimeout(0); fileWatch.m_pendingFileQueue->setTrackingTime(0); QSignalSpy spy(&fileWatch, SIGNAL(installedWatches())); QVERIFY(spy.isValid()); fileWatch.watchIndexedFolders(); QVERIFY(spy.count() || spy.wait()); QSignalSpy spyIndexNew(&fileWatch, SIGNAL(indexNewFile(QString))); QSignalSpy spyIndexModified(&fileWatch, SIGNAL(indexModifiedFile(QString))); QSignalSpy spyIndexXattr(&fileWatch, SIGNAL(indexXAttr(QString))); QVERIFY(spyIndexNew.isValid()); QVERIFY(spyIndexModified.isValid()); QVERIFY(spyIndexXattr.isValid()); // Create a file and see if it is indexed QString fileUrl(includeDir.path() + "/t1"); QVERIFY(createFile(fileUrl)); QVERIFY(spyIndexNew.wait()); QCOMPARE(spyIndexNew.count(), 1); QCOMPARE(spyIndexModified.count(), 0); QCOMPARE(spyIndexXattr.count(), 0); spyIndexNew.clear(); spyIndexModified.clear(); spyIndexXattr.clear(); // // Modify the file // modifyFile(fileUrl); QVERIFY(spyIndexModified.wait()); QCOMPARE(spyIndexNew.count(), 0); QCOMPARE(spyIndexModified.count(), 1); QCOMPARE(spyIndexXattr.count(), 0); spyIndexNew.clear(); spyIndexModified.clear(); spyIndexXattr.clear(); // // Set an Xattr // KFileMetaData::UserMetaData umd(fileUrl); if (!umd.isSupported()) { qWarning() << "Xattr not supported on this filesystem:" << fileUrl; return; } const QString userComment(QStringLiteral("UserComment")); QVERIFY(umd.setUserComment(userComment) == KFileMetaData::UserMetaData::NoError); QVERIFY(spyIndexXattr.wait()); QCOMPARE(spyIndexNew.count(), 0); QCOMPARE(spyIndexModified.count(), 0); QCOMPARE(spyIndexXattr.count(), 1); spyIndexNew.clear(); spyIndexModified.clear(); spyIndexXattr.clear(); } void FileWatchTest::testConfigChange() { QTemporaryDir tmpDir; Database db(tmpDir.path()); db.open(Baloo::Database::CreateDatabase); QString d1 = tmpDir.path() + QStringLiteral("/d1"); QString d2 = tmpDir.path() + QStringLiteral("/d2"); QString d11 = tmpDir.path() + QStringLiteral("/d1/d11"); QString d21 = tmpDir.path() + QStringLiteral("/d2/d21"); QString d22 = tmpDir.path() + QStringLiteral("/d2/d22"); QDir().mkpath(d11); QDir().mkpath(d21); QDir().mkpath(d22); // parameters: includeFolders list, excludeFolders list Test::writeIndexerConfig({d1, d2}, {d11, d21}); FileIndexerConfig config; FileWatch fileWatch(&db, &config); fileWatch.m_pendingFileQueue->setMaximumTimeout(0); fileWatch.m_pendingFileQueue->setMinimumTimeout(0); fileWatch.m_pendingFileQueue->setTrackingTime(0); QSignalSpy spy(&fileWatch, SIGNAL(installedWatches())); QVERIFY(spy.isValid()); fileWatch.updateIndexedFoldersWatches(); QVERIFY(spy.count() || spy.wait()); QSignalSpy spyIndexNew(&fileWatch, SIGNAL(indexNewFile(QString))); QVERIFY(spyIndexNew.isValid()); QVERIFY(createFile(d1 + "/t1")); QVERIFY(createFile(d2 + "/t2")); QVERIFY(spyIndexNew.wait()); QCOMPARE(spyIndexNew.count(), 2); spyIndexNew.clear(); // dir d22 is not yet excluded, so one event is expected QVERIFY(createFile(d11 + "/tx1")); QVERIFY(createFile(d21 + "/tx2")); QVERIFY(createFile(d22 + "/tx3")); QVERIFY(spyIndexNew.wait()); QCOMPARE(spyIndexNew.count(), 1); QList event = spyIndexNew.at(0); QCOMPARE(event.at(0).toString(), d22 + "/tx3"); spyIndexNew.clear(); Test::writeIndexerConfig({d2}, {d22}); config.forceConfigUpdate(); fileWatch.updateIndexedFoldersWatches(); // dir d1 is no longer included QVERIFY(createFile(d1 + "/tx1a")); QVERIFY(createFile(d2 + "/tx2a")); QVERIFY(spyIndexNew.wait()); QList result; for (const QList& event : qAsConst(spyIndexNew)) { result.append(event.at(0).toString()); } QEXPECT_FAIL("", "Removal of included folders not deteced", Continue); QCOMPARE(result, {d2 + "/tx2a"}); spyIndexNew.clear(); result.clear(); // d11 is implicitly excluded, as d1 is no longer included // d22 is explicitly excluded now, d21 is included QVERIFY(createFile(d11 + "/tx1b")); QVERIFY(createFile(d21 + "/tx2b")); QVERIFY(createFile(d22 + "/tx3b")); - QEXPECT_FAIL("", "Removal of excluded folders not deteced", Continue); QVERIFY(spyIndexNew.wait(500)); for (const QList& event : qAsConst(spyIndexNew)) { result.append(event.at(0).toString()); } - QEXPECT_FAIL("", "Removal of excluded folders not deteced", Continue); QCOMPARE(result, {d21 + "/tx2b"}); } QTEST_MAIN(FileWatchTest) #include "filewatchtest.moc" diff --git a/autotests/unit/file/kinotifytest.cpp b/autotests/unit/file/kinotifytest.cpp index b5d630bf..bdce82b2 100644 --- a/autotests/unit/file/kinotifytest.cpp +++ b/autotests/unit/file/kinotifytest.cpp @@ -1,440 +1,440 @@ /* Copyright (C) 2010 by Sebastian Trueg Copyright (C) 2014 by Vishesh Handa 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "kinotify.h" #include #include #include #include #include #include #include class KInotifyTest : public QObject { Q_OBJECT private Q_SLOTS: void testDeleteFile(); void testDeleteFolder(); void testCreateFolder(); void testRenameFile(); void testMoveFile(); void testRenameFolder(); void testMoveFolder(); void testMoveFromUnwatchedFolder(); void testMoveRootFolder(); void testFileClosedAfterWrite(); }; namespace { void touchFile(const QString& path) { QFile file(path); file.open(QIODevice::WriteOnly); QTextStream s(&file); s << KRandom::randomString(10); } void mkdir(const QString& path) { QDir().mkpath(path); QVERIFY(QDir(path).exists()); } } void KInotifyTest::testDeleteFile() { // create some test files QTemporaryDir dir; const QString f1(QStringLiteral("%1/randomJunk1").arg(dir.path())); touchFile(f1); // start the inotify watcher KInotify kn(nullptr /*no config*/); kn.addWatch(dir.path(), KInotify::EventAll); // listen to the desired signal QSignalSpy spy(&kn, SIGNAL(deleted(QString,bool))); // test removing a file QFile::remove(f1); QVERIFY(spy.wait()); QCOMPARE(spy.count(), 1); QCOMPARE(spy.takeFirst().at(0).toString(), f1); } void KInotifyTest::testDeleteFolder() { // create some test files QTemporaryDir dir; - const QString d1(QStringLiteral("%1/randomJunk4").arg(dir.path())); + const QString d1(QStringLiteral("%1/randomJunk4/").arg(dir.path())); mkdir(d1); // start the inotify watcher KInotify kn(nullptr /*no config*/); kn.addWatch(dir.path(), KInotify::EventAll); // listen to the desired signal QSignalSpy spy(&kn, SIGNAL(deleted(QString,bool))); // test removing a folder QDir().rmdir(d1); QVERIFY(spy.wait()); QCOMPARE(spy.count(), 1); QCOMPARE(spy.first().at(0).toString(), d1); QCOMPARE(spy.first().at(1).toBool(), true); // make sure we do not watch the removed folder anymore QVERIFY(!kn.watchingPath(d1)); } void KInotifyTest::testCreateFolder() { QTemporaryDir dir; // start the inotify watcher KInotify kn(nullptr /*no config*/); kn.addWatch(dir.path(), KInotify::EventAll); // listen to the desired signal QSignalSpy createdSpy(&kn, SIGNAL(created(QString,bool))); // create the subdir - const QString d1(QStringLiteral("%1/randomJunk1").arg(dir.path())); + const QString d1(QStringLiteral("%1/randomJunk1/").arg(dir.path())); mkdir(d1); QVERIFY(createdSpy.wait()); QCOMPARE(createdSpy.count(), 1); QCOMPARE(createdSpy.takeFirst().at(0).toString(), d1); QVERIFY(kn.watchingPath(d1)); // lets go one level deeper - const QString d2 = QStringLiteral("%1/subdir1").arg(d1); + const QString d2 = QStringLiteral("%1subdir1/").arg(d1); mkdir(d2); QVERIFY(createdSpy.wait()); QCOMPARE(createdSpy.count(), 1); QCOMPARE(createdSpy.takeFirst().at(0).toString(), d2); QVERIFY(kn.watchingPath(d2)); // although we are in the folder test lets try creating a file - const QString f1 = QStringLiteral("%1/somefile1").arg(d2); + const QString f1 = QStringLiteral("%1somefile1").arg(d2); touchFile(f1); QVERIFY(createdSpy.wait()); QCOMPARE(createdSpy.count(), 1); QCOMPARE(createdSpy.takeFirst().at(0).toString(), f1); } void KInotifyTest::testRenameFile() { // create some test files QTemporaryDir dir; const QString f1(QStringLiteral("%1/randomJunk1").arg(dir.path())); touchFile(f1); // start the inotify watcher KInotify kn(nullptr /*no config*/); kn.addWatch(dir.path(), KInotify::EventAll); // listen to the desired signal QSignalSpy spy(&kn, SIGNAL(moved(QString,QString))); // actually move the file const QString f2(QStringLiteral("%1/randomJunk2").arg(dir.path())); QFile::rename(f1, f2); // check the desired signal QVERIFY(spy.wait()); QCOMPARE(spy.count(), 1); QList args = spy.takeFirst(); QCOMPARE(args.at(0).toString(), f1); QCOMPARE(args.at(1).toString(), f2); // test a subsequent rename const QString f3(QStringLiteral("%1/randomJunk3").arg(dir.path())); QFile::rename(f2, f3); // check the desired signal QVERIFY(spy.wait()); QCOMPARE(spy.count(), 1); args = spy.takeFirst(); QCOMPARE(args.at(0).toString(), f2); QCOMPARE(args.at(1).toString(), f3); } void KInotifyTest::testMoveFile() { // create some test files QTemporaryDir dir1; QTemporaryDir dir2; const QString src(QStringLiteral("%1/randomJunk1").arg(dir1.path())); const QString dest(QStringLiteral("%1/randomJunk2").arg(dir2.path())); touchFile(src); // start the inotify watcher KInotify kn(nullptr /*no config*/); kn.addWatch(dir1.path(), KInotify::EventAll); kn.addWatch(dir2.path(), KInotify::EventAll); // listen to the desired signal QSignalSpy spy(&kn, SIGNAL(moved(QString,QString))); // actually move the file QFile::rename(src, dest); // check the desired signal QVERIFY(spy.wait()); QCOMPARE(spy.count(), 1); QList args = spy.takeFirst(); QCOMPARE(args.at(0).toString(), src); QCOMPARE(args.at(1).toString(), dest); // test a subsequent move (back to the original folder) const QString dest2(QStringLiteral("%1/randomJunk3").arg(dir1.path())); QFile::rename(dest, dest2); // check the desired signal QVERIFY(spy.wait()); QCOMPARE(spy.count(), 1); args = spy.takeFirst(); QCOMPARE(args.at(0).toString(), dest); QCOMPARE(args.at(1).toString(), dest2); } void KInotifyTest::testRenameFolder() { // create some test files QTemporaryDir dir; - const QString f1(QStringLiteral("%1/randomJunk1").arg(dir.path())); - mkdir(f1); + const QString d1(QStringLiteral("%1/randomJunk1/").arg(dir.path())); + mkdir(d1); // start the inotify watcher KInotify kn(nullptr /*no config*/); kn.addWatch(dir.path(), KInotify::EventAll); // listen to the desired signal QSignalSpy spy(&kn, SIGNAL(moved(QString,QString))); - // actually move the file - const QString f2(QStringLiteral("%1/randomJunk2").arg(dir.path())); - QFile::rename(f1, f2); + // actually rename the folder + const QString d2(QStringLiteral("%1/randomJunk2/").arg(dir.path())); + QFile::rename(d1, d2); // check the desired signal QVERIFY(spy.wait()); QCOMPARE(spy.count(), 1); QList args = spy.takeFirst(); - QCOMPARE(args.at(0).toString(), f1); - QCOMPARE(args.at(1).toString(), f2); + QCOMPARE(args.at(0).toString(), d1); + QCOMPARE(args.at(1).toString(), d2); // check the path cache - QVERIFY(!kn.watchingPath(f1)); - QVERIFY(kn.watchingPath(f2)); + QVERIFY(!kn.watchingPath(d1)); + QVERIFY(kn.watchingPath(d2)); // test a subsequent rename - const QString f3(QStringLiteral("%1/randomJunk3").arg(dir.path())); - QFile::rename(f2, f3); + const QString d3(QStringLiteral("%1/randomJunk3/").arg(dir.path())); + QFile::rename(d2, d3); // check the desired signal QVERIFY(spy.wait()); QCOMPARE(spy.count(), 1); args = spy.takeFirst(); - QCOMPARE(args.at(0).toString(), f2); - QCOMPARE(args.at(1).toString(), f3); + QCOMPARE(args.at(0).toString(), d2); + QCOMPARE(args.at(1).toString(), d3); // check the path cache - QVERIFY(!kn.watchingPath(f1)); - QVERIFY(!kn.watchingPath(f2)); - QVERIFY(kn.watchingPath(f3)); + QVERIFY(!kn.watchingPath(d1)); + QVERIFY(!kn.watchingPath(d2)); + QVERIFY(kn.watchingPath(d3)); // KInotify claims it has updated its data structures, lets see if that is true // by creating a file in the new folder // listen to the desired signal - const QString f4(QStringLiteral("%1/somefile").arg(f3)); + const QString f4(QStringLiteral("%1somefile").arg(d3)); QSignalSpy createdSpy(&kn, SIGNAL(created(QString,bool))); // test creating a file touchFile(f4); QVERIFY(createdSpy.wait()); QCOMPARE(createdSpy.count(), 1); QCOMPARE(createdSpy.takeFirst().at(0).toString(), f4); } void KInotifyTest::testMoveFolder() { // create some test files QTemporaryDir dir1; QTemporaryDir dir2; - const QString src(QStringLiteral("%1/randomJunk1").arg(dir1.path())); - const QString dest(QStringLiteral("%1/randomJunk2").arg(dir2.path())); + const QString src(QStringLiteral("%1/randomJunk1/").arg(dir1.path())); + const QString dest(QStringLiteral("%1/randomJunk2/").arg(dir2.path())); mkdir(src); // start the inotify watcher KInotify kn(nullptr /*no config*/); kn.addWatch(dir1.path(), KInotify::EventAll); kn.addWatch(dir2.path(), KInotify::EventAll); // listen to the desired signal QSignalSpy spy(&kn, SIGNAL(moved(QString,QString))); // actually move the file QFile::rename(src, dest); // check the desired signal QVERIFY(spy.wait()); QCOMPARE(spy.count(), 1); QList args = spy.takeFirst(); QCOMPARE(args.at(0).toString(), src); QCOMPARE(args.at(1).toString(), dest); // check the path cache QVERIFY(!kn.watchingPath(src)); QVERIFY(kn.watchingPath(dest)); // test a subsequent move - const QString dest2(QStringLiteral("%1/randomJunk3").arg(dir1.path())); + const QString dest2(QStringLiteral("%1/randomJunk3/").arg(dir1.path())); QFile::rename(dest, dest2); // check the desired signal QVERIFY(spy.wait()); QCOMPARE(spy.count(), 1); args = spy.takeFirst(); QCOMPARE(args.at(0).toString(), dest); QCOMPARE(args.at(1).toString(), dest2); // check the path cache QVERIFY(!kn.watchingPath(src)); QVERIFY(!kn.watchingPath(dest)); QVERIFY(kn.watchingPath(dest2)); // KInotify claims it has updated its data structures, lets see if that is true // by creating a file in the new folder // listen to the desired signal - const QString f4(QStringLiteral("%1/somefile").arg(dest2)); + const QString f4(QStringLiteral("%1somefile").arg(dest2)); QSignalSpy createdSpy(&kn, SIGNAL(created(QString,bool))); // test creating a file touchFile(f4); QVERIFY(createdSpy.wait()); QCOMPARE(createdSpy.count(), 1); QCOMPARE(createdSpy.takeFirst().at(0).toString(), f4); } void KInotifyTest::testMoveFromUnwatchedFolder() { // create some test folders QTemporaryDir dir; const QString src(QStringLiteral("%1/randomJunk1").arg(dir.path())); const QString dest(QStringLiteral("%1/randomJunk2").arg(dir.path())); mkdir(src); mkdir(dest); // Start watching only for destination KInotify kn(nullptr); kn.addWatch(dest, KInotify::EventAll); QSignalSpy spy(&kn, &KInotify::created); // Create stuff inside src mkdir(QStringLiteral("%1/sub").arg(src)); touchFile(QStringLiteral("%1/sub/file1").arg(src)); mkdir(QStringLiteral("%1/sub/sub1").arg(src)); touchFile(QStringLiteral("%1/sub/sub1/file2").arg(src)); // Now move QFile::rename(QStringLiteral("%1/sub").arg(src), QStringLiteral("%1/sub").arg(dest)); QVERIFY(spy.wait()); QCOMPARE(spy.count(), 4); // Checking if watches are installed QSignalSpy spy1(&kn, &KInotify::deleted); QDir dstdir(QStringLiteral("%1/sub").arg(dest)); dstdir.removeRecursively(); QVERIFY(spy1.wait()); QCOMPARE(spy1.count(), 4); } void KInotifyTest::testMoveRootFolder() { // create some test folders QTemporaryDir dir; const QString src(QStringLiteral("%1/randomJunk1").arg(dir.path())); const QString dest(QStringLiteral("%1/randomJunk2").arg(dir.path())); mkdir(src); // start watching the new subfolder only KInotify kn(nullptr /*no config*/); kn.addWatch(src, KInotify::EventAll); // listen for the moved signal QSignalSpy spy(&kn, SIGNAL(moved(QString,QString))); // actually move the file QFile::rename(src, dest); // check the desired signal QEXPECT_FAIL("", "KInotify cannot handle moving of top-level folders.", Abort); QVERIFY(spy.wait(500)); QCOMPARE(spy.count(), 1); QList args = spy.takeFirst(); QCOMPARE(args.at(0).toString(), src); QCOMPARE(args.at(1).toString(), dest); // check the path cache QVERIFY(!kn.watchingPath(src)); QVERIFY(kn.watchingPath(dest)); } void KInotifyTest::testFileClosedAfterWrite() { QTemporaryDir dir; touchFile(dir.path() + QLatin1String("/file")); KInotify kn(nullptr /*no config*/); kn.addWatch(dir.path(), KInotify::EventAll); QSignalSpy spy(&kn, SIGNAL(closedWrite(QString))); touchFile(dir.path() + QLatin1String("/file")); QVERIFY(spy.wait()); QCOMPARE(spy.count(), 1); QCOMPARE(spy.at(0).first().toString(), QString(dir.path() + QLatin1String("/file"))); } QTEST_GUILESS_MAIN(KInotifyTest) #include "kinotifytest.moc" diff --git a/src/file/kinotify.cpp b/src/file/kinotify.cpp index 11b06ff5..ece38ce3 100644 --- a/src/file/kinotify.cpp +++ b/src/file/kinotify.cpp @@ -1,512 +1,514 @@ /* This file is part of the KDE libraries Copyright (C) 2007-2010 Sebastian Trueg Copyright (C) 2012-2014 Vishesh Handa This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kinotify.h" #include "fileindexerconfig.h" #include "filtereddiriterator.h" #include "baloodebug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { -QByteArray stripTrailingSlash(const QByteArray& path) +QByteArray normalizeTrailingSlash(QByteArray&& path) { - QByteArray p(path); - if (p.endsWith('/')) - p.chop(1); - return p; + if (!path.endsWith('/')) + path.append('/'); + return path; } QByteArray concatPath(const QByteArray& p1, const QByteArray& p2) { QByteArray p(p1); if (p.isEmpty() || (!p2.isEmpty() && p[p.length() - 1] != '/')) p.append('/'); p.append(p2); return p; } } class KInotify::Private { public: Private(KInotify* parent) : userLimitReachedSignaled(false) , config(nullptr) , m_dirIter(nullptr) , m_inotifyFd(-1) , m_notifier(nullptr) , q(parent) { } ~Private() { close(); delete m_dirIter; } struct MovedFileCookie { QDeadlineTimer deadline; QByteArray path; WatchFlags flags; }; QHash cookies; QTimer cookieExpireTimer; // This variable is set to true if the watch limit is reached, and reset when it is raised bool userLimitReachedSignaled; // url <-> wd mappings QHash watchPathHash; QHash pathWatchHash; Baloo::FileIndexerConfig* config; QStringList m_paths; Baloo::FilteredDirIterator* m_dirIter; // FIXME: only stored from the last addWatch call WatchEvents mode; WatchFlags flags; int inotify() { if (m_inotifyFd < 0) { open(); } return m_inotifyFd; } void close() { delete m_notifier; m_notifier = nullptr; ::close(m_inotifyFd); m_inotifyFd = -1; } bool addWatch(const QString& path) { WatchEvents newMode = mode; WatchFlags newFlags = flags; // we always need the unmount event to maintain our path hash const int mask = newMode | newFlags | EventUnmount | FlagExclUnlink; - const QByteArray encpath = stripTrailingSlash(QFile::encodeName(path)); + const QByteArray encpath = normalizeTrailingSlash(QFile::encodeName(path)); int wd = inotify_add_watch(inotify(), encpath.data(), mask); if (wd > 0) { // qCDebug(BALOO) << "Successfully added watch for" << path << watchPathHash.count(); watchPathHash.insert(wd, encpath); pathWatchHash.insert(encpath, wd); return true; } else { qCDebug(BALOO) << "Failed to create watch for" << path << strerror(errno); //If we could not create the watch because we have hit the limit, try raising it. if (errno == ENOSPC) { //If we can't, fall back to signalling qCDebug(BALOO) << "User limit reached. Count: " << watchPathHash.count(); userLimitReachedSignaled = true; Q_EMIT q->watchUserLimitReached(path); } return false; } } void removeWatch(int wd) { pathWatchHash.remove(watchPathHash.take(wd)); inotify_rm_watch(inotify(), wd); } /** * Add one watch and call oneself asynchronously */ bool _k_addWatches() { bool addedWatchSuccessfully = false; //Do nothing if the inotify user limit has been signaled. if (userLimitReachedSignaled) { return false; } // It is much faster to add watches in batches instead of adding each one // asynchronously. Try out the inotify test to compare results. for (int i = 0; i < 1000; i++) { if (!m_dirIter || m_dirIter->next().isEmpty()) { if (!m_paths.isEmpty()) { delete m_dirIter; m_dirIter = new Baloo::FilteredDirIterator(config, m_paths.takeFirst(), Baloo::FilteredDirIterator::DirsOnly); } else { delete m_dirIter; m_dirIter = nullptr; break; } } else { QString path = m_dirIter->filePath(); addedWatchSuccessfully = addWatch(path); } } // asynchronously add the next batch if (m_dirIter) { QMetaObject::invokeMethod(q, "_k_addWatches", Qt::QueuedConnection); } else { Q_EMIT q->installedWatches(); } return addedWatchSuccessfully; } private: void open() { m_inotifyFd = inotify_init(); delete m_notifier; if (m_inotifyFd > 0) { fcntl(m_inotifyFd, F_SETFD, FD_CLOEXEC); m_notifier = new QSocketNotifier(m_inotifyFd, QSocketNotifier::Read); connect(m_notifier, &QSocketNotifier::activated, q, &KInotify::slotEvent); } else { Q_ASSERT_X(0, "kinotify", "Failed to initialize inotify"); } } int m_inotifyFd; QSocketNotifier* m_notifier; KInotify* q; }; KInotify::KInotify(Baloo::FileIndexerConfig* config, QObject* parent) : QObject(parent) , d(new Private(this)) { d->config = config; // 1 second is more than enough time for the EventMoveTo event to occur // after the EventMoveFrom event has occurred d->cookieExpireTimer.setInterval(1000); d->cookieExpireTimer.setSingleShot(true); connect(&d->cookieExpireTimer, &QTimer::timeout, this, &KInotify::slotClearCookies); } KInotify::~KInotify() { delete d; } bool KInotify::available() const { if (d->inotify() > 0) { // trueg: Copied from KDirWatch. struct utsname uts; int major, minor, patch = 0; if (uname(&uts) < 0) { return false; // *shrug* } else if (sscanf(uts.release, "%d.%d.%d", &major, &minor, &patch) != 3) { //Kernels > 3.0 can in principle have two-number versions. if (sscanf(uts.release, "%d.%d", &major, &minor) != 2) return false; // *shrug* } else if (major * 1000000 + minor * 1000 + patch < 2006014) { // <2.6.14 qCDebug(BALOO) << "Can't use INotify, Linux kernel too old"; return false; } return true; } else { return false; } } bool KInotify::watchingPath(const QString& path) const { - const QByteArray p(stripTrailingSlash(QFile::encodeName(path))); + const QByteArray p = normalizeTrailingSlash(QFile::encodeName(path)); return d->pathWatchHash.contains(p); } void KInotify::resetUserLimit() { d->userLimitReachedSignaled = false; } bool KInotify::addWatch(const QString& path, WatchEvents mode, WatchFlags flags) { // qCDebug(BALOO) << path; d->mode = mode; d->flags = flags; // If the inotify user limit has been signaled, // just queue this folder for watching. if (d->m_dirIter || d->userLimitReachedSignaled) { d->m_paths << path; return false; } d->m_dirIter = new Baloo::FilteredDirIterator(d->config, path, Baloo::FilteredDirIterator::DirsOnly); return d->_k_addWatches(); } bool KInotify::removeWatch(const QString& path) { // Stop all of the iterators which contain path QMutableListIterator iter(d->m_paths); while (iter.hasNext()) { if (iter.next().startsWith(path)) { iter.remove(); } } if (d->m_dirIter) { if (d->m_dirIter->filePath().startsWith(path)) { delete d->m_dirIter; d->m_dirIter = nullptr; } } // Remove all the watches QByteArray encodedPath(QFile::encodeName(path)); auto it = d->watchPathHash.begin(); while (it != d->watchPathHash.end()) { if (it.value().startsWith(encodedPath)) { inotify_rm_watch(d->inotify(), it.key()); d->pathWatchHash.remove(it.value()); it = d->watchPathHash.erase(it); } else { ++it; } } return true; } void KInotify::handleDirCreated(const QString& path) { Baloo::FilteredDirIterator it(d->config, path); // First entry is the directory itself (if not excluded) if (!it.next().isEmpty()) { d->addWatch(it.filePath()); } while (!it.next().isEmpty()) { Q_EMIT created(it.filePath(), it.fileInfo().isDir()); if (it.fileInfo().isDir()) { d->addWatch(it.filePath()); } } } void KInotify::slotEvent(int socket) { int avail; if (ioctl(socket, FIONREAD, &avail) == EINVAL) { qCDebug(BALOO) << "Did not receive an entire inotify event."; return; } char* buffer = (char*)malloc(avail); const int len = read(socket, buffer, avail); Q_ASSERT(len == avail); // deadline for MoveFrom events without matching MoveTo event QDeadlineTimer deadline(QDeadlineTimer::Forever); int i = 0; while (i < len) { const struct inotify_event* event = (struct inotify_event*)&buffer[i]; QByteArray path; // Overflow happens sometimes if we process the events too slowly if (event->wd < 0 && (event->mask & EventQueueOverflow)) { qWarning() << "Inotify - too many event - Overflowed"; free(buffer); return; } // the event name only contains an interesting value if we get an event for a file/folder inside // a watched folder. Otherwise we should ignore it if (event->mask & (EventDeleteSelf | EventMoveSelf)) { path = d->watchPathHash.value(event->wd); } else { // we cannot use event->len here since it contains the size of the buffer and not the length of the string const QByteArray eventName = QByteArray::fromRawData(event->name, qstrnlen(event->name, event->len)); const QByteArray hashedPath = d->watchPathHash.value(event->wd); path = concatPath(hashedPath, eventName); + if (event->mask & IN_ISDIR) { + path = normalizeTrailingSlash(std::move(path)); + } } Q_ASSERT(!path.isEmpty() || event->mask & EventIgnored); Q_ASSERT(path != "/" || event->mask & EventIgnored || event->mask & EventUnmount); // All events which need a decoded path, i.e. everything // but EventMoveFrom | EventQueueOverflow | EventIgnored uint32_t fileEvents = EventAll & ~(EventMoveFrom | EventQueueOverflow | EventIgnored); const QString fname = (event->mask & fileEvents) ? QFile::decodeName(path) : QString(); // now signal the event if (event->mask & EventAccess) { // qCDebug(BALOO) << path << "EventAccess"; Q_EMIT accessed(fname); } if (event->mask & EventAttributeChange) { // qCDebug(BALOO) << path << "EventAttributeChange"; Q_EMIT attributeChanged(fname); } if (event->mask & EventCloseWrite) { // qCDebug(BALOO) << path << "EventCloseWrite"; Q_EMIT closedWrite(fname); } if (event->mask & EventCloseRead) { // qCDebug(BALOO) << path << "EventCloseRead"; Q_EMIT closedRead(fname); } if (event->mask & EventCreate) { // qCDebug(BALOO) << path << "EventCreate"; Q_EMIT created(fname, event->mask & IN_ISDIR); if (event->mask & IN_ISDIR) { // Files/directories inside the new directory may be created before the watch // is installed. Ensure created events for all children are issued at least once handleDirCreated(fname); } } if (event->mask & EventDeleteSelf) { // qCDebug(BALOO) << path << "EventDeleteSelf"; d->removeWatch(event->wd); Q_EMIT deleted(fname, true); } if (event->mask & EventDelete) { // qCDebug(BALOO) << path << "EventDelete"; // we watch all folders recursively. Thus, folder removing is reported in DeleteSelf. if (!(event->mask & IN_ISDIR)) Q_EMIT deleted(fname, false); } if (event->mask & EventModify) { // qCDebug(BALOO) << path << "EventModify"; Q_EMIT modified(fname); } if (event->mask & EventMoveSelf) { // qCDebug(BALOO) << path << "EventMoveSelf"; qWarning() << "EventMoveSelf: THIS CASE IS NOT HANDLED PROPERLY!"; } if (event->mask & EventMoveFrom) { // qCDebug(BALOO) << path << "EventMoveFrom"; if (deadline.isForever()) { deadline = QDeadlineTimer(1000); // 1 second } d->cookies[event->cookie] = Private::MovedFileCookie{ deadline, path, WatchFlags(event->mask) }; } if (event->mask & EventMoveTo) { // check if we have a cookie for this one if (d->cookies.contains(event->cookie)) { const QByteArray oldPath = d->cookies.take(event->cookie).path; // update the path cache if (event->mask & IN_ISDIR) { auto it = d->pathWatchHash.find(oldPath); if (it != d->pathWatchHash.end()) { // qCDebug(BALOO) << oldPath << path; const int wd = it.value(); d->watchPathHash[wd] = path; d->pathWatchHash.erase(it); d->pathWatchHash.insert(path, wd); } } // qCDebug(BALOO) << oldPath << "EventMoveTo" << path; Q_EMIT moved(QFile::decodeName(oldPath), fname); } else { // qCDebug(BALOO) << "No cookie for move information of" << path << "simulating new file event"; Q_EMIT created(fname, event->mask & IN_ISDIR); if (event->mask & IN_ISDIR) { handleDirCreated(fname); } } } if (event->mask & EventOpen) { // qCDebug(BALOO) << path << "EventOpen"; Q_EMIT opened(fname); } if (event->mask & EventUnmount) { // qCDebug(BALOO) << path << "EventUnmount. removing from path hash"; if (event->mask & IN_ISDIR) { d->removeWatch(event->wd); } // This is present because a unmount event is sent by inotify after unmounting, by // which time the watches have already been removed. if (path != "/") { Q_EMIT unmounted(fname); } } if (event->mask & EventIgnored) { // qCDebug(BALOO) << path << "EventIgnored"; } i += sizeof(struct inotify_event) + event->len; } if (d->cookies.empty()) { d->cookieExpireTimer.stop(); } else { if (!d->cookieExpireTimer.isActive()) { d->cookieExpireTimer.start(); } } if (len < 0) { qCDebug(BALOO) << "Failed to read event."; } free(buffer); } void KInotify::slotClearCookies() { auto now = QDeadlineTimer::current(); auto it = d->cookies.begin(); while (it != d->cookies.end()) { if (now > (*it).deadline) { const QString fname = QFile::decodeName((*it).path); removeWatch(fname); Q_EMIT deleted(fname, (*it).flags & IN_ISDIR); it = d->cookies.erase(it); } else { ++it; } } if (!d->cookies.empty()) { d->cookieExpireTimer.start(); } } #include "moc_kinotify.cpp"