blob: 6194bf7c713a829ffa97d80472d8945c10b55429 [file] [log] [blame]
/****************************************************************************
**
** 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