| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the QtCore module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include <qplatformdefs.h> |
| |
| #include "qdiriterator.h" |
| #include "qfilesystemwatcher.h" |
| #include "qfilesystemwatcher_fsevents_p.h" |
| #include "private/qcore_unix_p.h" |
| #include "kernel/qcore_mac_p.h" |
| |
| #include <qdebug.h> |
| #include <qdir.h> |
| #include <qfile.h> |
| #include <qfileinfo.h> |
| #include <qvarlengtharray.h> |
| #include <qscopeguard.h> |
| |
| #undef FSEVENT_DEBUG |
| #ifdef FSEVENT_DEBUG |
| # define DEBUG if (true) qDebug |
| #else |
| # define DEBUG if (false) qDebug |
| #endif |
| |
| QT_BEGIN_NAMESPACE |
| |
| static void callBackFunction(ConstFSEventStreamRef streamRef, |
| void *clientCallBackInfo, |
| size_t numEvents, |
| void *eventPaths, |
| const FSEventStreamEventFlags eventFlags[], |
| const FSEventStreamEventId eventIds[]) |
| { |
| QMacAutoReleasePool pool; |
| |
| char **paths = static_cast<char **>(eventPaths); |
| QFseventsFileSystemWatcherEngine *engine = static_cast<QFseventsFileSystemWatcherEngine *>(clientCallBackInfo); |
| engine->processEvent(streamRef, numEvents, paths, eventFlags, eventIds); |
| } |
| |
| bool QFseventsFileSystemWatcherEngine::checkDir(DirsByName::iterator &it) |
| { |
| bool needsRestart = false; |
| |
| QT_STATBUF st; |
| const QString &name = it.key(); |
| Info &info = it->dirInfo; |
| const int res = QT_STAT(QFile::encodeName(name), &st); |
| if (res == -1) { |
| needsRestart |= derefPath(info.watchedPath); |
| emit emitDirectoryChanged(info.origPath, true); |
| it = watchingState.watchedDirectories.erase(it); |
| } else if (st.st_ctimespec != info.ctime || st.st_mode != info.mode) { |
| info.ctime = st.st_ctimespec; |
| info.mode = st.st_mode; |
| emit emitDirectoryChanged(info.origPath, false); |
| ++it; |
| } else { |
| bool dirChanged = false; |
| InfoByName &entries = it->entries; |
| // check known entries: |
| for (InfoByName::iterator i = entries.begin(); i != entries.end(); ) { |
| if (QT_STAT(QFile::encodeName(i.key()), &st) == -1) { |
| // entry disappeared |
| dirChanged = true; |
| i = entries.erase(i); |
| } else { |
| if (i->ctime != st.st_ctimespec || i->mode != st.st_mode) { |
| // entry changed |
| dirChanged = true; |
| i->ctime = st.st_ctimespec; |
| i->mode = st.st_mode; |
| } |
| ++i; |
| } |
| } |
| // check for new entries: |
| QDirIterator dirIt(name); |
| while (dirIt.hasNext()) { |
| dirIt.next(); |
| QString entryName = dirIt.filePath(); |
| if (!entries.contains(entryName)) { |
| dirChanged = true; |
| QT_STATBUF st; |
| if (QT_STAT(QFile::encodeName(entryName), &st) == -1) |
| continue; |
| entries.insert(entryName, Info(QString(), st.st_ctimespec, st.st_mode, QString())); |
| |
| } |
| } |
| if (dirChanged) |
| emit emitDirectoryChanged(info.origPath, false); |
| ++it; |
| } |
| |
| return needsRestart; |
| } |
| |
| bool QFseventsFileSystemWatcherEngine::rescanDirs(const QString &path) |
| { |
| bool needsRestart = false; |
| |
| for (DirsByName::iterator it = watchingState.watchedDirectories.begin(); |
| it != watchingState.watchedDirectories.end(); ) { |
| if (it.key().startsWith(path)) |
| needsRestart |= checkDir(it); |
| else |
| ++it; |
| } |
| |
| return needsRestart; |
| } |
| |
| bool QFseventsFileSystemWatcherEngine::rescanFiles(InfoByName &filesInPath) |
| { |
| bool needsRestart = false; |
| |
| for (InfoByName::iterator it = filesInPath.begin(); it != filesInPath.end(); ) { |
| QT_STATBUF st; |
| QString name = it.key(); |
| const int res = QT_STAT(QFile::encodeName(name), &st); |
| if (res == -1) { |
| needsRestart |= derefPath(it->watchedPath); |
| emit emitFileChanged(it.value().origPath, true); |
| it = filesInPath.erase(it); |
| continue; |
| } else if (st.st_ctimespec != it->ctime || st.st_mode != it->mode) { |
| it->ctime = st.st_ctimespec; |
| it->mode = st.st_mode; |
| emit emitFileChanged(it.value().origPath, false); |
| } |
| |
| ++it; |
| } |
| |
| return needsRestart; |
| } |
| |
| bool QFseventsFileSystemWatcherEngine::rescanFiles(const QString &path) |
| { |
| bool needsRestart = false; |
| |
| for (FilesByPath::iterator i = watchingState.watchedFiles.begin(); |
| i != watchingState.watchedFiles.end(); ) { |
| if (i.key().startsWith(path)) { |
| needsRestart |= rescanFiles(i.value()); |
| if (i.value().isEmpty()) { |
| i = watchingState.watchedFiles.erase(i); |
| continue; |
| } |
| } |
| |
| ++i; |
| } |
| |
| return needsRestart; |
| } |
| |
| void QFseventsFileSystemWatcherEngine::processEvent(ConstFSEventStreamRef streamRef, |
| size_t numEvents, |
| char **eventPaths, |
| const FSEventStreamEventFlags eventFlags[], |
| const FSEventStreamEventId eventIds[]) |
| { |
| #if defined(Q_OS_OSX) |
| Q_UNUSED(streamRef); |
| |
| bool needsRestart = false; |
| |
| QMutexLocker locker(&lock); |
| |
| for (size_t i = 0; i < numEvents; ++i) { |
| FSEventStreamEventFlags eFlags = eventFlags[i]; |
| DEBUG("Change %llu in %s, flags %x", eventIds[i], eventPaths[i], (unsigned int)eFlags); |
| |
| if (eFlags & kFSEventStreamEventFlagEventIdsWrapped) { |
| DEBUG("\tthe event ids wrapped"); |
| lastReceivedEvent = 0; |
| } |
| lastReceivedEvent = qMax(lastReceivedEvent, eventIds[i]); |
| |
| QString path = QFile::decodeName(eventPaths[i]); |
| if (path.endsWith(QDir::separator())) |
| path = path.mid(0, path.size() - 1); |
| |
| if (eFlags & kFSEventStreamEventFlagMustScanSubDirs) { |
| DEBUG("\tmust rescan directory because of coalesced events"); |
| if (eFlags & kFSEventStreamEventFlagUserDropped) |
| DEBUG("\t\t... user dropped."); |
| if (eFlags & kFSEventStreamEventFlagKernelDropped) |
| DEBUG("\t\t... kernel dropped."); |
| needsRestart |= rescanDirs(path); |
| needsRestart |= rescanFiles(path); |
| continue; |
| } |
| |
| if (eFlags & kFSEventStreamEventFlagRootChanged) { |
| // re-check everything: |
| DirsByName::iterator dirIt = watchingState.watchedDirectories.find(path); |
| if (dirIt != watchingState.watchedDirectories.end()) |
| needsRestart |= checkDir(dirIt); |
| needsRestart |= rescanFiles(path); |
| continue; |
| } |
| |
| if ((eFlags & kFSEventStreamEventFlagItemIsDir) && (eFlags & kFSEventStreamEventFlagItemRemoved)) |
| needsRestart |= rescanDirs(path); |
| |
| // check watched directories: |
| DirsByName::iterator dirIt = watchingState.watchedDirectories.find(path); |
| if (dirIt != watchingState.watchedDirectories.end()) |
| needsRestart |= checkDir(dirIt); |
| |
| // check watched files: |
| FilesByPath::iterator pIt = watchingState.watchedFiles.find(path); |
| if (pIt != watchingState.watchedFiles.end()) |
| needsRestart |= rescanFiles(pIt.value()); |
| } |
| |
| if (needsRestart) |
| emit scheduleStreamRestart(); |
| #else |
| Q_UNUSED(streamRef); |
| Q_UNUSED(numEvents); |
| Q_UNUSED(eventPaths); |
| Q_UNUSED(eventFlags); |
| Q_UNUSED(eventIds); |
| #endif |
| } |
| |
| void QFseventsFileSystemWatcherEngine::doEmitFileChanged(const QString &path, bool removed) |
| { |
| DEBUG() << "emitting fileChanged for" << path << "with removed =" << removed; |
| emit fileChanged(path, removed); |
| } |
| |
| void QFseventsFileSystemWatcherEngine::doEmitDirectoryChanged(const QString &path, bool removed) |
| { |
| DEBUG() << "emitting directoryChanged for" << path << "with removed =" << removed; |
| emit directoryChanged(path, removed); |
| } |
| |
| bool QFseventsFileSystemWatcherEngine::restartStream() |
| { |
| QMutexLocker locker(&lock); |
| stopStream(); |
| return startStream(); |
| } |
| |
| QFseventsFileSystemWatcherEngine *QFseventsFileSystemWatcherEngine::create(QObject *parent) |
| { |
| return new QFseventsFileSystemWatcherEngine(parent); |
| } |
| |
| QFseventsFileSystemWatcherEngine::QFseventsFileSystemWatcherEngine(QObject *parent) |
| : QFileSystemWatcherEngine(parent) |
| , stream(0) |
| , lastReceivedEvent(kFSEventStreamEventIdSinceNow) |
| { |
| |
| // We cannot use signal-to-signal queued connections, because the |
| // QSignalSpy cannot spot signals fired from other/alien threads. |
| connect(this, SIGNAL(emitDirectoryChanged(QString,bool)), |
| this, SLOT(doEmitDirectoryChanged(QString,bool)), Qt::QueuedConnection); |
| connect(this, SIGNAL(emitFileChanged(QString,bool)), |
| this, SLOT(doEmitFileChanged(QString,bool)), Qt::QueuedConnection); |
| connect(this, SIGNAL(scheduleStreamRestart()), |
| this, SLOT(restartStream()), Qt::QueuedConnection); |
| |
| queue = dispatch_queue_create("org.qt-project.QFseventsFileSystemWatcherEngine", NULL); |
| } |
| |
| QFseventsFileSystemWatcherEngine::~QFseventsFileSystemWatcherEngine() |
| { |
| QMacAutoReleasePool pool; |
| |
| // Stop the stream in case we have to wait for the lock below to be acquired. |
| if (stream) |
| FSEventStreamStop(stream); |
| |
| // The assumption with the locking strategy is that this class cannot and will not be subclassed! |
| QMutexLocker locker(&lock); |
| |
| stopStream(true); |
| dispatch_release(queue); |
| } |
| |
| QStringList QFseventsFileSystemWatcherEngine::addPaths(const QStringList &paths, |
| QStringList *files, |
| QStringList *directories) |
| { |
| QMacAutoReleasePool pool; |
| |
| if (stream) { |
| DEBUG("Flushing, last id is %llu", FSEventStreamGetLatestEventId(stream)); |
| FSEventStreamFlushSync(stream); |
| } |
| |
| QMutexLocker locker(&lock); |
| |
| bool wasRunning = stream != nullptr; |
| bool needsRestart = false; |
| |
| WatchingState oldState = watchingState; |
| QStringList unhandled; |
| for (const QString &path : paths) { |
| auto sg = qScopeGuard([&]{ unhandled.push_back(path); }); |
| QString origPath = path.normalized(QString::NormalizationForm_C); |
| QString realPath = origPath; |
| if (realPath.endsWith(QDir::separator())) |
| realPath = realPath.mid(0, realPath.size() - 1); |
| QString watchedPath, parentPath; |
| |
| realPath = QFileInfo(realPath).canonicalFilePath(); |
| QFileInfo fi(realPath); |
| if (realPath.isEmpty()) |
| continue; |
| |
| QT_STATBUF st; |
| if (QT_STAT(QFile::encodeName(realPath), &st) == -1) |
| continue; |
| |
| const bool isDir = S_ISDIR(st.st_mode); |
| if (isDir) { |
| if (watchingState.watchedDirectories.contains(realPath)) |
| continue; |
| directories->append(origPath); |
| watchedPath = realPath; |
| } else { |
| if (files->contains(origPath)) |
| continue; |
| files->append(origPath); |
| |
| watchedPath = fi.path(); |
| parentPath = watchedPath; |
| } |
| |
| sg.dismiss(); |
| |
| for (PathRefCounts::const_iterator i = watchingState.watchedPaths.begin(), |
| ei = watchingState.watchedPaths.end(); i != ei; ++i) { |
| if (watchedPath.startsWith(i.key() % QDir::separator())) { |
| watchedPath = i.key(); |
| break; |
| } |
| } |
| |
| PathRefCounts::iterator it = watchingState.watchedPaths.find(watchedPath); |
| if (it == watchingState.watchedPaths.end()) { |
| needsRestart = true; |
| watchingState.watchedPaths.insert(watchedPath, 1); |
| DEBUG("Adding '%s' to watchedPaths", qPrintable(watchedPath)); |
| } else { |
| ++it.value(); |
| } |
| |
| Info info(origPath, st.st_ctimespec, st.st_mode, watchedPath); |
| if (isDir) { |
| DirInfo dirInfo; |
| dirInfo.dirInfo = info; |
| dirInfo.entries = scanForDirEntries(realPath); |
| watchingState.watchedDirectories.insert(realPath, dirInfo); |
| DEBUG("-- Also adding '%s' to watchedDirectories", qPrintable(realPath)); |
| } else { |
| watchingState.watchedFiles[parentPath].insert(realPath, info); |
| DEBUG("-- Also adding '%s' to watchedFiles", qPrintable(realPath)); |
| } |
| } |
| |
| if (needsRestart) { |
| stopStream(); |
| if (!startStream()) { |
| // ok, something went wrong, let's try to restore the previous state |
| watchingState = std::move(oldState); |
| // and because we don't know which path caused the issue (if any), fail on all of them |
| unhandled = paths; |
| |
| if (wasRunning) |
| startStream(); |
| } |
| } |
| |
| return unhandled; |
| } |
| |
| QStringList QFseventsFileSystemWatcherEngine::removePaths(const QStringList &paths, |
| QStringList *files, |
| QStringList *directories) |
| { |
| QMacAutoReleasePool pool; |
| |
| QMutexLocker locker(&lock); |
| |
| bool needsRestart = false; |
| |
| WatchingState oldState = watchingState; |
| QStringList unhandled; |
| for (const QString &origPath : paths) { |
| auto sg = qScopeGuard([&]{ unhandled.push_back(origPath); }); |
| QString realPath = origPath; |
| if (realPath.endsWith(QDir::separator())) |
| realPath = realPath.mid(0, realPath.size() - 1); |
| |
| QFileInfo fi(realPath); |
| realPath = fi.canonicalFilePath(); |
| |
| if (fi.isDir()) { |
| DirsByName::iterator dirIt = watchingState.watchedDirectories.find(realPath); |
| if (dirIt != watchingState.watchedDirectories.end()) { |
| needsRestart |= derefPath(dirIt->dirInfo.watchedPath); |
| watchingState.watchedDirectories.erase(dirIt); |
| directories->removeAll(origPath); |
| sg.dismiss(); |
| DEBUG("Removed directory '%s'", qPrintable(realPath)); |
| } |
| } else { |
| QFileInfo fi(realPath); |
| QString parentPath = fi.path(); |
| FilesByPath::iterator pIt = watchingState.watchedFiles.find(parentPath); |
| if (pIt != watchingState.watchedFiles.end()) { |
| InfoByName &filesInDir = pIt.value(); |
| InfoByName::iterator fIt = filesInDir.find(realPath); |
| if (fIt != filesInDir.end()) { |
| needsRestart |= derefPath(fIt->watchedPath); |
| filesInDir.erase(fIt); |
| if (filesInDir.isEmpty()) |
| watchingState.watchedFiles.erase(pIt); |
| files->removeAll(origPath); |
| sg.dismiss(); |
| DEBUG("Removed file '%s'", qPrintable(realPath)); |
| } |
| } |
| } |
| } |
| |
| locker.unlock(); |
| |
| if (needsRestart) { |
| if (!restartStream()) { |
| watchingState = std::move(oldState); |
| startStream(); |
| } |
| } |
| |
| return unhandled; |
| } |
| |
| // Returns false if FSEventStream* calls failed for some mysterious reason, true if things got a |
| // thumbs-up. |
| bool QFseventsFileSystemWatcherEngine::startStream() |
| { |
| Q_ASSERT(stream == 0); |
| if (stream) // Ok, this really shouldn't happen, esp. not after the assert. But let's be nice in release mode and still handle it. |
| stopStream(); |
| |
| QMacAutoReleasePool pool; |
| |
| if (watchingState.watchedPaths.isEmpty()) |
| return true; // we succeeded in doing nothing |
| |
| DEBUG() << "Starting stream with paths" << watchingState.watchedPaths.keys(); |
| |
| NSMutableArray<NSString *> *pathsToWatch = [NSMutableArray<NSString *> arrayWithCapacity:watchingState.watchedPaths.size()]; |
| for (PathRefCounts::const_iterator i = watchingState.watchedPaths.begin(), ei = watchingState.watchedPaths.end(); i != ei; ++i) |
| [pathsToWatch addObject:i.key().toNSString()]; |
| |
| struct FSEventStreamContext callBackInfo = { |
| 0, |
| this, |
| NULL, |
| NULL, |
| NULL |
| }; |
| const CFAbsoluteTime latency = .5; // in seconds |
| |
| // Never start with kFSEventStreamEventIdSinceNow, because this will generate an invalid |
| // soft-assert in FSEventStreamFlushSync in CarbonCore when no event occurred. |
| if (lastReceivedEvent == kFSEventStreamEventIdSinceNow) |
| lastReceivedEvent = FSEventsGetCurrentEventId(); |
| stream = FSEventStreamCreate(NULL, |
| &callBackFunction, |
| &callBackInfo, |
| reinterpret_cast<CFArrayRef>(pathsToWatch), |
| lastReceivedEvent, |
| latency, |
| FSEventStreamCreateFlags(0)); |
| |
| if (!stream) { // nope, no way to know what went wrong, so just fail |
| DEBUG() << "Failed to create stream!"; |
| return false; |
| } |
| |
| FSEventStreamSetDispatchQueue(stream, queue); |
| |
| if (FSEventStreamStart(stream)) { |
| DEBUG() << "Stream started successfully with sinceWhen =" << lastReceivedEvent; |
| return true; |
| } else { // again, no way to know what went wrong, so just clean up and fail |
| DEBUG() << "Stream failed to start!"; |
| FSEventStreamInvalidate(stream); |
| FSEventStreamRelease(stream); |
| stream = 0; |
| return false; |
| } |
| } |
| |
| void QFseventsFileSystemWatcherEngine::stopStream(bool isStopped) |
| { |
| QMacAutoReleasePool pool; |
| if (stream) { |
| if (!isStopped) |
| FSEventStreamStop(stream); |
| FSEventStreamInvalidate(stream); |
| FSEventStreamRelease(stream); |
| stream = 0; |
| DEBUG() << "Stream stopped. Last event ID:" << lastReceivedEvent; |
| } |
| } |
| |
| QFseventsFileSystemWatcherEngine::InfoByName QFseventsFileSystemWatcherEngine::scanForDirEntries(const QString &path) |
| { |
| InfoByName entries; |
| |
| QDirIterator it(path); |
| while (it.hasNext()) { |
| it.next(); |
| QString entryName = it.filePath(); |
| QT_STATBUF st; |
| if (QT_STAT(QFile::encodeName(entryName), &st) == -1) |
| continue; |
| entries.insert(entryName, Info(QString(), st.st_ctimespec, st.st_mode, QString())); |
| } |
| |
| return entries; |
| } |
| |
| bool QFseventsFileSystemWatcherEngine::derefPath(const QString &watchedPath) |
| { |
| PathRefCounts::iterator it = watchingState.watchedPaths.find(watchedPath); |
| if (it != watchingState.watchedPaths.end() && --it.value() < 1) { |
| watchingState.watchedPaths.erase(it); |
| DEBUG("Removing '%s' from watchedPaths.", qPrintable(watchedPath)); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| QT_END_NAMESPACE |