SPDX-FileCopyrightText: 1998 Sven Radej <sven@lisa.exp.univie.ac.at>
SPDX-FileCopyrightText: 2006 Dirk Mueller <mueller@kde.org>
SPDX-FileCopyrightText: 2007 Flavio Castelli <flavio.castelli@gmail.com>
SPDX-FileCopyrightText: 2008 Rafal Rzepecki <divided.mind@gmail.com>
SPDX-FileCopyrightText: 2010 David Faure <faure@kde.org>
SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
SPDX-License-Identifier: LGPL-2.0-only
*/
#include "kdirwatch.h"
#include "kcoreaddons_debug.h"
#include "kdirwatch_p.h"
#include "kfilesystemtype.h"
#include "knetworkmounts.h"
#include <io/config-kdirwatch.h>
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QLoggingCategory>
#include <QSocketNotifier>
#include <QThread>
#include <QThreadStorage>
#include <QTimer>
#include <assert.h>
#include <cerrno>
#include <sys/stat.h>
#include <qplatformdefs.h>
#include <stdlib.h>
#include <string.h>
#if HAVE_SYS_INOTIFY_H
#include <fcntl.h>
#include <sys/inotify.h>
#include <unistd.h>
#ifndef IN_DONT_FOLLOW
#define IN_DONT_FOLLOW 0x02000000
#endif
#ifndef IN_ONLYDIR
#define IN_ONLYDIR 0x01000000
#endif
#include <sys/ioctl.h>
#include <sys/utsname.h>
#endif
Q_DECLARE_LOGGING_CATEGORY(KDIRWATCH)
Q_LOGGING_CATEGORY(KDIRWATCH, "kf.coreaddons.kdirwatch", QtWarningMsg)
static bool s_verboseDebug = false;
static QThreadStorage<KDirWatchPrivate *> dwp_self;
static KDirWatchPrivate *createPrivate()
{
if (!dwp_self.hasLocalData()) {
dwp_self.setLocalData(new KDirWatchPrivate);
}
return dwp_self.localData();
}
static void destroyPrivate()
{
dwp_self.localData()->deleteLater();
dwp_self.setLocalData(nullptr);
}
static KDirWatch::Method methodFromString(const QByteArray &method)
{
if (method == "Stat") {
return KDirWatch::Stat;
} else if (method == "QFSWatch") {
return KDirWatch::QFSWatch;
} else {
#if HAVE_SYS_INOTIFY_H
return KDirWatch::INotify;
#else
return KDirWatch::QFSWatch;
#endif
}
}
static const char *methodToString(KDirWatch::Method method)
{
switch (method) {
case KDirWatch::INotify:
return "INotify";
case KDirWatch::Stat:
return "Stat";
case KDirWatch::QFSWatch:
return "QFSWatch";
}
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";
* 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()
: m_statRescanTimer()
, freq(3600000)
,
statEntries(0)
, delayRemove(false)
, rescan_all(false)
, rescan_timer()
,
#if HAVE_SYS_INOTIFY_H
mSn(nullptr)
,
#endif
_isStopped(false)
{
if (qAppName() == QLatin1String("kservicetest") || qAppName() == QLatin1String("filetypestest")) {
s_verboseDebug = true;
}
m_statRescanTimer.setObjectName(QStringLiteral("KDirWatchPrivate::timer"));
connect(&m_statRescanTimer, &QTimer::timeout, this, &KDirWatchPrivate::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");
m_nfsPreferredMethod = methodFromString(qEnvironmentVariableIsSet(s_envNfsMethod) ? qgetenv(s_envNfsMethod) : "Stat");
QList<QByteArray> availableMethods;
availableMethods << "Stat";
rescan_timer.setObjectName(QStringLiteral("KDirWatchPrivate::rescan_timer"));
rescan_timer.setSingleShot(true);
connect(&rescan_timer, &QTimer::timeout, this, &KDirWatchPrivate::slotRescan);
#if HAVE_SYS_INOTIFY_H
m_inotify_fd = inotify_init();
supports_inotify = m_inotify_fd > 0;
if (!supports_inotify) {
qCDebug(KDIRWATCH) << "Can't use Inotify, kernel doesn't support it:" << strerror(errno);
} else {
availableMethods << "INotify";
(void)fcntl(m_inotify_fd, F_SETFD, FD_CLOEXEC);
mSn = new QSocketNotifier(m_inotify_fd, QSocketNotifier::Read, this);
connect(mSn, &QSocketNotifier::activated, this, &KDirWatchPrivate::inotifyEventReceived);
}
#endif
#if HAVE_QFILESYSTEMWATCHER
availableMethods << "QFileSystemWatcher";
fsWatcher = nullptr;
#endif
qCDebug(KDIRWATCH) << "Available methods: " << availableMethods << "preferred=" << methodToString(m_preferredMethod);
}
KDirWatchPrivate::~KDirWatchPrivate()
{
m_statRescanTimer.stop();
for (auto it = m_mapEntries.begin(); it != m_mapEntries.end(); it++) {
auto &entry = it.value();
for (auto &client : entry.m_clients) {
client.instance->d = nullptr;
}
}
for (auto &referenceObject : m_referencesObjects) {
referenceObject->d = nullptr;
}
#if HAVE_SYS_INOTIFY_H
if (supports_inotify) {
QT_CLOSE(m_inotify_fd);
}
#endif
#if HAVE_QFILESYSTEMWATCHER
delete fsWatcher;
#endif
}
void KDirWatchPrivate::inotifyEventReceived()
{
#if HAVE_SYS_INOTIFY_H
if (!supports_inotify) {
return;
}
int pending = -1;
int offsetStartRead = 0;
char buf[8192];
assert(m_inotify_fd > -1);
ioctl(m_inotify_fd, FIONREAD, &pending);
while (pending > 0) {
const int bytesToRead = qMin<int>(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<inotify_event *>(&buf[offsetCurrent]);
if (event->mask & IN_Q_OVERFLOW) {
qCWarning(KDIRWATCH) << "Inotify Event queue overflowed, check max_queued_events value";
return;
}
const int eventSize = sizeof(struct inotify_event) + event->len;
if (bytesAvailable < eventSize) {
break;
}
bytesAvailable -= eventSize;
offsetCurrent += eventSize;
QString 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;
}
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;
qCDebug(KDIRWATCH).nospace() << "got event " << inotifyEventName(event) << " for entry " << e->path
<< (event->mask & IN_ISDIR ? " [directory] " : " [file] ") << path;
if (event->mask & IN_DELETE_SELF) {
e->m_status = NonExistent;
m_inotify_wd_to_entry.remove(e->wd);
e->wd = -1;
e->m_ctime = invalid_ctime;
emitEvent(e, Deleted);
Entry *parentEntry = entry(e->parentDirectory());
if (parentEntry) {
parentEntry->dirty = true;
}
addEntry(nullptr, e->parentDirectory(), e, true );
}
if (event->mask & IN_IGNORED) {
}
if (event->mask & (IN_CREATE | IN_MOVED_TO)) {
Entry *sub_entry = e->findSubEntry(tpath);
qCDebug(KDIRWATCH) << "-->got CREATE signal for" << (tpath) << "sub_entry=" << sub_entry;
if (sub_entry) {
sub_entry->dirty = true;
rescan_timer.start(0);
} else if (e->isDir && !e->m_clients.empty()) {
const QList<const Client *> clients = e->inotifyClientsForFileOrDir(isDir);
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);
}
}
}
if (event->mask & (IN_DELETE | IN_MOVED_FROM)) {
if ((e->isDir) && (!e->m_clients.empty())) {
const KDirWatch::WatchModes flag = isDir ? KDirWatch::WatchSubDirs : KDirWatch::WatchFiles;
int counter = std::count_if(e->m_clients.cbegin(), e->m_clients.cend(), [flag](const Client &client) {
return client.m_watchModes & flag;
});
if (counter != 0) {
emitEvent(e, Deleted, tpath);
}
}
}
if (event->mask & (IN_MODIFY | IN_ATTRIB)) {
if ((e->isDir) && (!e->m_clients.empty())) {
e->m_pendingFileChanges.append(tpath);
e->dirty = (wasDirty || (path.isEmpty() && (event->mask & IN_ATTRIB)));
}
}
if (!rescan_timer.isActive()) {
rescan_timer.start(m_PollInterval);
}
}
if (bytesAvailable > 0) {
memmove(buf, &buf[offsetCurrent], bytesAvailable);
offsetStartRead = bytesAvailable;
}
}
#endif
}
KDirWatchPrivate::Entry::~Entry()
{
}
* We first need to mark all yet nonexistent, but possible created
* entries as dirty...
*/
void KDirWatchPrivate::Entry::propagate_dirty()
{
for (Entry *sub_entry : std::as_const(m_entries)) {
if (!sub_entry->dirty) {
sub_entry->dirty = true;
sub_entry->propagate_dirty();
}
}
}
* this file/Dir entry.
*/
void KDirWatchPrivate::Entry::addClient(KDirWatch *instance, KDirWatch::WatchModes watchModes)
{
if (instance == nullptr) {
return;
}
auto it = findInstance(instance);
if (it != m_clients.end()) {
Client &client = *it;
++client.count;
client.m_watchModes = watchModes;
return;
}
m_clients.emplace_back(instance, watchModes);
}
void KDirWatchPrivate::Entry::removeClient(KDirWatch *instance)
{
auto it = findInstance(instance);
if (it != m_clients.end()) {
Client &client = *it;
--client.count;
if (client.count == 0) {
m_clients.erase(it);
}
}
}
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<const KDirWatchPrivate::Client *> KDirWatchPrivate::Entry::clientsForFileOrDir(const QString &tpath, bool *isDir) const
{
QList<const Client *> 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 {
}
return ret;
}
QList<const KDirWatchPrivate::Client *> KDirWatchPrivate::Entry::inotifyClientsForFileOrDir(bool isDir) const
{
QList<const Client *> 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 KDirWatch & )
{
if (!dwp_self.hasLocalData()) {
debug << "KDirWatch not used";
return debug;
}
debug << dwp_self.localData();
return debug;
}
QDebug operator<<(QDebug debug, const KDirWatchPrivate &dwp)
{
debug << "Entries watched:";
if (dwp.m_mapEntries.count() == 0) {
debug << " None.";
} else {
auto it = dwp.m_mapEntries.cbegin();
for (; it != dwp.m_mapEntries.cend(); ++it) {
const KDirWatchPrivate::Entry &e = it.value();
debug << " " << e;
for (const KDirWatchPrivate::Client &c : e.m_clients) {
QByteArray pending;
if (c.watchingStopped) {
if (c.pending & KDirWatchPrivate::Deleted) {
pending += "deleted ";
}
if (c.pending & KDirWatchPrivate::Created) {
pending += "created ";
}
if (c.pending & KDirWatchPrivate::Changed) {
pending += "changed ";
}
if (!pending.isEmpty()) {
pending = " (pending: " + pending + ')';
}
pending = ", stopped" + pending;
}
debug << " by " << c.instance->objectName() << " (" << c.count << " times)" << pending;
}
if (!e.m_entries.isEmpty()) {
debug << " dependent entries:";
for (KDirWatchPrivate::Entry *d : e.m_entries) {
debug << " " << d << d->path << (d->m_status == KDirWatchPrivate::NonExistent ? "NonExistent" : "EXISTS this is an ERROR!");
if (s_verboseDebug) {
Q_ASSERT(d->m_status == KDirWatchPrivate::NonExistent);
}
}
}
}
}
return debug;
}
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::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 : std::as_const(entry.m_entries)) {
debug << subEntry << subEntry->path;
}
}
debug << ']';
return debug;
}
KDirWatchPrivate::Entry *KDirWatchPrivate::entry(const QString &_path)
{
if (_path.isEmpty()) {
return nullptr;
}
QString path(_path);
if (path.length() > 1 && path.endsWith(QLatin1Char('/'))) {
path.chop(1);
}
auto it = m_mapEntries.find(path);
return it != m_mapEntries.end() ? &it.value() : nullptr;
}
void KDirWatchPrivate::useFreq(Entry *e, int newFreq)
{
e->freq = newFreq;
if (e->freq < freq) {
freq = e->freq;
if (m_statRescanTimer.isActive()) {
m_statRescanTimer.start(freq);
}
qCDebug(KDIRWATCH) << "Global Poll Freq is now" << freq << "msec";
}
}
#if HAVE_SYS_INOTIFY_H
bool KDirWatchPrivate::useINotify(Entry *e)
{
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;
}
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)) != -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;
}
if (errno == ENOSPC) {
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 );
return true;
}
if (!fsWatcher) {
fsWatcher = new QFileSystemWatcher();
connect(fsWatcher, &QFileSystemWatcher::directoryChanged, this, &KDirWatchPrivate::fswEventReceived);
connect(fsWatcher, &QFileSystemWatcher::fileChanged, this, &KDirWatchPrivate::fswEventReceived);
}
fsWatcher->addPath(e->path);
return true;
}
#endif
bool KDirWatchPrivate::useStat(Entry *e)
{
if (KFileSystemType::fileSystemType(e->path) == KFileSystemType::Nfs) {
useFreq(e, m_nfsPollInterval);
} else {
useFreq(e, m_PollInterval);
}
if (e->m_mode != StatMode) {
e->m_mode = StatMode;
statEntries++;
if (statEntries == 1) {
m_statRescanTimer.start(freq);
qCDebug(KDIRWATCH) << " Started Polling Timer, freq " << freq;
}
}
qCDebug(KDIRWATCH) << " Setup Stat (freq " << e->freq << ") for " << e->path;
return true;
}
* providing in <isDir> the type of the entry to be watched.
* Sometimes, entries are dependent on each other: if <sub_entry> !=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;
}
if (path.length() > 1 && path.endsWith(QLatin1Char('/'))) {
path.chop(1);
}
auto it = m_mapEntries.find(path);
if (it != m_mapEntries.end()) {
Entry &entry = it.value();
if (sub_entry) {
entry.m_entries.append(sub_entry);
if (s_verboseDebug) {
qCDebug(KDIRWATCH) << "Added already watched Entry" << path << "(for" << sub_entry->path << ")";
}
} else {
entry.addClient(instance, watchModes);
if (s_verboseDebug) {
qCDebug(KDIRWATCH) << "Added already watched Entry" << path << "(now" << entry.clientCount() << "clients)"
<< QStringLiteral("[%1]").arg(instance->objectName());
}
}
return;
}
QT_STATBUF stat_buf;
bool exists = (QT_STAT(QFile::encodeName(path).constData(), &stat_buf) == 0);
auto newIt = m_mapEntries.insert(path, Entry());
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) {
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
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()) << "]";
}
e->m_mode = UnknownMode;
e->msecLeft = 0;
if (isNoisyFile(QFile::encodeName(path).data())) {
return;
}
if (exists && e->isDir && (watchModes != KDirWatch::WatchDirOnly)) {
QFlags<QDir::Filter> 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 (m_preferredMethod == KDirWatch::INotify) {
filters &= ~QDir::Files;
}
#endif
QDir basedir(e->path);
const QFileInfoList contents = basedir.entryInfoList(filters);
for (const QFileInfo &fileInfo : contents) {
bool isDir = fileInfo.isDir() && !fileInfo.isSymLink();
addEntry(instance, fileInfo.absoluteFilePath(), nullptr, isDir, isDir ? watchModes : KDirWatch::WatchDirOnly);
}
}
addWatch(e);
}
void KDirWatchPrivate::addWatch(Entry *e)
{
KDirWatch::Method preferredMethod = m_preferredMethod;
if (m_nfsPreferredMethod != m_preferredMethod) {
if (KFileSystemType::fileSystemType(e->path) == KFileSystemType::Nfs) {
preferredMethod = m_nfsPreferredMethod;
}
}
bool inotifyFailed = false;
bool entryAdded = false;
switch (preferredMethod) {
#if HAVE_SYS_INOTIFY_H
case KDirWatch::INotify:
entryAdded = useINotify(e);
if (!entryAdded) {
inotifyFailed = true;
}
break;
#else
case KDirWatch::INotify:
entryAdded = false;
break;
#endif
#if HAVE_QFILESYSTEMWATCHER
case KDirWatch::QFSWatch:
entryAdded = useQFSWatch(e);
break;
#else
case KDirWatch::QFSWatch:
entryAdded = false;
break;
#endif
case KDirWatch::Stat:
entryAdded = useStat(e);
break;
}
if (!entryAdded) {
#if HAVE_SYS_INOTIFY_H
if (preferredMethod != KDirWatch::INotify && useINotify(e)) {
return;
}
#endif
#if HAVE_QFILESYSTEMWATCHER
if (preferredMethod != KDirWatch::QFSWatch && !inotifyFailed && useQFSWatch(e)) {
return;
}
#endif
if (preferredMethod != KDirWatch::Stat) {
useStat(e);
}
}
}
void KDirWatchPrivate::removeWatch(Entry *e)
{
#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)
{
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);
return;
}
if (e->m_status == Normal) {
removeWatch(e);
} else {
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) {
m_statRescanTimer.stop();
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;
#if HAVE_SYS_INOTIFY_H
m_inotify_wd_to_entry.remove(e->wd);
#endif
m_mapEntries.remove(p);
}
* remove <instance> as client from all entries
*/
void KDirWatchPrivate::removeEntries(KDirWatch *instance)
{
int minfreq = 3600000;
QStringList pathList;
for (auto it = m_mapEntries.begin(); it != m_mapEntries.end(); ++it) {
Entry &entry = it.value();
auto clientIt = entry.findInstance(instance);
if (clientIt != entry.m_clients.end()) {
clientIt->count = 1;
pathList.append(entry.path);
} else if (entry.m_mode == StatMode && entry.freq < minfreq) {
minfreq = entry.freq;
}
}
for (const QString &path : std::as_const(pathList)) {
removeEntry(instance, path, nullptr);
}
if (minfreq > freq) {
freq = minfreq;
if (m_statRescanTimer.isActive()) {
m_statRescanTimer.start(freq);
}
qCDebug(KDIRWATCH) << "Poll Freq now" << freq << "msec";
}
}
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) {
e->m_ctime = invalid_ctime;
}
return true;
}
bool KDirWatchPrivate::restartEntryScan(KDirWatch *instance, Entry *e, bool notify)
{
int wasWatching = 0;
int 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)";
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) {
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;
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;
}
void KDirWatchPrivate::stopScan(KDirWatch *instance)
{
for (auto it = m_mapEntries.begin(); it != m_mapEntries.end(); ++it) {
stopEntryScan(instance, &it.value());
}
}
void KDirWatchPrivate::startScan(KDirWatch *instance, bool notify, bool skippedToo)
{
if (!notify) {
resetList(instance, skippedToo);
}
for (auto it = m_mapEntries.begin(); it != m_mapEntries.end(); ++it) {
restartEntryScan(instance, &it.value(), notify);
}
}
void KDirWatchPrivate::resetList(KDirWatch *instance, bool skippedToo)
{
Q_UNUSED(instance);
for (auto it = m_mapEntries.begin(); it != m_mapEntries.end(); ++it) {
for (Client &client : it.value().m_clients) {
if (!client.watchingStopped || skippedToo) {
client.pending = NoChange;
}
}
}
}
int KDirWatchPrivate::scanEntry(Entry *e)
{
if (e->m_mode == UnknownMode) {
return NoChange;
}
if (e->m_mode == INotifyMode) {
if (!e->dirty) {
return NoChange;
}
e->dirty = false;
}
if (e->m_mode == StatMode) {
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) {
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;
}
removeEntry(nullptr, e->parentDirectory(), e);
return Created;
}
#if 1
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
|| 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) {
removeWatch(e);
addWatch(e);
e->m_ino = stat_buf.st_ino;
return (Deleted | Created);
} else {
return Changed;
}
}
return NoChange;
}
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;
}
* 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)
path += QStringView(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) {
continue;
}
if (event == NoChange || event == Changed) {
event |= c.pending;
}
c.pending = NoChange;
if (event == NoChange) {
continue;
}
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);
}
if (event & Changed) {
QMetaObject::invokeMethod(
c.instance,
[c, path]() {
c.instance->setDirty(path);
},
Qt::QueuedConnection);
}
}
}
void KDirWatchPrivate::slotRemoveDelayed()
{
delayRemove = false;
while (!removeList.isEmpty()) {
Entry *entry = *removeList.begin();
removeEntry(nullptr, entry, nullptr);
}
}
* when polling. inotify uses a single-shot timer to call this slot delayed.
*/
void KDirWatchPrivate::slotRescan()
{
if (s_verboseDebug) {
qCDebug(KDIRWATCH);
}
EntryMap::Iterator it;
bool timerRunning = m_statRescanTimer.isActive();
if (timerRunning) {
m_statRescanTimer.stop();
}
delayRemove = true;
if (rescan_all) {
it = m_mapEntries.begin();
for (; it != m_mapEntries.end(); ++it) {
(*it).dirty = true;
}
rescan_all = false;
} else {
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<Entry *> cList;
#endif
it = m_mapEntries.begin();
for (; it != m_mapEntries.end(); ++it) {
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 QFSWatchMode:
if (ev == Created) {
addWatch(entry);
}
break;
default:
break;
}
#if HAVE_SYS_INOTIFY_H
if (entry->isDir) {
QStringList pendingFileChanges = entry->m_pendingFileChanges;
pendingFileChanges.removeDuplicates();
for (const QString &changedFilename : std::as_const(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) {
m_statRescanTimer.start(freq);
}
#if HAVE_SYS_INOTIFY_H
for (Entry *e : std::as_const(cList)) {
removeEntry(nullptr, e->parentDirectory(), e);
}
#endif
QTimer::singleShot(0, this, &KDirWatchPrivate::slotRemoveDelayed);
}
bool KDirWatchPrivate::isNoisyFile(const char *filename)
{
if (*filename == '.') {
if (strncmp(filename, ".X.err", 6) == 0) {
return true;
}
if (strncmp(filename, ".xsession-errors", 16) == 0) {
return true;
}
if (strncmp(filename, ".fonts.cache", 12) == 0) {
return true;
}
}
return false;
}
void KDirWatchPrivate::ref(KDirWatch *watch)
{
m_referencesObjects.push_back(watch);
}
void KDirWatchPrivate::unref(KDirWatch *watch)
{
m_referencesObjects.removeOne(watch);
if (m_referencesObjects.isEmpty()) {
destroyPrivate();
}
}
#if HAVE_SYS_INOTIFY_H
QString KDirWatchPrivate::inotifyEventName(const inotify_event *event) const
{
if (event->mask & IN_OPEN)
return QStringLiteral("OPEN");
else if (event->mask & IN_CLOSE_NOWRITE)
return QStringLiteral("CLOSE_NOWRITE");
else if (event->mask & IN_CLOSE_WRITE)
return QStringLiteral("CLOSE_WRITE");
else if (event->mask & IN_MOVED_TO)
return QStringLiteral("MOVED_TO");
else if (event->mask & IN_MOVED_FROM)
return QStringLiteral("MOVED_FROM");
else if (event->mask & IN_MOVE)
return QStringLiteral("MOVE");
else if (event->mask & IN_CREATE)
return QStringLiteral("CREATE");
else if (event->mask & IN_DELETE)
return QStringLiteral("DELETE");
else if (event->mask & IN_DELETE_SELF)
return QStringLiteral("DELETE_SELF");
else if (event->mask & IN_MOVE_SELF)
return QStringLiteral("MOVE_SELF");
else if (event->mask & IN_ATTRIB)
return QStringLiteral("ATTRIB");
else if (event->mask & IN_MODIFY)
return QStringLiteral("MODIFY");
if (event->mask & IN_ACCESS)
return QStringLiteral("ACCESS");
if (event->mask & IN_IGNORED)
return QStringLiteral("IGNORED");
if (event->mask & IN_UNMOUNT)
return QStringLiteral("IN_UNMOUNT");
else
return QStringLiteral("UNKWOWN");
}
#endif
#if HAVE_QFILESYSTEMWATCHER
void KDirWatchPrivate::fswEventReceived(const QString &path)
{
if (s_verboseDebug) {
qCDebug(KDIRWATCH) << path;
}
auto it = m_mapEntries.find(path);
if (it != m_mapEntries.end()) {
Entry *entry = &it.value();
entry->dirty = true;
const int ev = scanEntry(entry);
if (s_verboseDebug) {
qCDebug(KDIRWATCH) << "scanEntry for" << entry->path << "says" << ev;
}
if (ev != NoChange) {
emitEvent(entry, ev);
}
if (ev == Deleted) {
if (entry->isDir) {
addEntry(nullptr, entry->parentDirectory(), entry, true);
} else {
addEntry(nullptr, QFileInfo(entry->path).absolutePath(), entry, true);
}
} else if (ev == Created) {
addWatch(entry);
} else if (entry->isDir) {
for (Entry *sub_entry : std::as_const(entry->m_entries)) {
fswEventReceived(sub_entry->path);
}
} else {
* 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(entry->path);
}
}
}
#else
void KDirWatchPrivate::fswEventReceived(const QString &path)
{
Q_UNUSED(path);
qCWarning(KCOREADDONS_DEBUG) << "QFileSystemWatcher event received but QFileSystemWatcher is not supported";
}
#endif
Q_GLOBAL_STATIC(KDirWatch, s_pKDirWatchSelf)
KDirWatch *KDirWatch::self()
{
return s_pKDirWatchSelf();
}
bool KDirWatch::exists()
{
return s_pKDirWatchSelf.exists() && dwp_self.hasLocalData();
}
KDirWatch::KDirWatch(QObject *parent)
: QObject(parent)
, d(createPrivate())
{
d->ref(this);
static QBasicAtomicInt nameCounter = Q_BASIC_ATOMIC_INITIALIZER(1);
const int counter = nameCounter.fetchAndAddRelaxed(1);
setObjectName(QStringLiteral("KDirWatch-%1").arg(counter));
}
KDirWatch::~KDirWatch()
{
if (d) {
d->removeEntries(this);
d->unref(this);
}
}
void KDirWatch::addDir(const QString &_path, WatchModes watchModes)
{
if (KNetworkMounts::self()->isOptionEnabledForPath(_path, KNetworkMounts::KDirWatchDontAddWatches)) {
return;
}
if (d) {
d->addEntry(this, _path, nullptr, true, watchModes);
}
}
void KDirWatch::addFile(const QString &_path)
{
if (KNetworkMounts::self()->isOptionEnabledForPath(_path, KNetworkMounts::KDirWatchDontAddWatches)) {
return;
}
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)
{
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::setCreated(const QString &_file)
{
qCDebug(KDIRWATCH) << objectName() << "emitting created" << _file;
Q_EMIT created(_file);
}
void KDirWatch::setDirty(const QString &_file)
{
Q_EMIT dirty(_file);
}
void KDirWatch::setDeleted(const QString &_file)
{
qCDebug(KDIRWATCH) << objectName() << "emitting deleted" << _file;
Q_EMIT deleted(_file);
}
KDirWatch::Method KDirWatch::internalMethod() const
{
switch (d->m_preferredMethod) {
case KDirWatch::INotify:
#if HAVE_SYS_INOTIFY_H
if (d->supports_inotify) {
return KDirWatch::INotify;
}
#endif
break;
case KDirWatch::QFSWatch:
#if HAVE_QFILESYSTEMWATCHER
return KDirWatch::QFSWatch;
#else
break;
#endif
case KDirWatch::Stat:
return KDirWatch::Stat;
}
#if HAVE_SYS_INOTIFY_H
if (d->supports_inotify) {
return KDirWatch::INotify;
}
#endif
#if HAVE_QFILESYSTEMWATCHER
return KDirWatch::QFSWatch;
#else
return KDirWatch::Stat;
#endif
}
bool KDirWatch::event(QEvent *event)
{
if (Q_LIKELY(event->type() != QEvent::ThreadChange)) {
return QObject::event(event);
}
qCCritical(KDIRWATCH) << "KDirwatch is moving its thread. This is not supported at this time; your watch will not watch anything anymore!"
<< "Create and use watches on the correct thread"
<< "Watch:" << this;
Q_ASSERT(thread() == d->thread());
d->removeEntries(this);
d->unref(this);
d = nullptr;
QMetaObject::invokeMethod(
this,
[this] {
d = createPrivate();
},
Qt::QueuedConnection);
return QObject::event(event);
}
#include "moc_kdirwatch.cpp"
#include "moc_kdirwatch_p.cpp"