Revision control

++ // Copyright (C) 2016 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++ #include "qfilesystemwatcher.h"
++ #include "qfilesystemwatcher_p.h"
++
++ #include <qdatetime.h>
++ #include <qdir.h>
++ #include <qfileinfo.h>
++ #include <qloggingcategory.h>
++ #include <qset.h>
++
++ #if (defined(Q_OS_LINUX) || defined(Q_OS_QNX) || defined(Q_OS_FREEBSD)) && QT_CONFIG(inotify)
++ # define USE_INOTIFY
++ #endif
++
++ #include "qfilesystemwatcher_polling_p.h"
++ #if defined(Q_OS_WIN)
++ # include "qfilesystemwatcher_win_p.h"
++ #elif defined(USE_INOTIFY)
++ # include "qfilesystemwatcher_inotify_p.h"
++ #elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) || defined(Q_OS_OPENBSD) || defined(QT_PLATFORM_UIKIT)
++ # include "qfilesystemwatcher_kqueue_p.h"
++ #elif defined(Q_OS_MACOS)
++ # include "qfilesystemwatcher_fsevents_p.h"
++ #endif
++
++ #include <algorithm>
++ #include <iterator>
++
++ QT_BEGIN_NAMESPACE
++
++ using namespace Qt::StringLiterals;
++
++ Q_STATIC_LOGGING_CATEGORY(lcWatcher, "qt.core.filesystemwatcher")
++
++ QFileSystemWatcherEngine *QFileSystemWatcherPrivate::createNativeEngine(QObject *parent)
++ {
++ #if defined(Q_OS_WIN)
++ return new QWindowsFileSystemWatcherEngine(parent);
++ #elif defined(USE_INOTIFY)
++ // there is a chance that inotify may fail on Linux pre-2.6.13 (August
++ // 2005), so we can't just new inotify directly.
++ return QInotifyFileSystemWatcherEngine::create(parent);
++ #elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) || defined(Q_OS_OPENBSD) || defined(QT_PLATFORM_UIKIT)
++ return QKqueueFileSystemWatcherEngine::create(parent);
++ #elif defined(Q_OS_MACOS)
++ return QFseventsFileSystemWatcherEngine::create(parent);
++ #else
++ Q_UNUSED(parent);
++ return 0;
++ #endif
++ }
++
++ QFileSystemWatcherPrivate::QFileSystemWatcherPrivate()
++ : native(nullptr), poller(nullptr)
++ {
++ }
++
++ void QFileSystemWatcherPrivate::connectEngine(QFileSystemWatcherEngine *engine)
++ {
++ QObjectPrivate::connect(engine, &QFileSystemWatcherEngine::fileChanged,
++ this, &QFileSystemWatcherPrivate::fileChanged);
++ QObjectPrivate::connect(engine, &QFileSystemWatcherEngine::directoryChanged,
++ this, &QFileSystemWatcherPrivate::directoryChanged);
++ }
++
++ void QFileSystemWatcherPrivate::init()
++ {
++ Q_Q(QFileSystemWatcher);
++ native = createNativeEngine(q);
++ if (native) {
++ connectEngine(native);
++ #if defined(Q_OS_WIN)
++ auto *windowsWatcher = static_cast<QWindowsFileSystemWatcherEngine *>(native);
++ using WinE = QWindowsFileSystemWatcherEngine;
++ QObjectPrivate::connect(windowsWatcher, &WinE::driveLockForRemoval,
++ this, &QFileSystemWatcherPrivate::winDriveLockForRemoval);
++ QObjectPrivate::connect(windowsWatcher, &WinE::driveLockForRemovalFailed,
++ this, &QFileSystemWatcherPrivate::winDriveLockForRemovalFailed);
++ QObjectPrivate::connect(windowsWatcher, &WinE::driveRemoved,
++ this, &QFileSystemWatcherPrivate::winDriveRemoved);
++ #endif // Q_OS_WIN
++ }
++ }
++
++ void QFileSystemWatcherPrivate::initPollerEngine()
++ {
++ if (poller)
++ return;
++
++ Q_Q(QFileSystemWatcher);
++ poller = new QPollingFileSystemWatcherEngine(q); // that was a mouthful
++ connectEngine(poller);
++ }
++
++ void QFileSystemWatcherPrivate::fileChanged(const QString &path, bool removed)
++ {
++ Q_Q(QFileSystemWatcher);
++ qCDebug(lcWatcher) << "file changed" << path << "removed?" << removed << "watching?" << files.contains(path);
++ if (!files.contains(path)) {
++ // the path was removed after a change was detected, but before we delivered the signal
++ return;
++ }
++ if (removed)
++ files.removeAll(path);
++ emit q->fileChanged(path, QFileSystemWatcher::QPrivateSignal());
++ }
++
++ void QFileSystemWatcherPrivate::directoryChanged(const QString &path, bool removed)
++ {
++ Q_Q(QFileSystemWatcher);
++ qCDebug(lcWatcher) << "directory changed" << path << "removed?" << removed << "watching?" << directories.contains(path);
++ if (!directories.contains(path)) {
++ // perhaps the path was removed after a change was detected, but before we delivered the signal
++ return;
++ }
++ if (removed)
++ directories.removeAll(path);
++ emit q->directoryChanged(path, QFileSystemWatcher::QPrivateSignal());
++ }
++
++ #if defined(Q_OS_WIN)
++
++ void QFileSystemWatcherPrivate::winDriveLockForRemoval(const QString &path)
++ {
++ // Windows: Request to lock a (removable/USB) drive for removal, release
++ // its paths under watch, temporarily storing them should the lock fail.
++ Q_Q(QFileSystemWatcher);
++ QStringList pathsToBeRemoved;
++ auto pred = [&path] (const QString &f) { return !f.startsWith(path, Qt::CaseInsensitive); };
++ std::remove_copy_if(files.cbegin(), files.cend(),
++ std::back_inserter(pathsToBeRemoved), pred);
++ std::remove_copy_if(directories.cbegin(), directories.cend(),
++ std::back_inserter(pathsToBeRemoved), pred);
++ if (!pathsToBeRemoved.isEmpty()) {
++ q->removePaths(pathsToBeRemoved);
++ temporarilyRemovedPaths.insert(path.at(0), pathsToBeRemoved);
++ }
++ }
++
++ void QFileSystemWatcherPrivate::winDriveLockForRemovalFailed(const QString &path)
++ {
++ // Windows: Request to lock a (removable/USB) drive failed (blocked by other
++ // application), restore the watched paths.
++ Q_Q(QFileSystemWatcher);
++ if (!path.isEmpty()) {
++ const auto it = temporarilyRemovedPaths.find(path.at(0));
++ if (it != temporarilyRemovedPaths.end()) {
++ q->addPaths(it.value());
++ temporarilyRemovedPaths.erase(it);
++ }
++ }
++ }
++
++ void QFileSystemWatcherPrivate::winDriveRemoved(const QString &path)
++ {
++ // Windows: Drive finally removed, clear out paths stored in lock request.
++ if (!path.isEmpty())
++ temporarilyRemovedPaths.remove(path.at(0));
++ }
++ #endif // Q_OS_WIN
++
++ /*!
++ \class QFileSystemWatcher
++ \inmodule QtCore
++ \brief The QFileSystemWatcher class provides an interface for monitoring files and directories for modifications.
++ \ingroup io
++ \since 4.2
++ \reentrant
++
++ QFileSystemWatcher monitors the file system for changes to files
++ and directories by watching a list of specified paths.
++
++ Call addPath() to watch a particular file or directory. Multiple
++ paths can be added using the addPaths() function. Existing paths can
++ be removed by using the removePath() and removePaths() functions.
++
++ QFileSystemWatcher examines each path added to it. Files that have
++ been added to the QFileSystemWatcher can be accessed using the
++ files() function, and directories using the directories() function.
++
++ The fileChanged() signal is emitted when a file has been modified,
++ renamed or removed from disk. Similarly, the directoryChanged()
++ signal is emitted when a directory or its contents is modified or
++ removed. Note that QFileSystemWatcher stops monitoring files once
++ they have been renamed or removed from disk, and directories once
++ they have been removed from disk.
++
++ \list
++ \li \b Notes:
++ \list
++ \li On systems running a Linux kernel without inotify support,
++ file systems that contain watched paths cannot be unmounted.
++
++ \li The act of monitoring files and directories for
++ modifications consumes system resources. This implies there is a
++ limit to the number of files and directories your process can
++ monitor simultaneously. On all BSD variants, for
++ example, an open file descriptor is required for each monitored
++ file. Some system limits the number of open file descriptors to 256
++ by default. This means that addPath() and addPaths() will fail if
++ your process tries to add more than 256 files or directories to
++ the file system monitor. Also note that your process may have
++ other file descriptors open in addition to the ones for files
++ being monitored, and these other open descriptors also count in
++ the total. \macos uses a different backend and does not
++ suffer from this issue.
++ \endlist
++ \endlist
++
++ \sa QFile, QDir
++ */
++
++
++ /*!
++ Constructs a new file system watcher object with the given \a parent.
++ */
++ QFileSystemWatcher::QFileSystemWatcher(QObject *parent)
++ : QObject(*new QFileSystemWatcherPrivate, parent)
++ {
++ d_func()->init();
++ }
++
++ /*!
++ Constructs a new file system watcher object with the given \a parent
++ which monitors the specified \a paths list.
++ */
++ QFileSystemWatcher::QFileSystemWatcher(const QStringList &paths, QObject *parent)
++ : QObject(*new QFileSystemWatcherPrivate, parent)
++ {
++ d_func()->init();
++ addPaths(paths);
++ }
++
++ /*!
++ Destroys the file system watcher.
++ */
++ QFileSystemWatcher::~QFileSystemWatcher()
++ { }
++
++ /*!
++ Adds \a path to the file system watcher if \a path exists. The
++ path is not added if it does not exist, or if it is already being
++ monitored by the file system watcher.
++
++ If \a path specifies a directory, the directoryChanged() signal
++ will be emitted when \a path is modified or removed from disk;
++ otherwise the fileChanged() signal is emitted when \a path is
++ modified, renamed or removed.
++
++ If the watch was successful, true is returned.
++
++ Reasons for a watch failure are generally system-dependent, but
++ may include the resource not existing, access failures, or the
++ total watch count limit, if the platform has one.
++
++ \note There may be a system dependent limit to the number of
++ files and directories that can be monitored simultaneously.
++ If this limit is been reached, \a path will not be monitored,
++ and false is returned.
++
++ \sa addPaths(), removePath()
++ */
++ bool QFileSystemWatcher::addPath(const QString &path)
++ {
++ if (path.isEmpty()) {
++ qWarning("QFileSystemWatcher::addPath: path is empty");
++ return true;
++ }
++
++ QStringList paths = addPaths(QStringList(path));
++ return paths.isEmpty();
++ }
++
++ static QStringList empty_paths_pruned(const QStringList &paths)
++ {
++ QStringList p;
++ p.reserve(paths.size());
++ const auto isEmpty = [](const QString &s) { return s.isEmpty(); };
++ std::remove_copy_if(paths.begin(), paths.end(),
++ std::back_inserter(p),
++ isEmpty);
++ return p;
++ }
++
++ /*!
++ Adds each path in \a paths to the file system watcher. Paths are
++ not added if they not exist, or if they are already being
++ monitored by the file system watcher.
++
++ If a path specifies a directory, the directoryChanged() signal
++ will be emitted when the path is modified or removed from disk;
++ otherwise the fileChanged() signal is emitted when the path is
++ modified, renamed, or removed.
++
++ The return value is a list of paths that could not be watched.
++
++ Reasons for a watch failure are generally system-dependent, but
++ may include the resource not existing, access failures, or the
++ total watch count limit, if the platform has one.
++
++ \note There may be a system dependent limit to the number of
++ files and directories that can be monitored simultaneously.
++ If this limit has been reached, the excess \a paths will not
++ be monitored, and they will be added to the returned QStringList.
++
++ \sa addPath(), removePaths()
++ */
++ QStringList QFileSystemWatcher::addPaths(const QStringList &paths)
++ {
++ Q_D(QFileSystemWatcher);
++
++ QStringList p = empty_paths_pruned(paths);
++
++ if (p.isEmpty()) {
++ qWarning("QFileSystemWatcher::addPaths: list is empty");
++ return p;
++ }
++ qCDebug(lcWatcher) << "adding" << paths;
++ const auto selectEngine = [this, d]() -> QFileSystemWatcherEngine* {
++ #ifdef QT_BUILD_INTERNAL
++ const QString on = objectName();
++
++ if (Q_UNLIKELY(on.startsWith("_qt_autotest_force_engine_"_L1))) {
++ // Autotest override case - use the explicitly selected engine only
++ const auto forceName = QStringView{on}.mid(26);
++ if (forceName == "poller"_L1) {
++ qCDebug(lcWatcher, "QFileSystemWatcher: skipping native engine, using only polling engine");
++ d_func()->initPollerEngine();
++ return d->poller;
++ } else if (forceName == "native"_L1) {
++ qCDebug(lcWatcher, "QFileSystemWatcher: skipping polling engine, using only native engine");
++ return d->native;
++ }
++ return nullptr;
++ }
++ #endif
++ // Normal runtime case - search intelligently for best engine
++ if (d->native) {
++ return d->native;
++ } else {
++ d_func()->initPollerEngine();
++ return d->poller;
++ }
++ };
++
++ if (auto engine = selectEngine())
++ p = engine->addPaths(p, &d->files, &d->directories);
++
++ return p;
++ }
++
++ /*!
++ Removes the specified \a path from the file system watcher.
++
++ If the watch is successfully removed, true is returned.
++
++ Reasons for watch removal failing are generally system-dependent,
++ but may be due to the path having already been deleted, for example.
++
++ \sa removePaths(), addPath()
++ */
++ bool QFileSystemWatcher::removePath(const QString &path)
++ {
++ if (path.isEmpty()) {
++ qWarning("QFileSystemWatcher::removePath: path is empty");
++ return true;
++ }
++
++ QStringList paths = removePaths(QStringList(path));
++ return paths.isEmpty();
++ }
++
++ /*!
++ Removes the specified \a paths from the file system watcher.
++
++ The return value is a list of paths which were not able to be
++ unwatched successfully.
++
++ Reasons for watch removal failing are generally system-dependent,
++ but may be due to the path having already been deleted, for example.
++
++ \sa removePath(), addPaths()
++ */
++ QStringList QFileSystemWatcher::removePaths(const QStringList &paths)
++ {
++ Q_D(QFileSystemWatcher);
++
++ QStringList p = empty_paths_pruned(paths);
++
++ if (p.isEmpty()) {
++ qWarning("QFileSystemWatcher::removePaths: list is empty");
++ return p;
++ }
++ qCDebug(lcWatcher) << "removing" << paths;
++
++ if (d->native)
++ p = d->native->removePaths(p, &d->files, &d->directories);
++ if (d->poller)
++ p = d->poller->removePaths(p, &d->files, &d->directories);
++
++ return p;
++ }
++
++ /*!
++ \fn void QFileSystemWatcher::fileChanged(const QString &path)
++
++ This signal is emitted when the file at the specified \a path is
++ modified, renamed or removed from disk.
++
++ \note As a safety measure, many applications save an open file by
++ writing a new file and then deleting the old one. In your slot
++ function, you can check \c watcher.files().contains(path).
++ If it returns \c false, check whether the file still exists
++ and then call \c addPath() to continue watching it.
++
++ \sa directoryChanged()
++ */
++
++ /*!
++ \fn void QFileSystemWatcher::directoryChanged(const QString &path)
++
++ This signal is emitted when the directory at a specified \a path
++ is modified (e.g., when a file is added or deleted) or removed
++ from disk. Note that if there are several changes during a short
++ period of time, some of the changes might not emit this signal.
++ However, the last change in the sequence of changes will always
++ generate this signal.
++
++ \sa fileChanged()
++ */
++
++ /*!
++ \fn QStringList QFileSystemWatcher::directories() const
++
++ Returns a list of paths to directories that are being watched.
++
++ \sa files()
++ */
++
++ /*!
++ \fn QStringList QFileSystemWatcher::files() const
++
++ Returns a list of paths to files that are being watched.
++
++ \sa directories()
++ */
++
++ QStringList QFileSystemWatcher::directories() const
++ {
++ Q_D(const QFileSystemWatcher);
++ return d->directories;
++ }
++
++ QStringList QFileSystemWatcher::files() const
++ {
++ Q_D(const QFileSystemWatcher);
++ return d->files;
++ }
++
++ QT_END_NAMESPACE
++
++ #include "moc_qfilesystemwatcher.cpp"
++ #include "moc_qfilesystemwatcher_p.cpp"
++