KCoreAddons

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

KDE's Doxygen guidelines are available online.