| 1 | // Copyright (C) 2016 The Qt Company Ltd. | 
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only | 
| 3 |  | 
| 4 | #include "qfilesystemwatcher.h" | 
| 5 | #include "qfilesystemwatcher_p.h" | 
| 6 |  | 
| 7 | #include <qdatetime.h> | 
| 8 | #include <qdir.h> | 
| 9 | #include <qfileinfo.h> | 
| 10 | #include <qloggingcategory.h> | 
| 11 | #include <qset.h> | 
| 12 | #include <qtimer.h> | 
| 13 |  | 
| 14 | #if (defined(Q_OS_LINUX) || defined(Q_OS_QNX)) && QT_CONFIG(inotify) | 
| 15 | #define USE_INOTIFY | 
| 16 | #endif | 
| 17 |  | 
| 18 | #include "qfilesystemwatcher_polling_p.h" | 
| 19 | #if defined(Q_OS_WIN) | 
| 20 | #  include "qfilesystemwatcher_win_p.h" | 
| 21 | #elif defined(USE_INOTIFY) | 
| 22 | #  include "qfilesystemwatcher_inotify_p.h" | 
| 23 | #elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) || defined(Q_OS_OPENBSD) || defined(QT_PLATFORM_UIKIT) | 
| 24 | #  include "qfilesystemwatcher_kqueue_p.h" | 
| 25 | #elif defined(Q_OS_MACOS) | 
| 26 | #  include "qfilesystemwatcher_fsevents_p.h" | 
| 27 | #endif | 
| 28 |  | 
| 29 | #include <algorithm> | 
| 30 | #include <iterator> | 
| 31 |  | 
| 32 | QT_BEGIN_NAMESPACE | 
| 33 |  | 
| 34 | using namespace Qt::StringLiterals; | 
| 35 |  | 
| 36 | Q_LOGGING_CATEGORY(lcWatcher, "qt.core.filesystemwatcher" ) | 
| 37 |  | 
| 38 | QFileSystemWatcherEngine *QFileSystemWatcherPrivate::createNativeEngine(QObject *parent) | 
| 39 | { | 
| 40 | #if defined(Q_OS_WIN) | 
| 41 |     return new QWindowsFileSystemWatcherEngine(parent); | 
| 42 | #elif defined(USE_INOTIFY) | 
| 43 |     // there is a chance that inotify may fail on Linux pre-2.6.13 (August | 
| 44 |     // 2005), so we can't just new inotify directly. | 
| 45 |     return QInotifyFileSystemWatcherEngine::create(parent); | 
| 46 | #elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) || defined(Q_OS_OPENBSD) || defined(QT_PLATFORM_UIKIT) | 
| 47 |     return QKqueueFileSystemWatcherEngine::create(parent); | 
| 48 | #elif defined(Q_OS_MACOS) | 
| 49 |     return QFseventsFileSystemWatcherEngine::create(parent); | 
| 50 | #else | 
| 51 |     Q_UNUSED(parent); | 
| 52 |     return 0; | 
| 53 | #endif | 
| 54 | } | 
| 55 |  | 
| 56 | QFileSystemWatcherPrivate::QFileSystemWatcherPrivate() | 
| 57 |     : native(nullptr), poller(nullptr) | 
| 58 | { | 
| 59 | } | 
| 60 |  | 
| 61 | void QFileSystemWatcherPrivate::connectEngine(QFileSystemWatcherEngine *engine) | 
| 62 | { | 
| 63 |     QObjectPrivate::connect(sender: engine, signal: &QFileSystemWatcherEngine::fileChanged, | 
| 64 |                             receiverPrivate: this, slot: &QFileSystemWatcherPrivate::fileChanged); | 
| 65 |     QObjectPrivate::connect(sender: engine, signal: &QFileSystemWatcherEngine::directoryChanged, | 
| 66 |                             receiverPrivate: this, slot: &QFileSystemWatcherPrivate::directoryChanged); | 
| 67 | } | 
| 68 |  | 
| 69 | void QFileSystemWatcherPrivate::init() | 
| 70 | { | 
| 71 |     Q_Q(QFileSystemWatcher); | 
| 72 |     native = createNativeEngine(parent: q); | 
| 73 |     if (native) { | 
| 74 |         connectEngine(engine: native); | 
| 75 | #if defined(Q_OS_WIN) | 
| 76 |         auto *windowsWatcher = static_cast<QWindowsFileSystemWatcherEngine *>(native); | 
| 77 |         using WinE = QWindowsFileSystemWatcherEngine; | 
| 78 |         QObjectPrivate::connect(windowsWatcher, &WinE::driveLockForRemoval, | 
| 79 |                                 this, &QFileSystemWatcherPrivate::winDriveLockForRemoval); | 
| 80 |         QObjectPrivate::connect(windowsWatcher, &WinE::driveLockForRemovalFailed, | 
| 81 |                                 this, &QFileSystemWatcherPrivate::winDriveLockForRemovalFailed); | 
| 82 |         QObjectPrivate::connect(windowsWatcher, &WinE::driveRemoved, | 
| 83 |                                 this, &QFileSystemWatcherPrivate::winDriveRemoved); | 
| 84 | #endif  // Q_OS_WIN | 
| 85 |     } | 
| 86 | } | 
| 87 |  | 
| 88 | void QFileSystemWatcherPrivate::initPollerEngine() | 
| 89 | { | 
| 90 |     if (poller) | 
| 91 |         return; | 
| 92 |  | 
| 93 |     Q_Q(QFileSystemWatcher); | 
| 94 |     poller = new QPollingFileSystemWatcherEngine(q); // that was a mouthful | 
| 95 |     connectEngine(engine: poller); | 
| 96 | } | 
| 97 |  | 
| 98 | void QFileSystemWatcherPrivate::fileChanged(const QString &path, bool removed) | 
| 99 | { | 
| 100 |     Q_Q(QFileSystemWatcher); | 
| 101 |     qCDebug(lcWatcher) << "file changed"  << path << "removed?"  << removed << "watching?"  << files.contains(str: path); | 
| 102 |     if (!files.contains(str: path)) { | 
| 103 |         // the path was removed after a change was detected, but before we delivered the signal | 
| 104 |         return; | 
| 105 |     } | 
| 106 |     if (removed) | 
| 107 |         files.removeAll(t: path); | 
| 108 |     emit q->fileChanged(path, QFileSystemWatcher::QPrivateSignal()); | 
| 109 | } | 
| 110 |  | 
| 111 | void QFileSystemWatcherPrivate::directoryChanged(const QString &path, bool removed) | 
| 112 | { | 
| 113 |     Q_Q(QFileSystemWatcher); | 
| 114 |     qCDebug(lcWatcher) << "directory changed"  << path << "removed?"  << removed << "watching?"  << directories.contains(str: path); | 
| 115 |     if (!directories.contains(str: path)) { | 
| 116 |         // perhaps the path was removed after a change was detected, but before we delivered the signal | 
| 117 |         return; | 
| 118 |     } | 
| 119 |     if (removed) | 
| 120 |         directories.removeAll(t: path); | 
| 121 |     emit q->directoryChanged(path, QFileSystemWatcher::QPrivateSignal()); | 
| 122 | } | 
| 123 |  | 
| 124 | #if defined(Q_OS_WIN) | 
| 125 |  | 
| 126 | void QFileSystemWatcherPrivate::winDriveLockForRemoval(const QString &path) | 
| 127 | { | 
| 128 |     // Windows: Request to lock a (removable/USB) drive for removal, release | 
| 129 |     // its paths under watch, temporarily storing them should the lock fail. | 
| 130 |     Q_Q(QFileSystemWatcher); | 
| 131 |     QStringList pathsToBeRemoved; | 
| 132 |     auto pred = [&path] (const QString &f) { return !f.startsWith(path, Qt::CaseInsensitive); }; | 
| 133 |     std::remove_copy_if(files.cbegin(), files.cend(), | 
| 134 |                         std::back_inserter(pathsToBeRemoved), pred); | 
| 135 |     std::remove_copy_if(directories.cbegin(), directories.cend(), | 
| 136 |                         std::back_inserter(pathsToBeRemoved), pred); | 
| 137 |     if (!pathsToBeRemoved.isEmpty()) { | 
| 138 |         q->removePaths(pathsToBeRemoved); | 
| 139 |         temporarilyRemovedPaths.insert(path.at(0), pathsToBeRemoved); | 
| 140 |     } | 
| 141 | } | 
| 142 |  | 
| 143 | void QFileSystemWatcherPrivate::winDriveLockForRemovalFailed(const QString &path) | 
| 144 | { | 
| 145 |     // Windows: Request to lock a (removable/USB) drive failed (blocked by other | 
| 146 |     // application), restore the watched paths. | 
| 147 |     Q_Q(QFileSystemWatcher); | 
| 148 |     if (!path.isEmpty()) { | 
| 149 |         const auto it = temporarilyRemovedPaths.find(path.at(0)); | 
| 150 |         if (it != temporarilyRemovedPaths.end()) { | 
| 151 |             q->addPaths(it.value()); | 
| 152 |             temporarilyRemovedPaths.erase(it); | 
| 153 |         } | 
| 154 |     } | 
| 155 | } | 
| 156 |  | 
| 157 | void  QFileSystemWatcherPrivate::winDriveRemoved(const QString &path) | 
| 158 | { | 
| 159 |     // Windows: Drive finally removed, clear out paths stored in lock request. | 
| 160 |     if (!path.isEmpty()) | 
| 161 |         temporarilyRemovedPaths.remove(path.at(0)); | 
| 162 | } | 
| 163 | #endif // Q_OS_WIN | 
| 164 |  | 
| 165 | /*! | 
| 166 |     \class QFileSystemWatcher | 
| 167 |     \inmodule QtCore | 
| 168 |     \brief The QFileSystemWatcher class provides an interface for monitoring files and directories for modifications. | 
| 169 |     \ingroup io | 
| 170 |     \since 4.2 | 
| 171 |     \reentrant | 
| 172 |  | 
| 173 |     QFileSystemWatcher monitors the file system for changes to files | 
| 174 |     and directories by watching a list of specified paths. | 
| 175 |  | 
| 176 |     Call addPath() to watch a particular file or directory. Multiple | 
| 177 |     paths can be added using the addPaths() function. Existing paths can | 
| 178 |     be removed by using the removePath() and removePaths() functions. | 
| 179 |  | 
| 180 |     QFileSystemWatcher examines each path added to it. Files that have | 
| 181 |     been added to the QFileSystemWatcher can be accessed using the | 
| 182 |     files() function, and directories using the directories() function. | 
| 183 |  | 
| 184 |     The fileChanged() signal is emitted when a file has been modified, | 
| 185 |     renamed or removed from disk. Similarly, the directoryChanged() | 
| 186 |     signal is emitted when a directory or its contents is modified or | 
| 187 |     removed.  Note that QFileSystemWatcher stops monitoring files once | 
| 188 |     they have been renamed or removed from disk, and directories once | 
| 189 |     they have been removed from disk. | 
| 190 |  | 
| 191 |     \list | 
| 192 |     \li \b Notes: | 
| 193 |     \list | 
| 194 |         \li On systems running a Linux kernel without inotify support, | 
| 195 |         file systems that contain watched paths cannot be unmounted. | 
| 196 |  | 
| 197 |          \li The act of monitoring files and directories for | 
| 198 |          modifications consumes system resources. This implies there is a | 
| 199 |          limit to the number of files and directories your process can | 
| 200 |          monitor simultaneously. On all BSD variants, for | 
| 201 |          example, an open file descriptor is required for each monitored | 
| 202 |          file. Some system limits the number of open file descriptors to 256 | 
| 203 |          by default. This means that addPath() and addPaths() will fail if | 
| 204 |          your process tries to add more than 256 files or directories to | 
| 205 |          the file system monitor. Also note that your process may have | 
| 206 |          other file descriptors open in addition to the ones for files | 
| 207 |          being monitored, and these other open descriptors also count in | 
| 208 |          the total. \macos uses a different backend and does not | 
| 209 |          suffer from this issue. | 
| 210 |     \endlist | 
| 211 |     \endlist | 
| 212 |  | 
| 213 |     \sa QFile, QDir | 
| 214 | */ | 
| 215 |  | 
| 216 |  | 
| 217 | /*! | 
| 218 |     Constructs a new file system watcher object with the given \a parent. | 
| 219 | */ | 
| 220 | QFileSystemWatcher::QFileSystemWatcher(QObject *parent) | 
| 221 |     : QObject(*new QFileSystemWatcherPrivate, parent) | 
| 222 | { | 
| 223 |     d_func()->init(); | 
| 224 | } | 
| 225 |  | 
| 226 | /*! | 
| 227 |     Constructs a new file system watcher object with the given \a parent | 
| 228 |     which monitors the specified \a paths list. | 
| 229 | */ | 
| 230 | QFileSystemWatcher::QFileSystemWatcher(const QStringList &paths, QObject *parent) | 
| 231 |     : QObject(*new QFileSystemWatcherPrivate, parent) | 
| 232 | { | 
| 233 |     d_func()->init(); | 
| 234 |     addPaths(files: paths); | 
| 235 | } | 
| 236 |  | 
| 237 | /*! | 
| 238 |     Destroys the file system watcher. | 
| 239 | */ | 
| 240 | QFileSystemWatcher::~QFileSystemWatcher() | 
| 241 | { } | 
| 242 |  | 
| 243 | /*! | 
| 244 |     Adds \a path to the file system watcher if \a path exists. The | 
| 245 |     path is not added if it does not exist, or if it is already being | 
| 246 |     monitored by the file system watcher. | 
| 247 |  | 
| 248 |     If \a path specifies a directory, the directoryChanged() signal | 
| 249 |     will be emitted when \a path is modified or removed from disk; | 
| 250 |     otherwise the fileChanged() signal is emitted when \a path is | 
| 251 |     modified, renamed or removed. | 
| 252 |  | 
| 253 |     If the watch was successful, true is returned. | 
| 254 |  | 
| 255 |     Reasons for a watch failure are generally system-dependent, but | 
| 256 |     may include the resource not existing, access failures, or the | 
| 257 |     total watch count limit, if the platform has one. | 
| 258 |  | 
| 259 |     \note There may be a system dependent limit to the number of | 
| 260 |     files and directories that can be monitored simultaneously. | 
| 261 |     If this limit is been reached, \a path will not be monitored, | 
| 262 |     and false is returned. | 
| 263 |  | 
| 264 |     \sa addPaths(), removePath() | 
| 265 | */ | 
| 266 | bool QFileSystemWatcher::addPath(const QString &path) | 
| 267 | { | 
| 268 |     if (path.isEmpty()) { | 
| 269 |         qWarning(msg: "QFileSystemWatcher::addPath: path is empty" ); | 
| 270 |         return true; | 
| 271 |     } | 
| 272 |  | 
| 273 |     QStringList paths = addPaths(files: QStringList(path)); | 
| 274 |     return paths.isEmpty(); | 
| 275 | } | 
| 276 |  | 
| 277 | static QStringList empty_paths_pruned(const QStringList &paths) | 
| 278 | { | 
| 279 |     QStringList p; | 
| 280 |     p.reserve(asize: paths.size()); | 
| 281 |     const auto isEmpty = [](const QString &s) { return s.isEmpty(); }; | 
| 282 |     std::remove_copy_if(first: paths.begin(), last: paths.end(), | 
| 283 |                         result: std::back_inserter(x&: p), | 
| 284 |                         pred: isEmpty); | 
| 285 |     return p; | 
| 286 | } | 
| 287 |  | 
| 288 | /*! | 
| 289 |     Adds each path in \a paths to the file system watcher. Paths are | 
| 290 |     not added if they not exist, or if they are already being | 
| 291 |     monitored by the file system watcher. | 
| 292 |  | 
| 293 |     If a path specifies a directory, the directoryChanged() signal | 
| 294 |     will be emitted when the path is modified or removed from disk; | 
| 295 |     otherwise the fileChanged() signal is emitted when the path is | 
| 296 |     modified, renamed, or removed. | 
| 297 |  | 
| 298 |     The return value is a list of paths that could not be watched. | 
| 299 |  | 
| 300 |     Reasons for a watch failure are generally system-dependent, but | 
| 301 |     may include the resource not existing, access failures, or the | 
| 302 |     total watch count limit, if the platform has one. | 
| 303 |  | 
| 304 |     \note There may be a system dependent limit to the number of | 
| 305 |     files and directories that can be monitored simultaneously. | 
| 306 |     If this limit has been reached, the excess \a paths will not | 
| 307 |     be monitored, and they will be added to the returned QStringList. | 
| 308 |  | 
| 309 |     \sa addPath(), removePaths() | 
| 310 | */ | 
| 311 | QStringList QFileSystemWatcher::addPaths(const QStringList &paths) | 
| 312 | { | 
| 313 |     Q_D(QFileSystemWatcher); | 
| 314 |  | 
| 315 |     QStringList p = empty_paths_pruned(paths); | 
| 316 |  | 
| 317 |     if (p.isEmpty()) { | 
| 318 |         qWarning(msg: "QFileSystemWatcher::addPaths: list is empty" ); | 
| 319 |         return p; | 
| 320 |     } | 
| 321 |     qCDebug(lcWatcher) << "adding"  << paths; | 
| 322 |     const auto selectEngine = [this, d]() -> QFileSystemWatcherEngine* { | 
| 323 | #ifdef QT_BUILD_INTERNAL | 
| 324 |         const QString on = objectName(); | 
| 325 |  | 
| 326 |         if (Q_UNLIKELY(on.startsWith("_qt_autotest_force_engine_"_L1 ))) { | 
| 327 |             // Autotest override case - use the explicitly selected engine only | 
| 328 |             const auto forceName = QStringView{on}.mid(pos: 26); | 
| 329 |             if (forceName == "poller"_L1 ) { | 
| 330 |                 qCDebug(lcWatcher, "QFileSystemWatcher: skipping native engine, using only polling engine" ); | 
| 331 |                 d_func()->initPollerEngine(); | 
| 332 |                 return d->poller; | 
| 333 |             } else if (forceName == "native"_L1 ) { | 
| 334 |                 qCDebug(lcWatcher, "QFileSystemWatcher: skipping polling engine, using only native engine" ); | 
| 335 |                 return d->native; | 
| 336 |             } | 
| 337 |             return nullptr; | 
| 338 |         } | 
| 339 | #endif | 
| 340 |         // Normal runtime case - search intelligently for best engine | 
| 341 |         if (d->native) { | 
| 342 |             return d->native; | 
| 343 |         } else { | 
| 344 |             d_func()->initPollerEngine(); | 
| 345 |             return d->poller; | 
| 346 |         } | 
| 347 |     }; | 
| 348 |  | 
| 349 |     if (auto engine = selectEngine()) | 
| 350 |         p = engine->addPaths(paths: p, files: &d->files, directories: &d->directories); | 
| 351 |  | 
| 352 |     return p; | 
| 353 | } | 
| 354 |  | 
| 355 | /*! | 
| 356 |     Removes the specified \a path from the file system watcher. | 
| 357 |  | 
| 358 |     If the watch is successfully removed, true is returned. | 
| 359 |  | 
| 360 |     Reasons for watch removal failing are generally system-dependent, | 
| 361 |     but may be due to the path having already been deleted, for example. | 
| 362 |  | 
| 363 |     \sa removePaths(), addPath() | 
| 364 | */ | 
| 365 | bool QFileSystemWatcher::removePath(const QString &path) | 
| 366 | { | 
| 367 |     if (path.isEmpty()) { | 
| 368 |         qWarning(msg: "QFileSystemWatcher::removePath: path is empty" ); | 
| 369 |         return true; | 
| 370 |     } | 
| 371 |  | 
| 372 |     QStringList paths = removePaths(files: QStringList(path)); | 
| 373 |     return paths.isEmpty(); | 
| 374 | } | 
| 375 |  | 
| 376 | /*! | 
| 377 |     Removes the specified \a paths from the file system watcher. | 
| 378 |  | 
| 379 |     The return value is a list of paths which were not able to be | 
| 380 |     unwatched successfully. | 
| 381 |  | 
| 382 |     Reasons for watch removal failing are generally system-dependent, | 
| 383 |     but may be due to the path having already been deleted, for example. | 
| 384 |  | 
| 385 |     \sa removePath(), addPaths() | 
| 386 | */ | 
| 387 | QStringList QFileSystemWatcher::removePaths(const QStringList &paths) | 
| 388 | { | 
| 389 |     Q_D(QFileSystemWatcher); | 
| 390 |  | 
| 391 |     QStringList p = empty_paths_pruned(paths); | 
| 392 |  | 
| 393 |     if (p.isEmpty()) { | 
| 394 |         qWarning(msg: "QFileSystemWatcher::removePaths: list is empty" ); | 
| 395 |         return p; | 
| 396 |     } | 
| 397 |     qCDebug(lcWatcher) << "removing"  << paths; | 
| 398 |  | 
| 399 |     if (d->native) | 
| 400 |         p = d->native->removePaths(paths: p, files: &d->files, directories: &d->directories); | 
| 401 |     if (d->poller) | 
| 402 |         p = d->poller->removePaths(paths: p, files: &d->files, directories: &d->directories); | 
| 403 |  | 
| 404 |     return p; | 
| 405 | } | 
| 406 |  | 
| 407 | /*! | 
| 408 |     \fn void QFileSystemWatcher::fileChanged(const QString &path) | 
| 409 |  | 
| 410 |     This signal is emitted when the file at the specified \a path is | 
| 411 |     modified, renamed or removed from disk. | 
| 412 |  | 
| 413 |     \note As a safety measure, many applications save an open file by | 
| 414 |     writing a new file and then deleting the old one. In your slot | 
| 415 |     function, you can check \c watcher.files().contains(path). | 
| 416 |     If it returns \c false, check whether the file still exists | 
| 417 |     and then call \c addPath() to continue watching it. | 
| 418 |  | 
| 419 |     \sa directoryChanged() | 
| 420 | */ | 
| 421 |  | 
| 422 | /*! | 
| 423 |     \fn void QFileSystemWatcher::directoryChanged(const QString &path) | 
| 424 |  | 
| 425 |     This signal is emitted when the directory at a specified \a path | 
| 426 |     is modified (e.g., when a file is added or deleted) or removed | 
| 427 |     from disk. Note that if there are several changes during a short | 
| 428 |     period of time, some of the changes might not emit this signal. | 
| 429 |     However, the last change in the sequence of changes will always | 
| 430 |     generate this signal. | 
| 431 |  | 
| 432 |     \sa fileChanged() | 
| 433 | */ | 
| 434 |  | 
| 435 | /*! | 
| 436 |     \fn QStringList QFileSystemWatcher::directories() const | 
| 437 |  | 
| 438 |     Returns a list of paths to directories that are being watched. | 
| 439 |  | 
| 440 |     \sa files() | 
| 441 | */ | 
| 442 |  | 
| 443 | /*! | 
| 444 |     \fn QStringList QFileSystemWatcher::files() const | 
| 445 |  | 
| 446 |     Returns a list of paths to files that are being watched. | 
| 447 |  | 
| 448 |     \sa directories() | 
| 449 | */ | 
| 450 |  | 
| 451 | QStringList QFileSystemWatcher::directories() const | 
| 452 | { | 
| 453 |     Q_D(const QFileSystemWatcher); | 
| 454 |     return d->directories; | 
| 455 | } | 
| 456 |  | 
| 457 | QStringList QFileSystemWatcher::files() const | 
| 458 | { | 
| 459 |     Q_D(const QFileSystemWatcher); | 
| 460 |     return d->files; | 
| 461 | } | 
| 462 |  | 
| 463 | QT_END_NAMESPACE | 
| 464 |  | 
| 465 | #include "moc_qfilesystemwatcher.cpp" | 
| 466 | #include "moc_qfilesystemwatcher_p.cpp" | 
| 467 |  | 
| 468 |  |