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 entryAdded = false;
890 switch (preferredMethod) {
891#if HAVE_SYS_INOTIFY_H
892 case KDirWatch::INotify:
893 entryAdded = useINotify(e);
894 break;
895#else
896 case KDirWatch::INotify:
897 entryAdded = false;
898 break;
899#endif
900#if HAVE_QFILESYSTEMWATCHER
901 case KDirWatch::QFSWatch:
902 entryAdded = useQFSWatch(e);
903 break;
904#else
905 case KDirWatch::QFSWatch:
906 entryAdded = false;
907 break;
908#endif
909 case KDirWatch::Stat:
910 entryAdded = useStat(e);
911 break;
912 }
913
914 // Failing that try in order INotify, QFSWatch, Stat
915 if (!entryAdded) {
916#if HAVE_SYS_INOTIFY_H
917 if (preferredMethod != KDirWatch::INotify && useINotify(e)) {
918 return;
919 }
920#endif
921#if HAVE_QFILESYSTEMWATCHER
922 if (preferredMethod != KDirWatch::QFSWatch && useQFSWatch(e)) {
923 return;
924 }
925#endif
926 if (preferredMethod != KDirWatch::Stat) {
927 useStat(e);
928 }
929 }
930}
931
932void KDirWatchPrivate::removeWatch(Entry *e)
933{
934#if HAVE_SYS_INOTIFY_H
935 if (e->m_mode == INotifyMode) {
936 m_inotify_wd_to_entry.remove(e->wd);
937 (void)inotify_rm_watch(m_inotify_fd, e->wd);
938 if (s_verboseDebug) {
939 qCDebug(KDIRWATCH).nospace() << "Cancelled INotify (fd " << m_inotify_fd << ", " << e->wd << ") for " << e->path;
940 }
941 }
942#endif
943#if HAVE_QFILESYSTEMWATCHER
944 if (e->m_mode == QFSWatchMode && fsWatcher) {
945 if (s_verboseDebug) {
946 qCDebug(KDIRWATCH) << "fsWatcher->removePath" << e->path;
947 }
948 fsWatcher->removePath(e->path);
949 }
950#endif
951}
952
953void KDirWatchPrivate::removeEntry(KDirWatch *instance, const QString &_path, Entry *sub_entry)
954{
955 qCDebug(KDIRWATCH) << "path=" << _path << "sub_entry:" << sub_entry;
956
957 Entry *e = entry(_path);
958 if (e) {
959 removeEntry(instance, e, sub_entry);
960 }
961}
962
963void KDirWatchPrivate::removeEntry(KDirWatch *instance, Entry *e, Entry *sub_entry)
964{
965 removeList.remove(e);
966
967 if (sub_entry) {
968 e->m_entries.removeAll(sub_entry);
969 } else {
970 e->removeClient(instance);
971 }
972
973 if (!e->m_clients.empty() || !e->m_entries.empty()) {
974 return;
975 }
976
977 if (delayRemove) {
978 removeList.insert(e);
979 // now e->isValid() is false
980 return;
981 }
982
983 if (e->m_status == Normal) {
984 removeWatch(e);
985 } else {
986 // Removed a NonExistent entry - we just remove it from the parent
987 if (e->isDir) {
988 removeEntry(nullptr, e->parentDirectory(), e);
989 } else {
990 removeEntry(nullptr, QFileInfo(e->path).absolutePath(), e);
991 }
992 }
993
994 if (e->m_mode == StatMode) {
995 statEntries--;
996 if (statEntries == 0) {
997 m_statRescanTimer.stop(); // stop timer if lists are empty
998 qCDebug(KDIRWATCH) << " Stopped Polling Timer";
999 }
1000 }
1001
1002 if (s_verboseDebug) {
1003 qCDebug(KDIRWATCH).nospace() << "Removed " << (e->isDir ? "Dir " : "File ") << e->path << " for " << (sub_entry ? sub_entry->path : QString()) << " ["
1004 << (instance ? instance->objectName() : QString()) << "]";
1005 }
1006 QString p = e->path; // take a copy, QMap::remove takes a reference and deletes, since e points into the map
1007#if HAVE_SYS_INOTIFY_H
1008 m_inotify_wd_to_entry.remove(e->wd);
1009#endif
1010 m_mapEntries.remove(p); // <e> not valid any more
1011}
1012
1013/* Called from KDirWatch destructor:
1014 * remove <instance> as client from all entries
1015 */
1016void KDirWatchPrivate::removeEntries(KDirWatch *instance)
1017{
1018 int minfreq = 3600000;
1019
1020 QStringList pathList;
1021 // put all entries where instance is a client in list
1022 for (auto it = m_mapEntries.begin(); it != m_mapEntries.end(); ++it) {
1023 Entry &entry = it.value();
1024 auto clientIt = entry.findInstance(instance);
1025 if (clientIt != entry.m_clients.end()) {
1026 clientIt->count = 1; // forces deletion of instance as client
1027 pathList.append(entry.path);
1028 } else if (entry.m_mode == StatMode && entry.freq < minfreq) {
1029 minfreq = entry.freq;
1030 }
1031 }
1032
1033 for (const QString &path : std::as_const(pathList)) {
1034 removeEntry(instance, path, nullptr);
1035 }
1036
1037 if (minfreq > freq) {
1038 // we can decrease the global polling frequency
1039 freq = minfreq;
1040 if (m_statRescanTimer.isActive()) {
1041 m_statRescanTimer.start(freq);
1042 }
1043 qCDebug(KDIRWATCH) << "Poll Freq now" << freq << "msec";
1044 }
1045}
1046
1047// instance ==0: stop scanning for all instances
1048bool KDirWatchPrivate::stopEntryScan(KDirWatch *instance, Entry *e)
1049{
1050 int stillWatching = 0;
1051 for (Client &client : e->m_clients) {
1052 if (!instance || instance == client.instance) {
1053 client.watchingStopped = true;
1054 } else if (!client.watchingStopped) {
1055 stillWatching += client.count;
1056 }
1057 }
1058
1059 qCDebug(KDIRWATCH) << (instance ? instance->objectName() : QStringLiteral("all")) << "stopped scanning" << e->path << "(now" << stillWatching
1060 << "watchers)";
1061
1062 if (stillWatching == 0) {
1063 // if nobody is interested, we don't watch, and we don't report
1064 // changes that happened while not watching
1065 e->m_ctime = invalid_ctime; // invalid
1066
1067 // Changing m_status like this would create wrong "created" events in stat mode.
1068 // To really "stop watching" we would need to determine 'stillWatching==0' in scanEntry...
1069 // e->m_status = NonExistent;
1070 }
1071 return true;
1072}
1073
1074// instance ==0: start scanning for all instances
1075bool KDirWatchPrivate::restartEntryScan(KDirWatch *instance, Entry *e, bool notify)
1076{
1077 int wasWatching = 0;
1078 int newWatching = 0;
1079 for (Client &client : e->m_clients) {
1080 if (!client.watchingStopped) {
1081 wasWatching += client.count;
1082 } else if (!instance || instance == client.instance) {
1083 client.watchingStopped = false;
1084 newWatching += client.count;
1085 }
1086 }
1087 if (newWatching == 0) {
1088 return false;
1089 }
1090
1091 qCDebug(KDIRWATCH) << (instance ? instance->objectName() : QStringLiteral("all")) << "restarted scanning" << e->path << "(now" << wasWatching + newWatching
1092 << "watchers)";
1093
1094 // restart watching and emit pending events
1095
1096 int ev = NoChange;
1097 if (wasWatching == 0) {
1098 if (!notify) {
1099 QT_STATBUF stat_buf;
1100 bool exists = (QT_STAT(QFile::encodeName(e->path).constData(), &stat_buf) == 0);
1101 if (exists) {
1102 // ctime is the 'creation time' on windows, but with qMax
1103 // we get the latest change of any kind, on any platform.
1104 e->m_ctime = qMax(stat_buf.st_ctime, stat_buf.st_mtime);
1105 e->m_status = Normal;
1106 if (s_verboseDebug) {
1107 qCDebug(KDIRWATCH) << "Setting status to Normal for" << e << e->path;
1108 }
1109 e->m_nlink = stat_buf.st_nlink;
1110 e->m_ino = stat_buf.st_ino;
1111
1112 // Same as in scanEntry: ensure no subentry in parent dir
1113 removeEntry(nullptr, e->parentDirectory(), e);
1114 } else {
1115 e->m_ctime = invalid_ctime;
1116 e->m_status = NonExistent;
1117 e->m_nlink = 0;
1118 if (s_verboseDebug) {
1119 qCDebug(KDIRWATCH) << "Setting status to NonExistent for" << e << e->path;
1120 }
1121 }
1122 }
1123 e->msecLeft = 0;
1124 ev = scanEntry(e);
1125 }
1126 emitEvent(e, ev);
1127
1128 return true;
1129}
1130
1131// instance ==0: stop scanning for all instances
1132void KDirWatchPrivate::stopScan(KDirWatch *instance)
1133{
1134 for (auto it = m_mapEntries.begin(); it != m_mapEntries.end(); ++it) {
1135 stopEntryScan(instance, &it.value());
1136 }
1137}
1138
1139void KDirWatchPrivate::startScan(KDirWatch *instance, bool notify, bool skippedToo)
1140{
1141 if (!notify) {
1142 resetList(instance, skippedToo);
1143 }
1144
1145 for (auto it = m_mapEntries.begin(); it != m_mapEntries.end(); ++it) {
1146 restartEntryScan(instance, &it.value(), notify);
1147 }
1148
1149 // timer should still be running when in polling mode
1150}
1151
1152// clear all pending events, also from stopped
1153void KDirWatchPrivate::resetList(KDirWatch *instance, bool skippedToo)
1154{
1155 Q_UNUSED(instance);
1156
1157 for (auto it = m_mapEntries.begin(); it != m_mapEntries.end(); ++it) {
1158 for (Client &client : it.value().m_clients) {
1159 if (!client.watchingStopped || skippedToo) {
1160 client.pending = NoChange;
1161 }
1162 }
1163 }
1164}
1165
1166// Return event happened on <e>
1167//
1168int KDirWatchPrivate::scanEntry(Entry *e)
1169{
1170 // Shouldn't happen: Ignore "unknown" notification method
1171 if (e->m_mode == UnknownMode) {
1172 return NoChange;
1173 }
1174
1175 if (e->m_mode == INotifyMode) {
1176 // we know nothing has changed, no need to stat
1177 if (!e->dirty) {
1178 return NoChange;
1179 }
1180 e->dirty = false;
1181 }
1182
1183 if (e->m_mode == StatMode) {
1184 // only scan if timeout on entry timer happens;
1185 // e.g. when using 500msec global timer, a entry
1186 // with freq=5000 is only watched every 10th time
1187
1188 e->msecLeft -= freq;
1189 if (e->msecLeft > 0) {
1190 return NoChange;
1191 }
1192 e->msecLeft += e->freq;
1193 }
1194
1195 QT_STATBUF stat_buf;
1196 const bool exists = (QT_STAT(QFile::encodeName(e->path).constData(), &stat_buf) == 0);
1197 if (exists) {
1198 if (e->m_status == NonExistent) {
1199 // ctime is the 'creation time' on windows, but with qMax
1200 // we get the latest change of any kind, on any platform.
1201 e->m_ctime = qMax(stat_buf.st_ctime, stat_buf.st_mtime);
1202 e->m_status = Normal;
1203 e->m_ino = stat_buf.st_ino;
1204 if (s_verboseDebug) {
1205 qCDebug(KDIRWATCH) << "Setting status to Normal for just created" << e << e->path;
1206 }
1207 // We need to make sure the entry isn't listed in its parent's subentries... (#222974, testMoveTo)
1208 removeEntry(nullptr, e->parentDirectory(), e);
1209
1210 return Created;
1211 }
1212
1213#if 1 // for debugging the if() below
1214 if (s_verboseDebug) {
1215 struct tm *tmp = localtime(&e->m_ctime);
1216 char outstr[200];
1217 strftime(outstr, sizeof(outstr), "%H:%M:%S", tmp);
1218 qCDebug(KDIRWATCH) << e->path << "e->m_ctime=" << e->m_ctime << outstr << "stat_buf.st_ctime=" << stat_buf.st_ctime
1219 << "stat_buf.st_mtime=" << stat_buf.st_mtime << "e->m_nlink=" << e->m_nlink << "stat_buf.st_nlink=" << stat_buf.st_nlink
1220 << "e->m_ino=" << e->m_ino << "stat_buf.st_ino=" << stat_buf.st_ino;
1221 }
1222#endif
1223
1224 if ((e->m_ctime != invalid_ctime)
1225 && (qMax(stat_buf.st_ctime, stat_buf.st_mtime) != e->m_ctime || stat_buf.st_ino != e->m_ino
1226 || int(stat_buf.st_nlink) != int(e->m_nlink)
1227#ifdef Q_OS_WIN
1228 // on Windows, we trust QFSW to get it right, the ctime comparisons above
1229 // fail for example when adding files to directories on Windows
1230 // which doesn't change the mtime of the directory
1231 || e->m_mode == QFSWatchMode
1232#endif
1233 )) {
1234 e->m_ctime = qMax(stat_buf.st_ctime, stat_buf.st_mtime);
1235 e->m_nlink = stat_buf.st_nlink;
1236 if (e->m_ino != stat_buf.st_ino) {
1237 // The file got deleted and recreated. We need to watch it again.
1238 removeWatch(e);
1239 addWatch(e);
1240 e->m_ino = stat_buf.st_ino;
1241 return (Deleted | Created);
1242 } else {
1243 return Changed;
1244 }
1245 }
1246
1247 return NoChange;
1248 }
1249
1250 // dir/file doesn't exist
1251
1252 e->m_nlink = 0;
1253 e->m_ino = 0;
1254 e->m_status = NonExistent;
1255
1256 if (e->m_ctime == invalid_ctime) {
1257 return NoChange;
1258 }
1259
1260 e->m_ctime = invalid_ctime;
1261 return Deleted;
1262}
1263
1264/* Notify all interested KDirWatch instances about a given event on an entry
1265 * and stored pending events. When watching is stopped, the event is
1266 * added to the pending events.
1267 */
1268void KDirWatchPrivate::emitEvent(Entry *e, int event, const QString &fileName)
1269{
1270 QString path(e->path);
1271 if (!fileName.isEmpty()) {
1272 if (!QDir::isRelativePath(fileName)) {
1273 path = fileName;
1274 } else {
1275#ifdef Q_OS_UNIX
1276 path += QLatin1Char('/') + fileName;
1277#elif defined(Q_OS_WIN)
1278 // current drive is passed instead of /
1279 path += QStringView(QDir::currentPath()).left(2) + QLatin1Char('/') + fileName;
1280#endif
1281 }
1282 }
1283
1284 if (s_verboseDebug) {
1285 qCDebug(KDIRWATCH) << event << path << e->m_clients.size() << "clients";
1286 }
1287
1288 for (Client &c : e->m_clients) {
1289 if (c.instance == nullptr || c.count == 0) {
1290 continue;
1291 }
1292
1293 if (c.watchingStopped) {
1294 // Do not add event to a list of pending events, the docs say restartDirScan won't emit!
1295 continue;
1296 }
1297 // not stopped
1298 if (event == NoChange || event == Changed) {
1299 event |= c.pending;
1300 }
1301 c.pending = NoChange;
1302 if (event == NoChange) {
1303 continue;
1304 }
1305
1306 // Emit the signals delayed, to avoid unexpected re-entrance from the slots (#220153)
1307
1308 if (event & Deleted) {
1310 c.instance,
1311 [c, path]() {
1312 c.instance->setDeleted(path);
1313 },
1315 }
1316
1317 if (event & Created) {
1319 c.instance,
1320 [c, path]() {
1321 c.instance->setCreated(path);
1322 },
1324 // possible emit Change event after creation
1325 }
1326
1327 if (event & Changed) {
1329 c.instance,
1330 [c, path]() {
1331 c.instance->setDirty(path);
1332 },
1334 }
1335 }
1336}
1337
1338// Remove entries which were marked to be removed
1339void KDirWatchPrivate::slotRemoveDelayed()
1340{
1341 delayRemove = false;
1342 // Removing an entry could also take care of removing its parent
1343 // (e.g. in inotify mode), which would remove other entries in removeList,
1344 // so don't use Q_FOREACH or iterators here...
1345 while (!removeList.isEmpty()) {
1346 Entry *entry = *removeList.begin();
1347 removeEntry(nullptr, entry, nullptr); // this will remove entry from removeList
1348 }
1349}
1350
1351/* Scan all entries to be watched for changes. This is done regularly
1352 * when polling. inotify uses a single-shot timer to call this slot delayed.
1353 */
1354void KDirWatchPrivate::slotRescan()
1355{
1356 if (s_verboseDebug) {
1357 qCDebug(KDIRWATCH);
1358 }
1359
1360 EntryMap::Iterator it;
1361
1362 // People can do very long things in the slot connected to dirty(),
1363 // like showing a message box. We don't want to keep polling during
1364 // that time, otherwise the value of 'delayRemove' will be reset.
1365 // ### TODO: now the emitEvent delays emission, this can be cleaned up
1366 bool timerRunning = m_statRescanTimer.isActive();
1367 if (timerRunning) {
1368 m_statRescanTimer.stop();
1369 }
1370
1371 // We delay deletions of entries this way.
1372 // removeDir(), when called in slotDirty(), can cause a crash otherwise
1373 // ### TODO: now the emitEvent delays emission, this can be cleaned up
1374 delayRemove = true;
1375
1376 if (rescan_all) {
1377 // mark all as dirty
1378 it = m_mapEntries.begin();
1379 for (; it != m_mapEntries.end(); ++it) {
1380 (*it).dirty = true;
1381 }
1382 rescan_all = false;
1383 } else {
1384 // propagate dirty flag to dependent entries (e.g. file watches)
1385 it = m_mapEntries.begin();
1386 for (; it != m_mapEntries.end(); ++it) {
1387 if (((*it).m_mode == INotifyMode || (*it).m_mode == QFSWatchMode) && (*it).dirty) {
1388 (*it).propagate_dirty();
1389 }
1390 }
1391 }
1392
1393#if HAVE_SYS_INOTIFY_H
1394 QList<Entry *> cList;
1395#endif
1396
1397 it = m_mapEntries.begin();
1398 for (; it != m_mapEntries.end(); ++it) {
1399 // we don't check invalid entries (i.e. remove delayed)
1400 Entry *entry = &(*it);
1401 if (!entry->isValid()) {
1402 continue;
1403 }
1404
1405 const int ev = scanEntry(entry);
1406 if (s_verboseDebug) {
1407 qCDebug(KDIRWATCH) << "scanEntry for" << entry->path << "says" << ev;
1408 }
1409
1410 switch (entry->m_mode) {
1411#if HAVE_SYS_INOTIFY_H
1412 case INotifyMode:
1413 if (ev == Deleted) {
1414 if (s_verboseDebug) {
1415 qCDebug(KDIRWATCH) << "scanEntry says" << entry->path << "was deleted";
1416 }
1417 addEntry(nullptr, entry->parentDirectory(), entry, true);
1418 } else if (ev == Created) {
1419 if (s_verboseDebug) {
1420 qCDebug(KDIRWATCH) << "scanEntry says" << entry->path << "was created. wd=" << entry->wd;
1421 }
1422 if (entry->wd < 0) {
1423 cList.append(entry);
1424 addWatch(entry);
1425 }
1426 }
1427 break;
1428#endif
1429 case QFSWatchMode:
1430 if (ev == Created) {
1431 addWatch(entry);
1432 }
1433 break;
1434 default:
1435 // dunno about StatMode...
1436 break;
1437 }
1438
1439#if HAVE_SYS_INOTIFY_H
1440 if (entry->isDir) {
1441 // Report and clear the list of files that have changed in this directory.
1442 // Remove duplicates by changing to set and back again:
1443 // we don't really care about preserving the order of the
1444 // original changes.
1445 QStringList pendingFileChanges = entry->m_pendingFileChanges;
1446 pendingFileChanges.removeDuplicates();
1447 for (const QString &changedFilename : std::as_const(pendingFileChanges)) {
1448 if (s_verboseDebug) {
1449 qCDebug(KDIRWATCH) << "processing pending file change for" << changedFilename;
1450 }
1451 emitEvent(entry, Changed, changedFilename);
1452 }
1453 entry->m_pendingFileChanges.clear();
1454 }
1455#endif
1456
1457 if (ev != NoChange) {
1458 emitEvent(entry, ev);
1459 }
1460 }
1461
1462 if (timerRunning) {
1463 m_statRescanTimer.start(freq);
1464 }
1465
1466#if HAVE_SYS_INOTIFY_H
1467 // Remove watch of parent of new created directories
1468 for (Entry *e : std::as_const(cList)) {
1469 removeEntry(nullptr, e->parentDirectory(), e);
1470 }
1471#endif
1472
1473 QTimer::singleShot(0, this, &KDirWatchPrivate::slotRemoveDelayed);
1474}
1475
1476bool KDirWatchPrivate::isNoisyFile(const char *filename)
1477{
1478 // $HOME/.X.err grows with debug output, so don't notify change
1479 if (*filename == '.') {
1480 if (strncmp(filename, ".X.err", 6) == 0) {
1481 return true;
1482 }
1483 if (strncmp(filename, ".xsession-errors", 16) == 0) {
1484 return true;
1485 }
1486 // fontconfig updates the cache on every KDE app start
1487 // as well as during kio_thumbnail worker execution
1488 // TODO:; check which fontconfig version this file was deprecated and the check can be removed
1489 if (strncmp(filename, ".fonts.cache", 12) == 0) {
1490 return true;
1491 }
1492 }
1493
1494 return false;
1495}
1496
1497void KDirWatchPrivate::ref(KDirWatch *watch)
1498{
1499 m_referencesObjects.push_back(watch);
1500}
1501
1502void KDirWatchPrivate::unref(KDirWatch *watch)
1503{
1504 m_referencesObjects.removeOne(watch);
1505 if (m_referencesObjects.isEmpty()) {
1506 destroyPrivate();
1507 }
1508}
1509
1510#if HAVE_SYS_INOTIFY_H
1511QString KDirWatchPrivate::inotifyEventName(const inotify_event *event) const
1512{
1513 if (event->mask & IN_OPEN)
1514 return QStringLiteral("OPEN");
1515 else if (event->mask & IN_CLOSE_NOWRITE)
1516 return QStringLiteral("CLOSE_NOWRITE");
1517 else if (event->mask & IN_CLOSE_WRITE)
1518 return QStringLiteral("CLOSE_WRITE");
1519 else if (event->mask & IN_MOVED_TO)
1520 return QStringLiteral("MOVED_TO");
1521 else if (event->mask & IN_MOVED_FROM)
1522 return QStringLiteral("MOVED_FROM");
1523 else if (event->mask & IN_MOVE)
1524 return QStringLiteral("MOVE");
1525 else if (event->mask & IN_CREATE)
1526 return QStringLiteral("CREATE");
1527 else if (event->mask & IN_DELETE)
1528 return QStringLiteral("DELETE");
1529 else if (event->mask & IN_DELETE_SELF)
1530 return QStringLiteral("DELETE_SELF");
1531 else if (event->mask & IN_MOVE_SELF)
1532 return QStringLiteral("MOVE_SELF");
1533 else if (event->mask & IN_ATTRIB)
1534 return QStringLiteral("ATTRIB");
1535 else if (event->mask & IN_MODIFY)
1536 return QStringLiteral("MODIFY");
1537 if (event->mask & IN_ACCESS)
1538 return QStringLiteral("ACCESS");
1539 if (event->mask & IN_IGNORED)
1540 return QStringLiteral("IGNORED");
1541 if (event->mask & IN_UNMOUNT)
1542 return QStringLiteral("IN_UNMOUNT");
1543 else
1544 return QStringLiteral("UNKWOWN");
1545}
1546#endif
1547
1548#if HAVE_QFILESYSTEMWATCHER
1549// Slot for QFileSystemWatcher
1550void KDirWatchPrivate::fswEventReceived(const QString &path)
1551{
1552 if (s_verboseDebug) {
1553 qCDebug(KDIRWATCH) << path;
1554 }
1555
1556 auto it = m_mapEntries.find(path);
1557 if (it != m_mapEntries.end()) {
1558 Entry *entry = &it.value();
1559 entry->dirty = true;
1560 const int ev = scanEntry(entry);
1561 if (s_verboseDebug) {
1562 qCDebug(KDIRWATCH) << "scanEntry for" << entry->path << "says" << ev;
1563 }
1564 if (ev != NoChange) {
1565 emitEvent(entry, ev);
1566 }
1567 if (ev == Deleted) {
1568 if (entry->isDir) {
1569 addEntry(nullptr, entry->parentDirectory(), entry, true);
1570 } else {
1571 addEntry(nullptr, QFileInfo(entry->path).absolutePath(), entry, true);
1572 }
1573 } else if (ev == Created) {
1574 // We were waiting for it to appear; now watch it
1575 addWatch(entry);
1576 } else if (entry->isDir) {
1577 // Check if any file or dir was created under this directory, that we were waiting for
1578 for (Entry *sub_entry : std::as_const(entry->m_entries)) {
1579 fswEventReceived(sub_entry->path); // recurse, to call scanEntry and see if something changed
1580 }
1581 } else {
1582 /* Even though QFileSystemWatcher only reported the file as modified, it is possible that the file
1583 * was in fact just deleted and then immediately recreated. If the file was deleted, QFileSystemWatcher
1584 * will delete the watch, and will ignore the file, even after it is recreated. Since it is impossible
1585 * to reliably detect this case, always re-request the watch on a dirty signal, to avoid losing the
1586 * underlying OS monitor.
1587 */
1588 fsWatcher->addPath(entry->path);
1589 }
1590 }
1591}
1592#else
1593void KDirWatchPrivate::fswEventReceived(const QString &path)
1594{
1595 Q_UNUSED(path);
1596 qCWarning(KCOREADDONS_DEBUG) << "QFileSystemWatcher event received but QFileSystemWatcher is not supported";
1597}
1598#endif // HAVE_QFILESYSTEMWATCHER
1599
1600//
1601// Class KDirWatch
1602//
1603
1604Q_GLOBAL_STATIC(KDirWatch, s_pKDirWatchSelf)
1606{
1607 return s_pKDirWatchSelf();
1608}
1609
1610// <steve> is this used anywhere?
1611// <dfaure> yes, see kio/src/core/kcoredirlister_p.h:328
1613{
1614 return s_pKDirWatchSelf.exists() && dwp_self.hasLocalData();
1615}
1616
1618 : QObject(parent)
1619 , d(createPrivate())
1620{
1621 d->ref(this);
1622 static QBasicAtomicInt nameCounter = Q_BASIC_ATOMIC_INITIALIZER(1);
1623 const int counter = nameCounter.fetchAndAddRelaxed(1); // returns the old value
1624 setObjectName(QStringLiteral("KDirWatch-%1").arg(counter));
1625}
1626
1628{
1629 if (d) {
1630 d->removeEntries(this);
1631 d->unref(this);
1632 }
1633}
1634
1635void KDirWatch::addDir(const QString &_path, WatchModes watchModes)
1636{
1637 if (KNetworkMounts::self()->isOptionEnabledForPath(_path, KNetworkMounts::KDirWatchDontAddWatches)) {
1638 return;
1639 }
1640
1641 if (d) {
1642 d->addEntry(this, _path, nullptr, true, watchModes);
1643 }
1644}
1645
1646void KDirWatch::addFile(const QString &_path)
1647{
1648 if (KNetworkMounts::self()->isOptionEnabledForPath(_path, KNetworkMounts::KDirWatchDontAddWatches)) {
1649 return;
1650 }
1651
1652 if (!d) {
1653 return;
1654 }
1655
1656 d->addEntry(this, _path, nullptr, false);
1657}
1658
1660{
1661 KDirWatchPrivate::Entry *e = d->entry(_path);
1662
1663 if (!e) {
1664 return QDateTime();
1665 }
1666
1667 return QDateTime::fromSecsSinceEpoch(e->m_ctime);
1668}
1669
1671{
1672 if (d) {
1673 d->removeEntry(this, _path, nullptr);
1674 }
1675}
1676
1678{
1679 if (d) {
1680 d->removeEntry(this, _path, nullptr);
1681 }
1682}
1683
1685{
1686 if (d) {
1687 KDirWatchPrivate::Entry *e = d->entry(_path);
1688 if (e && e->isDir) {
1689 return d->stopEntryScan(this, e);
1690 }
1691 }
1692 return false;
1693}
1694
1696{
1697 if (d) {
1698 KDirWatchPrivate::Entry *e = d->entry(_path);
1699 if (e && e->isDir)
1700 // restart without notifying pending events
1701 {
1702 return d->restartEntryScan(this, e, false);
1703 }
1704 }
1705 return false;
1706}
1707
1709{
1710 if (d) {
1711 d->stopScan(this);
1712 d->_isStopped = true;
1713 }
1714}
1715
1717{
1718 return d->_isStopped;
1719}
1720
1721void KDirWatch::startScan(bool notify, bool skippedToo)
1722{
1723 if (d) {
1724 d->_isStopped = false;
1725 d->startScan(this, notify, skippedToo);
1726 }
1727}
1728
1729bool KDirWatch::contains(const QString &_path) const
1730{
1731 KDirWatchPrivate::Entry *e = d->entry(_path);
1732 if (!e) {
1733 return false;
1734 }
1735
1736 for (const KDirWatchPrivate::Client &client : e->m_clients) {
1737 if (client.instance == this) {
1738 return true;
1739 }
1740 }
1741
1742 return false;
1743}
1744
1746{
1747 qCDebug(KDIRWATCH) << objectName() << "emitting created" << _file;
1748 Q_EMIT created(_file);
1749}
1750
1752{
1753 Q_EMIT dirty(_file);
1754}
1755
1757{
1758 qCDebug(KDIRWATCH) << objectName() << "emitting deleted" << _file;
1759 Q_EMIT deleted(_file);
1760}
1761
1762KDirWatch::Method KDirWatch::internalMethod() const
1763{
1764 // This reproduces the logic in KDirWatchPrivate::addWatch
1765 switch (d->m_preferredMethod) {
1766 case KDirWatch::INotify:
1767#if HAVE_SYS_INOTIFY_H
1768 if (d->supports_inotify) {
1769 return KDirWatch::INotify;
1770 }
1771#endif
1772 break;
1773 case KDirWatch::QFSWatch:
1774#if HAVE_QFILESYSTEMWATCHER
1775 return KDirWatch::QFSWatch;
1776#else
1777 break;
1778#endif
1779 case KDirWatch::Stat:
1780 return KDirWatch::Stat;
1781 }
1782
1783#if HAVE_SYS_INOTIFY_H
1784 if (d->supports_inotify) {
1785 return KDirWatch::INotify;
1786 }
1787#endif
1788#if HAVE_QFILESYSTEMWATCHER
1789 return KDirWatch::QFSWatch;
1790#else
1791 return KDirWatch::Stat;
1792#endif
1793}
1794
1796{
1797 if (Q_LIKELY(event->type() != QEvent::ThreadChange)) {
1798 return QObject::event(event);
1799 }
1800
1801 qCCritical(KDIRWATCH) << "KDirwatch is moving its thread. This is not supported at this time; your watch will not watch anything anymore!"
1802 << "Create and use watches on the correct thread"
1803 << "Watch:" << this;
1804
1805 // We are still in the old thread when the event runs, so this is safe.
1806 Q_ASSERT(thread() == d->thread());
1807 d->removeEntries(this);
1808 d->unref(this);
1809 d = nullptr;
1810
1811 // Schedule the creation of the new private in the new thread.
1813 this,
1814 [this] {
1815 d = createPrivate();
1816 },
1818
1819 // NOTE: to actually support moving watches across threads we'd have to make Entry copyable and schedule a complete
1820 // re-installation of watches on the new thread after createPrivate.
1821
1822 return QObject::event(event);
1823}
1824
1825#include "moc_kdirwatch.cpp"
1826#include "moc_kdirwatch_p.cpp"
1827
1828// 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::LchaDouble &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
T value(qsizetype i) 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 & append(QChar ch)
QString arg(Args &&... args) const const
void chop(qsizetype n)
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString & insert(qsizetype position, QChar ch)
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 Sat Apr 27 2024 22:07:13 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.