1/* This file is part of the KDE libraries
2 SPDX-FileCopyrightText: 1998 Sven Radej <sven@lisa.exp.univie.ac.at>
3 SPDX-FileCopyrightText: 2006 Dirk Mueller <mueller@kde.org>
4 SPDX-FileCopyrightText: 2007 Flavio Castelli <flavio.castelli@gmail.com>
5 SPDX-FileCopyrightText: 2008 Rafal Rzepecki <divided.mind@gmail.com>
6 SPDX-FileCopyrightText: 2010 David Faure <faure@kde.org>
7 SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
8
9 SPDX-License-Identifier: LGPL-2.0-only
10*/
11
12// CHANGES:
13// Jul 30, 2008 - Don't follow symlinks when recursing to avoid loops (Rafal)
14// Aug 6, 2007 - KDirWatch::WatchModes support complete, flags work fine also
15// when using FAMD (Flavio Castelli)
16// Aug 3, 2007 - Handled KDirWatch::WatchModes flags when using inotify, now
17// recursive and file monitoring modes are implemented (Flavio Castelli)
18// Jul 30, 2007 - Substituted addEntry boolean params with KDirWatch::WatchModes
19// flag (Flavio Castelli)
20// Oct 4, 2005 - Inotify support (Dirk Mueller)
21// February 2002 - Add file watching and remote mount check for STAT
22// Mar 30, 2001 - Native support for Linux dir change notification.
23// Jan 28, 2000 - Usage of FAM service on IRIX (Josef.Weidendorfer@in.tum.de)
24// May 24. 1998 - List of times introduced, and some bugs are fixed. (sven)
25// May 23. 1998 - Removed static pointer - you can have more instances.
26// It was Needed for KRegistry. KDirWatch now emits signals and doesn't
27// call (or need) KFM. No more URL's - just plain paths. (sven)
28// Mar 29. 1998 - added docs, stop/restart for particular Dirs and
29// deep copies for list of dirs. (sven)
30// Mar 28. 1998 - Created. (sven)
31
32#include "kdirwatch.h"
33#include "kcoreaddons_debug.h"
34#include "kdirwatch_p.h"
35#include "kfilesystemtype.h"
36#include "knetworkmounts.h"
37
38#include <io/config-kdirwatch.h>
39
40#include <QCoreApplication>
41#include <QDir>
42#include <QFile>
43#include <QLoggingCategory>
44#include <QSocketNotifier>
45#include <QThread>
46#include <QThreadStorage>
47#include <QTimer>
48#include <assert.h>
49#include <cerrno>
50#include <sys/stat.h>
51
52#include <qplatformdefs.h> // QT_LSTAT, QT_STAT, QT_STATBUF
53
54#include <stdlib.h>
55#include <string.h>
56
57#if HAVE_SYS_INOTIFY_H
58#include <fcntl.h>
59#include <sys/inotify.h>
60#include <unistd.h>
61
62#ifndef IN_DONT_FOLLOW
63#define IN_DONT_FOLLOW 0x02000000
64#endif
65
66#ifndef IN_ONLYDIR
67#define IN_ONLYDIR 0x01000000
68#endif
69
70// debug
71#include <sys/ioctl.h>
72
73#include <sys/utsname.h>
74
75#endif // HAVE_SYS_INOTIFY_H
76
77Q_DECLARE_LOGGING_CATEGORY(KDIRWATCH)
78// logging category for this framework, default: log stuff >= warning
79Q_LOGGING_CATEGORY(KDIRWATCH, "kf.coreaddons.kdirwatch", QtWarningMsg)
80
81// set this to true for much more verbose debug output
82static bool s_verboseDebug = false;
83
84static QThreadStorage<KDirWatchPrivate *> dwp_self;
85static KDirWatchPrivate *createPrivate()
86{
87 if (!dwp_self.hasLocalData()) {
88 dwp_self.setLocalData(new KDirWatchPrivate);
89 }
90 return dwp_self.localData();
91}
92static void destroyPrivate()
93{
94 dwp_self.localData()->deleteLater();
95 dwp_self.setLocalData(nullptr);
96}
97
98// Convert a string into a watch Method
99static KDirWatch::Method methodFromString(const QByteArray &method)
100{
101 if (method == "Stat") {
102 return KDirWatch::Stat;
103 } else if (method == "QFSWatch") {
104 return KDirWatch::QFSWatch;
105 } else {
106#if HAVE_SYS_INOTIFY_H
107 // inotify supports delete+recreate+modify, which QFSWatch doesn't support
108 return KDirWatch::INotify;
109#else
110 return KDirWatch::QFSWatch;
111#endif
112 }
113}
114
115static const char *methodToString(KDirWatch::Method method)
116{
117 switch (method) {
118 case KDirWatch::INotify:
119 return "INotify";
120 case KDirWatch::Stat:
121 return "Stat";
122 case KDirWatch::QFSWatch:
123 return "QFSWatch";
124 }
125 // not reached
126 return nullptr;
127}
128
129static const char s_envNfsPoll[] = "KDIRWATCH_NFSPOLLINTERVAL";
130static const char s_envPoll[] = "KDIRWATCH_POLLINTERVAL";
131static const char s_envMethod[] = "KDIRWATCH_METHOD";
132static const char s_envNfsMethod[] = "KDIRWATCH_NFSMETHOD";
133
134//
135// Class KDirWatchPrivate (singleton)
136//
137
138/* All entries (files/directories) to be watched in the
139 * application (coming from multiple KDirWatch instances)
140 * are registered in a single KDirWatchPrivate instance.
141 *
142 * At the moment, the following methods for file watching
143 * are supported:
144 * - Polling: All files to be watched are polled regularly
145 * using stat (more precise: QFileInfo.lastModified()).
146 * The polling frequency is determined from global kconfig
147 * settings, defaulting to 500 ms for local directories
148 * and 5000 ms for remote mounts
149 * - FAM (File Alternation Monitor): first used on IRIX, SGI
150 * has ported this method to LINUX. It uses a kernel part
151 * (IMON, sending change events to /dev/imon) and a user
152 * level daemon (fam), to which applications connect for
153 * notification of file changes. For NFS, the fam daemon
154 * on the NFS server machine is used; if IMON is not built
155 * into the kernel, fam uses polling for local files.
156 * - INOTIFY: In LINUX 2.6.13, inode change notification was
157 * introduced. You're now able to watch arbitrary inode's
158 * for changes, and even get notification when they're
159 * unmounted.
160 */
161
162KDirWatchPrivate::KDirWatchPrivate()
163 : m_statRescanTimer()
164 , freq(3600000)
165 , // 1 hour as upper bound
166 statEntries(0)
167 , delayRemove(false)
168 , rescan_all(false)
169 , rescan_timer()
170 ,
171#if HAVE_SYS_INOTIFY_H
172 mSn(nullptr)
173 ,
174#endif
175 _isStopped(false)
176{
177 // Debug unittest on CI
178 if (qAppName() == QLatin1String("kservicetest") || qAppName() == QLatin1String("filetypestest")) {
179 s_verboseDebug = true;
180 }
181 m_statRescanTimer.setObjectName(QStringLiteral("KDirWatchPrivate::timer"));
182 connect(sender: &m_statRescanTimer, signal: &QTimer::timeout, context: this, slot: &KDirWatchPrivate::slotRescan);
183
184 m_nfsPollInterval = qEnvironmentVariableIsSet(varName: s_envNfsPoll) ? qEnvironmentVariableIntValue(varName: s_envNfsPoll) : 5000;
185 m_PollInterval = qEnvironmentVariableIsSet(varName: s_envPoll) ? qEnvironmentVariableIntValue(varName: s_envPoll) : 500;
186
187 m_preferredMethod = methodFromString(method: qEnvironmentVariableIsSet(varName: s_envMethod) ? qgetenv(varName: s_envMethod) : "inotify");
188 // The nfs method defaults to the normal (local) method
189 m_nfsPreferredMethod = methodFromString(method: qEnvironmentVariableIsSet(varName: s_envNfsMethod) ? qgetenv(varName: s_envNfsMethod) : "Stat");
190
191 QList<QByteArray> availableMethods;
192
193 availableMethods << "Stat";
194
195 // used for inotify
196 rescan_timer.setObjectName(QStringLiteral("KDirWatchPrivate::rescan_timer"));
197 rescan_timer.setSingleShot(true);
198 connect(sender: &rescan_timer, signal: &QTimer::timeout, context: this, slot: &KDirWatchPrivate::slotRescan);
199
200#if HAVE_SYS_INOTIFY_H
201#if HAVE_INOTIFY_DIRECT_READV
202 m_inotify_fd = inotify_init1(IN_DIRECT);
203#else
204 m_inotify_fd = inotify_init();
205#endif
206 supports_inotify = m_inotify_fd > 0;
207
208 if (!supports_inotify) {
209 qCDebug(KDIRWATCH) << "Can't use Inotify, kernel doesn't support it:" << strerror(errno);
210 } else {
211 availableMethods << "INotify";
212 (void)fcntl(fd: m_inotify_fd, F_SETFD, FD_CLOEXEC);
213
214 mSn = new QSocketNotifier(m_inotify_fd, QSocketNotifier::Read, this);
215 connect(sender: mSn, signal: &QSocketNotifier::activated, context: this, slot: &KDirWatchPrivate::inotifyEventReceived);
216 }
217#endif
218#if HAVE_QFILESYSTEMWATCHER
219 availableMethods << "QFileSystemWatcher";
220 fsWatcher = nullptr;
221#endif
222
223 qCDebug(KDIRWATCH) << "Available methods: " << availableMethods << "preferred=" << methodToString(method: m_preferredMethod);
224}
225
226// This is called on app exit (deleted by QThreadStorage)
227KDirWatchPrivate::~KDirWatchPrivate()
228{
229 m_statRescanTimer.stop();
230
231 // Unset us as d pointer. This indicates to the KDirWatch that the private has already been destroyed and therefore
232 // needs no additional cleanup from its destructor.
233 for (auto it = m_mapEntries.begin(); it != m_mapEntries.end(); it++) {
234 auto &entry = it.value();
235 for (auto &client : entry.m_clients) {
236 client.instance->d = nullptr;
237 }
238 }
239 // Also cover all instance that hold a reference to us (even when they don't have an entry)
240 for (auto &referenceObject : m_referencesObjects) {
241 referenceObject->d = nullptr;
242 }
243
244#if HAVE_SYS_INOTIFY_H
245 if (supports_inotify) {
246#if HAVE_INOTIFY_DIRECT_READV
247 // We need to call a special closing function instead of the usual close(),
248 // so reimplement QT_CLOSE here
249 int ret;
250 do {
251 ret = libinotify_direct_close(m_inotify_fd);
252 } while (ret == -1 && errno == EINTR);
253#else
254 QT_CLOSE(fd: m_inotify_fd);
255#endif
256 }
257#endif
258#if HAVE_QFILESYSTEMWATCHER
259 delete fsWatcher;
260#endif
261}
262
263void KDirWatchPrivate::inotifyEventReceived()
264{
265#if HAVE_SYS_INOTIFY_H
266 if (!supports_inotify) {
267 return;
268 }
269
270 assert(m_inotify_fd > -1);
271
272 auto processEvent = [this](const struct inotify_event *const event)
273 {
274 // strip trailing null chars, see inotify_event documentation
275 // these must not end up in the final QString version of path
276 int len = event->len;
277 while (len > 1 && !event->name[len - 1]) {
278 --len;
279 }
280 QByteArray cpath(event->name, len);
281 const QString path = len ? QFile::decodeName(localFileName: cpath) : QString();
282
283 if (!path.isEmpty() && isNoisyFile(filename: cpath.data())) {
284 return;
285 }
286
287 // Is set to true if the new event is a directory, false otherwise. This prevents a stat call in clientsForFileOrDir
288 const bool isDir = (event->mask & (IN_ISDIR));
289
290 Entry *e = m_inotify_wd_to_entry.value(key: event->wd);
291 if (!e) {
292 return;
293 }
294 const bool wasDirty = e->dirty;
295 e->dirty = true;
296
297 const QString tpath = e->path + QLatin1Char('/') + path;
298
299 qCDebug(KDIRWATCH).nospace() << "got event " << inotifyEventName(event) << " for entry " << e->path
300 << (event->mask & IN_ISDIR ? " [directory] " : " [file] ") << path;
301
302 if (event->mask & IN_DELETE_SELF) {
303 e->m_status = NonExistent;
304 m_inotify_wd_to_entry.remove(key: e->wd);
305 e->wd = -1;
306 e->m_ctime = invalid_ctime;
307 emitEvent(e, event: Deleted);
308
309 // don't walk up over / or drives
310 if (!e->isRoot()) {
311 // If the parent dir was already watched, tell it something changed
312 Entry *parentEntry = entry(path: e->parentDirectory());
313 if (parentEntry) {
314 parentEntry->dirty = true;
315 }
316 // Add entry to parent dir to notice if the entry gets recreated
317 addEntry(instance: nullptr, path: e->parentDirectory(), sub_entry: e, isDir: true /*isDir*/);
318 }
319 }
320 if (event->mask & IN_IGNORED) {
321 // Causes bug #207361 with kernels 2.6.31 and 2.6.32!
322 // e->wd = -1;
323 }
324 if (event->mask & (IN_CREATE | IN_MOVED_TO)) {
325 Entry *sub_entry = e->findSubEntry(path: tpath);
326
327 qCDebug(KDIRWATCH) << "-->got CREATE signal for" << (tpath) << "sub_entry=" << sub_entry;
328
329 if (sub_entry) {
330 // We were waiting for this new file/dir to be created
331 sub_entry->dirty = true;
332 rescan_timer.start(msec: 0); // process this asap, to start watching that dir
333 } else if (e->isDir && !e->m_clients.empty()) {
334 const QList<const Client *> clients = e->inotifyClientsForFileOrDir(isDir);
335 // See discussion in addEntry for why we don't addEntry for individual
336 // files in WatchFiles mode with inotify.
337 if (isDir) {
338 for (const Client *client : clients) {
339 addEntry(instance: client->instance, path: tpath, sub_entry: nullptr, isDir, watchModes: isDir ? client->m_watchModes : KDirWatch::WatchDirOnly);
340 }
341 }
342 if (!clients.isEmpty()) {
343 emitEvent(e, event: Created, fileName: tpath);
344 qCDebug(KDIRWATCH).nospace() << clients.count() << " instance(s) monitoring the new " << (isDir ? "dir " : "file ") << tpath;
345 }
346 e->m_pendingFileChanges.append(t: e->path);
347 if (!rescan_timer.isActive()) {
348 rescan_timer.start(msec: m_PollInterval); // singleshot
349 }
350 }
351 }
352 if (event->mask & (IN_DELETE | IN_MOVED_FROM)) {
353 if ((e->isDir) && (!e->m_clients.empty())) {
354 // A file in this directory has been removed. It wasn't an explicitly
355 // watched file as it would have its own watch descriptor, so
356 // no addEntry/ removeEntry bookkeeping should be required. Emit
357 // the event immediately if any clients are interested.
358 const KDirWatch::WatchModes flag = isDir ? KDirWatch::WatchSubDirs : KDirWatch::WatchFiles;
359 int counter = std::count_if(first: e->m_clients.cbegin(), last: e->m_clients.cend(), pred: [flag](const Client &client) {
360 return client.m_watchModes & flag;
361 });
362
363 if (counter != 0) {
364 emitEvent(e, event: Deleted, fileName: tpath);
365 }
366 }
367 }
368 if (event->mask & (IN_MODIFY | IN_ATTRIB)) {
369 if ((e->isDir) && (!e->m_clients.empty())) {
370 // A file in this directory has been changed. No
371 // addEntry/ removeEntry bookkeeping should be required.
372 // Add the path to the list of pending file changes if
373 // there are any interested clients.
374 // QT_STATBUF stat_buf;
375 // QByteArray tpath = QFile::encodeName(e->path+'/'+path);
376 // QT_STAT(tpath, &stat_buf);
377 // bool isDir = S_ISDIR(stat_buf.st_mode);
378
379 // The API doc is somewhat vague as to whether we should emit
380 // dirty() for implicitly watched files when WatchFiles has
381 // not been specified - we'll assume they are always interested,
382 // regardless.
383 // Don't worry about duplicates for the time
384 // being; this is handled in slotRescan.
385 e->m_pendingFileChanges.append(t: tpath);
386 // Avoid stat'ing the directory if only an entry inside it changed.
387 e->dirty = (wasDirty || (path.isEmpty() && (event->mask & IN_ATTRIB)));
388 }
389 }
390
391 if (!rescan_timer.isActive()) {
392 rescan_timer.start(msec: m_PollInterval); // singleshot
393 }
394 };
395
396#if HAVE_INOTIFY_DIRECT_READV
397 struct iovec *received[10];
398 int num_events = libinotify_direct_readv(m_inotify_fd, received, (sizeof (received) / sizeof ((received)[0])), /* no_block=*/ 0);
399 for (int i = 0; i < num_events; i++) {
400 struct iovec *cur_event = received[i];
401 while (cur_event->iov_base) {
402 const struct inotify_event *const event = (struct inotify_event *) cur_event->iov_base;
403
404 processEvent(event);
405
406 cur_event++;
407 }
408 libinotify_free_iovec(received[i]);
409 }
410#else
411 int pending = -1;
412 int offsetStartRead = 0; // where we read into buffer
413 char buf[8192];
414
415 ioctl(fd: m_inotify_fd, FIONREAD, &pending);
416
417 while (pending > 0) {
418 const int bytesToRead = qMin<int>(a: pending, b: sizeof(buf) - offsetStartRead);
419
420 int bytesAvailable = read(fd: m_inotify_fd, buf: &buf[offsetStartRead], nbytes: bytesToRead);
421 pending -= bytesAvailable;
422 bytesAvailable += offsetStartRead;
423 offsetStartRead = 0;
424
425 int offsetCurrent = 0;
426 while (bytesAvailable >= int(sizeof(struct inotify_event))) {
427 const struct inotify_event *const event = reinterpret_cast<inotify_event *>(&buf[offsetCurrent]);
428
429 if (event->mask & IN_Q_OVERFLOW) {
430 qCWarning(KDIRWATCH) << "Inotify Event queue overflowed, check max_queued_events value";
431 return;
432 }
433
434 const int eventSize = sizeof(struct inotify_event) + event->len;
435 if (bytesAvailable < eventSize) {
436 break;
437 }
438
439 bytesAvailable -= eventSize;
440 offsetCurrent += eventSize;
441
442 processEvent(event);
443 }
444 if (bytesAvailable > 0) {
445 // copy partial event to beginning of buffer
446 memmove(dest: buf, src: &buf[offsetCurrent], n: bytesAvailable);
447 offsetStartRead = bytesAvailable;
448 }
449 }
450#endif
451#endif
452}
453
454KDirWatchPrivate::Entry::~Entry()
455{
456}
457
458/* In inotify mode, only entries which are marked dirty are scanned.
459 * We first need to mark all yet nonexistent, but possible created
460 * entries as dirty...
461 */
462void KDirWatchPrivate::Entry::propagate_dirty()
463{
464 for (Entry *sub_entry : std::as_const(t&: m_entries)) {
465 if (!sub_entry->dirty) {
466 sub_entry->dirty = true;
467 sub_entry->propagate_dirty();
468 }
469 }
470}
471
472/* A KDirWatch instance is interested in getting events for
473 * this file/Dir entry.
474 */
475void KDirWatchPrivate::Entry::addClient(KDirWatch *instance, KDirWatch::WatchModes watchModes)
476{
477 if (instance == nullptr) {
478 return;
479 }
480
481 auto it = findInstance(other: instance);
482 if (it != m_clients.end()) {
483 Client &client = *it;
484 ++client.count;
485 client.m_watchModes = watchModes;
486 return;
487 }
488
489 m_clients.emplace_back(args&: instance, args&: watchModes);
490}
491
492void KDirWatchPrivate::Entry::removeClient(KDirWatch *instance)
493{
494 auto it = findInstance(other: instance);
495 if (it != m_clients.end()) {
496 Client &client = *it;
497 --client.count;
498 if (client.count == 0) {
499 m_clients.erase(position: it);
500 }
501 }
502}
503
504/* get number of clients */
505int KDirWatchPrivate::Entry::clientCount() const
506{
507 int clients = 0;
508 for (const Client &client : m_clients) {
509 clients += client.count;
510 }
511
512 return clients;
513}
514
515bool KDirWatchPrivate::Entry::isRoot() const
516{
517 return QDir(path).isRoot();
518}
519
520QString KDirWatchPrivate::Entry::parentDirectory() const
521{
522 return QFileInfo(path).absolutePath();
523}
524
525QList<const KDirWatchPrivate::Client *> KDirWatchPrivate::Entry::clientsForFileOrDir(const QString &tpath, bool *isDir) const
526{
527 QList<const Client *> ret;
528 QFileInfo fi(tpath);
529 if (fi.exists()) {
530 *isDir = fi.isDir();
531 const KDirWatch::WatchModes flag = *isDir ? KDirWatch::WatchSubDirs : KDirWatch::WatchFiles;
532 for (const Client &client : m_clients) {
533 if (client.m_watchModes & flag) {
534 ret.append(t: &client);
535 }
536 }
537 } else {
538 // Happens frequently, e.g. ERROR: couldn't stat "/home/dfaure/.viminfo.tmp"
539 // qCDebug(KDIRWATCH) << "ERROR: couldn't stat" << tpath;
540 // In this case isDir is not set, but ret is empty anyway
541 // so isDir won't be used.
542 }
543 return ret;
544}
545
546// inotify specific function that doesn't call KDE::stat to figure out if we have a file or folder.
547// isDir is determined through inotify's "IN_ISDIR" flag in KDirWatchPrivate::inotifyEventReceived
548QList<const KDirWatchPrivate::Client *> KDirWatchPrivate::Entry::inotifyClientsForFileOrDir(bool isDir) const
549{
550 QList<const Client *> ret;
551 const KDirWatch::WatchModes flag = isDir ? KDirWatch::WatchSubDirs : KDirWatch::WatchFiles;
552 for (const Client &client : m_clients) {
553 if (client.m_watchModes & flag) {
554 ret.append(t: &client);
555 }
556 }
557 return ret;
558}
559
560QDebug operator<<(QDebug debug, const KDirWatch & /* watch */)
561{
562 if (!dwp_self.hasLocalData()) {
563 debug << "KDirWatch not used";
564 return debug;
565 }
566 debug << dwp_self.localData();
567 return debug;
568}
569
570QDebug operator<<(QDebug debug, const KDirWatchPrivate &dwp)
571{
572 debug << "Entries watched:";
573 if (dwp.m_mapEntries.count() == 0) {
574 debug << " None.";
575 } else {
576 auto it = dwp.m_mapEntries.cbegin();
577 for (; it != dwp.m_mapEntries.cend(); ++it) {
578 const KDirWatchPrivate::Entry &e = it.value();
579 debug << " " << e;
580
581 for (const KDirWatchPrivate::Client &c : e.m_clients) {
582 QByteArray pending;
583 if (c.watchingStopped) {
584 if (c.pending & KDirWatchPrivate::Deleted) {
585 pending += "deleted ";
586 }
587 if (c.pending & KDirWatchPrivate::Created) {
588 pending += "created ";
589 }
590 if (c.pending & KDirWatchPrivate::Changed) {
591 pending += "changed ";
592 }
593 if (!pending.isEmpty()) {
594 pending = " (pending: " + pending + ')';
595 }
596 pending = ", stopped" + pending;
597 }
598 debug << " by " << c.instance->objectName() << " (" << c.count << " times)" << pending;
599 }
600 if (!e.m_entries.isEmpty()) {
601 debug << " dependent entries:";
602 for (KDirWatchPrivate::Entry *d : e.m_entries) {
603 debug << " " << d << d->path << (d->m_status == KDirWatchPrivate::NonExistent ? "NonExistent" : "EXISTS this is an ERROR!");
604 if (s_verboseDebug) {
605 Q_ASSERT(d->m_status == KDirWatchPrivate::NonExistent); // it doesn't belong here otherwise
606 }
607 }
608 }
609 }
610 }
611 return debug;
612}
613
614QDebug operator<<(QDebug debug, const KDirWatchPrivate::Entry &entry)
615{
616 debug.nospace() << "[ Entry for " << entry.path << ", " << (entry.isDir ? "dir" : "file");
617 if (entry.m_status == KDirWatchPrivate::NonExistent) {
618 debug << ", non-existent";
619 }
620 debug << ", using "
621 << ((entry.m_mode == KDirWatchPrivate::INotifyMode) ? "INotify"
622 : (entry.m_mode == KDirWatchPrivate::QFSWatchMode) ? "QFSWatch"
623 : (entry.m_mode == KDirWatchPrivate::StatMode) ? "Stat"
624 : "Unknown Method");
625#if HAVE_SYS_INOTIFY_H
626 if (entry.m_mode == KDirWatchPrivate::INotifyMode) {
627 debug << " inotify_wd=" << entry.wd;
628 }
629#endif
630 debug << ", has " << entry.m_clients.size() << " clients";
631 debug.space();
632 if (!entry.m_entries.isEmpty()) {
633 debug << ", nonexistent subentries:";
634 for (KDirWatchPrivate::Entry *subEntry : std::as_const(t: entry.m_entries)) {
635 debug << subEntry << subEntry->path;
636 }
637 }
638 debug << ']';
639 return debug;
640}
641
642KDirWatchPrivate::Entry *KDirWatchPrivate::entry(const QString &_path)
643{
644 if (_path.isEmpty()) {
645 return nullptr;
646 }
647
648 QString path(_path);
649
650 if (path.length() > 1 && path.endsWith(c: QLatin1Char('/'))) {
651 path.chop(n: 1);
652 }
653
654 auto it = m_mapEntries.find(key: path);
655 return it != m_mapEntries.end() ? &it.value() : nullptr;
656}
657
658// set polling frequency for a entry and adjust global freq if needed
659void KDirWatchPrivate::useFreq(Entry *e, int newFreq)
660{
661 e->freq = newFreq;
662
663 // a reasonable frequency for the global polling timer
664 if (e->freq < freq) {
665 freq = e->freq;
666 if (m_statRescanTimer.isActive()) {
667 m_statRescanTimer.start(msec: freq);
668 }
669 qCDebug(KDIRWATCH) << "Global Poll Freq is now" << freq << "msec";
670 }
671}
672
673#if HAVE_SYS_INOTIFY_H
674// setup INotify notification, returns false if not possible
675bool KDirWatchPrivate::useINotify(Entry *e)
676{
677 e->wd = -1;
678 e->dirty = false;
679
680 if (!supports_inotify) {
681 return false;
682 }
683
684 e->m_mode = INotifyMode;
685
686 if (e->m_status == NonExistent) {
687 // be safe, don't walk upwards on /
688 if (e->isRoot()) {
689 return false;
690 }
691
692 addEntry(instance: nullptr, path: e->parentDirectory(), sub_entry: e, isDir: true);
693 return true;
694 }
695
696 // May as well register for almost everything - it's free!
697 int mask = IN_DELETE | IN_DELETE_SELF | IN_CREATE | IN_MOVE | IN_MOVE_SELF | IN_DONT_FOLLOW | IN_MOVED_FROM | IN_MODIFY | IN_ATTRIB;
698
699 if ((e->wd = inotify_add_watch(fd: m_inotify_fd, name: QFile::encodeName(fileName: e->path).data(), mask: mask)) != -1) {
700 m_inotify_wd_to_entry.insert(key: e->wd, value: e);
701 if (s_verboseDebug) {
702 qCDebug(KDIRWATCH) << "inotify successfully used for monitoring" << e->path << "wd=" << e->wd;
703 }
704 return true;
705 }
706
707 if (errno == ENOSPC) {
708 // Inotify max_user_watches was reached (/proc/sys/fs/inotify/max_user_watches)
709 // See man inotify_add_watch, https://github.com/guard/listen/wiki/Increasing-the-amount-of-inotify-watchers
710 qCWarning(KDIRWATCH) << "inotify failed for monitoring" << e->path << "\n"
711 << "Because it reached its max_user_watches,\n"
712 << "you can increase the maximum number of file watches per user,\n"
713 << "by setting an appropriate fs.inotify.max_user_watches parameter in your /etc/sysctl.conf";
714 } else {
715 qCDebug(KDIRWATCH) << "inotify failed for monitoring" << e->path << ":" << strerror(errno) << " (errno:" << errno << ")";
716 }
717 return false;
718}
719#endif
720#if HAVE_QFILESYSTEMWATCHER
721bool KDirWatchPrivate::useQFSWatch(Entry *e)
722{
723 e->m_mode = QFSWatchMode;
724 e->dirty = false;
725
726 if (e->m_status == NonExistent) {
727 // be safe, don't walk upwards on drive level or /
728 // on e.g. Windows we can end up with a removed drive Y:
729 // parentDirectory() will then just keep looping with that, see bug 499865
730 // just abort if we loop, is correct for all platforms
731 if (e->isRoot()) {
732 return false;
733 }
734
735 addEntry(instance: nullptr, path: e->parentDirectory(), sub_entry: e, isDir: true /*isDir*/);
736 return true;
737 }
738
739 // qCDebug(KDIRWATCH) << "fsWatcher->addPath" << e->path;
740 if (!fsWatcher) {
741 fsWatcher = new QFileSystemWatcher();
742 connect(sender: fsWatcher, signal: &QFileSystemWatcher::directoryChanged, context: this, slot: &KDirWatchPrivate::fswEventReceived);
743 connect(sender: fsWatcher, signal: &QFileSystemWatcher::fileChanged, context: this, slot: &KDirWatchPrivate::fswEventReceived);
744 }
745 fsWatcher->addPath(file: e->path);
746 return true;
747}
748#endif
749
750bool KDirWatchPrivate::useStat(Entry *e)
751{
752 if (KFileSystemType::fileSystemType(path: e->path) == KFileSystemType::Nfs) { // TODO: or Smbfs?
753 useFreq(e, newFreq: m_nfsPollInterval);
754 } else {
755 useFreq(e, newFreq: m_PollInterval);
756 }
757
758 if (e->m_mode != StatMode) {
759 e->m_mode = StatMode;
760 statEntries++;
761
762 if (statEntries == 1) {
763 // if this was first STAT entry (=timer was stopped)
764 m_statRescanTimer.start(msec: freq); // then start the timer
765 qCDebug(KDIRWATCH) << " Started Polling Timer, freq " << freq;
766 }
767 }
768
769 qCDebug(KDIRWATCH) << " Setup Stat (freq " << e->freq << ") for " << e->path;
770
771 return true;
772}
773
774/* If <instance> !=0, this KDirWatch instance wants to watch at <_path>,
775 * providing in <isDir> the type of the entry to be watched.
776 * Sometimes, entries are dependent on each other: if <sub_entry> !=0,
777 * this entry needs another entry to watch itself (when notExistent).
778 */
779void KDirWatchPrivate::addEntry(KDirWatch *instance, const QString &_path, Entry *sub_entry, bool isDir, KDirWatch::WatchModes watchModes)
780{
781 QString path(_path);
782 if (path.startsWith(s: QLatin1String(":/"))) {
783 qCWarning(KDIRWATCH) << "Cannot watch QRC-like path" << path;
784 return;
785 }
786 if (path.isEmpty()
787#ifndef Q_OS_WIN
788 || path == QLatin1String("/dev")
789 || (path.startsWith(s: QLatin1String("/dev/")) && !path.startsWith(s: QLatin1String("/dev/.")) && !path.startsWith(s: QLatin1String("/dev/shm")))
790#endif
791 ) {
792 return; // Don't even go there.
793 }
794
795 if (path.length() > 1 && path.endsWith(c: QLatin1Char('/'))) {
796 path.chop(n: 1);
797 }
798
799 auto it = m_mapEntries.find(key: path);
800 if (it != m_mapEntries.end()) {
801 Entry &entry = it.value();
802 if (sub_entry) {
803 entry.m_entries.append(t: sub_entry);
804 if (s_verboseDebug) {
805 qCDebug(KDIRWATCH) << "Added already watched Entry" << path << "(for" << sub_entry->path << ")";
806 }
807 } else {
808 entry.addClient(instance, watchModes);
809 if (s_verboseDebug) {
810 qCDebug(KDIRWATCH) << "Added already watched Entry" << path << "(now" << entry.clientCount() << "clients)"
811 << QStringLiteral("[%1]").arg(a: instance->objectName());
812 }
813 }
814 return;
815 }
816
817 // we have a new path to watch
818
819 QT_STATBUF stat_buf;
820 bool exists = (QT_STAT(file: QFile::encodeName(fileName: path).constData(), buf: &stat_buf) == 0);
821
822 auto newIt = m_mapEntries.insert(key: path, value: Entry());
823 // the insert does a copy, so we have to use <e> now
824 Entry *e = &(*newIt);
825
826 if (exists) {
827 e->isDir = (stat_buf.st_mode & QT_STAT_MASK) == QT_STAT_DIR;
828
829#ifndef Q_OS_WIN
830 if (e->isDir && !isDir) {
831 if (QT_LSTAT(file: QFile::encodeName(fileName: path).constData(), buf: &stat_buf) == 0) {
832 if ((stat_buf.st_mode & QT_STAT_MASK) == QT_STAT_LNK) {
833 // if it's a symlink, don't follow it
834 e->isDir = false;
835 }
836 }
837 }
838#endif
839
840 if (e->isDir && !isDir) {
841 qCWarning(KCOREADDONS_DEBUG) << "KDirWatch:" << path << "is a directory. Use addDir!";
842 } else if (!e->isDir && isDir) {
843 qCWarning(KCOREADDONS_DEBUG) << "KDirWatch:" << path << "is a file. Use addFile!";
844 }
845
846 if (!e->isDir && (watchModes != KDirWatch::WatchDirOnly)) {
847 qCWarning(KCOREADDONS_DEBUG) << "KDirWatch:" << path
848 << "is a file. You can't use recursive or "
849 "watchFiles options";
850 watchModes = KDirWatch::WatchDirOnly;
851 }
852
853#ifdef Q_OS_WIN
854 // ctime is the 'creation time' on windows - use mtime instead
855 e->m_ctime = stat_buf.st_mtime;
856#else
857 e->m_ctime = stat_buf.st_ctime;
858#endif
859 e->m_status = Normal;
860 e->m_nlink = stat_buf.st_nlink;
861 e->m_ino = stat_buf.st_ino;
862 } else {
863 e->isDir = isDir;
864 e->m_ctime = invalid_ctime;
865 e->m_status = NonExistent;
866 e->m_nlink = 0;
867 e->m_ino = 0;
868 }
869
870 e->path = path;
871 if (sub_entry) {
872 e->m_entries.append(t: sub_entry);
873 } else {
874 e->addClient(instance, watchModes);
875 }
876
877 if (s_verboseDebug) {
878 qCDebug(KDIRWATCH).nospace() << "Added " << (e->isDir ? "Dir " : "File ") << path << (e->m_status == NonExistent ? " NotExisting" : "") << " for "
879 << (sub_entry ? sub_entry->path : QString()) << " [" << (instance ? instance->objectName() : QString()) << "]";
880 }
881
882 // now setup the notification method
883 e->m_mode = UnknownMode;
884 e->msecLeft = 0;
885
886 if (isNoisyFile(filename: QFile::encodeName(fileName: path).data())) {
887 return;
888 }
889
890 if (exists && e->isDir && (watchModes != KDirWatch::WatchDirOnly)) {
891 // recursive watch for folders
892 QFlags<QDir::Filter> filters = QDir::NoDotAndDotDot;
893
894 if ((watchModes & KDirWatch::WatchSubDirs) && (watchModes & KDirWatch::WatchFiles)) {
895 filters |= (QDir::Dirs | QDir::Files);
896 } else if (watchModes & KDirWatch::WatchSubDirs) {
897 filters |= QDir::Dirs;
898 } else if (watchModes & KDirWatch::WatchFiles) {
899 filters |= QDir::Files;
900 }
901
902#if HAVE_SYS_INOTIFY_H
903 if (m_preferredMethod == KDirWatch::INotify) {
904 // qCDebug(KDIRWATCH) << "Ignoring WatchFiles directive - this is implicit with inotify";
905 // Placing a watch on individual files is redundant with inotify
906 // (inotify gives us WatchFiles functionality "for free") and indeed
907 // actively harmful, so prevent it. WatchSubDirs is necessary, though.
908 filters &= ~QDir::Files;
909 }
910#endif
911
912 QDir basedir(e->path);
913 const QFileInfoList contents = basedir.entryInfoList(filters);
914 for (const QFileInfo &fileInfo : contents) {
915 // treat symlinks as files--don't follow them.
916 bool isDir = fileInfo.isDir() && !fileInfo.isSymLink();
917
918 addEntry(instance, path: fileInfo.absoluteFilePath(), sub_entry: nullptr, isDir, watchModes: isDir ? watchModes : KDirWatch::WatchDirOnly);
919 }
920 }
921
922 addWatch(entry: e);
923}
924
925void KDirWatchPrivate::addWatch(Entry *e)
926{
927 // If the watch is on a network filesystem use the nfsPreferredMethod as the
928 // default, otherwise use preferredMethod as the default, if the methods are
929 // the same we can skip the mountpoint check
930
931 // This allows to configure a different method for NFS mounts, since inotify
932 // cannot detect changes made by other machines. However as a default inotify
933 // is fine, since the most common case is a NFS-mounted home, where all changes
934 // are made locally. #177892.
935
936 KDirWatch::Method preferredMethod = m_preferredMethod;
937 if (m_nfsPreferredMethod != m_preferredMethod) {
938 if (KFileSystemType::fileSystemType(path: e->path) == KFileSystemType::Nfs) {
939 preferredMethod = m_nfsPreferredMethod;
940 }
941 }
942
943 // Try the appropriate preferred method from the config first
944 bool inotifyFailed = false;
945 bool entryAdded = false;
946 switch (preferredMethod) {
947#if HAVE_SYS_INOTIFY_H
948 case KDirWatch::INotify:
949 entryAdded = useINotify(e);
950 if (!entryAdded) {
951 inotifyFailed = true;
952 }
953 break;
954#else
955 case KDirWatch::INotify:
956 entryAdded = false;
957 break;
958#endif
959#if HAVE_QFILESYSTEMWATCHER
960 case KDirWatch::QFSWatch:
961 entryAdded = useQFSWatch(e);
962 break;
963#else
964 case KDirWatch::QFSWatch:
965 entryAdded = false;
966 break;
967#endif
968 case KDirWatch::Stat:
969 entryAdded = useStat(e);
970 break;
971 }
972
973 // Failing that try in order INotify, QFSWatch, Stat
974 if (!entryAdded) {
975#if HAVE_SYS_INOTIFY_H
976 if (preferredMethod != KDirWatch::INotify && useINotify(e)) {
977 return;
978 }
979#endif
980#if HAVE_QFILESYSTEMWATCHER
981 // QFileSystemWatcher uses inotify internally if it's supported by the platform, so
982 // if useInotify() already failed, don't try inotify again through useQFSWatch().
983 if (preferredMethod != KDirWatch::QFSWatch && !inotifyFailed && useQFSWatch(e)) {
984 return;
985 }
986#endif
987 if (preferredMethod != KDirWatch::Stat) {
988 useStat(e);
989 }
990 }
991}
992
993void KDirWatchPrivate::removeWatch(Entry *e)
994{
995#if HAVE_SYS_INOTIFY_H
996 if (e->m_mode == INotifyMode) {
997 m_inotify_wd_to_entry.remove(key: e->wd);
998 (void)inotify_rm_watch(fd: m_inotify_fd, wd: e->wd);
999 if (s_verboseDebug) {
1000 qCDebug(KDIRWATCH).nospace() << "Cancelled INotify (fd " << m_inotify_fd << ", " << e->wd << ") for " << e->path;
1001 }
1002 }
1003#endif
1004#if HAVE_QFILESYSTEMWATCHER
1005 if (e->m_mode == QFSWatchMode && fsWatcher) {
1006 if (s_verboseDebug) {
1007 qCDebug(KDIRWATCH) << "fsWatcher->removePath" << e->path;
1008 }
1009 fsWatcher->removePath(file: e->path);
1010 }
1011#endif
1012}
1013
1014void KDirWatchPrivate::removeEntry(KDirWatch *instance, const QString &_path, Entry *sub_entry)
1015{
1016 qCDebug(KDIRWATCH) << "path=" << _path << "sub_entry:" << sub_entry;
1017
1018 Entry *e = entry(_path);
1019 if (e) {
1020 removeEntry(instance, e, sub_entry);
1021 }
1022}
1023
1024void KDirWatchPrivate::removeEntry(KDirWatch *instance, Entry *e, Entry *sub_entry)
1025{
1026 removeList.remove(value: e);
1027
1028 if (sub_entry) {
1029 e->m_entries.removeAll(t: sub_entry);
1030 } else {
1031 e->removeClient(instance);
1032 }
1033
1034 if (!e->m_clients.empty() || !e->m_entries.empty()) {
1035 return;
1036 }
1037
1038 if (delayRemove) {
1039 removeList.insert(value: e);
1040 // now e->isValid() is false
1041 return;
1042 }
1043
1044 if (e->m_status == Normal) {
1045 removeWatch(e);
1046 } else {
1047 // Removed a NonExistent entry - we just remove it from the parent
1048 removeEntry(instance: nullptr, path: e->parentDirectory(), sub_entry: e);
1049 }
1050
1051 if (e->m_mode == StatMode) {
1052 statEntries--;
1053 if (statEntries == 0) {
1054 m_statRescanTimer.stop(); // stop timer if lists are empty
1055 qCDebug(KDIRWATCH) << " Stopped Polling Timer";
1056 }
1057 }
1058
1059 if (s_verboseDebug) {
1060 qCDebug(KDIRWATCH).nospace() << "Removed " << (e->isDir ? "Dir " : "File ") << e->path << " for " << (sub_entry ? sub_entry->path : QString()) << " ["
1061 << (instance ? instance->objectName() : QString()) << "]";
1062 }
1063 QString p = e->path; // take a copy, QMap::remove takes a reference and deletes, since e points into the map
1064#if HAVE_SYS_INOTIFY_H
1065 m_inotify_wd_to_entry.remove(key: e->wd);
1066#endif
1067 m_mapEntries.remove(key: p); // <e> not valid any more
1068}
1069
1070/* Called from KDirWatch destructor:
1071 * remove <instance> as client from all entries
1072 */
1073void KDirWatchPrivate::removeEntries(KDirWatch *instance)
1074{
1075 int minfreq = 3600000;
1076
1077 QStringList pathList;
1078 // put all entries where instance is a client in list
1079 for (auto it = m_mapEntries.begin(); it != m_mapEntries.end(); ++it) {
1080 Entry &entry = it.value();
1081 auto clientIt = entry.findInstance(other: instance);
1082 if (clientIt != entry.m_clients.end()) {
1083 clientIt->count = 1; // forces deletion of instance as client
1084 pathList.append(t: entry.path);
1085 } else if (entry.m_mode == StatMode && entry.freq < minfreq) {
1086 minfreq = entry.freq;
1087 }
1088 }
1089
1090 for (const QString &path : std::as_const(t&: pathList)) {
1091 removeEntry(instance, path: path, sub_entry: nullptr);
1092 }
1093
1094 if (minfreq > freq) {
1095 // we can decrease the global polling frequency
1096 freq = minfreq;
1097 if (m_statRescanTimer.isActive()) {
1098 m_statRescanTimer.start(msec: freq);
1099 }
1100 qCDebug(KDIRWATCH) << "Poll Freq now" << freq << "msec";
1101 }
1102}
1103
1104// instance ==0: stop scanning for all instances
1105bool KDirWatchPrivate::stopEntryScan(KDirWatch *instance, Entry *e)
1106{
1107 int stillWatching = 0;
1108 for (Client &client : e->m_clients) {
1109 if (!instance || instance == client.instance) {
1110 client.watchingStopped = true;
1111 } else if (!client.watchingStopped) {
1112 stillWatching += client.count;
1113 }
1114 }
1115
1116 qCDebug(KDIRWATCH) << (instance ? instance->objectName() : QStringLiteral("all")) << "stopped scanning" << e->path << "(now" << stillWatching
1117 << "watchers)";
1118
1119 if (stillWatching == 0) {
1120 // if nobody is interested, we don't watch, and we don't report
1121 // changes that happened while not watching
1122 e->m_ctime = invalid_ctime; // invalid
1123
1124 // Changing m_status like this would create wrong "created" events in stat mode.
1125 // To really "stop watching" we would need to determine 'stillWatching==0' in scanEntry...
1126 // e->m_status = NonExistent;
1127 }
1128 return true;
1129}
1130
1131// instance ==0: start scanning for all instances
1132bool KDirWatchPrivate::restartEntryScan(KDirWatch *instance, Entry *e, bool notify)
1133{
1134 int wasWatching = 0;
1135 int newWatching = 0;
1136 for (Client &client : e->m_clients) {
1137 if (!client.watchingStopped) {
1138 wasWatching += client.count;
1139 } else if (!instance || instance == client.instance) {
1140 client.watchingStopped = false;
1141 newWatching += client.count;
1142 }
1143 }
1144 if (newWatching == 0) {
1145 return false;
1146 }
1147
1148 qCDebug(KDIRWATCH) << (instance ? instance->objectName() : QStringLiteral("all")) << "restarted scanning" << e->path << "(now" << wasWatching + newWatching
1149 << "watchers)";
1150
1151 // restart watching and emit pending events
1152
1153 int ev = NoChange;
1154 if (wasWatching == 0) {
1155 if (!notify) {
1156 QT_STATBUF stat_buf;
1157 bool exists = (QT_STAT(file: QFile::encodeName(fileName: e->path).constData(), buf: &stat_buf) == 0);
1158 if (exists) {
1159 // ctime is the 'creation time' on windows, but with qMax
1160 // we get the latest change of any kind, on any platform.
1161 e->m_ctime = qMax(a: stat_buf.st_ctime, b: stat_buf.st_mtime);
1162 e->m_status = Normal;
1163 if (s_verboseDebug) {
1164 qCDebug(KDIRWATCH) << "Setting status to Normal for" << e << e->path;
1165 }
1166 e->m_nlink = stat_buf.st_nlink;
1167 e->m_ino = stat_buf.st_ino;
1168
1169 // Same as in scanEntry: ensure no subentry in parent dir
1170 removeEntry(instance: nullptr, path: e->parentDirectory(), sub_entry: e);
1171 } else {
1172 e->m_ctime = invalid_ctime;
1173 e->m_status = NonExistent;
1174 e->m_nlink = 0;
1175 if (s_verboseDebug) {
1176 qCDebug(KDIRWATCH) << "Setting status to NonExistent for" << e << e->path;
1177 }
1178 }
1179 }
1180 e->msecLeft = 0;
1181 ev = scanEntry(e);
1182 }
1183 emitEvent(e, event: ev);
1184
1185 return true;
1186}
1187
1188// instance ==0: stop scanning for all instances
1189void KDirWatchPrivate::stopScan(KDirWatch *instance)
1190{
1191 for (auto it = m_mapEntries.begin(); it != m_mapEntries.end(); ++it) {
1192 stopEntryScan(instance, e: &it.value());
1193 }
1194}
1195
1196void KDirWatchPrivate::startScan(KDirWatch *instance, bool notify, bool skippedToo)
1197{
1198 if (!notify) {
1199 resetList(instance, skippedToo);
1200 }
1201
1202 for (auto it = m_mapEntries.begin(); it != m_mapEntries.end(); ++it) {
1203 restartEntryScan(instance, e: &it.value(), notify);
1204 }
1205
1206 // timer should still be running when in polling mode
1207}
1208
1209// clear all pending events, also from stopped
1210void KDirWatchPrivate::resetList(KDirWatch *instance, bool skippedToo)
1211{
1212 Q_UNUSED(instance);
1213
1214 for (auto it = m_mapEntries.begin(); it != m_mapEntries.end(); ++it) {
1215 for (Client &client : it.value().m_clients) {
1216 if (!client.watchingStopped || skippedToo) {
1217 client.pending = NoChange;
1218 }
1219 }
1220 }
1221}
1222
1223// Return event happened on <e>
1224//
1225int KDirWatchPrivate::scanEntry(Entry *e)
1226{
1227 // Shouldn't happen: Ignore "unknown" notification method
1228 if (e->m_mode == UnknownMode) {
1229 return NoChange;
1230 }
1231
1232 if (e->m_mode == INotifyMode) {
1233 // we know nothing has changed, no need to stat
1234 if (!e->dirty) {
1235 return NoChange;
1236 }
1237 e->dirty = false;
1238 }
1239
1240 if (e->m_mode == StatMode) {
1241 // only scan if timeout on entry timer happens;
1242 // e.g. when using 500msec global timer, a entry
1243 // with freq=5000 is only watched every 10th time
1244
1245 e->msecLeft -= freq;
1246 if (e->msecLeft > 0) {
1247 return NoChange;
1248 }
1249 e->msecLeft += e->freq;
1250 }
1251
1252 QT_STATBUF stat_buf;
1253 const bool exists = (QT_STAT(file: QFile::encodeName(fileName: e->path).constData(), buf: &stat_buf) == 0);
1254 if (exists) {
1255 if (e->m_status == NonExistent) {
1256 // ctime is the 'creation time' on windows, but with qMax
1257 // we get the latest change of any kind, on any platform.
1258 e->m_ctime = qMax(a: stat_buf.st_ctime, b: stat_buf.st_mtime);
1259 e->m_status = Normal;
1260 e->m_ino = stat_buf.st_ino;
1261 if (s_verboseDebug) {
1262 qCDebug(KDIRWATCH) << "Setting status to Normal for just created" << e << e->path;
1263 }
1264 // We need to make sure the entry isn't listed in its parent's subentries... (#222974, testMoveTo)
1265 removeEntry(instance: nullptr, path: e->parentDirectory(), sub_entry: e);
1266
1267 return Created;
1268 }
1269
1270#if 1 // for debugging the if() below
1271 if (s_verboseDebug) {
1272 struct tm *tmp = localtime(timer: &e->m_ctime);
1273 char outstr[200];
1274 strftime(s: outstr, maxsize: sizeof(outstr), format: "%H:%M:%S", tp: tmp);
1275 qCDebug(KDIRWATCH) << e->path << "e->m_ctime=" << e->m_ctime << outstr << "stat_buf.st_ctime=" << stat_buf.st_ctime
1276 << "stat_buf.st_mtime=" << stat_buf.st_mtime << "e->m_nlink=" << e->m_nlink << "stat_buf.st_nlink=" << stat_buf.st_nlink
1277 << "e->m_ino=" << e->m_ino << "stat_buf.st_ino=" << stat_buf.st_ino;
1278 }
1279#endif
1280
1281 if ((e->m_ctime != invalid_ctime)
1282 && (qMax(a: stat_buf.st_ctime, b: stat_buf.st_mtime) != e->m_ctime || stat_buf.st_ino != e->m_ino
1283 || int(stat_buf.st_nlink) != int(e->m_nlink)
1284#ifdef Q_OS_WIN
1285 // on Windows, we trust QFSW to get it right, the ctime comparisons above
1286 // fail for example when adding files to directories on Windows
1287 // which doesn't change the mtime of the directory
1288 || e->m_mode == QFSWatchMode
1289#endif
1290 )) {
1291 e->m_ctime = qMax(a: stat_buf.st_ctime, b: stat_buf.st_mtime);
1292 e->m_nlink = stat_buf.st_nlink;
1293 if (e->m_ino != stat_buf.st_ino) {
1294 // The file got deleted and recreated. We need to watch it again.
1295 removeWatch(e);
1296 addWatch(e);
1297 e->m_ino = stat_buf.st_ino;
1298 return (Deleted | Created);
1299 } else {
1300 return Changed;
1301 }
1302 }
1303
1304 return NoChange;
1305 }
1306
1307 // dir/file doesn't exist
1308
1309 e->m_nlink = 0;
1310 e->m_ino = 0;
1311 e->m_status = NonExistent;
1312
1313 if (e->m_ctime == invalid_ctime) {
1314 return NoChange;
1315 }
1316
1317 e->m_ctime = invalid_ctime;
1318 return Deleted;
1319}
1320
1321/* Notify all interested KDirWatch instances about a given event on an entry
1322 * and stored pending events. When watching is stopped, the event is
1323 * added to the pending events.
1324 */
1325void KDirWatchPrivate::emitEvent(Entry *e, int event, const QString &fileName)
1326{
1327 QString path(e->path);
1328 if (!fileName.isEmpty()) {
1329 if (!QDir::isRelativePath(path: fileName)) {
1330 path = fileName;
1331 } else {
1332#ifdef Q_OS_UNIX
1333 path += QLatin1Char('/') + fileName;
1334#elif defined(Q_OS_WIN)
1335 // current drive is passed instead of /
1336 path += QStringView(QDir::currentPath()).left(2) + QLatin1Char('/') + fileName;
1337#endif
1338 }
1339 }
1340
1341 if (s_verboseDebug) {
1342 qCDebug(KDIRWATCH) << event << path << e->m_clients.size() << "clients";
1343 }
1344
1345 for (Client &c : e->m_clients) {
1346 if (c.instance == nullptr || c.count == 0) {
1347 continue;
1348 }
1349
1350 if (c.watchingStopped) {
1351 // Do not add event to a list of pending events, the docs say restartDirScan won't emit!
1352 continue;
1353 }
1354 // not stopped
1355 if (event == NoChange || event == Changed) {
1356 event |= c.pending;
1357 }
1358 c.pending = NoChange;
1359 if (event == NoChange) {
1360 continue;
1361 }
1362
1363 // Emit the signals delayed, to avoid unexpected re-entrance from the slots (#220153)
1364
1365 if (event & Deleted) {
1366 QMetaObject::invokeMethod(
1367 object: c.instance,
1368 function: [c, path]() {
1369 c.instance->setDeleted(path);
1370 },
1371 type: Qt::QueuedConnection);
1372 }
1373
1374 if (event & Created) {
1375 QMetaObject::invokeMethod(
1376 object: c.instance,
1377 function: [c, path]() {
1378 c.instance->setCreated(path);
1379 },
1380 type: Qt::QueuedConnection);
1381 // possible emit Change event after creation
1382 }
1383
1384 if (event & Changed) {
1385 QMetaObject::invokeMethod(
1386 object: c.instance,
1387 function: [c, path]() {
1388 c.instance->setDirty(path);
1389 },
1390 type: Qt::QueuedConnection);
1391 }
1392 }
1393}
1394
1395// Remove entries which were marked to be removed
1396void KDirWatchPrivate::slotRemoveDelayed()
1397{
1398 delayRemove = false;
1399 // Removing an entry could also take care of removing its parent
1400 // (e.g. in inotify mode), which would remove other entries in removeList,
1401 // so don't use Q_FOREACH or iterators here...
1402 while (!removeList.isEmpty()) {
1403 Entry *entry = *removeList.begin();
1404 removeEntry(instance: nullptr, e: entry, sub_entry: nullptr); // this will remove entry from removeList
1405 }
1406}
1407
1408/* Scan all entries to be watched for changes. This is done regularly
1409 * when polling. inotify uses a single-shot timer to call this slot delayed.
1410 */
1411void KDirWatchPrivate::slotRescan()
1412{
1413 if (s_verboseDebug) {
1414 qCDebug(KDIRWATCH);
1415 }
1416
1417 EntryMap::Iterator it;
1418
1419 // People can do very long things in the slot connected to dirty(),
1420 // like showing a message box. We don't want to keep polling during
1421 // that time, otherwise the value of 'delayRemove' will be reset.
1422 // ### TODO: now the emitEvent delays emission, this can be cleaned up
1423 bool timerRunning = m_statRescanTimer.isActive();
1424 if (timerRunning) {
1425 m_statRescanTimer.stop();
1426 }
1427
1428 // We delay deletions of entries this way.
1429 // removeDir(), when called in slotDirty(), can cause a crash otherwise
1430 // ### TODO: now the emitEvent delays emission, this can be cleaned up
1431 delayRemove = true;
1432
1433 if (rescan_all) {
1434 // mark all as dirty
1435 it = m_mapEntries.begin();
1436 for (; it != m_mapEntries.end(); ++it) {
1437 (*it).dirty = true;
1438 }
1439 rescan_all = false;
1440 } else {
1441 // propagate dirty flag to dependent entries (e.g. file watches)
1442 it = m_mapEntries.begin();
1443 for (; it != m_mapEntries.end(); ++it) {
1444 if (((*it).m_mode == INotifyMode || (*it).m_mode == QFSWatchMode) && (*it).dirty) {
1445 (*it).propagate_dirty();
1446 }
1447 }
1448 }
1449
1450#if HAVE_SYS_INOTIFY_H
1451 QList<Entry *> cList;
1452#endif
1453
1454 it = m_mapEntries.begin();
1455 for (; it != m_mapEntries.end(); ++it) {
1456 // we don't check invalid entries (i.e. remove delayed)
1457 Entry *entry = &(*it);
1458 if (!entry->isValid()) {
1459 continue;
1460 }
1461
1462 const int ev = scanEntry(e: entry);
1463 if (s_verboseDebug) {
1464 qCDebug(KDIRWATCH) << "scanEntry for" << entry->path << "says" << ev;
1465 }
1466
1467 switch (entry->m_mode) {
1468#if HAVE_SYS_INOTIFY_H
1469 case INotifyMode:
1470 if (ev == Deleted) {
1471 if (s_verboseDebug) {
1472 qCDebug(KDIRWATCH) << "scanEntry says" << entry->path << "was deleted";
1473 }
1474
1475 // be safe, don't walk upwards on drive level or /
1476 if (!entry->isRoot()) {
1477 addEntry(instance: nullptr, path: entry->parentDirectory(), sub_entry: entry, isDir: true);
1478 }
1479 } else if (ev == Created) {
1480 if (s_verboseDebug) {
1481 qCDebug(KDIRWATCH) << "scanEntry says" << entry->path << "was created. wd=" << entry->wd;
1482 }
1483 if (entry->wd < 0) {
1484 cList.append(t: entry);
1485 addWatch(e: entry);
1486 }
1487 }
1488 break;
1489#endif
1490 case QFSWatchMode:
1491 if (ev == Created) {
1492 addWatch(e: entry);
1493 }
1494 break;
1495 default:
1496 // dunno about StatMode...
1497 break;
1498 }
1499
1500#if HAVE_SYS_INOTIFY_H
1501 if (entry->isDir) {
1502 // Report and clear the list of files that have changed in this directory.
1503 // Remove duplicates by changing to set and back again:
1504 // we don't really care about preserving the order of the
1505 // original changes.
1506 QStringList pendingFileChanges = entry->m_pendingFileChanges;
1507 pendingFileChanges.removeDuplicates();
1508 for (const QString &changedFilename : std::as_const(t&: pendingFileChanges)) {
1509 if (s_verboseDebug) {
1510 qCDebug(KDIRWATCH) << "processing pending file change for" << changedFilename;
1511 }
1512 emitEvent(e: entry, event: Changed, fileName: changedFilename);
1513 }
1514 entry->m_pendingFileChanges.clear();
1515 }
1516#endif
1517
1518 if (ev != NoChange) {
1519 emitEvent(e: entry, event: ev);
1520 }
1521 }
1522
1523 if (timerRunning) {
1524 m_statRescanTimer.start(msec: freq);
1525 }
1526
1527#if HAVE_SYS_INOTIFY_H
1528 // Remove watch of parent of new created directories
1529 for (Entry *e : std::as_const(t&: cList)) {
1530 removeEntry(instance: nullptr, path: e->parentDirectory(), sub_entry: e);
1531 }
1532#endif
1533
1534 QTimer::singleShot(interval: 0, receiver: this, slot: &KDirWatchPrivate::slotRemoveDelayed);
1535}
1536
1537bool KDirWatchPrivate::isNoisyFile(const char *filename)
1538{
1539 // $HOME/.X.err grows with debug output, so don't notify change
1540 if (*filename == '.') {
1541 if (strncmp(s1: filename, s2: ".X.err", n: 6) == 0) {
1542 return true;
1543 }
1544 if (strncmp(s1: filename, s2: ".xsession-errors", n: 16) == 0) {
1545 return true;
1546 }
1547 // fontconfig updates the cache on every KDE app start
1548 // as well as during kio_thumbnail worker execution
1549 // TODO:; check which fontconfig version this file was deprecated and the check can be removed
1550 if (strncmp(s1: filename, s2: ".fonts.cache", n: 12) == 0) {
1551 return true;
1552 }
1553 }
1554
1555 return false;
1556}
1557
1558void KDirWatchPrivate::ref(KDirWatch *watch)
1559{
1560 m_referencesObjects.push_back(t: watch);
1561}
1562
1563void KDirWatchPrivate::unref(KDirWatch *watch)
1564{
1565 m_referencesObjects.removeOne(t: watch);
1566 if (m_referencesObjects.isEmpty()) {
1567 destroyPrivate();
1568 }
1569}
1570
1571#if HAVE_SYS_INOTIFY_H
1572QString KDirWatchPrivate::inotifyEventName(const inotify_event *event) const
1573{
1574 if (event->mask & IN_OPEN)
1575 return QStringLiteral("OPEN");
1576 else if (event->mask & IN_CLOSE_NOWRITE)
1577 return QStringLiteral("CLOSE_NOWRITE");
1578 else if (event->mask & IN_CLOSE_WRITE)
1579 return QStringLiteral("CLOSE_WRITE");
1580 else if (event->mask & IN_MOVED_TO)
1581 return QStringLiteral("MOVED_TO");
1582 else if (event->mask & IN_MOVED_FROM)
1583 return QStringLiteral("MOVED_FROM");
1584 else if (event->mask & IN_MOVE)
1585 return QStringLiteral("MOVE");
1586 else if (event->mask & IN_CREATE)
1587 return QStringLiteral("CREATE");
1588 else if (event->mask & IN_DELETE)
1589 return QStringLiteral("DELETE");
1590 else if (event->mask & IN_DELETE_SELF)
1591 return QStringLiteral("DELETE_SELF");
1592 else if (event->mask & IN_MOVE_SELF)
1593 return QStringLiteral("MOVE_SELF");
1594 else if (event->mask & IN_ATTRIB)
1595 return QStringLiteral("ATTRIB");
1596 else if (event->mask & IN_MODIFY)
1597 return QStringLiteral("MODIFY");
1598 if (event->mask & IN_ACCESS)
1599 return QStringLiteral("ACCESS");
1600 if (event->mask & IN_IGNORED)
1601 return QStringLiteral("IGNORED");
1602 if (event->mask & IN_UNMOUNT)
1603 return QStringLiteral("IN_UNMOUNT");
1604 else
1605 return QStringLiteral("UNKWOWN");
1606}
1607#endif
1608
1609#if HAVE_QFILESYSTEMWATCHER
1610// Slot for QFileSystemWatcher
1611void KDirWatchPrivate::fswEventReceived(const QString &path)
1612{
1613 if (s_verboseDebug) {
1614 qCDebug(KDIRWATCH) << path;
1615 }
1616
1617 auto it = m_mapEntries.find(key: path);
1618 if (it != m_mapEntries.end()) {
1619 Entry *entry = &it.value();
1620 entry->dirty = true;
1621 const int ev = scanEntry(e: entry);
1622 if (s_verboseDebug) {
1623 qCDebug(KDIRWATCH) << "scanEntry for" << entry->path << "says" << ev;
1624 }
1625 if (ev != NoChange) {
1626 emitEvent(e: entry, event: ev);
1627 }
1628 if (ev == Deleted) {
1629 // be safe, don't walk upwards on drive level or /
1630 if (!entry->isRoot()) {
1631 addEntry(instance: nullptr, path: entry->parentDirectory(), sub_entry: entry, isDir: true);
1632 }
1633 } else if (ev == Created) {
1634 // We were waiting for it to appear; now watch it
1635 addWatch(e: entry);
1636 } else if (entry->isDir) {
1637 // Check if any file or dir was created under this directory, that we were waiting for
1638 for (Entry *sub_entry : std::as_const(t&: entry->m_entries)) {
1639 fswEventReceived(path: sub_entry->path); // recurse, to call scanEntry and see if something changed
1640 }
1641 } else {
1642 /* Even though QFileSystemWatcher only reported the file as modified, it is possible that the file
1643 * was in fact just deleted and then immediately recreated. If the file was deleted, QFileSystemWatcher
1644 * will delete the watch, and will ignore the file, even after it is recreated. Since it is impossible
1645 * to reliably detect this case, always re-request the watch on a dirty signal, to avoid losing the
1646 * underlying OS monitor.
1647 */
1648 fsWatcher->addPath(file: entry->path);
1649 }
1650 }
1651}
1652#else
1653void KDirWatchPrivate::fswEventReceived(const QString &path)
1654{
1655 Q_UNUSED(path);
1656 qCWarning(KCOREADDONS_DEBUG) << "QFileSystemWatcher event received but QFileSystemWatcher is not supported";
1657}
1658#endif // HAVE_QFILESYSTEMWATCHER
1659
1660//
1661// Class KDirWatch
1662//
1663
1664Q_GLOBAL_STATIC(KDirWatch, s_pKDirWatchSelf)
1665KDirWatch *KDirWatch::self()
1666{
1667 return s_pKDirWatchSelf();
1668}
1669
1670// <steve> is this used anywhere?
1671// <dfaure> yes, see kio/src/core/kcoredirlister_p.h:328
1672bool KDirWatch::exists()
1673{
1674 return s_pKDirWatchSelf.exists() && dwp_self.hasLocalData();
1675}
1676
1677KDirWatch::KDirWatch(QObject *parent)
1678 : QObject(parent)
1679 , d(createPrivate())
1680{
1681 d->ref(watch: this);
1682 static QBasicAtomicInt nameCounter = Q_BASIC_ATOMIC_INITIALIZER(1);
1683 const int counter = nameCounter.fetchAndAddRelaxed(valueToAdd: 1); // returns the old value
1684 setObjectName(QStringLiteral("KDirWatch-%1").arg(a: counter));
1685}
1686
1687KDirWatch::~KDirWatch()
1688{
1689 if (d) {
1690 d->removeEntries(instance: this);
1691 d->unref(watch: this);
1692 }
1693}
1694
1695void KDirWatch::addDir(const QString &_path, WatchModes watchModes)
1696{
1697 if (KNetworkMounts::self()->isOptionEnabledForPath(path: _path, option: KNetworkMounts::KDirWatchDontAddWatches)) {
1698 return;
1699 }
1700
1701 if (d) {
1702 d->addEntry(instance: this, _path, sub_entry: nullptr, isDir: true, watchModes);
1703 }
1704}
1705
1706void KDirWatch::addFile(const QString &_path)
1707{
1708 if (KNetworkMounts::self()->isOptionEnabledForPath(path: _path, option: KNetworkMounts::KDirWatchDontAddWatches)) {
1709 return;
1710 }
1711
1712 if (!d) {
1713 return;
1714 }
1715
1716 d->addEntry(instance: this, _path, sub_entry: nullptr, isDir: false);
1717}
1718
1719QDateTime KDirWatch::ctime(const QString &_path) const
1720{
1721 KDirWatchPrivate::Entry *e = d->entry(_path);
1722
1723 if (!e) {
1724 return QDateTime();
1725 }
1726
1727 return QDateTime::fromSecsSinceEpoch(secs: e->m_ctime);
1728}
1729
1730void KDirWatch::removeDir(const QString &_path)
1731{
1732 if (d) {
1733 d->removeEntry(instance: this, _path, sub_entry: nullptr);
1734 }
1735}
1736
1737void KDirWatch::removeFile(const QString &_path)
1738{
1739 if (d) {
1740 d->removeEntry(instance: this, _path, sub_entry: nullptr);
1741 }
1742}
1743
1744bool KDirWatch::stopDirScan(const QString &_path)
1745{
1746 if (d) {
1747 KDirWatchPrivate::Entry *e = d->entry(_path);
1748 if (e && e->isDir) {
1749 return d->stopEntryScan(instance: this, e);
1750 }
1751 }
1752 return false;
1753}
1754
1755bool KDirWatch::restartDirScan(const QString &_path)
1756{
1757 if (d) {
1758 KDirWatchPrivate::Entry *e = d->entry(_path);
1759 if (e && e->isDir)
1760 // restart without notifying pending events
1761 {
1762 return d->restartEntryScan(instance: this, e, notify: false);
1763 }
1764 }
1765 return false;
1766}
1767
1768void KDirWatch::stopScan()
1769{
1770 if (d) {
1771 d->stopScan(instance: this);
1772 d->_isStopped = true;
1773 }
1774}
1775
1776bool KDirWatch::isStopped()
1777{
1778 return d->_isStopped;
1779}
1780
1781void KDirWatch::startScan(bool notify, bool skippedToo)
1782{
1783 if (d) {
1784 d->_isStopped = false;
1785 d->startScan(instance: this, notify, skippedToo);
1786 }
1787}
1788
1789bool KDirWatch::contains(const QString &_path) const
1790{
1791 KDirWatchPrivate::Entry *e = d->entry(_path);
1792 if (!e) {
1793 return false;
1794 }
1795
1796 for (const KDirWatchPrivate::Client &client : e->m_clients) {
1797 if (client.instance == this) {
1798 return true;
1799 }
1800 }
1801
1802 return false;
1803}
1804
1805void KDirWatch::setCreated(const QString &_file)
1806{
1807 qCDebug(KDIRWATCH) << objectName() << "emitting created" << _file;
1808 Q_EMIT created(path: _file);
1809}
1810
1811void KDirWatch::setDirty(const QString &_file)
1812{
1813 Q_EMIT dirty(path: _file);
1814}
1815
1816void KDirWatch::setDeleted(const QString &_file)
1817{
1818 qCDebug(KDIRWATCH) << objectName() << "emitting deleted" << _file;
1819 Q_EMIT deleted(path: _file);
1820}
1821
1822KDirWatch::Method KDirWatch::internalMethod() const
1823{
1824 // This reproduces the logic in KDirWatchPrivate::addWatch
1825 switch (d->m_preferredMethod) {
1826 case KDirWatch::INotify:
1827#if HAVE_SYS_INOTIFY_H
1828 if (d->supports_inotify) {
1829 return KDirWatch::INotify;
1830 }
1831#endif
1832 break;
1833 case KDirWatch::QFSWatch:
1834#if HAVE_QFILESYSTEMWATCHER
1835 return KDirWatch::QFSWatch;
1836#else
1837 break;
1838#endif
1839 case KDirWatch::Stat:
1840 return KDirWatch::Stat;
1841 }
1842
1843#if HAVE_SYS_INOTIFY_H
1844 if (d->supports_inotify) {
1845 return KDirWatch::INotify;
1846 }
1847#endif
1848#if HAVE_QFILESYSTEMWATCHER
1849 return KDirWatch::QFSWatch;
1850#else
1851 return KDirWatch::Stat;
1852#endif
1853}
1854
1855bool KDirWatch::event(QEvent *event)
1856{
1857 if (Q_LIKELY(event->type() != QEvent::ThreadChange)) {
1858 return QObject::event(event);
1859 }
1860
1861 qCCritical(KDIRWATCH) << "KDirwatch is moving its thread. This is not supported at this time; your watch will not watch anything anymore!"
1862 << "Create and use watches on the correct thread"
1863 << "Watch:" << this;
1864
1865 // We are still in the old thread when the event runs, so this is safe.
1866 Q_ASSERT(thread() == d->thread());
1867 d->removeEntries(instance: this);
1868 d->unref(watch: this);
1869 d = nullptr;
1870
1871 // Schedule the creation of the new private in the new thread.
1872 QMetaObject::invokeMethod(
1873 object: this,
1874 function: [this] {
1875 d = createPrivate();
1876 },
1877 type: Qt::QueuedConnection);
1878
1879 // NOTE: to actually support moving watches across threads we'd have to make Entry copyable and schedule a complete
1880 // re-installation of watches on the new thread after createPrivate.
1881
1882 return QObject::event(event);
1883}
1884
1885#include "moc_kdirwatch.cpp"
1886#include "moc_kdirwatch_p.cpp"
1887
1888// sven
1889

source code of kcoreaddons/src/lib/io/kdirwatch.cpp