| 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 | |