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