/**************************************************************************** ** ** 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 #include "qdiriterator.h" #include "qfilesystemwatcher.h" #include "qfilesystemwatcher_fsevents_p.h" #include "private/qcore_unix_p.h" #include "kernel/qcore_mac_p.h" #include #include #include #include #include #include #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(eventPaths); QFseventsFileSystemWatcherEngine *engine = static_cast(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 *pathsToWatch = [NSMutableArray 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(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