diff --git a/src/lib/io/kdirwatch.cpp b/src/lib/io/kdirwatch.cpp index 4b62b1e..c5b1eeb 100644 --- a/src/lib/io/kdirwatch.cpp +++ b/src/lib/io/kdirwatch.cpp @@ -1,2054 +1,2063 @@ /* 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) // February 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 { #if defined(HAVE_SYS_INOTIFY_H) // 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 daemon (fam), to which applications connect for * notification of file changes. For NFS, the fam daemon * 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) ? qEnvironmentVariableIntValue(s_envNfsPoll) : 5000; m_PollInterval = qEnvironmentVariableIsSet(s_envPoll) ? qEnvironmentVariableIntValue(s_envPoll) : 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 = nullptr; #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:" << strerror(errno); 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)); Entry *e = m_inotify_wd_to_entry.value(event->wd); if (!e) { continue; } 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; m_inotify_wd_to_entry.remove(e->wd); 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 } } 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() { for (Entry *sub_entry : qAsConst(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:"; for (KDirWatchPrivate::Entry *subEntry : qAsConst(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.chop(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(nullptr, 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 = nullptr; 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(nullptr, 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 = nullptr; 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) { + QFile::encodeName(e->path).data(), mask)) != -1) { m_inotify_wd_to_entry.insert(e->wd, e); 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); + if (errno == ENOSPC) { + // Inotify max_user_watches was reached (/proc/sys/fs/inotify/max_user_watches) + // See man inotify_add_watch, https://github.com/guard/listen/wiki/Increasing-the-amount-of-inotify-watchers + qCWarning(KDIRWATCH) << "inotify failed for monitoring" << e->path << "\n" << + "Because it reached its max_user_watches,\n" << + "you can increase the maximum number of file watches per user,\n"<< + "by setting an appropriate fs.inotify.max_user_watches parameter in your /etc/sysctl.conf"; + } else { + qCDebug(KDIRWATCH) << "inotify failed for monitoring" << e->path << ":" << strerror(errno) << " (errno:" << 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 dependent on each other: if !=0, * this entry needs another entry to watch itself (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.chop(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) { qCWarning(KCOREADDONS_DEBUG) << "KDirWatch:" << path << "is a file. Use addFile!"; } 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) { m_inotify_wd_to_entry.remove(e->wd); (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 } void 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) { removeEntry(instance, e, sub_entry); } } 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 #if HAVE_SYS_INOTIFY_H m_inotify_wd_to_entry.remove(e->wd); #endif 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; } } for (const QString &path : qAsConst(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().leftRef(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-entrance from the slots (#220153) if (event & Deleted) { QMetaObject::invokeMethod(c.instance, [c, path]() { c.instance->setDeleted(path); }, Qt::QueuedConnection); } if (event & Created) { QMetaObject::invokeMethod(c.instance, [c, path]() { c.instance->setCreated(path); }, Qt::QueuedConnection); // possible emit Change event after creation } if (event & Changed) { QMetaObject::invokeMethod(c.instance, [c, path]() { c.instance->setDirty(path); }, Qt::QueuedConnection); } } } // 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 { // propagate dirty flag to dependent 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 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(); for (const QString &changedFilename : qAsConst(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 for (Entry *e : qAsConst(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 = nullptr; // 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 = nullptr; 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(nullptr, 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); 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; } } } 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.isEmpty()) { qCDebug(KDIRWATCH) << " dependent entries:"; for (Entry *d : qAsConst(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 for (Entry *sub_entry : qAsConst(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 (d && 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::fromSecsSinceEpoch(e->m_ctime); } void KDirWatch::removeDir(const QString &_path) { if (d) { d->removeEntry(this, _path, nullptr); } } void KDirWatch::removeFile(const QString &_path) { if (d) { d->removeEntry(this, _path, nullptr); } } 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