summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/corelib/io/io.pri5
-rw-r--r--src/corelib/io/qfilesystemwatcher.cpp10
-rw-r--r--src/corelib/io/qfilesystemwatcher_fsevents.cpp467
-rw-r--r--src/corelib/io/qfilesystemwatcher_fsevents_p.h127
-rw-r--r--tests/auto/qfilesystemwatcher/tst_qfilesystemwatcher.cpp80
5 files changed, 687 insertions, 2 deletions
diff --git a/src/corelib/io/io.pri b/src/corelib/io/io.pri
index 5033b21..bd41f5e 100644
--- a/src/corelib/io/io.pri
+++ b/src/corelib/io/io.pri
@@ -65,7 +65,10 @@ win32 {
SOURCES += io/qfsfileengine_unix.cpp
SOURCES += io/qfsfileengine_iterator_unix.cpp
SOURCES += io/qprocess_unix.cpp
- mac:SOURCES += io/qsettings_mac.cpp
+ macx-*: {
+ HEADERS += io/qfilesystemwatcher_fsevents_p.h
+ SOURCES += io/qsettings_mac.cpp io/qfilesystemwatcher_fsevents.cpp
+ }
linux-*:{
SOURCES += \
diff --git a/src/corelib/io/qfilesystemwatcher.cpp b/src/corelib/io/qfilesystemwatcher.cpp
index b321644..902e240 100644
--- a/src/corelib/io/qfilesystemwatcher.cpp
+++ b/src/corelib/io/qfilesystemwatcher.cpp
@@ -58,6 +58,9 @@
# include "qfilesystemwatcher_inotify_p.h"
# include "qfilesystemwatcher_dnotify_p.h"
#elif defined(Q_OS_FREEBSD) || defined(Q_OS_MAC)
+# if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
+# include "qfilesystemwatcher_fsevents_p.h"
+# endif //MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
# include "qfilesystemwatcher_kqueue_p.h"
#endif
@@ -243,7 +246,12 @@ QFileSystemWatcherEngine *QFileSystemWatcherPrivate::createNativeEngine()
eng = QDnotifyFileSystemWatcherEngine::create();
return eng;
#elif defined(Q_OS_FREEBSD) || defined(Q_OS_MAC)
- return QKqueueFileSystemWatcherEngine::create();
+# if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
+ if (QSysInfo::MacintoshVersion >= QSysInfo::MV_10_5)
+ return QFSEventsFileSystemWatcherEngine::create();
+ else
+# endif
+ return QKqueueFileSystemWatcherEngine::create();
#else
return 0;
#endif
diff --git a/src/corelib/io/qfilesystemwatcher_fsevents.cpp b/src/corelib/io/qfilesystemwatcher_fsevents.cpp
new file mode 100644
index 0000000..3e0aee8
--- /dev/null
+++ b/src/corelib/io/qfilesystemwatcher_fsevents.cpp
@@ -0,0 +1,467 @@
+/****************************************************************************
+**
+** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies).
+** Contact: Nokia Corporation (qt-info@nokia.com)
+**
+** This file is part of the QtCore module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** No Commercial Usage
+** This file contains pre-release code and may not be distributed.
+** You may use this file in accordance with the terms and conditions
+** contained in the either Technology Preview License Agreement or the
+** Beta Release License Agreement.
+**
+** 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, Nokia gives you certain
+** additional rights. These rights are described in the Nokia Qt LGPL
+** Exception version 1.0, 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.
+**
+** If you are unsure which license is appropriate for your use, please
+** contact the sales department at http://www.qtsoftware.com/contact.
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+
+#include <qplatformdefs.h>
+
+#include "qfilesystemwatcher.h"
+#include "qfilesystemwatcher_fsevents_p.h"
+
+#include <qdebug.h>
+#include <qfile.h>
+#include <qdatetime.h>
+#include <qfileinfo.h>
+#include <qvarlengtharray.h>
+
+#include <mach/mach.h>
+#include <sys/types.h>
+#include <CoreFoundation/CFRunLoop.h>
+#include <CoreFoundation/CFUUID.h>
+#include <CoreServices/CoreServices.h>
+#include <AvailabilityMacros.h>
+#include <private/qcore_mac_p.h>
+
+QT_BEGIN_NAMESPACE
+
+#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
+// Static operator overloading so for the sake of some convieniece.
+// They only live in this compilation unit to avoid polluting Qt in general.
+static bool operator==(const struct ::timespec &left, const struct ::timespec &right)
+{
+ return left.tv_sec == right.tv_sec
+ && left.tv_nsec == right.tv_nsec;
+}
+
+static bool operator==(const struct ::stat64 &left, const struct ::stat64 &right)
+{
+ return left.st_dev == right.st_dev
+ && left.st_mode == right.st_mode
+ && left.st_size == right.st_size
+ && left.st_ino == right.st_ino
+ && left.st_uid == right.st_uid
+ && left.st_gid == right.st_gid
+ && left.st_mtimespec == right.st_mtimespec
+ && left.st_ctimespec == right.st_ctimespec
+ && left.st_flags == right.st_flags;
+}
+
+static bool operator!=(const struct ::stat64 &left, const struct ::stat64 &right)
+{
+ return !(operator==(left, right));
+}
+
+
+static void addPathToHash(PathHash &pathHash, const QString &key, const QFileInfo &fileInfo,
+ const QString &path)
+{
+ PathInfoList &list = pathHash[key];
+ list.push_back(PathInfo(path,
+ fileInfo.absoluteFilePath().normalized(QString::NormalizationForm_D).toUtf8()));
+ pathHash.insert(key, list);
+}
+
+static void removePathFromHash(PathHash &pathHash, const QString &key, const QString &path)
+{
+ PathInfoList &list = pathHash[key];
+ // We make the assumption that the list contains unique paths
+ PathInfoList::iterator End = list.end();
+ PathInfoList::iterator it = list.begin();
+ while (it != End) {
+ if (it->originalPath == path) {
+ list.erase(it);
+ break;
+ }
+ ++it;
+ }
+ if (list.isEmpty())
+ pathHash.remove(key);
+}
+
+static void stopFSStream(FSEventStreamRef stream)
+{
+ if (stream) {
+ FSEventStreamStop(stream);
+ FSEventStreamInvalidate(stream);
+ }
+}
+
+static QString createFSStreamPath(const QString &absolutePath)
+{
+ // The path returned has a trailing slash, so ensure that here.
+ QString string = absolutePath;
+ string.reserve(string.size() + 1);
+ string.append(QLatin1Char('/'));
+ return string;
+}
+
+static void cleanupFSStream(FSEventStreamRef stream)
+{
+ if (stream)
+ FSEventStreamRelease(stream);
+}
+
+const FSEventStreamCreateFlags QtFSEventFlags = (kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagNoDefer /* | kFSEventStreamCreateFlagWatchRoot*/);
+
+const CFTimeInterval Latency = 0.033; // This will do updates 30 times a second which is probably more than you need.
+#endif
+
+QFSEventsFileSystemWatcherEngine::QFSEventsFileSystemWatcherEngine()
+ : fsStream(0), pathsToWatch(0), threadsRunLoop(0)
+{
+}
+
+QFSEventsFileSystemWatcherEngine::~QFSEventsFileSystemWatcherEngine()
+{
+#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
+ // I assume that at this point, QFileSystemWatcher has already called stop
+ // on me, so I don't need to invalidate or stop my stream, simply
+ // release it.
+ cleanupFSStream(fsStream);
+ if (pathsToWatch)
+ CFRelease(pathsToWatch);
+#endif
+}
+
+QFSEventsFileSystemWatcherEngine *QFSEventsFileSystemWatcherEngine::create()
+{
+ return new QFSEventsFileSystemWatcherEngine();
+}
+
+QStringList QFSEventsFileSystemWatcherEngine::addPaths(const QStringList &paths,
+ QStringList *files,
+ QStringList *directories)
+{
+#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
+ stop();
+ QMutexLocker locker(&mutex);
+ QStringList failedToAdd;
+ // if we have a running FSStreamEvent, we have to kill it, we'll re-add the stream soon.
+ FSEventStreamEventId idToCheck;
+ if (fsStream) {
+ idToCheck = FSEventStreamGetLatestEventId(fsStream);
+ cleanupFSStream(fsStream);
+ } else {
+ idToCheck = kFSEventStreamEventIdSinceNow;
+ }
+
+ // Brain-dead approach, but works. FSEvents actually can already read sub-trees, but since it's
+ // work to figure out if we are doing a double register, we just register it twice as FSEvents
+ // seems smart enough to only deliver one event. We also duplicate directory entries in here
+ // (e.g., if you watch five files in the same directory, you get that directory included in the
+ // array 5 times). This stupidity also makes remove work correctly though. I'll freely admit
+ // that we could make this a bit smarter. If you do, check the auto-tests, they should catch at
+ // least a couple of the issues.
+ QCFType<CFMutableArrayRef> tmpArray = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
+ for (int i = 0; i < paths.size(); ++i) {
+ const QString &path = paths.at(i);
+
+ QFileInfo fileInfo(path);
+ if (!fileInfo.exists()) {
+ failedToAdd.append(path);
+ continue;
+ }
+
+ if (fileInfo.isDir()) {
+ if (directories->contains(path)) {
+ failedToAdd.append(path);
+ continue;
+ } else {
+ directories->append(path);
+ // Full file path for dirs.
+ QCFString cfpath(createFSStreamPath(fileInfo.absoluteFilePath()));
+ addPathToHash(dirPathInfoHash, cfpath, fileInfo, path);
+ CFArrayAppendValue(tmpArray, cfpath);
+ }
+ } else {
+ if (files->contains(path)) {
+ failedToAdd.append(path);
+ continue;
+ } else {
+ // Just the absolute path (minus it's filename) for files.
+ QCFString cfpath(createFSStreamPath(fileInfo.absolutePath()));
+ files->append(path);
+ addPathToHash(filePathInfoHash, cfpath, fileInfo, path);
+ CFArrayAppendValue(tmpArray, cfpath);
+ }
+ }
+ }
+ if (CFArrayGetCount(tmpArray) > 0) {
+ if (pathsToWatch) {
+ CFArrayAppendArray(tmpArray, pathsToWatch, CFRangeMake(0, CFArrayGetCount(pathsToWatch)));
+ CFRelease(pathsToWatch);
+ }
+ pathsToWatch = CFArrayCreateCopy(kCFAllocatorDefault, tmpArray);
+ }
+ FSEventStreamContext context = { 0, this, 0, 0, 0 };
+ fsStream = FSEventStreamCreate(kCFAllocatorDefault,
+ QFSEventsFileSystemWatcherEngine::fseventsCallback,
+ &context, pathsToWatch,
+ idToCheck, Latency, QtFSEventFlags);
+ warmUpFSEvents();
+
+ return failedToAdd;
+#else
+ Q_UNUSED(paths);
+ Q_UNUSED(files);
+ Q_UNUSED(directories);
+ return QStringList();
+#endif
+}
+
+void QFSEventsFileSystemWatcherEngine::warmUpFSEvents()
+{
+#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
+ // This function assumes that the mutex has already been grabbed before calling it.
+ // It exits with the mutex still locked (Q_ASSERT(mutex.isLocked()) ;-).
+ start();
+ waitCondition.wait(&mutex);
+#endif
+}
+
+QStringList QFSEventsFileSystemWatcherEngine::removePaths(const QStringList &paths,
+ QStringList *files,
+ QStringList *directories)
+{
+#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
+ stop();
+ QMutexLocker locker(&mutex);
+ // short circuit for smarties that call remove before add and we have nothing.
+ if (pathsToWatch == 0)
+ return paths;
+ QStringList failedToRemove;
+ // if we have a running FSStreamEvent, we have to stop it, we'll re-add the stream soon.
+ FSEventStreamEventId idToCheck;
+ if (fsStream) {
+ idToCheck = FSEventStreamGetLatestEventId(fsStream);
+ cleanupFSStream(fsStream);
+ fsStream = 0;
+ } else {
+ idToCheck = kFSEventStreamEventIdSinceNow;
+ }
+
+ CFIndex itemCount = CFArrayGetCount(pathsToWatch);
+ QCFType<CFMutableArrayRef> tmpArray = CFArrayCreateMutableCopy(kCFAllocatorDefault, itemCount,
+ pathsToWatch);
+ CFRelease(pathsToWatch);
+ pathsToWatch = 0;
+ for (int i = 0; i < paths.size(); ++i) {
+ // Get the itemCount at the beginning to avoid any overruns during the iteration.
+ itemCount = CFArrayGetCount(tmpArray);
+ const QString &path = paths.at(i);
+ QFileInfo fi(path);
+ QCFString cfpath(createFSStreamPath(fi.absolutePath()));
+
+ CFIndex index = CFArrayGetFirstIndexOfValue(tmpArray, CFRangeMake(0, itemCount), cfpath);
+ if (index != -1) {
+ CFArrayRemoveValueAtIndex(tmpArray, index);
+ files->removeAll(path);
+ removePathFromHash(filePathInfoHash, cfpath, path);
+ } else {
+ // Could be a directory we are watching instead.
+ QCFString cfdirpath(createFSStreamPath(fi.absoluteFilePath()));
+ index = CFArrayGetFirstIndexOfValue(tmpArray, CFRangeMake(0, itemCount), cfdirpath);
+ if (index != -1) {
+ CFArrayRemoveValueAtIndex(tmpArray, index);
+ directories->removeAll(path);
+ removePathFromHash(dirPathInfoHash, cfpath, path);
+ } else {
+ failedToRemove.append(path);
+ }
+ }
+ }
+ itemCount = CFArrayGetCount(tmpArray);
+ if (itemCount != 0) {
+ pathsToWatch = CFArrayCreateCopy(kCFAllocatorDefault, tmpArray);
+
+ FSEventStreamContext context = { 0, this, 0, 0, 0 };
+ fsStream = FSEventStreamCreate(kCFAllocatorDefault,
+ QFSEventsFileSystemWatcherEngine::fseventsCallback,
+ &context, pathsToWatch, idToCheck, Latency, QtFSEventFlags);
+ warmUpFSEvents();
+ }
+ return failedToRemove;
+#else
+ Q_UNUSED(paths);
+ Q_UNUSED(files);
+ Q_UNUSED(directories);
+ return QStringList();
+#endif
+}
+
+#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
+void QFSEventsFileSystemWatcherEngine::updateList(PathInfoList &list, bool directory, bool emitSignals)
+{
+ PathInfoList::iterator End = list.end();
+ PathInfoList::iterator it = list.begin();
+ while (it != End) {
+ struct ::stat64 newInfo;
+ if (::stat64(it->absolutePath, &newInfo) == 0) {
+ if (emitSignals) {
+ if (newInfo != it->savedInfo) {
+ it->savedInfo = newInfo;
+ if (directory)
+ emit directoryChanged(it->originalPath, false);
+ else
+ emit fileChanged(it->originalPath, false);
+ }
+ } else {
+ it->savedInfo = newInfo;
+ }
+ } else {
+ if (errno == ENOENT) {
+ if (emitSignals) {
+ if (directory)
+ emit directoryChanged(it->originalPath, true);
+ else
+ emit fileChanged(it->originalPath, true);
+ }
+ it = list.erase(it);
+ continue;
+ } else {
+ qWarning("%s:%d:QFSEventsFileSystemWatcherEngine: stat error on %s:%s",
+ __FILE__, __LINE__, qPrintable(it->originalPath), strerror(errno));
+
+ }
+ }
+ ++it;
+ }
+}
+
+void QFSEventsFileSystemWatcherEngine::updateHash(PathHash &pathHash)
+{
+ PathHash::iterator HashEnd = pathHash.end();
+ PathHash::iterator it = pathHash.begin();
+ const bool IsDirectory = (&pathHash == &dirPathInfoHash);
+ while (it != HashEnd) {
+ updateList(it.value(), IsDirectory, false);
+ if (it.value().isEmpty())
+ it = pathHash.erase(it);
+ else
+ ++it;
+ }
+}
+#endif
+
+void QFSEventsFileSystemWatcherEngine::fseventsCallback(ConstFSEventStreamRef ,
+ void *clientCallBackInfo, size_t numEvents,
+ void *eventPaths,
+ const FSEventStreamEventFlags eventFlags[],
+ const FSEventStreamEventId [])
+{
+#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
+ QFSEventsFileSystemWatcherEngine *watcher = static_cast<QFSEventsFileSystemWatcherEngine *>(clientCallBackInfo);
+ QMutexLocker locker(&watcher->mutex);
+ CFArrayRef paths = static_cast<CFArrayRef>(eventPaths);
+ for (size_t i = 0; i < numEvents; ++i) {
+ const QString path = QCFString::toQString(
+ static_cast<CFStringRef>(CFArrayGetValueAtIndex(paths, i)));
+ const FSEventStreamEventFlags pathFlags = eventFlags[i];
+ // There are several flags that may be passed, but we really don't care about them ATM.
+ // Here they are and why we don't care.
+ // kFSEventStreamEventFlagHistoryDone--(very unlikely to be gotten, but even then, not much changes).
+ // kFSEventStreamEventFlagMustScanSubDirs--Likely means the data is very much out of date, we
+ // aren't coalescing our directories, so again not so much of an issue
+ // kFSEventStreamEventFlagRootChanged | kFSEventStreamEventFlagMount | kFSEventStreamEventFlagUnmount--
+ // These three flags indicate something has changed, but the stat will likely show this, so
+ // there's not really much to worry about.
+ // (btw, FSEvents is not the correct way of checking for mounts/unmounts,
+ // there are real CarbonCore events for that.)
+ Q_UNUSED(pathFlags);
+ if (watcher->filePathInfoHash.contains(path))
+ watcher->updateList(watcher->filePathInfoHash[path], false, true);
+
+ if (watcher->dirPathInfoHash.contains(path))
+ watcher->updateList(watcher->dirPathInfoHash[path], true, true);
+ }
+#else
+ Q_UNUSED(clientCallBackInfo);
+ Q_UNUSED(numEvents);
+ Q_UNUSED(eventPaths);
+ Q_UNUSED(eventFlags);
+#endif
+}
+
+void QFSEventsFileSystemWatcherEngine::stop()
+{
+#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
+ stopFSStream(fsStream);
+ if (threadsRunLoop)
+ CFRunLoopStop(threadsRunLoop);
+#endif
+}
+
+void QFSEventsFileSystemWatcherEngine::updateFiles()
+{
+#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
+ QMutexLocker locker(&mutex);
+ updateHash(filePathInfoHash);
+ updateHash(dirPathInfoHash);
+ if (filePathInfoHash.isEmpty() && dirPathInfoHash.isEmpty()) {
+ // Everything disappeared before we got to start, don't bother.
+ stop();
+ cleanupFSStream(fsStream);
+ }
+ waitCondition.wakeAll();
+#endif
+}
+
+void QFSEventsFileSystemWatcherEngine::run()
+{
+#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
+ threadsRunLoop = CFRunLoopGetCurrent();
+ FSEventStreamScheduleWithRunLoop(fsStream, threadsRunLoop, kCFRunLoopDefaultMode);
+ bool startedOK = FSEventStreamStart(fsStream);
+ // It's recommended by Apple that you only update the files after you've started
+ // the stream, because otherwise you might miss an update in between starting it.
+ updateFiles();
+#ifdef QT_NO_DEBUG
+ Q_UNUSED(startedOK);
+#else
+ Q_ASSERT(startedOK);
+#endif
+ // If for some reason we called stop up above (and invalidated our stream), this call will return
+ // immediately.
+ CFRunLoopRun();
+ threadsRunLoop = 0;
+#endif
+}
+
+QT_END_NAMESPACE
diff --git a/src/corelib/io/qfilesystemwatcher_fsevents_p.h b/src/corelib/io/qfilesystemwatcher_fsevents_p.h
new file mode 100644
index 0000000..2e8b788
--- /dev/null
+++ b/src/corelib/io/qfilesystemwatcher_fsevents_p.h
@@ -0,0 +1,127 @@
+/****************************************************************************
+**
+** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies).
+** Contact: Nokia Corporation (qt-info@nokia.com)
+**
+** This file is part of the QtCore module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** No Commercial Usage
+** This file contains pre-release code and may not be distributed.
+** You may use this file in accordance with the terms and conditions
+** contained in the either Technology Preview License Agreement or the
+** Beta Release License Agreement.
+**
+** 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, Nokia gives you certain
+** additional rights. These rights are described in the Nokia Qt LGPL
+** Exception version 1.0, 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.
+**
+** If you are unsure which license is appropriate for your use, please
+** contact the sales department at http://www.qtsoftware.com/contact.
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+
+#ifndef FILEWATCHER_FSEVENTS_P_H
+#define FILEWATCHER_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/qwaitcondition.h>
+#include <QtCore/qthread.h>
+#include <QtCore/QHash>
+#include <QtCore/QLinkedList>
+#include <private/qcore_mac_p.h>
+#include <sys/stat.h>
+
+typedef struct __FSEventStream *FSEventStreamRef;
+typedef const struct __FSEventStream *ConstFSEventStreamRef;
+typedef const struct __CFArray *CFArrayRef;
+typedef uint FSEventStreamEventFlags;
+typedef uint64_t FSEventStreamEventId;
+
+QT_BEGIN_NAMESPACE
+
+#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
+// Yes, I use a stat64 element here. QFileInfo requires too much knowledge about implementation
+// details to be used as a long-standing record. Since I'm going to have to store this information, I can
+// do the stat myself too.
+struct PathInfo {
+ PathInfo(const QString &path, const QByteArray &absPath)
+ : originalPath(path), absolutePath(absPath) {}
+ QString originalPath; // The path we need to emit
+ QByteArray absolutePath; // The path we need to stat.
+ struct ::stat64 savedInfo; // All the info for the path so we can compare it.
+};
+typedef QLinkedList<PathInfo> PathInfoList;
+typedef QHash<QString, PathInfoList> PathHash;
+#endif
+
+class QFSEventsFileSystemWatcherEngine : public QFileSystemWatcherEngine
+{
+ Q_OBJECT
+public:
+ ~QFSEventsFileSystemWatcherEngine();
+
+ static QFSEventsFileSystemWatcherEngine *create();
+
+ QStringList addPaths(const QStringList &paths, QStringList *files, QStringList *directories);
+ QStringList removePaths(const QStringList &paths, QStringList *files, QStringList *directories);
+
+ void stop();
+
+private:
+ QFSEventsFileSystemWatcherEngine();
+ void warmUpFSEvents();
+ void updateFiles();
+
+ static void fseventsCallback(ConstFSEventStreamRef streamRef, void *clientCallBackInfo, size_t numEvents,
+ void *eventPaths, const FSEventStreamEventFlags eventFlags[],
+ const FSEventStreamEventId eventIds[]);
+ void run();
+ FSEventStreamRef fsStream;
+ CFArrayRef pathsToWatch;
+ CFRunLoopRef threadsRunLoop;
+ QMutex mutex;
+ QWaitCondition waitCondition;
+#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
+ PathHash filePathInfoHash;
+ PathHash dirPathInfoHash;
+ void updateHash(PathHash &pathHash);
+ void updateList(PathInfoList &list, bool directory, bool emitSignals);
+#endif
+};
+
+#endif
+
+QT_END_NAMESPACE
diff --git a/tests/auto/qfilesystemwatcher/tst_qfilesystemwatcher.cpp b/tests/auto/qfilesystemwatcher/tst_qfilesystemwatcher.cpp
index da06742..c883c63 100644
--- a/tests/auto/qfilesystemwatcher/tst_qfilesystemwatcher.cpp
+++ b/tests/auto/qfilesystemwatcher/tst_qfilesystemwatcher.cpp
@@ -73,6 +73,8 @@ private slots:
void removePath();
void addPaths();
void removePaths();
+ void watchFileAndItsDirectory();
+ void watchFileAndItsDirectory_data() { basicTest_data(); }
private:
QStringList do_force_engines;
@@ -397,5 +399,83 @@ void tst_QFileSystemWatcher::removePaths()
watcher.removePaths(paths);
}
+void tst_QFileSystemWatcher::watchFileAndItsDirectory()
+{
+ QFETCH(QString, backend);
+ QDir().mkdir("testDir");
+ QDir testDir("testDir");
+
+ QString testFileName = testDir.filePath("testFile.txt");
+ QString secondFileName = testDir.filePath("testFile2.txt");
+ QFile::remove(secondFileName);
+
+ QFile testFile(testFileName);
+ testFile.setPermissions(QFile::ReadOwner | QFile::WriteOwner);
+ testFile.remove();
+
+ QVERIFY(testFile.open(QIODevice::WriteOnly | QIODevice::Truncate));
+ testFile.write(QByteArray("hello"));
+ testFile.close();
+
+ QFileSystemWatcher watcher;
+ watcher.setObjectName(QLatin1String("_qt_autotest_force_engine_") + backend);
+
+ watcher.addPath(testDir.dirName());
+ watcher.addPath(testFileName);
+
+ QSignalSpy fileChangedSpy(&watcher, SIGNAL(fileChanged(const QString &)));
+ QSignalSpy dirChangedSpy(&watcher, SIGNAL(directoryChanged(const QString &)));
+ QEventLoop eventLoop;
+ QTimer timer;
+ connect(&timer, SIGNAL(timeout()), &eventLoop, SLOT(quit()));
+
+ // resolution of the modification time is system dependent, but it's at most 1 second when using
+ // the polling engine. From what I know, FAT32 has a 2 second resolution. So we have to
+ // wait before modifying the directory...
+ QTest::qWait(2000);
+
+ QVERIFY(testFile.open(QIODevice::WriteOnly | QIODevice::Truncate));
+ testFile.write(QByteArray("hello again"));
+ testFile.close();
+
+ timer.start(3000);
+ eventLoop.exec();
+ QCOMPARE(fileChangedSpy.count(), 1);
+ QCOMPARE(dirChangedSpy.count(), 0);
+
+ fileChangedSpy.clear();
+ QFile secondFile(secondFileName);
+ secondFile.open(QIODevice::WriteOnly | QIODevice::Truncate);
+ secondFile.write("Foo");
+ secondFile.close();
+
+ timer.start(3000);
+ eventLoop.exec();
+ QCOMPARE(fileChangedSpy.count(), 0);
+ QCOMPARE(dirChangedSpy.count(), 1);
+
+ dirChangedSpy.clear();
+
+ QFile::remove(testFileName);
+
+ timer.start(3000);
+ eventLoop.exec();
+ QCOMPARE(fileChangedSpy.count(), 1);
+ QCOMPARE(dirChangedSpy.count(), 1);
+
+ fileChangedSpy.clear();
+ dirChangedSpy.clear();
+
+ watcher.removePath(testFileName);
+ QFile::remove(secondFileName);
+
+ timer.start(3000);
+ eventLoop.exec();
+ QCOMPARE(fileChangedSpy.count(), 0);
+ QCOMPARE(dirChangedSpy.count(), 1);
+
+ QVERIFY(QDir().rmdir("testDir"));
+}
+
QTEST_MAIN(tst_QFileSystemWatcher)
#include "tst_qfilesystemwatcher.moc"