/**************************************************************************** | |
** | |
** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). | |
** All rights reserved. | |
** Contact: Nokia Corporation (qt-info@nokia.com) | |
** | |
** This file is part of the QtCore module of the Qt Toolkit. | |
** | |
** $QT_BEGIN_LICENSE:LGPL$ | |
** GNU Lesser General Public License Usage | |
** 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.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. | |
** | |
** Other Usage | |
** Alternatively, this file may be used in accordance with the terms and | |
** conditions contained in a signed written agreement between you and Nokia. | |
** | |
** | |
** | |
** | |
** | |
** $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.canonicalFilePath().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(); | |
wait(); | |
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.canonicalFilePath())); | |
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.canonicalPath())); | |
files->append(path); | |
addPathToHash(filePathInfoHash, cfpath, fileInfo, path); | |
CFArrayAppendValue(tmpArray, cfpath); | |
} | |
} | |
} | |
if (!pathsToWatch && failedToAdd.size() == paths.size()) { | |
return failedToAdd; | |
} | |
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(); | |
wait(); | |
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.canonicalPath())); | |
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.canonicalFilePath())); | |
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 | |
QMutexLocker locker(&mutex); | |
stopFSStream(fsStream); | |
if (threadsRunLoop) { | |
CFRunLoopStop(threadsRunLoop); | |
waitForStop.wait(&mutex); | |
} | |
#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. | |
#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5 | |
// Code duplicated from stop(), with the exception that we | |
// don't wait on waitForStop here. Doing this will lead to | |
// a deadlock since this function is called from the worker | |
// thread. (waitForStop.wakeAll() is only called from the | |
// end of run()). | |
stopFSStream(fsStream); | |
if (threadsRunLoop) | |
CFRunLoopStop(threadsRunLoop); | |
#endif | |
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; | |
QMutexLocker locker(&mutex); | |
waitForStop.wakeAll(); | |
#endif | |
} | |
QT_END_NAMESPACE |