Mac: FSEvents-based QFileSystemWatcherEngine.

Use FSEvents to monitor changes in the filesystem instead of the kqueue
implementation. This removes the limit of wathed files: kqueue uses a
file descriptor for each file monitored, for which the ulimit was set by
default to 256. Now the OSX implementation on par with the other major
desktop platforms.

Change-Id: I2d46cca811978621989fd35201138df88a37c0fb
Reviewed-by: Morten Johan Sørvig <morten.sorvig@digia.com>
This commit is contained in:
Erik Verbruggen 2013-11-29 12:55:59 +01:00 committed by The Qt Project
parent 97c187da3c
commit 4273c14e57
4 changed files with 659 additions and 2 deletions

View File

@ -135,6 +135,10 @@ win32 {
OBJECTIVE_SOURCES += io/qurl_mac.mm
}
mac {
osx {
OBJECTIVE_SOURCES += io/qfilesystemwatcher_fsevents.mm
HEADERS += io/qfilesystemwatcher_fsevents_p.h
}
macx {
SOURCES += io/qstandardpaths_mac.cpp
} else:ios {

View File

@ -60,8 +60,10 @@
# include "qfilesystemwatcher_win_p.h"
#elif defined(USE_INOTIFY)
# include "qfilesystemwatcher_inotify_p.h"
#elif defined(Q_OS_FREEBSD) || defined(Q_OS_MAC)
#elif defined(Q_OS_FREEBSD) || defined(Q_OS_IOS) || (defined(Q_OS_OSX) && MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_7)
# include "qfilesystemwatcher_kqueue_p.h"
#elif defined(Q_OS_OSX) && MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_6
# include "qfilesystemwatcher_fsevents_p.h"
#endif
QT_BEGIN_NAMESPACE
@ -74,8 +76,10 @@ QFileSystemWatcherEngine *QFileSystemWatcherPrivate::createNativeEngine(QObject
// 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_MAC)
#elif defined(Q_OS_FREEBSD) || defined(Q_OS_IOS) || (defined(Q_OS_OSX) && MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_7)
return QKqueueFileSystemWatcherEngine::create(parent);
#elif defined(Q_OS_OSX) && MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_6
return QFseventsFileSystemWatcherEngine::create(parent);
#else
Q_UNUSED(parent);
return 0;

View File

@ -0,0 +1,507 @@
/****************************************************************************
**
** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
** Contact: http://www.qt-project.org/legal
**
** 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 Digia. For licensing terms and
** conditions see http://qt.digia.com/licensing. For further information
** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Digia gives you certain additional
** rights. These rights are described in the Digia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3.0 as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL included in the
** packaging of this file. Please review the following information to
** ensure the GNU General Public License version 3.0 requirements will be
** met: http://www.gnu.org/copyleft/gpl.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"
#ifndef QT_NO_FILESYSTEMWATCHER
#include <qdebug.h>
#include <qdir.h>
#include <qfile.h>
#include <qfileinfo.h>
#include <qvarlengtharray.h>
//#define 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[])
{
char **paths = static_cast<char **>(eventPaths);
QFseventsFileSystemWatcherEngine *engine = static_cast<QFseventsFileSystemWatcherEngine *>(clientCallBackInfo);
engine->processEvent(streamRef, numEvents, paths, eventFlags, eventIds);
}
void QFseventsFileSystemWatcherEngine::checkDir(DirsByName::iterator &it)
{
QT_STATBUF st;
const QString &name = it.key();
Info &info = it->dirInfo;
const int res = QT_STAT(QFile::encodeName(name), &st);
if (res == -1) {
derefPath(info.watchedPath);
emit emitDirectoryChanged(info.origPath, true);
it = 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);
}
}
void QFseventsFileSystemWatcherEngine::rescanDirs(const QString &path)
{
for (DirsByName::iterator it = watchedDirectories.begin(); it != watchedDirectories.end(); ) {
if (it.key().startsWith(path))
checkDir(it);
else
++it;
}
}
void QFseventsFileSystemWatcherEngine::rescanFiles(InfoByName &filesInPath)
{
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) {
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;
}
}
void QFseventsFileSystemWatcherEngine::rescanFiles(const QString &path)
{
for (FilesByPath::iterator i = watchedFiles.begin(); i != watchedFiles.end(); ) {
if (i.key().startsWith(path)) {
rescanFiles(i.value());
if (i.value().isEmpty()) {
i = watchedFiles.erase(i);
continue;
}
}
++i;
}
}
void QFseventsFileSystemWatcherEngine::processEvent(ConstFSEventStreamRef streamRef,
size_t numEvents,
char **eventPaths,
const FSEventStreamEventFlags eventFlags[],
const FSEventStreamEventId eventIds[])
{
#if defined(Q_OS_OSX) && MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_6
Q_UNUSED(streamRef);
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);
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.");
rescanDirs(path);
rescanFiles(path);
continue;
}
if (eFlags & kFSEventStreamEventFlagEventIdsWrapped) {
DEBUG("\tthe event ids wrapped");
// TODO: verify if we need to do something
}
if (eFlags & kFSEventStreamEventFlagRootChanged) {
// re-check everything:
DirsByName::iterator dirIt = watchedDirectories.find(path);
if (dirIt != watchedDirectories.end())
checkDir(dirIt);
rescanFiles(path);
continue;
}
if ((eFlags & kFSEventStreamEventFlagItemIsDir) && (eFlags & kFSEventStreamEventFlagItemRemoved))
rescanDirs(path);
// check watched directories:
DirsByName::iterator dirIt = watchedDirectories.find(path);
if (dirIt != watchedDirectories.end())
checkDir(dirIt);
// check watched files:
FilesByPath::iterator pIt = watchedFiles.find(path);
if (pIt != watchedFiles.end())
rescanFiles(pIt.value());
}
#else
// This is a work-around for moc: when we put the version check at the top of the header file,
// moc will still see the Q_OBJECT macro and generate a meta-object when compiling for 10.6,
// which obviously won't link.
//
// So the trick is to still compile this class on 10.6, but never instantiate it.
Q_UNUSED(streamRef);
Q_UNUSED(numEvents);
Q_UNUSED(eventPaths);
Q_UNUSED(eventFlags);
Q_UNUSED(eventIds);
#endif
}
void QFseventsFileSystemWatcherEngine::doEmitFileChanged(const QString path, bool removed)
{
emit fileChanged(path, removed);
}
void QFseventsFileSystemWatcherEngine::doEmitDirectoryChanged(const QString path, bool removed)
{
emit directoryChanged(path, removed);
}
QFseventsFileSystemWatcherEngine *QFseventsFileSystemWatcherEngine::create(QObject *parent)
{
return new QFseventsFileSystemWatcherEngine(parent);
}
QFseventsFileSystemWatcherEngine::QFseventsFileSystemWatcherEngine(QObject *parent)
: QFileSystemWatcherEngine(parent)
, stream(0)
{
// We cannot use signal-to-signal queued connections, because the
// QSignalSpy cannot spot signals fired from other/alien threads.
connect(this, SIGNAL(emitDirectoryChanged(const QString, bool)),
this, SLOT(doEmitDirectoryChanged(const QString, bool)), Qt::QueuedConnection);
connect(this, SIGNAL(emitFileChanged(const QString, bool)),
this, SLOT(doEmitFileChanged(const QString, bool)), Qt::QueuedConnection);
queue = dispatch_queue_create("org.qt-project.QFseventsFileSystemWatcherEngine", NULL);
}
QFseventsFileSystemWatcherEngine::~QFseventsFileSystemWatcherEngine()
{
if (stream)
FSEventStreamStop(stream);
// The assumption with the locking strategy is that this class cannot and will not be subclassed!
QMutexLocker locker(&lock);
stopStream();
dispatch_release(queue);
}
QStringList QFseventsFileSystemWatcherEngine::addPaths(const QStringList &paths,
QStringList *files,
QStringList *directories)
{
if (stream)
FSEventStreamFlushSync(stream);
QMutexLocker locker(&lock);
bool newWatchPathsFound = false;
QStringList p = paths;
QMutableListIterator<QString> it(p);
while (it.hasNext()) {
QString origPath = it.next();
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 (watchedDirectories.contains(realPath))
continue;
directories->append(origPath);
watchedPath = realPath;
it.remove();
} else {
if (files->contains(origPath))
continue;
files->append(origPath);
it.remove();
watchedPath = fi.path();
parentPath = watchedPath;
}
for (PathRefCounts::const_iterator i = watchedPaths.begin(), ei = watchedPaths.end(); i != ei; ++i) {
if (watchedPath.startsWith(i.key())) {
watchedPath = i.key();
break;
}
}
PathRefCounts::iterator it = watchedPaths.find(watchedPath);
if (it == watchedPaths.end()) {
newWatchPathsFound = true;
watchedPaths.insert(watchedPath, 1);
} else {
++it.value();
}
Info info(origPath, st.st_ctimespec, st.st_mode, watchedPath);
if (isDir) {
DirInfo dirInfo;
dirInfo.dirInfo = info;
dirInfo.entries = scanForDirEntries(realPath);
watchedDirectories.insert(realPath, dirInfo);
} else {
watchedFiles[parentPath].insert(realPath, info);
}
}
if (newWatchPathsFound) {
stopStream();
if (!startStream())
p = paths;
}
return p;
}
QStringList QFseventsFileSystemWatcherEngine::removePaths(const QStringList &paths,
QStringList *files,
QStringList *directories)
{
QMutexLocker locker(&lock);
QStringList p = paths;
QMutableListIterator<QString> it(p);
while (it.hasNext()) {
QString origPath = it.next();
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 = watchedDirectories.find(realPath);
if (dirIt != watchedDirectories.end()) {
derefPath(dirIt->dirInfo.watchedPath);
watchedDirectories.erase(dirIt);
directories->removeAll(origPath);
it.remove();
}
} else {
QFileInfo fi(realPath);
QString parentPath = fi.path();
FilesByPath::iterator pIt = watchedFiles.find(parentPath);
if (pIt != watchedFiles.end()) {
InfoByName &filesInDir = pIt.value();
InfoByName::iterator fIt = filesInDir.find(realPath);
if (fIt != filesInDir.end()) {
derefPath(fIt->watchedPath);
filesInDir.erase(fIt);
if (filesInDir.isEmpty())
watchedFiles.erase(pIt);
files->removeAll(origPath);
it.remove();
}
}
}
}
return p;
}
bool QFseventsFileSystemWatcherEngine::startStream()
{
if (watchedPaths.isEmpty())
return false;
DEBUG() << "Starting stream with paths" << watchedPaths.keys();
NSMutableArray *pathsToWatch = [NSMutableArray arrayWithCapacity:watchedPaths.size()];
for (PathRefCounts::const_iterator i = watchedPaths.begin(), ei = watchedPaths.end(); i != ei; ++i)
[pathsToWatch addObject:reinterpret_cast<const NSString *>(QCFString::toCFStringRef(i.key()))];
struct FSEventStreamContext callBackInfo = {
0,
this,
NULL,
NULL,
NULL
};
const CFAbsoluteTime latency = .5; // in seconds
FSEventStreamCreateFlags flags = kFSEventStreamCreateFlagWatchRoot;
stream = FSEventStreamCreate(NULL,
&callBackFunction,
&callBackInfo,
reinterpret_cast<CFArrayRef>(pathsToWatch),
kFSEventStreamEventIdSinceNow,
latency,
flags);
if (!stream) {
DEBUG() << "Failed to create stream!";
return false;
}
FSEventStreamSetDispatchQueue(stream, queue);
if (FSEventStreamStart(stream)) {
DEBUG() << "Stream started successfully.";
return true;
} else {
DEBUG() << "Stream failed to start!";
return false;
}
}
void QFseventsFileSystemWatcherEngine::stopStream(bool isStopped)
{
if (stream) {
if (!isStopped)
FSEventStreamStop(stream);
FSEventStreamInvalidate(stream);
FSEventStreamRelease(stream);
stream = 0;
DEBUG() << "Stream stopped.";
}
}
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;
}
void QFseventsFileSystemWatcherEngine::derefPath(const QString &watchedPath)
{
PathRefCounts::iterator it = watchedPaths.find(watchedPath);
if (it == watchedPaths.end())
return;
if (--it.value() < 1) {
watchedPaths.erase(it);
stopStream();
startStream();
}
}
#endif //QT_NO_FILESYSTEMWATCHER
QT_END_NAMESPACE

View File

@ -0,0 +1,142 @@
/****************************************************************************
**
** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
** Contact: http://www.qt-project.org/legal
**
** 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 Digia. For licensing terms and
** conditions see http://qt.digia.com/licensing. For further information
** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Digia gives you certain additional
** rights. These rights are described in the Digia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3.0 as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL included in the
** packaging of this file. Please review the following information to
** ensure the GNU General Public License version 3.0 requirements will be
** met: http://www.gnu.org/copyleft/gpl.html.
**
**
** $QT_END_LICENSE$
**
****************************************************************************/
#ifndef QFILESYSTEMWATCHER_FSEVENTS_P_H
#define QFILESYSTEMWATCHER_FSEVENTS_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists for the convenience
// of the QLibrary class. This header file may change from
// version to version without notice, or even be removed.
//
// We mean it.
//
#include "qfilesystemwatcher_p.h"
#include <QtCore/qmutex.h>
#include <QtCore/qhash.h>
#include <QtCore/qthread.h>
#include <QtCore/qvector.h>
#include <QtCore/qsocketnotifier.h>
#include <dispatch/dispatch.h>
#include <CoreServices/CoreServices.h>
#ifndef QT_NO_FILESYSTEMWATCHER
QT_BEGIN_NAMESPACE
class QFseventsFileSystemWatcherEngine : public QFileSystemWatcherEngine
{
Q_OBJECT
public:
~QFseventsFileSystemWatcherEngine();
static QFseventsFileSystemWatcherEngine *create(QObject *parent);
QStringList addPaths(const QStringList &paths, QStringList *files, QStringList *directories);
QStringList removePaths(const QStringList &paths, QStringList *files, QStringList *directories);
void processEvent(ConstFSEventStreamRef streamRef, size_t numEvents, char **eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[]);
Q_SIGNALS:
void emitFileChanged(const QString path, bool removed);
void emitDirectoryChanged(const QString path, bool removed);
private slots:
void doEmitFileChanged(const QString path, bool removed);
void doEmitDirectoryChanged(const QString path, bool removed);
private:
struct Info {
QString origPath;
timespec ctime;
mode_t mode;
QString watchedPath;
Info(): mode(0)
{
ctime.tv_sec = 0;
ctime.tv_nsec = 0;
}
Info(const QString &origPath, const timespec &ctime, mode_t mode, const QString &watchedPath)
: origPath(origPath)
, ctime(ctime)
, mode(mode)
, watchedPath(watchedPath)
{}
};
typedef QHash<QString, Info> InfoByName;
typedef QHash<QString, InfoByName> FilesByPath;
struct DirInfo {
Info dirInfo;
InfoByName entries;
};
typedef QHash<QString, DirInfo> DirsByName;
typedef QHash<QString, qint64> PathRefCounts;
QFseventsFileSystemWatcherEngine(QObject *parent);
bool startStream();
void stopStream(bool isStopped = false);
InfoByName scanForDirEntries(const QString &path);
void derefPath(const QString &watchedPath);
void checkDir(DirsByName::iterator &it);
void rescanDirs(const QString &path);
void rescanFiles(InfoByName &filesInPath);
void rescanFiles(const QString &path);
QMutex lock;
dispatch_queue_t queue;
FSEventStreamRef stream;
FilesByPath watchedFiles;
DirsByName watchedDirectories;
PathRefCounts watchedPaths;
};
QT_END_NAMESPACE
#endif //QT_NO_FILESYSTEMWATCHER
#endif // QFILESYSTEMWATCHER_FSEVENTS_P_H