Skip to content

Instantly share code, notes, and snippets.

@danechitoaie
Forked from robertknight/gist:3149811
Created October 4, 2015 09:44
Show Gist options
  • Save danechitoaie/9efcecd126364f5138e6 to your computer and use it in GitHub Desktop.
Save danechitoaie/9efcecd126364f5138e6 to your computer and use it in GitHub Desktop.
Mac FSEvents wrapper
// MacFSEventsWatcher.h
#pragma once
#include "libMendeleyExport.h"
#include <QtCore/QObject>
#include <QtCore/QStringList>
typedef struct __FSEventStream* FSEventStreamRef;
/** Utility to watch a file system tree using FSEvents.
*
* Note that to receive notifications, a CFRunLoop must be running. Qt
* integrates CFRunLoop with the main thread automatically in GUI applications
* on Mac. In non-GUI applications and non-GUI threads, this must be
* done separately - eg. using CFRunLoopRun().
*/
class LIB_MENDELEY_EXPORT MacFSEventsWatcher : public QObject
{
Q_OBJECT
public:
MacFSEventsWatcher(QObject* parent = 0);
virtual ~MacFSEventsWatcher();
/** Sets the requested latency of notifications following a file-system event
* in seconds.
*
* The default latency is 2.0 seconds.
*/
void setLatency(qreal seconds);
qreal latency() const;
/** Add a path to the list of paths being watched by MacFSEventsWatcher and
* start watching for events.
*
* Note: fsevent processing is asynchronous, so it is possible that directories
* starting with @p path which have changed very recently but before the call to
* addDir() may be reported via directoryChanged() after addDir() returns.
*
* @param path Path to a directory to watch
*/
void addDir(const QString& path);
/** Remove a path from the list of paths watched by MacFSEventsWatcher and
* stop watching events.
*
* @param path Path to a directory to stop watching
*/
void removeDir(const QString& path);
/** Return a list of directories currently being watched by MacFSEventsWatcher
*/
QStringList dirs() const;
/** Returns true if file system events are
* being watched.
*/
bool isRunning() const;
// internal
void notifyChange(quint64 eventId, const QString& path);
Q_SIGNALS:
/** Emitted when the contents of a directory changed. FSEvents does
* not report any details of the change so the user has to scan
* the directory tree.
*/
void directoryChanged(const QString& path);
private:
void start();
void shutdown();
void restart();
QStringList m_dirs;
FSEventStreamRef m_eventStream;
quint64 m_maxEventId;
bool m_isRestaring;
qreal m_latency;
};
// MacFSEventsWatcher.mm
// to avoid a naming conflict, Cocoa.h needs to be included before
// any Qt headers
#include <Cocoa/Cocoa.h>
#include <CoreServices/CoreServices.h>
#include <CoreFoundation/CoreFoundation.h>
#include <CoreFoundation/CFArray.h>
#include "MacFSEventsWatcher.h"
#include <QtDebug>
#define TEST_FLAG(flags,flagStrings,flag) \
if (flags & flag) \
flagStrings << #flag;
QStringList eventFlagsToString(int flags)
{
QStringList flagStrings;
TEST_FLAG(flags,flagStrings,kFSEventStreamEventFlagMustScanSubDirs);
TEST_FLAG(flags,flagStrings,kFSEventStreamEventFlagUserDropped);
TEST_FLAG(flags,flagStrings,kFSEventStreamEventFlagKernelDropped);
TEST_FLAG(flags,flagStrings,kFSEventStreamEventFlagEventIdsWrapped);
TEST_FLAG(flags,flagStrings,kFSEventStreamEventFlagHistoryDone);
TEST_FLAG(flags,flagStrings,kFSEventStreamEventFlagRootChanged);
TEST_FLAG(flags,flagStrings,kFSEventStreamEventFlagMount);
TEST_FLAG(flags,flagStrings,kFSEventStreamEventFlagUnmount);
return flagStrings;
}
void fsEventsCallback(ConstFSEventStreamRef streamRef,
void* clientCallbackInfo,
size_t eventCount,
void* eventPaths,
const FSEventStreamEventFlags* eventFlags,
const FSEventStreamEventId* eventIds)
{
static const bool eventDebugging = false;
Q_UNUSED(streamRef);
Q_UNUSED(eventFlags);
char** paths = static_cast<char**>(eventPaths);
MacFSEventsWatcher* watcher = static_cast<MacFSEventsWatcher*>(clientCallbackInfo);
Q_ASSERT(watcher);
for (size_t i=0; i < eventCount; i++)
{
FSEventStreamEventFlags flags = eventFlags[i];
QString path = QString::fromUtf8(paths[i]);
if (flags & kFSEventStreamEventFlagHistoryDone)
{
// this is a dummy event to mark the end of historical events
// sent after setting up an fsevents stream
continue;
}
if (eventDebugging)
{
qWarning() << "received event:" << eventIds[i];
qWarning() << "path:" << path;
qWarning() << "flags:" << eventFlagsToString(eventFlags[i]).join(", ");
}
watcher->notifyChange(eventIds[i],path);
}
}
MacFSEventsWatcher::MacFSEventsWatcher(QObject* parent)
: QObject(parent)
, m_eventStream(0)
, m_maxEventId(0)
, m_isRestaring(false)
, m_latency(2.0)
{
}
MacFSEventsWatcher::~MacFSEventsWatcher()
{
shutdown();
}
void MacFSEventsWatcher::setLatency(qreal latency)
{
m_latency = latency;
}
qreal MacFSEventsWatcher::latency() const
{
return m_latency;
}
void MacFSEventsWatcher::addDir(const QString& path)
{
if (m_dirs.contains(path))
{
return;
}
m_dirs << path;
restart();
}
void MacFSEventsWatcher::removeDir(const QString& path)
{
m_dirs.removeAll(path);
restart();
}
QStringList MacFSEventsWatcher::dirs() const
{
return m_dirs;
}
void MacFSEventsWatcher::restart()
{
shutdown();
start();
}
bool MacFSEventsWatcher::isRunning() const
{
return m_eventStream != 0;
}
void MacFSEventsWatcher::notifyChange(quint64 eventId, const QString& path)
{
// record the max ID of all events seen so far, so that
// we can avoid missing changes when restarting the watcher
m_maxEventId = qMax(m_maxEventId,eventId);
emit directoryChanged(path);
}
void MacFSEventsWatcher::start()
{
if (m_dirs.isEmpty() || isRunning())
{
return;
}
NSMutableArray* pathsToWatch = [[NSMutableArray alloc] init];
Q_FOREACH (const QString& dir, m_dirs)
{
NSString* path = [[NSString alloc] initWithUTF8String: dir.toUtf8().constData()];
[pathsToWatch addObject: path];
[path release];
}
FSEventStreamContext streamContext;
streamContext.version = 0;
streamContext.info = this;
streamContext.retain = 0;
streamContext.release = 0;
streamContext.copyDescription = 0;
quint64 sinceEventId;
if (m_maxEventId > 0)
{
sinceEventId = m_maxEventId;
}
else
{
sinceEventId = kFSEventStreamEventIdSinceNow;
}
m_eventStream = FSEventStreamCreate(kCFAllocatorDefault,
&fsEventsCallback,
&streamContext,
reinterpret_cast<CFArrayRef>(pathsToWatch),
sinceEventId,
m_latency,
kFSEventStreamCreateFlagNone
);
FSEventStreamScheduleWithRunLoop(m_eventStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
bool success = FSEventStreamStart(m_eventStream);
if (!success)
{
qWarning() << "Failed to start event stream";
}
[pathsToWatch release];
}
void MacFSEventsWatcher::shutdown()
{
if (!isRunning())
{
return;
}
FSEventStreamStop(m_eventStream);
FSEventStreamUnscheduleFromRunLoop(m_eventStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
FSEventStreamInvalidate(m_eventStream);
FSEventStreamRelease(m_eventStream);
m_eventStream = 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment