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