diff --git a/autotests/kdirwatch_unittest.cpp b/autotests/kdirwatch_unittest.cpp index b436eb4..e574b5a 100644 --- a/autotests/kdirwatch_unittest.cpp +++ b/autotests/kdirwatch_unittest.cpp @@ -1,752 +1,776 @@ /* This file is part of the KDE libraries Copyright (c) 2009 David Faure 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 #include #include #include #include #include #include #ifdef Q_OS_UNIX #include // ::link() #endif // Debugging notes: to see which inotify signals are emitted, either set s_verboseDebug=true // at the top of kdirwatch.cpp, or use the command-line tool "inotifywait -m /path" // Note that kdirlistertest and kdirmodeltest also exercise KDirWatch quite a lot. static const char *methodToString(KDirWatch::Method method) { switch (method) { case KDirWatch::FAM: return "Fam"; case KDirWatch::INotify: return "INotify"; case KDirWatch::Stat: return "Stat"; case KDirWatch::QFSWatch: return "QFSWatch"; } return "ERROR!"; } class StaticObject { public: KDirWatch m_dirWatch; }; Q_GLOBAL_STATIC(StaticObject, s_staticObject) class StaticObjectUsingSelf // like KSambaShare does, bug 353080 { public: StaticObjectUsingSelf() { KDirWatch::self(); } ~StaticObjectUsingSelf() { if (KDirWatch::exists() && KDirWatch::self()->contains(QDir::homePath())) { KDirWatch::self()->removeDir(QDir::homePath()); } } }; Q_GLOBAL_STATIC(StaticObjectUsingSelf, s_staticObjectUsingSelf) class KDirWatch_UnitTest : public QObject { Q_OBJECT public: KDirWatch_UnitTest() { s_staticObjectUsingSelf(); m_path = m_tempDir.path() + QLatin1Char('/'); // Speed up the test by making the kdirwatch timer (to compress changes) faster qputenv("KDIRWATCH_POLLINTERVAL", "50"); qputenv("KDIRWATCH_METHOD", KDIRWATCH_TEST_METHOD); KDirWatch *dirW = &s_staticObject()->m_dirWatch; m_slow = (dirW->internalMethod() == KDirWatch::FAM || dirW->internalMethod() == KDirWatch::Stat); qDebug() << "Using method" << methodToString(dirW->internalMethod()); } private Q_SLOTS: // test methods void initTestCase() { // By creating the files upfront, we save waiting a full second for an mtime change createFile(m_path + QLatin1String("ExistingFile")); createFile(m_path + QLatin1String("TestFile")); createFile(m_path + QLatin1String("nested_0")); createFile(m_path + QLatin1String("nested_1")); s_staticObject()->m_dirWatch.addFile(m_path + QLatin1String("ExistingFile")); } void touchOneFile(); void touch1000Files(); void watchAndModifyOneFile(); void removeAndReAdd(); void watchNonExistent(); void watchNonExistentWithSingleton(); void testDelete(); void testDeleteAndRecreateFile(); void testDeleteAndRecreateDir(); void testMoveTo(); void nestedEventLoop(); void testHardlinkChange(); void stopAndRestart(); + void shouldIgnoreQrcPaths(); protected Q_SLOTS: // internal slots void nestedEventLoopSlot(); private: void waitUntilMTimeChange(const QString &path); void waitUntilNewSecond(); void waitUntilAfter(const QDateTime &ctime); QList waitForDirtySignal(KDirWatch &watch, int expected); QList waitForDeletedSignal(KDirWatch &watch, int expected); bool waitForOneSignal(KDirWatch &watch, const char *sig, const QString &path); bool waitForRecreationSignal(KDirWatch &watch, const QString &path); bool verifySignalPath(QSignalSpy &spy, const char *sig, const QString &expectedPath); void createFile(const QString &path); QString createFile(int num); void removeFile(int num); void appendToFile(const QString &path); void appendToFile(int num); QTemporaryDir m_tempDir; QString m_path; bool m_slow; }; QTEST_MAIN(KDirWatch_UnitTest) // Just to make the inotify packets bigger static const char s_filePrefix[] = "This_is_a_test_file_"; static const int s_maxTries = 50; // helper method: create a file void KDirWatch_UnitTest::createFile(const QString &path) { QFile file(path); bool ok = file.open(QIODevice::WriteOnly); Q_UNUSED(ok) // silence warnings Q_ASSERT(ok); file.write(QByteArray("foo")); file.close(); //qDebug() << path; } // helper method: create a file (identified by number) QString KDirWatch_UnitTest::createFile(int num) { const QString fileName = QLatin1String(s_filePrefix) + QString::number(num); createFile(m_path + fileName); return m_path + fileName; } // helper method: delete a file (identified by number) void KDirWatch_UnitTest::removeFile(int num) { const QString fileName = QLatin1String(s_filePrefix) + QString::number(num); QFile::remove(m_path + fileName); } void KDirWatch_UnitTest::waitUntilMTimeChange(const QString &path) { // Wait until the current second is more than the file's mtime // otherwise this change will go unnoticed QFileInfo fi(path); QVERIFY(fi.exists()); const QDateTime ctime = qMax(fi.lastModified(), fi.created()); waitUntilAfter(ctime); } void KDirWatch_UnitTest::waitUntilNewSecond() { QDateTime now = QDateTime::currentDateTime(); waitUntilAfter(now); } void KDirWatch_UnitTest::waitUntilAfter(const QDateTime &ctime) { int totalWait = 0; QDateTime now; Q_FOREVER { now = QDateTime::currentDateTime(); if (now.toTime_t() == ctime.toTime_t()) // truncate milliseconds { totalWait += 50; QTest::qWait(50); } else { QVERIFY(now > ctime); // can't go back in time ;) QTest::qWait(50); // be safe break; } } //if (totalWait > 0) qDebug() << "Waited" << totalWait << "ms so that now" << now.toString(Qt::ISODate) << "is >" << ctime.toString(Qt::ISODate); } // helper method: modifies a file void KDirWatch_UnitTest::appendToFile(const QString &path) { QVERIFY(QFile::exists(path)); waitUntilMTimeChange(path); //const QString directory = QDir::cleanPath(path+"/.."); //waitUntilMTimeChange(directory); QFile file(path); QVERIFY(file.open(QIODevice::Append | QIODevice::WriteOnly)); file.write(QByteArray("foobar")); file.close(); #if 0 QFileInfo fi(path); QVERIFY(fi.exists()); qDebug() << "After append: file ctime=" << fi.lastModified().toString(Qt::ISODate); QVERIFY(fi.exists()); qDebug() << "After append: directory mtime=" << fi.created().toString(Qt::ISODate); #endif } // helper method: modifies a file (identified by number) void KDirWatch_UnitTest::appendToFile(int num) { const QString fileName = QLatin1String(s_filePrefix) + QString::number(num); appendToFile(m_path + fileName); } static QString removeTrailingSlash(const QString &path) { if (path.endsWith(QLatin1Char('/'))) { return path.left(path.length() - 1); } else { return path; } } // helper method QList KDirWatch_UnitTest::waitForDirtySignal(KDirWatch &watch, int expected) { QSignalSpy spyDirty(&watch, SIGNAL(dirty(QString))); int numTries = 0; // Give time for KDirWatch to notify us while (spyDirty.count() < expected) { if (++numTries > s_maxTries) { qWarning() << "Timeout waiting for KDirWatch. Got" << spyDirty.count() << "dirty() signals, expected" << expected; return spyDirty; } QTest::qWait(50); } return spyDirty; } bool KDirWatch_UnitTest::waitForOneSignal(KDirWatch &watch, const char *sig, const QString &path) { const QString expectedPath = removeTrailingSlash(path); while (true) { QSignalSpy spyDirty(&watch, sig); int numTries = 0; // Give time for KDirWatch to notify us while (spyDirty.isEmpty()) { if (++numTries > s_maxTries) { qWarning() << "Timeout waiting for KDirWatch signal" << QByteArray(sig).mid(1) << "(" << path << ")"; return false; } QTest::qWait(50); } return verifySignalPath(spyDirty, sig, expectedPath); } } bool KDirWatch_UnitTest::verifySignalPath(QSignalSpy &spy, const char *sig, const QString &expectedPath) { for (int i = 0; i < spy.count(); ++i) { const QString got = spy[i][0].toString(); if (got == expectedPath) { return true; } if (got.startsWith(expectedPath + QLatin1Char('/'))) { qDebug() << "Ignoring (inotify) notification of" << (sig + 1) << '(' << got << ')'; continue; } qWarning() << "Expected" << sig << '(' << expectedPath << ')' << "but got" << sig << '(' << got << ')'; return false; } return false; } bool KDirWatch_UnitTest::waitForRecreationSignal(KDirWatch &watch, const QString &path) { // When watching for a deleted + created signal pair, the two might come so close that // using waitForOneSignal will miss the created signal. This function monitors both all // the time to ensure both are received. const QString expectedPath = removeTrailingSlash(path); QSignalSpy spyDeleted(&watch, SIGNAL(deleted(QString))); QSignalSpy spyCreated(&watch, SIGNAL(created(QString))); if(!spyDeleted.wait(50 * s_maxTries)) { qWarning() << "Timeout waiting for KDirWatch signal deleted(QString) (" << path << ")"; return false; } // Don't bother waiting for the created signal if the signal spy already received a signal. if(spyCreated.isEmpty() && !spyCreated.wait(50 * s_maxTries)) { qWarning() << "Timeout waiting for KDirWatch signal created(QString) (" << path << ")"; return false; } return verifySignalPath(spyDeleted, "deleted(QString)", expectedPath) && verifySignalPath(spyCreated, "created(QString)", expectedPath); } QList KDirWatch_UnitTest::waitForDeletedSignal(KDirWatch &watch, int expected) { QSignalSpy spyDeleted(&watch, SIGNAL(created(QString))); int numTries = 0; // Give time for KDirWatch to notify us while (spyDeleted.count() < expected) { if (++numTries > s_maxTries) { qWarning() << "Timeout waiting for KDirWatch. Got" << spyDeleted.count() << "deleted() signals, expected" << expected; return spyDeleted; } QTest::qWait(50); } return spyDeleted; } void KDirWatch_UnitTest::touchOneFile() // watch a dir, create a file in it { KDirWatch watch; watch.addDir(m_path); watch.startScan(); waitUntilMTimeChange(m_path); // dirty(the directory) should be emitted. QSignalSpy spyCreated(&watch, SIGNAL(created(QString))); const QString file0 = createFile(0); QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path)); QCOMPARE(spyCreated.count(), 0); // "This is not emitted when creating a file is created in a watched directory." removeFile(0); } void KDirWatch_UnitTest::touch1000Files() { KDirWatch watch; watch.addDir(m_path); watch.startScan(); waitUntilMTimeChange(m_path); const int fileCount = 100; for (int i = 0; i < fileCount; ++i) { createFile(i); } QList spy = waitForDirtySignal(watch, fileCount); if (watch.internalMethod() == KDirWatch::INotify) { QVERIFY(spy.count() >= fileCount); qDebug() << spy.count(); } else { // More stupid backends just see one mtime change on the directory QVERIFY(spy.count() >= 1); } for (int i = 0; i < fileCount; ++i) { removeFile(i); } } void KDirWatch_UnitTest::watchAndModifyOneFile() // watch a specific file, and modify it { KDirWatch watch; const QString existingFile = m_path + QLatin1String("ExistingFile"); watch.addFile(existingFile); watch.startScan(); if (m_slow) { waitUntilNewSecond(); } appendToFile(existingFile); QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), existingFile)); } void KDirWatch_UnitTest::removeAndReAdd() { KDirWatch watch; watch.addDir(m_path); watch.startScan(); if (watch.internalMethod() != KDirWatch::INotify) { waitUntilNewSecond(); // necessary for mtime checks in scanEntry } const QString file0 = createFile(0); QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path)); // Just like KDirLister does: remove the watch, then re-add it. watch.removeDir(m_path); watch.addDir(m_path); if (watch.internalMethod() != KDirWatch::INotify) { waitUntilMTimeChange(m_path); // necessary for FAM and QFSWatcher } const QString file1 = createFile(1); //qDebug() << "created" << file1; QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path)); } void KDirWatch_UnitTest::watchNonExistent() { KDirWatch watch; // Watch "subdir", that doesn't exist yet const QString subdir = m_path + QLatin1String("subdir"); QVERIFY(!QFile::exists(subdir)); watch.addDir(subdir); watch.startScan(); if (m_slow) { waitUntilNewSecond(); } // Now create it, KDirWatch should emit created() qDebug() << "Creating" << subdir; QDir().mkdir(subdir); QVERIFY(waitForOneSignal(watch, SIGNAL(created(QString)), subdir)); KDirWatch::statistics(); // Play with addDir/removeDir, just for fun watch.addDir(subdir); watch.removeDir(subdir); watch.addDir(subdir); // Now watch files that don't exist yet const QString file = subdir + QLatin1String("/0"); watch.addFile(file); // doesn't exist yet const QString file1 = subdir + QLatin1String("/1"); watch.addFile(file1); // doesn't exist yet watch.removeFile(file1); // forget it again KDirWatch::statistics(); QVERIFY(!QFile::exists(file)); // Now create it, KDirWatch should emit created qDebug() << "Creating" << file; createFile(file); QVERIFY(waitForOneSignal(watch, SIGNAL(created(QString)), file)); appendToFile(file); QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), file)); // Create the file after all; we're not watching for it, but the dir will emit dirty createFile(file1); QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), subdir)); } void KDirWatch_UnitTest::watchNonExistentWithSingleton() { const QString file = QLatin1String("/root/.ssh/authorized_keys"); KDirWatch::self()->addFile(file); // When running this test in KDIRWATCH_METHOD=QFSWatch, or when FAM is not available // and we fallback to qfswatch when inotify fails above, we end up creating the fsWatch // in the kdirwatch singleton. Bug 261541 discovered that Qt hanged when deleting fsWatch // once QCoreApp was gone, this is what this test is about. } void KDirWatch_UnitTest::testDelete() { const QString file1 = m_path + QLatin1String("del"); if (!QFile::exists(file1)) { createFile(file1); } waitUntilMTimeChange(file1); // Watch the file, then delete it, KDirWatch will emit deleted (and possibly dirty for the dir, if mtime changed) KDirWatch watch; watch.addFile(file1); KDirWatch::statistics(); QSignalSpy spyDirty(&watch, SIGNAL(dirty(QString))); QFile::remove(file1); QVERIFY(waitForOneSignal(watch, SIGNAL(deleted(QString)), file1)); QTest::qWait(40); // just in case delayed processing would emit it QCOMPARE(spyDirty.count(), 0); } void KDirWatch_UnitTest::testDeleteAndRecreateFile() // Useful for /etc/localtime for instance { const QString subdir = m_path + QLatin1String("subdir"); QDir().mkdir(subdir); const QString file1 = subdir + QLatin1String("/1"); if (!QFile::exists(file1)) { createFile(file1); } waitUntilMTimeChange(file1); // Watch the file, then delete it, KDirWatch will emit deleted (and possibly dirty for the dir, if mtime changed) KDirWatch watch; watch.addFile(file1); //KDE_struct_stat stat_buf; //QCOMPARE(KDE::stat(QFile::encodeName(file1), &stat_buf), 0); //qDebug() << "initial inode" << stat_buf.st_ino; // Make sure this even works multiple times, as needed for ksycoca for (int i = 0; i < 5; ++i) { QFile::remove(file1); // And recreate immediately, to try and fool KDirWatch with unchanged ctime/mtime ;) // (This emulates the /etc/localtime case) createFile(file1); //QCOMPARE(KDE::stat(QFile::encodeName(file1), &stat_buf), 0); //qDebug() << "new inode" << stat_buf.st_ino; // same! { QSignalSpy spyDirty(&watch, SIGNAL(dirty(QString))); if(!waitForRecreationSignal(watch, file1)) { // We may get a dirty signal here instead of a deleted/created set. if (spyDirty.isEmpty() || !verifySignalPath(spyDirty, SIGNAL(dirty(QString)), file1)) { QFAIL("Failed to detect file deletion and recreation through either a deleted/created signal pair or through a dirty signal!"); } } } } waitUntilMTimeChange(file1); appendToFile(file1); QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), file1)); } void KDirWatch_UnitTest::testDeleteAndRecreateDir() { // Like KDirModelTest::testOverwriteFileWithDir does at the end. // The linux-2.6.31 bug made kdirwatch emit deletion signals about the -new- dir! QTemporaryDir *tempDir1 = new QTemporaryDir(QDir::tempPath() + QLatin1Char('/') + QLatin1String("olddir-")); KDirWatch watch; const QString path1 = tempDir1->path() + QLatin1Char('/'); watch.addDir(path1); delete tempDir1; QTemporaryDir *tempDir2 = new QTemporaryDir(QDir::tempPath() + QLatin1Char('/') + QLatin1String("newdir-")); const QString path2 = tempDir2->path() + QLatin1Char('/'); watch.addDir(path2); QVERIFY(waitForOneSignal(watch, SIGNAL(deleted(QString)), path1)); delete tempDir2; } void KDirWatch_UnitTest::testMoveTo() { // This reproduces the famous digikam crash, #222974 // A watched file was being rewritten (overwritten by ksavefile), // which gives inotify notifications "moved_to" followed by "delete_self" // // What happened then was that the delayed slotRescan // would adjust things, making it status==Normal but the entry was // listed as a "non-existent sub-entry" for the parent directory. // That's inconsistent, and after removeFile() a dangling sub-entry would be left. // Initial data: creating file subdir/1 const QString file1 = m_path + QLatin1String("moveTo"); createFile(file1); KDirWatch watch; watch.addDir(m_path); watch.addFile(file1); watch.startScan(); if (watch.internalMethod() != KDirWatch::INotify) { waitUntilMTimeChange(m_path); } // Atomic rename of "temp" to "file1", much like KAutoSave would do when saving file1 again // ### TODO: this isn't an atomic rename anymore. We need ::rename for that, or API from Qt. const QString filetemp = m_path + QLatin1String("temp"); createFile(filetemp); QFile::remove(file1); QVERIFY(QFile::rename(filetemp, file1)); // overwrite file1 with the tempfile qDebug() << "Overwrite file1 with tempfile"; QSignalSpy spyCreated(&watch, SIGNAL(created(QString))); QSignalSpy spyDirty(&watch, SIGNAL(dirty(QString))); QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path)); // Getting created() on an unwatched file is an inotify bonus, it's not part of the requirements. if (watch.internalMethod() == KDirWatch::INotify) { QCOMPARE(spyCreated.count(), 1); QCOMPARE(spyCreated[0][0].toString(), file1); QCOMPARE(spyDirty.size(), 2); QCOMPARE(spyDirty[1][0].toString(), filetemp); } // make sure we're still watching it appendToFile(file1); QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), file1)); //qDebug() << "after created"; //KDirWatch::statistics(); watch.removeFile(file1); // now we remove it //qDebug() << "after removeFile"; //KDirWatch::statistics(); // Just touch another file to trigger a findSubEntry - this where the crash happened waitUntilMTimeChange(m_path); createFile(filetemp); #ifdef Q_OS_WIN if (watch.internalMethod() == KDirWatch::QFSWatch) { QEXPECT_FAIL(nullptr, "QFSWatch fails here on Windows!", Continue); } #endif QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path)); } void KDirWatch_UnitTest::nestedEventLoop() // #220153: watch two files, and modify 2nd while in slot for 1st { KDirWatch watch; const QString file0 = m_path + QLatin1String("nested_0"); watch.addFile(file0); const QString file1 = m_path + QLatin1String("nested_1"); watch.addFile(file1); watch.startScan(); if (m_slow) { waitUntilNewSecond(); } appendToFile(file0); // use own spy, to connect it before nestedEventLoopSlot, otherwise it reverses order QSignalSpy spyDirty(&watch, SIGNAL(dirty(QString))); connect(&watch, SIGNAL(dirty(QString)), this, SLOT(nestedEventLoopSlot())); waitForDirtySignal(watch, 1); QVERIFY(spyDirty.count() >= 2); QCOMPARE(spyDirty[0][0].toString(), file0); QCOMPARE(spyDirty[spyDirty.count() - 1][0].toString(), file1); } void KDirWatch_UnitTest::nestedEventLoopSlot() { const KDirWatch *const_watch = qobject_cast(sender()); KDirWatch *watch = const_cast(const_watch); // let's not come in this slot again disconnect(watch, SIGNAL(dirty(QString)), this, SLOT(nestedEventLoopSlot())); const QString file1 = m_path + QLatin1String("nested_1"); appendToFile(file1); //qDebug() << "now waiting for signal"; // The nested event processing here was from a messagebox in #220153 QList spy = waitForDirtySignal(*watch, 1); QVERIFY(spy.count() >= 1); QCOMPARE(spy[spy.count() - 1][0].toString(), file1); //qDebug() << "done"; // Now the user pressed reload... const QString file0 = m_path + QLatin1String("nested_0"); watch->removeFile(file0); watch->addFile(file0); } void KDirWatch_UnitTest::testHardlinkChange() { #ifdef Q_OS_UNIX // The unittest for the "detecting hardlink change to /etc/localtime" problem // described on kde-core-devel (2009-07-03). // It shows that watching a specific file doesn't inform us that the file is // being recreated. Better watch the directory, for that. // Well, it works with inotify (and fam - which uses inotify I guess?) const QString existingFile = m_path + QLatin1String("ExistingFile"); KDirWatch watch; watch.addFile(existingFile); watch.startScan(); //waitUntilMTimeChange(existingFile); //waitUntilMTimeChange(m_path); QFile::remove(existingFile); const QString testFile = m_path + QLatin1String("TestFile"); QVERIFY(::link(QFile::encodeName(testFile).constData(), QFile::encodeName(existingFile).constData()) == 0); // make ExistingFile "point" to TestFile QVERIFY(QFile::exists(existingFile)); QVERIFY(waitForRecreationSignal(watch, existingFile)); //KDirWatch::statistics(); appendToFile(existingFile); QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), existingFile)); #else QSKIP("Unix-specific"); #endif } void KDirWatch_UnitTest::stopAndRestart() { KDirWatch watch; watch.addDir(m_path); watch.startScan(); waitUntilMTimeChange(m_path); watch.stopDirScan(m_path); //qDebug() << "create file 2 at" << QDateTime::currentDateTime().toTime_t(); const QString file2 = createFile(2); QSignalSpy spyDirty(&watch, SIGNAL(dirty(QString))); QTest::qWait(200); QCOMPARE(spyDirty.count(), 0);// suspended -> no signal watch.restartDirScan(m_path); QTest::qWait(200); #ifndef Q_OS_WIN QCOMPARE(spyDirty.count(), 0); // as documented by restartDirScan: no signal // On Windows, however, signals will get emitted, due to the ifdef Q_OS_WIN in the timestamp // comparison ("trust QFSW since the mtime of dirs isn't modified") #endif KDirWatch::statistics(); waitUntilMTimeChange(m_path); // necessary for the mtime comparison in scanEntry //qDebug() << "create file 3 at" << QDateTime::currentDateTime().toTime_t(); const QString file3 = createFile(3); #ifdef Q_OS_WIN if (watch.internalMethod() == KDirWatch::QFSWatch) { QEXPECT_FAIL(nullptr, "QFSWatch fails here on Windows!", Continue); } #endif QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path)); QFile::remove(file2); QFile::remove(file3); } +void KDirWatch_UnitTest::shouldIgnoreQrcPaths() +{ + const auto oldCwd = QDir::currentPath(); + QVERIFY(QDir::setCurrent(QDir::homePath())); + + KDirWatch watch; + watch.addDir(QDir::homePath()); + // This triggers bug #374075. + watch.addDir(QStringLiteral(":/kio5/newfile-templates")); + + QSignalSpy dirtySpy(&watch, &KDirWatch::dirty); + + QFile file(QStringLiteral("bug374075.txt")); + QVERIFY(file.open(QIODevice::WriteOnly)); + QVERIFY(file.write(QByteArrayLiteral("test"))); + file.close(); + QVERIFY(file.exists()); + QVERIFY(dirtySpy.wait()); + QVERIFY(dirtySpy.count() > 0); + QVERIFY(file.remove()); + QVERIFY(QDir::setCurrent(oldCwd)); +} + #include "kdirwatch_unittest.moc" diff --git a/src/lib/io/kdirwatch.cpp b/src/lib/io/kdirwatch.cpp index 060037b..2278b71 100644 --- a/src/lib/io/kdirwatch.cpp +++ b/src/lib/io/kdirwatch.cpp @@ -1,2060 +1,2064 @@ /* This file is part of the KDE libraries Copyright (C) 1998 Sven Radej Copyright (C) 2006 Dirk Mueller Copyright (C) 2007 Flavio Castelli Copyright (C) 2008 Rafal Rzepecki Copyright (C) 2010 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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. */ // CHANGES: // Jul 30, 2008 - Don't follow symlinks when recursing to avoid loops (Rafal) // Aug 6, 2007 - KDirWatch::WatchModes support complete, flags work fine also // when using FAMD (Flavio Castelli) // Aug 3, 2007 - Handled KDirWatch::WatchModes flags when using inotify, now // recursive and file monitoring modes are implemented (Flavio Castelli) // Jul 30, 2007 - Substituted addEntry boolean params with KDirWatch::WatchModes // flag (Flavio Castelli) // Oct 4, 2005 - Inotify support (Dirk Mueller) // Februar 2002 - Add file watching and remote mount check for STAT // Mar 30, 2001 - Native support for Linux dir change notification. // Jan 28, 2000 - Usage of FAM service on IRIX (Josef.Weidendorfer@in.tum.de) // May 24. 1998 - List of times introduced, and some bugs are fixed. (sven) // May 23. 1998 - Removed static pointer - you can have more instances. // It was Needed for KRegistry. KDirWatch now emits signals and doesn't // call (or need) KFM. No more URL's - just plain paths. (sven) // Mar 29. 1998 - added docs, stop/restart for particular Dirs and // deep copies for list of dirs. (sven) // Mar 28. 1998 - Created. (sven) #include "kdirwatch.h" #include "kdirwatch_p.h" #include "kfilesystemtype.h" #include "kcoreaddons_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include // QT_LSTAT, QT_STAT, QT_STATBUF #include #include #if HAVE_SYS_INOTIFY_H #include #include #include #ifndef IN_DONT_FOLLOW #define IN_DONT_FOLLOW 0x02000000 #endif #ifndef IN_ONLYDIR #define IN_ONLYDIR 0x01000000 #endif // debug #include #include #endif // HAVE_SYS_INOTIFY_H Q_DECLARE_LOGGING_CATEGORY(KDIRWATCH) // logging category for this framework, default: log stuff >= warning Q_LOGGING_CATEGORY(KDIRWATCH, "kf5.kcoreaddons.kdirwatch", QtWarningMsg) // set this to true for much more verbose debug output static bool s_verboseDebug = false; static QThreadStorage dwp_self; static KDirWatchPrivate *createPrivate() { if (!dwp_self.hasLocalData()) { dwp_self.setLocalData(new KDirWatchPrivate); } return dwp_self.localData(); } // Convert a string into a watch Method static KDirWatch::Method methodFromString(const QByteArray &method) { if (method == "Fam") { return KDirWatch::FAM; } else if (method == "Stat") { return KDirWatch::Stat; } else if (method == "QFSWatch") { return KDirWatch::QFSWatch; } else { #ifdef Q_OS_LINUX // inotify supports delete+recreate+modify, which QFSWatch doesn't support return KDirWatch::INotify; #else return KDirWatch::QFSWatch; #endif } } static const char *methodToString(KDirWatch::Method method) { switch (method) { case KDirWatch::FAM: return "Fam"; case KDirWatch::INotify: return "INotify"; case KDirWatch::Stat: return "Stat"; case KDirWatch::QFSWatch: return "QFSWatch"; } // not reached return nullptr; } static const char s_envNfsPoll[] = "KDIRWATCH_NFSPOLLINTERVAL"; static const char s_envPoll[] = "KDIRWATCH_POLLINTERVAL"; static const char s_envMethod[] = "KDIRWATCH_METHOD"; static const char s_envNfsMethod[] = "KDIRWATCH_NFSMETHOD"; // // Class KDirWatchPrivate (singleton) // /* All entries (files/directories) to be watched in the * application (coming from multiple KDirWatch instances) * are registered in a single KDirWatchPrivate instance. * * At the moment, the following methods for file watching * are supported: * - Polling: All files to be watched are polled regularly * using stat (more precise: QFileInfo.lastModified()). * The polling frequency is determined from global kconfig * settings, defaulting to 500 ms for local directories * and 5000 ms for remote mounts * - FAM (File Alternation Monitor): first used on IRIX, SGI * has ported this method to LINUX. It uses a kernel part * (IMON, sending change events to /dev/imon) and a user * level damon (fam), to which applications connect for * notification of file changes. For NFS, the fam damon * on the NFS server machine is used; if IMON is not built * into the kernel, fam uses polling for local files. * - INOTIFY: In LINUX 2.6.13, inode change notification was * introduced. You're now able to watch arbitrary inode's * for changes, and even get notification when they're * unmounted. */ KDirWatchPrivate::KDirWatchPrivate() : timer(), freq(3600000), // 1 hour as upper bound statEntries(0), delayRemove(false), rescan_all(false), rescan_timer(), #if HAVE_SYS_INOTIFY_H mSn(nullptr), #endif _isStopped(false) { // Debug unittest on CI if (qAppName() == QLatin1String("kservicetest") || qAppName() == QLatin1String("filetypestest")) { s_verboseDebug = true; } timer.setObjectName(QStringLiteral("KDirWatchPrivate::timer")); connect(&timer, SIGNAL(timeout()), this, SLOT(slotRescan())); m_nfsPollInterval = qEnvironmentVariableIsSet(s_envNfsPoll) ? qgetenv(s_envNfsPoll).toInt() : 5000; m_PollInterval = qEnvironmentVariableIsSet(s_envPoll) ? qgetenv(s_envPoll).toInt() : 500; m_preferredMethod = methodFromString(qEnvironmentVariableIsSet(s_envMethod) ? qgetenv(s_envMethod) : "inotify"); // The nfs method defaults to the normal (local) method m_nfsPreferredMethod = methodFromString(qEnvironmentVariableIsSet(s_envNfsMethod) ? qgetenv(s_envNfsMethod) : "Fam"); QList availableMethods; availableMethods << "Stat"; // used for FAM and inotify rescan_timer.setObjectName(QStringLiteral("KDirWatchPrivate::rescan_timer")); rescan_timer.setSingleShot(true); connect(&rescan_timer, SIGNAL(timeout()), this, SLOT(slotRescan())); #if HAVE_FAM availableMethods << "FAM"; use_fam = true; sn = 0; #endif #if HAVE_SYS_INOTIFY_H supports_inotify = true; m_inotify_fd = inotify_init(); if (m_inotify_fd <= 0) { qCDebug(KDIRWATCH) << "Can't use Inotify, kernel doesn't support it"; supports_inotify = false; } //qCDebug(KDIRWATCH) << "INotify available: " << supports_inotify; if (supports_inotify) { availableMethods << "INotify"; (void)fcntl(m_inotify_fd, F_SETFD, FD_CLOEXEC); mSn = new QSocketNotifier(m_inotify_fd, QSocketNotifier::Read, this); connect(mSn, SIGNAL(activated(int)), this, SLOT(inotifyEventReceived())); } #endif #if HAVE_QFILESYSTEMWATCHER availableMethods << "QFileSystemWatcher"; fsWatcher = nullptr; #endif if (s_verboseDebug) { qCDebug(KDIRWATCH) << "Available methods: " << availableMethods << "preferred=" << methodToString(m_preferredMethod); } } // This is called on app exit (deleted by QThreadStorage) KDirWatchPrivate::~KDirWatchPrivate() { timer.stop(); #if HAVE_FAM if (use_fam && sn) { FAMClose(&fc); } #endif #if HAVE_SYS_INOTIFY_H if (supports_inotify) { QT_CLOSE(m_inotify_fd); } #endif #if HAVE_QFILESYSTEMWATCHER delete fsWatcher; #endif } void KDirWatchPrivate::inotifyEventReceived() { //qCDebug(KDIRWATCH); #if HAVE_SYS_INOTIFY_H if (!supports_inotify) { return; } int pending = -1; int offsetStartRead = 0; // where we read into buffer char buf[8192]; assert(m_inotify_fd > -1); ioctl(m_inotify_fd, FIONREAD, &pending); while (pending > 0) { const int bytesToRead = qMin(pending, sizeof(buf) - offsetStartRead); int bytesAvailable = read(m_inotify_fd, &buf[offsetStartRead], bytesToRead); pending -= bytesAvailable; bytesAvailable += offsetStartRead; offsetStartRead = 0; int offsetCurrent = 0; while (bytesAvailable >= int(sizeof(struct inotify_event))) { const struct inotify_event *const event = reinterpret_cast(&buf[offsetCurrent]); const int eventSize = sizeof(struct inotify_event) + event->len; if (bytesAvailable < eventSize) { break; } bytesAvailable -= eventSize; offsetCurrent += eventSize; QString path; // strip trailing null chars, see inotify_event documentation // these must not end up in the final QString version of path int len = event->len; while (len > 1 && !event->name[len - 1]) { --len; } QByteArray cpath(event->name, len); if (len) { path = QFile::decodeName(cpath); } if (!path.isEmpty() && isNoisyFile(cpath.data())) { continue; } // Is set to true if the new event is a directory, false otherwise. This prevents a stat call in clientsForFileOrDir const bool isDir = (event->mask & (IN_ISDIR)); // now we're in deep trouble of finding the // associated entries // for now, we suck and iterate for (EntryMap::Iterator it = m_mapEntries.begin(); it != m_mapEntries.end();) { Entry *e = &(*it); ++it; if (e->wd == event->wd) { const bool wasDirty = e->dirty; e->dirty = true; const QString tpath = e->path + QLatin1Char('/') + path; if (s_verboseDebug) { qCDebug(KDIRWATCH).nospace() << "got event 0x" << qPrintable(QString::number(event->mask, 16)) << " for " << e->path; } if (event->mask & IN_DELETE_SELF) { if (s_verboseDebug) { qCDebug(KDIRWATCH) << "-->got deleteself signal for" << e->path; } e->m_status = NonExistent; e->wd = -1; e->m_ctime = invalid_ctime; emitEvent(e, Deleted, e->path); // If the parent dir was already watched, tell it something changed Entry *parentEntry = entry(e->parentDirectory()); if (parentEntry) { parentEntry->dirty = true; } // Add entry to parent dir to notice if the entry gets recreated addEntry(nullptr, e->parentDirectory(), e, true /*isDir*/); } if (event->mask & IN_IGNORED) { // Causes bug #207361 with kernels 2.6.31 and 2.6.32! //e->wd = -1; } if (event->mask & (IN_CREATE | IN_MOVED_TO)) { Entry *sub_entry = e->findSubEntry(tpath); if (s_verboseDebug) { qCDebug(KDIRWATCH) << "-->got CREATE signal for" << (tpath) << "sub_entry=" << sub_entry; qCDebug(KDIRWATCH) << *e; } // The code below is very similar to the one in checkFAMEvent... if (sub_entry) { // We were waiting for this new file/dir to be created sub_entry->dirty = true; rescan_timer.start(0); // process this asap, to start watching that dir } else if (e->isDir && !e->m_clients.empty()) { const QList clients = e->inotifyClientsForFileOrDir(isDir); // See discussion in addEntry for why we don't addEntry for individual // files in WatchFiles mode with inotify. if (isDir) { for (const Client *client : clients) { addEntry(client->instance, tpath, nullptr, isDir, isDir ? client->m_watchModes : KDirWatch::WatchDirOnly); } } if (!clients.isEmpty()) { emitEvent(e, Created, tpath); qCDebug(KDIRWATCH).nospace() << clients.count() << " instance(s) monitoring the new " << (isDir ? "dir " : "file ") << tpath; } e->m_pendingFileChanges.append(e->path); if (!rescan_timer.isActive()) { rescan_timer.start(m_PollInterval); // singleshot } } } if (event->mask & (IN_DELETE | IN_MOVED_FROM)) { if (s_verboseDebug) { qCDebug(KDIRWATCH) << "-->got DELETE signal for" << tpath; } if ((e->isDir) && (!e->m_clients.empty())) { // A file in this directory has been removed. It wasn't an explicitly // watched file as it would have its own watch descriptor, so // no addEntry/ removeEntry bookkeeping should be required. Emit // the event immediately if any clients are interested. KDirWatch::WatchModes flag = isDir ? KDirWatch::WatchSubDirs : KDirWatch::WatchFiles; int counter = 0; for (const Client &client : e->m_clients) { if (client.m_watchModes & flag) { counter++; } } if (counter != 0) { emitEvent(e, Deleted, tpath); } } } if (event->mask & (IN_MODIFY | IN_ATTRIB)) { if ((e->isDir) && (!e->m_clients.empty())) { if (s_verboseDebug) { qCDebug(KDIRWATCH) << "-->got MODIFY signal for" << (tpath); } // A file in this directory has been changed. No // addEntry/ removeEntry bookkeeping should be required. // Add the path to the list of pending file changes if // there are any interested clients. //QT_STATBUF stat_buf; //QByteArray tpath = QFile::encodeName(e->path+'/'+path); //QT_STAT(tpath, &stat_buf); //bool isDir = S_ISDIR(stat_buf.st_mode); // The API doc is somewhat vague as to whether we should emit // dirty() for implicitly watched files when WatchFiles has // not been specified - we'll assume they are always interested, // regardless. // Don't worry about duplicates for the time // being; this is handled in slotRescan. e->m_pendingFileChanges.append(tpath); // Avoid stat'ing the directory if only an entry inside it changed. e->dirty = (wasDirty || (path.isEmpty() && (event->mask & IN_ATTRIB))); } } if (!rescan_timer.isActive()) { rescan_timer.start(m_PollInterval); // singleshot } break; } } } if (bytesAvailable > 0) { // copy partial event to beginning of buffer memmove(buf, &buf[offsetCurrent], bytesAvailable); offsetStartRead = bytesAvailable; } } #endif } KDirWatchPrivate::Entry::~Entry() { } /* In FAM mode, only entries which are marked dirty are scanned. * We first need to mark all yet nonexistent, but possible created * entries as dirty... */ void KDirWatchPrivate::Entry::propagate_dirty() { Q_FOREACH (Entry *sub_entry, m_entries) { if (!sub_entry->dirty) { sub_entry->dirty = true; sub_entry->propagate_dirty(); } } } /* A KDirWatch instance is interested in getting events for * this file/Dir entry. */ void KDirWatchPrivate::Entry::addClient(KDirWatch *instance, KDirWatch::WatchModes watchModes) { if (instance == nullptr) { return; } for (Client &client : m_clients) { if (client.instance == instance) { client.count++; client.m_watchModes = watchModes; return; } } m_clients.emplace_back(instance, watchModes); } void KDirWatchPrivate::Entry::removeClient(KDirWatch *instance) { auto it = m_clients.begin(); const auto end = m_clients.end(); for (; it != end; ++it) { Client &client = *it; if (client.instance == instance) { client.count--; if (client.count == 0) { m_clients.erase(it); } return; } } } /* get number of clients */ int KDirWatchPrivate::Entry::clientCount() const { int clients = 0; for (const Client &client : m_clients) { clients += client.count; } return clients; } QString KDirWatchPrivate::Entry::parentDirectory() const { return QDir::cleanPath(path + QLatin1String("/..")); } QList KDirWatchPrivate::Entry::clientsForFileOrDir(const QString &tpath, bool *isDir) const { QList ret; QFileInfo fi(tpath); if (fi.exists()) { *isDir = fi.isDir(); const KDirWatch::WatchModes flag = *isDir ? KDirWatch::WatchSubDirs : KDirWatch::WatchFiles; for (const Client &client : m_clients) { if (client.m_watchModes & flag) { ret.append(&client); } } } else { // Happens frequently, e.g. ERROR: couldn't stat "/home/dfaure/.viminfo.tmp" //qCDebug(KDIRWATCH) << "ERROR: couldn't stat" << tpath; // In this case isDir is not set, but ret is empty anyway // so isDir won't be used. } return ret; } // inotify specific function that doesn't call KDE::stat to figure out if we have a file or folder. // isDir is determined through inotify's "IN_ISDIR" flag in KDirWatchPrivate::inotifyEventReceived QList KDirWatchPrivate::Entry::inotifyClientsForFileOrDir(bool isDir) const { QList ret; const KDirWatch::WatchModes flag = isDir ? KDirWatch::WatchSubDirs : KDirWatch::WatchFiles; for (const Client &client : m_clients) { if (client.m_watchModes & flag) { ret.append(&client); } } return ret; } QDebug operator<<(QDebug debug, const KDirWatchPrivate::Entry &entry) { debug.nospace() << "[ Entry for " << entry.path << ", " << (entry.isDir ? "dir" : "file"); if (entry.m_status == KDirWatchPrivate::NonExistent) { debug << ", non-existent"; } debug << ", using " << ((entry.m_mode == KDirWatchPrivate::FAMMode) ? "FAM" : (entry.m_mode == KDirWatchPrivate::INotifyMode) ? "INotify" : (entry.m_mode == KDirWatchPrivate::QFSWatchMode) ? "QFSWatch" : (entry.m_mode == KDirWatchPrivate::StatMode) ? "Stat" : "Unknown Method"); #if HAVE_SYS_INOTIFY_H if (entry.m_mode == KDirWatchPrivate::INotifyMode) { debug << " inotify_wd=" << entry.wd; } #endif debug << ", has " << entry.m_clients.size() << " clients"; debug.space(); if (!entry.m_entries.isEmpty()) { debug << ", nonexistent subentries:"; Q_FOREACH (KDirWatchPrivate::Entry *subEntry, entry.m_entries) { debug << subEntry << subEntry->path; } } debug << ']'; return debug; } KDirWatchPrivate::Entry *KDirWatchPrivate::entry(const QString &_path) { // we only support absolute paths if (_path.isEmpty() || QDir::isRelativePath(_path)) { return nullptr; } QString path(_path); if (path.length() > 1 && path.endsWith(QLatin1Char('/'))) { path.truncate(path.length() - 1); } EntryMap::Iterator it = m_mapEntries.find(path); if (it == m_mapEntries.end()) { return nullptr; } else { return &(*it); } } // set polling frequency for a entry and adjust global freq if needed void KDirWatchPrivate::useFreq(Entry *e, int newFreq) { e->freq = newFreq; // a reasonable frequency for the global polling timer if (e->freq < freq) { freq = e->freq; if (timer.isActive()) { timer.start(freq); } qCDebug(KDIRWATCH) << "Global Poll Freq is now" << freq << "msec"; } } #if HAVE_FAM // setup FAM notification, returns false if not possible bool KDirWatchPrivate::useFAM(Entry *e) { if (!use_fam) { return false; } if (!sn) { if (FAMOpen(&fc) == 0) { sn = new QSocketNotifier(FAMCONNECTION_GETFD(&fc), QSocketNotifier::Read, this); connect(sn, SIGNAL(activated(int)), this, SLOT(famEventReceived())); } else { use_fam = false; return false; } } // handle FAM events to avoid deadlock // (FAM sends back all files in a directory when monitoring) famEventReceived(); e->m_mode = FAMMode; e->dirty = false; e->m_famReportedSeen = false; bool startedFAMMonitor = false; if (e->isDir) { if (e->m_status == NonExistent) { // If the directory does not exist we watch the parent directory addEntry(0, e->parentDirectory(), e, true); } else { int res = FAMMonitorDirectory(&fc, QFile::encodeName(e->path).data(), &(e->fr), e); startedFAMMonitor = true; if (res < 0) { e->m_mode = UnknownMode; use_fam = false; delete sn; sn = 0; return false; } qCDebug(KDIRWATCH).nospace() << " Setup FAM (Req " << FAMREQUEST_GETREQNUM(&(e->fr)) << ") for " << e->path; } } else { if (e->m_status == NonExistent) { // If the file does not exist we watch the directory addEntry(0, QFileInfo(e->path).absolutePath(), e, true); } else { int res = FAMMonitorFile(&fc, QFile::encodeName(e->path).data(), &(e->fr), e); startedFAMMonitor = true; if (res < 0) { e->m_mode = UnknownMode; use_fam = false; delete sn; sn = 0; return false; } qCDebug(KDIRWATCH).nospace() << " Setup FAM (Req " << FAMREQUEST_GETREQNUM(&(e->fr)) << ") for " << e->path; } } // handle FAM events to avoid deadlock // (FAM sends back all files in a directory when monitoring) do { famEventReceived(); if (startedFAMMonitor && !e->m_famReportedSeen) { // 50 is ~half the time it takes to setup a watch. If gamin's latency // gets better, this can be reduced. QThread::msleep(50); } } while (startedFAMMonitor &&!e->m_famReportedSeen); return true; } #endif #if HAVE_SYS_INOTIFY_H // setup INotify notification, returns false if not possible bool KDirWatchPrivate::useINotify(Entry *e) { //qCDebug(KDIRWATCH) << "trying to use inotify for monitoring"; e->wd = -1; e->dirty = false; if (!supports_inotify) { return false; } e->m_mode = INotifyMode; if (e->m_status == NonExistent) { addEntry(nullptr, e->parentDirectory(), e, true); return true; } // May as well register for almost everything - it's free! int mask = IN_DELETE | IN_DELETE_SELF | IN_CREATE | IN_MOVE | IN_MOVE_SELF | IN_DONT_FOLLOW | IN_MOVED_FROM | IN_MODIFY | IN_ATTRIB; if ((e->wd = inotify_add_watch(m_inotify_fd, QFile::encodeName(e->path).data(), mask)) >= 0) { if (s_verboseDebug) { qCDebug(KDIRWATCH) << "inotify successfully used for monitoring" << e->path << "wd=" << e->wd; } return true; } qCDebug(KDIRWATCH) << "inotify failed for monitoring" << e->path << ":" << strerror(errno); return false; } #endif #if HAVE_QFILESYSTEMWATCHER bool KDirWatchPrivate::useQFSWatch(Entry *e) { e->m_mode = QFSWatchMode; e->dirty = false; if (e->m_status == NonExistent) { addEntry(nullptr, e->parentDirectory(), e, true /*isDir*/); return true; } qCDebug(KDIRWATCH) << "fsWatcher->addPath" << e->path; if (!fsWatcher) { fsWatcher = new QFileSystemWatcher(); connect(fsWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(fswEventReceived(QString))); connect(fsWatcher, SIGNAL(fileChanged(QString)), this, SLOT(fswEventReceived(QString))); } fsWatcher->addPath(e->path); return true; } #endif bool KDirWatchPrivate::useStat(Entry *e) { if (KFileSystemType::fileSystemType(e->path) == KFileSystemType::Nfs) { // TODO: or Smbfs? useFreq(e, m_nfsPollInterval); } else { useFreq(e, m_PollInterval); } if (e->m_mode != StatMode) { e->m_mode = StatMode; statEntries++; if (statEntries == 1) { // if this was first STAT entry (=timer was stopped) timer.start(freq); // then start the timer qCDebug(KDIRWATCH) << " Started Polling Timer, freq " << freq; } } qCDebug(KDIRWATCH) << " Setup Stat (freq " << e->freq << ") for " << e->path; return true; } /* If !=0, this KDirWatch instance wants to watch at <_path>, * providing in the type of the entry to be watched. * Sometimes, entries are dependant on each other: if !=0, * this entry needs another entry to watch himself (when notExistent). */ void KDirWatchPrivate::addEntry(KDirWatch *instance, const QString &_path, Entry *sub_entry, bool isDir, KDirWatch::WatchModes watchModes) { QString path(_path); + if (path.startsWith(QLatin1String(":/"))) { + qCWarning(KDIRWATCH) << "Cannot watch QRC-like path" << path; + return; + } if (path.isEmpty() #ifndef Q_OS_WIN || path == QLatin1String("/dev") || (path.startsWith(QLatin1String("/dev/")) && !path.startsWith(QLatin1String("/dev/.")) && !path.startsWith(QLatin1String("/dev/shm"))) #endif ) { return; // Don't even go there. } if (path.length() > 1 && path.endsWith(QLatin1Char('/'))) { path.truncate(path.length() - 1); } EntryMap::Iterator it = m_mapEntries.find(path); if (it != m_mapEntries.end()) { if (sub_entry) { (*it).m_entries.append(sub_entry); if (s_verboseDebug) { qCDebug(KDIRWATCH) << "Added already watched Entry" << path << "(for" << sub_entry->path << ")"; } } else { (*it).addClient(instance, watchModes); if (s_verboseDebug) { qCDebug(KDIRWATCH) << "Added already watched Entry" << path << "(now" << (*it).clientCount() << "clients)" << QStringLiteral("[%1]").arg(instance->objectName()); } } return; } // we have a new path to watch QT_STATBUF stat_buf; bool exists = (QT_STAT(QFile::encodeName(path).constData(), &stat_buf) == 0); EntryMap::iterator newIt = m_mapEntries.insert(path, Entry()); // the insert does a copy, so we have to use now Entry *e = &(*newIt); if (exists) { e->isDir = (stat_buf.st_mode & QT_STAT_MASK) == QT_STAT_DIR; #ifndef Q_OS_WIN if (e->isDir && !isDir) { if (QT_LSTAT(QFile::encodeName(path).constData(), &stat_buf) == 0) { if ((stat_buf.st_mode & QT_STAT_MASK) == QT_STAT_LNK) { // if it's a symlink, don't follow it e->isDir = false; } } } #endif if (e->isDir && !isDir) { qCWarning(KCOREADDONS_DEBUG) << "KDirWatch:" << path << "is a directory. Use addDir!"; } else if (!e->isDir && isDir) { qWarning("KDirWatch: %s is a file. Use addFile!", qPrintable(path)); } if (!e->isDir && (watchModes != KDirWatch::WatchDirOnly)) { qCWarning(KCOREADDONS_DEBUG) << "KDirWatch:" << path << "is a file. You can't use recursive or " "watchFiles options"; watchModes = KDirWatch::WatchDirOnly; } #ifdef Q_OS_WIN // ctime is the 'creation time' on windows - use mtime instead e->m_ctime = stat_buf.st_mtime; #else e->m_ctime = stat_buf.st_ctime; #endif e->m_status = Normal; e->m_nlink = stat_buf.st_nlink; e->m_ino = stat_buf.st_ino; } else { e->isDir = isDir; e->m_ctime = invalid_ctime; e->m_status = NonExistent; e->m_nlink = 0; e->m_ino = 0; } e->path = path; if (sub_entry) { e->m_entries.append(sub_entry); } else { e->addClient(instance, watchModes); } if (s_verboseDebug) { qCDebug(KDIRWATCH).nospace() << "Added " << (e->isDir ? "Dir " : "File ") << path << (e->m_status == NonExistent ? " NotExisting" : "") << " for " << (sub_entry ? sub_entry->path : QString()) << " [" << (instance ? instance->objectName() : QString()) << "]"; } // now setup the notification method e->m_mode = UnknownMode; e->msecLeft = 0; if (isNoisyFile(QFile::encodeName(path).data())) { return; } if (exists && e->isDir && (watchModes != KDirWatch::WatchDirOnly)) { QFlags filters = QDir::NoDotAndDotDot; if ((watchModes & KDirWatch::WatchSubDirs) && (watchModes & KDirWatch::WatchFiles)) { filters |= (QDir::Dirs | QDir::Files); } else if (watchModes & KDirWatch::WatchSubDirs) { filters |= QDir::Dirs; } else if (watchModes & KDirWatch::WatchFiles) { filters |= QDir::Files; } #if HAVE_SYS_INOTIFY_H if (e->m_mode == INotifyMode || (e->m_mode == UnknownMode && m_preferredMethod == KDirWatch::INotify)) { //qCDebug(KDIRWATCH) << "Ignoring WatchFiles directive - this is implicit with inotify"; // Placing a watch on individual files is redundant with inotify // (inotify gives us WatchFiles functionality "for free") and indeed // actively harmful, so prevent it. WatchSubDirs is necessary, though. filters &= ~QDir::Files; } #endif QDir basedir(e->path); const QFileInfoList contents = basedir.entryInfoList(filters); for (QFileInfoList::const_iterator iter = contents.constBegin(); iter != contents.constEnd(); ++iter) { const QFileInfo &fileInfo = *iter; // treat symlinks as files--don't follow them. bool isDir = fileInfo.isDir() && !fileInfo.isSymLink(); addEntry(instance, fileInfo.absoluteFilePath(), nullptr, isDir, isDir ? watchModes : KDirWatch::WatchDirOnly); } } addWatch(e); } void KDirWatchPrivate::addWatch(Entry *e) { // If the watch is on a network filesystem use the nfsPreferredMethod as the // default, otherwise use preferredMethod as the default, if the methods are // the same we can skip the mountpoint check // This allows to configure a different method for NFS mounts, since inotify // cannot detect changes made by other machines. However as a default inotify // is fine, since the most common case is a NFS-mounted home, where all changes // are made locally. #177892. KDirWatch::Method preferredMethod = m_preferredMethod; if (m_nfsPreferredMethod != m_preferredMethod) { if (KFileSystemType::fileSystemType(e->path) == KFileSystemType::Nfs) { preferredMethod = m_nfsPreferredMethod; } } // Try the appropriate preferred method from the config first bool entryAdded = false; switch (preferredMethod) { #if HAVE_FAM case KDirWatch::FAM: entryAdded = useFAM(e); break; #endif #if HAVE_SYS_INOTIFY_H case KDirWatch::INotify: entryAdded = useINotify(e); break; #endif #if HAVE_QFILESYSTEMWATCHER case KDirWatch::QFSWatch: entryAdded = useQFSWatch(e); break; #endif case KDirWatch::Stat: entryAdded = useStat(e); break; } // Failing that try in order INotify, FAM, QFSWatch, Stat if (!entryAdded) { #if HAVE_SYS_INOTIFY_H if (useINotify(e)) { return; } #endif #if HAVE_FAM if (useFAM(e)) { return; } #endif #if HAVE_QFILESYSTEMWATCHER if (useQFSWatch(e)) { return; } #endif useStat(e); } } void KDirWatchPrivate::removeWatch(Entry *e) { #if HAVE_FAM if (e->m_mode == FAMMode) { FAMCancelMonitor(&fc, &(e->fr)); qCDebug(KDIRWATCH).nospace() << "Cancelled FAM (Req " << FAMREQUEST_GETREQNUM(&(e->fr)) << ") for " << e->path; } #endif #if HAVE_SYS_INOTIFY_H if (e->m_mode == INotifyMode) { (void) inotify_rm_watch(m_inotify_fd, e->wd); if (s_verboseDebug) { qCDebug(KDIRWATCH).nospace() << "Cancelled INotify (fd " << m_inotify_fd << ", " << e->wd << ") for " << e->path; } } #endif #if HAVE_QFILESYSTEMWATCHER if (e->m_mode == QFSWatchMode && fsWatcher) { if (s_verboseDebug) { qCDebug(KDIRWATCH) << "fsWatcher->removePath" << e->path; } fsWatcher->removePath(e->path); } #endif } bool KDirWatchPrivate::removeEntry(KDirWatch *instance, const QString &_path, Entry *sub_entry) { if (s_verboseDebug) { qCDebug(KDIRWATCH) << "path=" << _path << "sub_entry:" << sub_entry; } Entry *e = entry(_path); if (!e) { return false; } removeEntry(instance, e, sub_entry); return true; } void KDirWatchPrivate::removeEntry(KDirWatch *instance, Entry *e, Entry *sub_entry) { removeList.remove(e); if (sub_entry) { e->m_entries.removeAll(sub_entry); } else { e->removeClient(instance); } if (!e->m_clients.empty() || !e->m_entries.empty()) { return; } if (delayRemove) { removeList.insert(e); // now e->isValid() is false return; } if (e->m_status == Normal) { removeWatch(e); } else { // Removed a NonExistent entry - we just remove it from the parent if (e->isDir) { removeEntry(nullptr, e->parentDirectory(), e); } else { removeEntry(nullptr, QFileInfo(e->path).absolutePath(), e); } } if (e->m_mode == StatMode) { statEntries--; if (statEntries == 0) { timer.stop(); // stop timer if lists are empty qCDebug(KDIRWATCH) << " Stopped Polling Timer"; } } if (s_verboseDebug) { qCDebug(KDIRWATCH).nospace() << "Removed " << (e->isDir ? "Dir " : "File ") << e->path << " for " << (sub_entry ? sub_entry->path : QString()) << " [" << (instance ? instance->objectName() : QString()) << "]"; } QString p = e->path; // take a copy, QMap::remove takes a reference and deletes, since e points into the map m_mapEntries.remove(p); // not valid any more } /* Called from KDirWatch destructor: * remove as client from all entries */ void KDirWatchPrivate::removeEntries(KDirWatch *instance) { int minfreq = 3600000; QStringList pathList; // put all entries where instance is a client in list EntryMap::Iterator it = m_mapEntries.begin(); for (; it != m_mapEntries.end(); ++it) { Client *c = nullptr; for (Client &client : (*it).m_clients) { if (client.instance == instance) { c = &client; break; } } if (c) { c->count = 1; // forces deletion of instance as client pathList.append((*it).path); } else if ((*it).m_mode == StatMode && (*it).freq < minfreq) { minfreq = (*it).freq; } } Q_FOREACH (const QString &path, pathList) { removeEntry(instance, path, nullptr); } if (minfreq > freq) { // we can decrease the global polling frequency freq = minfreq; if (timer.isActive()) { timer.start(freq); } qCDebug(KDIRWATCH) << "Poll Freq now" << freq << "msec"; } } // instance ==0: stop scanning for all instances bool KDirWatchPrivate::stopEntryScan(KDirWatch *instance, Entry *e) { int stillWatching = 0; for (Client &client : e->m_clients) { if (!instance || instance == client.instance) { client.watchingStopped = true; } else if (!client.watchingStopped) { stillWatching += client.count; } } qCDebug(KDIRWATCH) << (instance ? instance->objectName() : QStringLiteral("all")) << "stopped scanning" << e->path << "(now" << stillWatching << "watchers)"; if (stillWatching == 0) { // if nobody is interested, we don't watch, and we don't report // changes that happened while not watching e->m_ctime = invalid_ctime; // invalid // Changing m_status like this would create wrong "created" events in stat mode. // To really "stop watching" we would need to determine 'stillWatching==0' in scanEntry... //e->m_status = NonExistent; } return true; } // instance ==0: start scanning for all instances bool KDirWatchPrivate::restartEntryScan(KDirWatch *instance, Entry *e, bool notify) { int wasWatching = 0, newWatching = 0; for (Client &client : e->m_clients) { if (!client.watchingStopped) { wasWatching += client.count; } else if (!instance || instance == client.instance) { client.watchingStopped = false; newWatching += client.count; } } if (newWatching == 0) { return false; } qCDebug(KDIRWATCH) << (instance ? instance->objectName() : QStringLiteral("all")) << "restarted scanning" << e->path << "(now" << wasWatching + newWatching << "watchers)"; // restart watching and emit pending events int ev = NoChange; if (wasWatching == 0) { if (!notify) { QT_STATBUF stat_buf; bool exists = (QT_STAT(QFile::encodeName(e->path).constData(), &stat_buf) == 0); if (exists) { // ctime is the 'creation time' on windows, but with qMax // we get the latest change of any kind, on any platform. e->m_ctime = qMax(stat_buf.st_ctime, stat_buf.st_mtime); e->m_status = Normal; if (s_verboseDebug) { qCDebug(KDIRWATCH) << "Setting status to Normal for" << e << e->path; } e->m_nlink = stat_buf.st_nlink; e->m_ino = stat_buf.st_ino; // Same as in scanEntry: ensure no subentry in parent dir removeEntry(nullptr, e->parentDirectory(), e); } else { e->m_ctime = invalid_ctime; e->m_status = NonExistent; e->m_nlink = 0; if (s_verboseDebug) { qCDebug(KDIRWATCH) << "Setting status to NonExistent for" << e << e->path; } } } e->msecLeft = 0; ev = scanEntry(e); } emitEvent(e, ev); return true; } // instance ==0: stop scanning for all instances void KDirWatchPrivate::stopScan(KDirWatch *instance) { EntryMap::Iterator it = m_mapEntries.begin(); for (; it != m_mapEntries.end(); ++it) { stopEntryScan(instance, &(*it)); } } void KDirWatchPrivate::startScan(KDirWatch *instance, bool notify, bool skippedToo) { if (!notify) { resetList(instance, skippedToo); } EntryMap::Iterator it = m_mapEntries.begin(); for (; it != m_mapEntries.end(); ++it) { restartEntryScan(instance, &(*it), notify); } // timer should still be running when in polling mode } // clear all pending events, also from stopped void KDirWatchPrivate::resetList(KDirWatch *instance, bool skippedToo) { Q_UNUSED(instance); EntryMap::Iterator it = m_mapEntries.begin(); for (; it != m_mapEntries.end(); ++it) { for (Client &client : (*it).m_clients) { if (!client.watchingStopped || skippedToo) { client.pending = NoChange; } } } } // Return event happened on // int KDirWatchPrivate::scanEntry(Entry *e) { // Shouldn't happen: Ignore "unknown" notification method if (e->m_mode == UnknownMode) { return NoChange; } if (e->m_mode == FAMMode || e->m_mode == INotifyMode) { // we know nothing has changed, no need to stat if (!e->dirty) { return NoChange; } e->dirty = false; } if (e->m_mode == StatMode) { // only scan if timeout on entry timer happens; // e.g. when using 500msec global timer, a entry // with freq=5000 is only watched every 10th time e->msecLeft -= freq; if (e->msecLeft > 0) { return NoChange; } e->msecLeft += e->freq; } QT_STATBUF stat_buf; const bool exists = (QT_STAT(QFile::encodeName(e->path).constData(), &stat_buf) == 0); if (exists) { if (e->m_status == NonExistent) { // ctime is the 'creation time' on windows, but with qMax // we get the latest change of any kind, on any platform. e->m_ctime = qMax(stat_buf.st_ctime, stat_buf.st_mtime); e->m_status = Normal; e->m_ino = stat_buf.st_ino; if (s_verboseDebug) { qCDebug(KDIRWATCH) << "Setting status to Normal for just created" << e << e->path; } // We need to make sure the entry isn't listed in its parent's subentries... (#222974, testMoveTo) removeEntry(nullptr, e->parentDirectory(), e); return Created; } #if 1 // for debugging the if() below if (s_verboseDebug) { struct tm *tmp = localtime(&e->m_ctime); char outstr[200]; strftime(outstr, sizeof(outstr), "%H:%M:%S", tmp); qCDebug(KDIRWATCH) << e->path << "e->m_ctime=" << e->m_ctime << outstr << "stat_buf.st_ctime=" << stat_buf.st_ctime << "stat_buf.st_mtime=" << stat_buf.st_mtime << "e->m_nlink=" << e->m_nlink << "stat_buf.st_nlink=" << stat_buf.st_nlink << "e->m_ino=" << e->m_ino << "stat_buf.st_ino=" << stat_buf.st_ino; } #endif if ((e->m_ctime != invalid_ctime) && (qMax(stat_buf.st_ctime, stat_buf.st_mtime) != e->m_ctime || stat_buf.st_ino != e->m_ino || int(stat_buf.st_nlink) != int(e->m_nlink) #ifdef Q_OS_WIN // on Windows, we trust QFSW to get it right, the ctime comparisons above // fail for example when adding files to directories on Windows // which doesn't change the mtime of the directory || e->m_mode == QFSWatchMode #endif )) { e->m_ctime = qMax(stat_buf.st_ctime, stat_buf.st_mtime); e->m_nlink = stat_buf.st_nlink; if (e->m_ino != stat_buf.st_ino) { // The file got deleted and recreated. We need to watch it again. removeWatch(e); addWatch(e); e->m_ino = stat_buf.st_ino; return (Deleted|Created); } else { return Changed; } } return NoChange; } // dir/file doesn't exist e->m_nlink = 0; e->m_ino = 0; e->m_status = NonExistent; if (e->m_ctime == invalid_ctime) { return NoChange; } e->m_ctime = invalid_ctime; return Deleted; } /* Notify all interested KDirWatch instances about a given event on an entry * and stored pending events. When watching is stopped, the event is * added to the pending events. */ void KDirWatchPrivate::emitEvent(Entry *e, int event, const QString &fileName) { QString path(e->path); if (!fileName.isEmpty()) { if (!QDir::isRelativePath(fileName)) { path = fileName; } else { #ifdef Q_OS_UNIX path += QLatin1Char('/') + fileName; #elif defined(Q_OS_WIN) //current drive is passed instead of / path += QDir::currentPath().left(2) + QLatin1Char('/') + fileName; #endif } } if (s_verboseDebug) { qCDebug(KDIRWATCH) << event << path << e->m_clients.size() << "clients"; } for (Client &c : e->m_clients) { if (c.instance == nullptr || c.count == 0) { continue; } if (c.watchingStopped) { // Do not add event to a list of pending events, the docs say restartDirScan won't emit! #if 0 if (event == Changed) { c.pending |= event; } else if (event == Created || event == Deleted) { c.pending = event; } #endif continue; } // not stopped if (event == NoChange || event == Changed) { event |= c.pending; } c.pending = NoChange; if (event == NoChange) { continue; } // Emit the signals delayed, to avoid unexpected re-entrancy from the slots (#220153) if (event & Deleted) { QMetaObject::invokeMethod(c.instance, "setDeleted", Qt::QueuedConnection, Q_ARG(QString, path)); } if (event & Created) { QMetaObject::invokeMethod(c.instance, "setCreated", Qt::QueuedConnection, Q_ARG(QString, path)); // possible emit Change event after creation } if (event & Changed) { QMetaObject::invokeMethod(c.instance, "setDirty", Qt::QueuedConnection, Q_ARG(QString, path)); } } } // Remove entries which were marked to be removed void KDirWatchPrivate::slotRemoveDelayed() { delayRemove = false; // Removing an entry could also take care of removing its parent // (e.g. in FAM or inotify mode), which would remove other entries in removeList, // so don't use Q_FOREACH or iterators here... while (!removeList.isEmpty()) { Entry *entry = *removeList.begin(); removeEntry(nullptr, entry, nullptr); // this will remove entry from removeList } } /* Scan all entries to be watched for changes. This is done regularly * when polling. FAM and inotify use a single-shot timer to call this slot delayed. */ void KDirWatchPrivate::slotRescan() { if (s_verboseDebug) { qCDebug(KDIRWATCH); } EntryMap::Iterator it; // People can do very long things in the slot connected to dirty(), // like showing a message box. We don't want to keep polling during // that time, otherwise the value of 'delayRemove' will be reset. // ### TODO: now the emitEvent delays emission, this can be cleaned up bool timerRunning = timer.isActive(); if (timerRunning) { timer.stop(); } // We delay deletions of entries this way. // removeDir(), when called in slotDirty(), can cause a crash otherwise // ### TODO: now the emitEvent delays emission, this can be cleaned up delayRemove = true; if (rescan_all) { // mark all as dirty it = m_mapEntries.begin(); for (; it != m_mapEntries.end(); ++it) { (*it).dirty = true; } rescan_all = false; } else { // progate dirty flag to dependant entries (e.g. file watches) it = m_mapEntries.begin(); for (; it != m_mapEntries.end(); ++it) if (((*it).m_mode == INotifyMode || (*it).m_mode == QFSWatchMode) && (*it).dirty) { (*it).propagate_dirty(); } } #if HAVE_SYS_INOTIFY_H QList cList; #endif it = m_mapEntries.begin(); for (; it != m_mapEntries.end(); ++it) { // we don't check invalid entries (i.e. remove delayed) Entry *entry = &(*it); if (!entry->isValid()) { continue; } const int ev = scanEntry(entry); if (s_verboseDebug) { qCDebug(KDIRWATCH) << "scanEntry for" << entry->path << "says" << ev; } switch (entry->m_mode) { #if HAVE_SYS_INOTIFY_H case INotifyMode: if (ev == Deleted) { if (s_verboseDebug) { qCDebug(KDIRWATCH) << "scanEntry says" << entry->path << "was deleted"; } addEntry(nullptr, entry->parentDirectory(), entry, true); } else if (ev == Created) { if (s_verboseDebug) { qCDebug(KDIRWATCH) << "scanEntry says" << entry->path << "was created. wd=" << entry->wd; } if (entry->wd < 0) { cList.append(entry); addWatch(entry); } } break; #endif case FAMMode: case QFSWatchMode: if (ev == Created) { addWatch(entry); } break; default: // dunno about StatMode... break; } #if HAVE_SYS_INOTIFY_H if (entry->isDir) { // Report and clear the the list of files that have changed in this directory. // Remove duplicates by changing to set and back again: // we don't really care about preserving the order of the // original changes. QStringList pendingFileChanges = entry->m_pendingFileChanges; pendingFileChanges.removeDuplicates(); Q_FOREACH (const QString &changedFilename, pendingFileChanges) { if (s_verboseDebug) { qCDebug(KDIRWATCH) << "processing pending file change for" << changedFilename; } emitEvent(entry, Changed, changedFilename); } entry->m_pendingFileChanges.clear(); } #endif if (ev != NoChange) { emitEvent(entry, ev); } } if (timerRunning) { timer.start(freq); } #if HAVE_SYS_INOTIFY_H // Remove watch of parent of new created directories Q_FOREACH (Entry *e, cList) { removeEntry(nullptr, e->parentDirectory(), e); } #endif QTimer::singleShot(0, this, SLOT(slotRemoveDelayed())); } bool KDirWatchPrivate::isNoisyFile(const char *filename) { // $HOME/.X.err grows with debug output, so don't notify change if (*filename == '.') { if (strncmp(filename, ".X.err", 6) == 0) { return true; } if (strncmp(filename, ".xsession-errors", 16) == 0) { return true; } // fontconfig updates the cache on every KDE app start // (inclusive kio_thumbnail slaves) if (strncmp(filename, ".fonts.cache", 12) == 0) { return true; } } return false; } #if HAVE_FAM void KDirWatchPrivate::famEventReceived() { static FAMEvent fe; delayRemove = true; //qCDebug(KDIRWATCH) << "Fam event received"; while (use_fam && FAMPending(&fc)) { if (FAMNextEvent(&fc, &fe) == -1) { qCWarning(KCOREADDONS_DEBUG) << "FAM connection problem, switching to polling."; use_fam = false; delete sn; sn = 0; // Replace all FAMMode entries with INotify/Stat EntryMap::Iterator it = m_mapEntries.begin(); for (; it != m_mapEntries.end(); ++it) if ((*it).m_mode == FAMMode && !(*it).m_clients.empty()) { Entry *e = &(*it); addWatch(e); } } else { checkFAMEvent(&fe); } } QTimer::singleShot(0, this, SLOT(slotRemoveDelayed())); } void KDirWatchPrivate::checkFAMEvent(FAMEvent *fe) { //qCDebug(KDIRWATCH); Entry *e = 0; EntryMap::Iterator it = m_mapEntries.begin(); for (; it != m_mapEntries.end(); ++it) if (FAMREQUEST_GETREQNUM(&((*it).fr)) == FAMREQUEST_GETREQNUM(&(fe->fr))) { e = &(*it); break; } // Don't be too verbose ;-) if ((fe->code == FAMExists) || (fe->code == FAMEndExist) || (fe->code == FAMAcknowledge)) { if (e) { e->m_famReportedSeen = true; } return; } if (isNoisyFile(fe->filename)) { return; } // Entry *e = static_cast(fe->userdata); if (s_verboseDebug) { // don't enable this except when debugging, see #88538 qCDebug(KDIRWATCH) << "Processing FAM event (" << ((fe->code == FAMChanged) ? "FAMChanged" : (fe->code == FAMDeleted) ? "FAMDeleted" : (fe->code == FAMStartExecuting) ? "FAMStartExecuting" : (fe->code == FAMStopExecuting) ? "FAMStopExecuting" : (fe->code == FAMCreated) ? "FAMCreated" : (fe->code == FAMMoved) ? "FAMMoved" : (fe->code == FAMAcknowledge) ? "FAMAcknowledge" : (fe->code == FAMExists) ? "FAMExists" : (fe->code == FAMEndExist) ? "FAMEndExist" : "Unknown Code") << ", " << fe->filename << ", Req " << FAMREQUEST_GETREQNUM(&(fe->fr)) << ") e=" << e; } if (!e) { // this happens e.g. for FAMAcknowledge after deleting a dir... // qCDebug(KDIRWATCH) << "No entry for FAM event ?!"; return; } if (e->m_status == NonExistent) { qCDebug(KDIRWATCH) << "FAM event for nonExistent entry " << e->path; return; } // Delayed handling. This rechecks changes with own stat calls. e->dirty = true; if (!rescan_timer.isActive()) { rescan_timer.start(m_PollInterval); // singleshot } // needed FAM control actions on FAM events switch (fe->code) { case FAMDeleted: // fe->filename is an absolute path when a watched file-or-dir is deleted if (!QDir::isRelativePath(QFile::decodeName(fe->filename))) { FAMCancelMonitor(&fc, &(e->fr)); // needed ? qCDebug(KDIRWATCH) << "Cancelled FAMReq" << FAMREQUEST_GETREQNUM(&(e->fr)) << "for" << e->path; e->m_status = NonExistent; e->m_ctime = invalid_ctime; emitEvent(e, Deleted, e->path); // If the parent dir was already watched, tell it something changed Entry *parentEntry = entry(e->parentDirectory()); if (parentEntry) { parentEntry->dirty = true; } // Add entry to parent dir to notice if the entry gets recreated addEntry(0, e->parentDirectory(), e, true /*isDir*/); } else { // A file in this directory has been removed, and wasn't explicitly watched. // We could still inform clients, like inotify does? But stat can't. // For now we just marked e dirty and slotRescan will emit the dir as dirty. //qCDebug(KDIRWATCH) << "Got FAMDeleted for" << QFile::decodeName(fe->filename) << "in" << e->path << ". Absolute path -> NOOP!"; } break; case FAMCreated: { // check for creation of a directory we have to watch QString tpath(e->path + QLatin1Char('/') + QFile::decodeName(fe->filename)); // This code is very similar to the one in inotifyEventReceived... Entry *sub_entry = e->findSubEntry(tpath); if (sub_entry /*&& sub_entry->isDir*/) { // We were waiting for this new file/dir to be created. We don't actually // emit an event here, as the rescan_timer will re-detect the creation and // do the signal emission there. sub_entry->dirty = true; rescan_timer.start(0); // process this asap, to start watching that dir } else if (e->isDir && !e->m_clients.empty()) { bool isDir = false; const QList clients = e->clientsForFileOrDir(tpath, &isDir); Q_FOREACH (const Client *client, clients) { addEntry(client->instance, tpath, 0, isDir, isDir ? client->m_watchModes : KDirWatch::WatchDirOnly); } if (!clients.isEmpty()) { emitEvent(e, Created, tpath); qCDebug(KDIRWATCH).nospace() << clients.count() << " instance(s) monitoring the new " << (isDir ? "dir " : "file ") << tpath; } } } break; default: break; } } #else void KDirWatchPrivate::famEventReceived() { qCWarning(KCOREADDONS_DEBUG) << "Fam event received but FAM is not supported"; } #endif void KDirWatchPrivate::statistics() { EntryMap::Iterator it; qCDebug(KDIRWATCH) << "Entries watched:"; if (m_mapEntries.count() == 0) { qCDebug(KDIRWATCH) << " None."; } else { it = m_mapEntries.begin(); for (; it != m_mapEntries.end(); ++it) { Entry *e = &(*it); qCDebug(KDIRWATCH) << " " << *e; for (const Client &c : e->m_clients) { QByteArray pending; if (c.watchingStopped) { if (c.pending & Deleted) { pending += "deleted "; } if (c.pending & Created) { pending += "created "; } if (c.pending & Changed) { pending += "changed "; } if (!pending.isEmpty()) { pending = " (pending: " + pending + ')'; } pending = ", stopped" + pending; } qCDebug(KDIRWATCH) << " by " << c.instance->objectName() << " (" << c.count << " times)" << pending; } if (e->m_entries.count() > 0) { qCDebug(KDIRWATCH) << " dependent entries:"; Q_FOREACH (Entry *d, e->m_entries) { qCDebug(KDIRWATCH) << " " << d << d->path << (d->m_status == NonExistent ? "NonExistent" : "EXISTS!!! ERROR!"); if (s_verboseDebug) { Q_ASSERT(d->m_status == NonExistent); // it doesn't belong here otherwise } } } } } } #if HAVE_QFILESYSTEMWATCHER // Slot for QFileSystemWatcher void KDirWatchPrivate::fswEventReceived(const QString &path) { if (s_verboseDebug) { qCDebug(KDIRWATCH) << path; } EntryMap::Iterator it = m_mapEntries.find(path); if (it != m_mapEntries.end()) { Entry *e = &(*it); e->dirty = true; const int ev = scanEntry(e); if (s_verboseDebug) { qCDebug(KDIRWATCH) << "scanEntry for" << e->path << "says" << ev; } if (ev != NoChange) { emitEvent(e, ev); } if (ev == Deleted) { if (e->isDir) { addEntry(nullptr, e->parentDirectory(), e, true); } else { addEntry(nullptr, QFileInfo(e->path).absolutePath(), e, true); } } else if (ev == Created) { // We were waiting for it to appear; now watch it addWatch(e); } else if (e->isDir) { // Check if any file or dir was created under this directory, that we were waiting for Q_FOREACH (Entry *sub_entry, e->m_entries) { fswEventReceived(sub_entry->path); // recurse, to call scanEntry and see if something changed } } else { /* Even though QFileSystemWatcher only reported the file as modified, it is possible that the file * was in fact just deleted and then immediately recreated. If the file was deleted, QFileSystemWatcher * will delete the watch, and will ignore the file, even after it is recreated. Since it is impossible * to reliably detect this case, always re-request the watch on a dirty signal, to avoid losing the * underlying OS monitor. */ fsWatcher->addPath(e->path); } } } #else void KDirWatchPrivate::fswEventReceived(const QString &path) { Q_UNUSED(path); qCWarning(KCOREADDONS_DEBUG) << "QFileSystemWatcher event received but QFileSystemWatcher is not supported"; } #endif // HAVE_QFILESYSTEMWATCHER // // Class KDirWatch // Q_GLOBAL_STATIC(KDirWatch, s_pKDirWatchSelf) KDirWatch *KDirWatch::self() { return s_pKDirWatchSelf(); } // is this used anywhere? // yes, see kio/src/core/kcoredirlister_p.h:328 bool KDirWatch::exists() { return s_pKDirWatchSelf.exists() && dwp_self.hasLocalData(); } static void postRoutine_KDirWatch() { if (s_pKDirWatchSelf.exists()) { s_pKDirWatchSelf()->deleteQFSWatcher(); } } KDirWatch::KDirWatch(QObject *parent) : QObject(parent), d(createPrivate()) { static QBasicAtomicInt nameCounter = Q_BASIC_ATOMIC_INITIALIZER(1); const int counter = nameCounter.fetchAndAddRelaxed(1); // returns the old value setObjectName(QStringLiteral("KDirWatch-%1").arg(counter)); if (counter == 1) { // very first KDirWatch instance // Must delete QFileSystemWatcher before qApp is gone - bug 261541 qAddPostRoutine(postRoutine_KDirWatch); } } KDirWatch::~KDirWatch() { if (dwp_self.hasLocalData()) { // skip this after app destruction d->removeEntries(this); } } void KDirWatch::addDir(const QString &_path, WatchModes watchModes) { if (d) { d->addEntry(this, _path, nullptr, true, watchModes); } } void KDirWatch::addFile(const QString &_path) { if (!d) { return; } d->addEntry(this, _path, nullptr, false); } QDateTime KDirWatch::ctime(const QString &_path) const { KDirWatchPrivate::Entry *e = d->entry(_path); if (!e) { return QDateTime(); } return QDateTime::fromTime_t(e->m_ctime); } void KDirWatch::removeDir(const QString &_path) { if (d) { if (!d->removeEntry(this, _path, nullptr)) { qCWarning(KCOREADDONS_DEBUG) << "doesn't know" << _path; } } } void KDirWatch::removeFile(const QString &_path) { if (d) { if (!d->removeEntry(this, _path, nullptr)) { qCWarning(KCOREADDONS_DEBUG) << "doesn't know" << _path; } } } bool KDirWatch::stopDirScan(const QString &_path) { if (d) { KDirWatchPrivate::Entry *e = d->entry(_path); if (e && e->isDir) { return d->stopEntryScan(this, e); } } return false; } bool KDirWatch::restartDirScan(const QString &_path) { if (d) { KDirWatchPrivate::Entry *e = d->entry(_path); if (e && e->isDir) // restart without notifying pending events { return d->restartEntryScan(this, e, false); } } return false; } void KDirWatch::stopScan() { if (d) { d->stopScan(this); d->_isStopped = true; } } bool KDirWatch::isStopped() { return d->_isStopped; } void KDirWatch::startScan(bool notify, bool skippedToo) { if (d) { d->_isStopped = false; d->startScan(this, notify, skippedToo); } } bool KDirWatch::contains(const QString &_path) const { KDirWatchPrivate::Entry *e = d->entry(_path); if (!e) { return false; } for (const KDirWatchPrivate::Client &client : e->m_clients) { if (client.instance == this) { return true; } } return false; } void KDirWatch::deleteQFSWatcher() { delete d->fsWatcher; d->fsWatcher = nullptr; d = nullptr; } void KDirWatch::statistics() { if (!dwp_self.hasLocalData()) { qCDebug(KDIRWATCH) << "KDirWatch not used"; return; } dwp_self.localData()->statistics(); } void KDirWatch::setCreated(const QString &_file) { qCDebug(KDIRWATCH) << objectName() << "emitting created" << _file; emit created(_file); } void KDirWatch::setDirty(const QString &_file) { //qCDebug(KDIRWATCH) << objectName() << "emitting dirty" << _file; emit dirty(_file); } void KDirWatch::setDeleted(const QString &_file) { qCDebug(KDIRWATCH) << objectName() << "emitting deleted" << _file; emit deleted(_file); } KDirWatch::Method KDirWatch::internalMethod() const { // This reproduces the logic in KDirWatchPrivate::addWatch switch (d->m_preferredMethod) { #if HAVE_FAM case KDirWatch::FAM: if (d->use_fam) { return KDirWatch::FAM; } break; #endif #if HAVE_SYS_INOTIFY_H case KDirWatch::INotify: if (d->supports_inotify) { return KDirWatch::INotify; } break; #endif #if HAVE_QFILESYSTEMWATCHER case KDirWatch::QFSWatch: return KDirWatch::QFSWatch; #endif case KDirWatch::Stat: return KDirWatch::Stat; } #if HAVE_SYS_INOTIFY_H if (d->supports_inotify) { return KDirWatch::INotify; } #endif #if HAVE_FAM if (d->use_fam) { return KDirWatch::FAM; } #endif #if HAVE_QFILESYSTEMWATCHER return KDirWatch::QFSWatch; #else return KDirWatch::Stat; #endif } #include "moc_kdirwatch.cpp" #include "moc_kdirwatch_p.cpp" //sven