Files
wxWidgets/src/osx/fswatcher_fsevents.cpp
Richard Gibson c3d5853faa Fix handling symlinks in wxOSX wxFileSystemWatcher
Set the correct file path for symlink creation/deletion events.

Closes https://github.com/wxWidgets/wxWidgets/pull/2611
2021-12-09 19:57:00 +01:00

522 lines
16 KiB
C++

/////////////////////////////////////////////////////////////////////////////
// Name: src/osx/fswatcher_fsevents.cpp
// Purpose: File System watcher that uses the FSEvents API
// of OS X to efficiently watch trees
// Author: Roberto Perpuly
// Created: 2015-04-24
// Copyright: (c) 2015 Roberto Perpuly <robertop2004@gmail.com>
// Licence: wxWindows licence
/////////////////////////////////////////////////////////////////////////////
// For compilers that support precompilation, includes "wx.h".
#include "wx/wxprec.h"
#if wxUSE_FSWATCHER && defined(wxHAVE_FSEVENTS_FILE_NOTIFICATIONS)
#include "wx/fswatcher.h"
#include "wx/osx/core/cfstring.h"
#include "wx/osx/core/private/strconv_cf.h"
#include <CoreFoundation/CoreFoundation.h>
#include <CoreServices/CoreServices.h>
// A small class that we will give the FSEvents
// framework, which will be forwarded to the function
// that gets called when files change.
class wxFSEventWatcherContext
{
public:
// Watcher pointer will not be owned by this class.
wxFSEventWatcherContext(wxFsEventsFileSystemWatcher* watcher,
int watcherEventFlags,
const wxString& filespec)
: m_watcher(watcher)
, m_watcherEventFlags(watcherEventFlags)
, m_filespec(filespec)
{
}
// Will return true if the given event file and flags
// match the filespec and event flags given to the
// AddTree method.
bool IsDesiredEvent(const wxFileName& eventFileName, int eventFlags)
{
// warning and errors are always sent to the event handler
if ( eventFlags & wxFSW_EVENT_ERROR )
{
return true;
}
if ( eventFlags & wxFSW_EVENT_WARNING )
{
return true;
}
if ( (m_watcherEventFlags & eventFlags) == 0 )
{
// event handler does not want to see this event
return false;
}
return m_filespec.empty() ||
wxMatchWild(m_filespec, eventFileName.GetFullName());
}
wxFsEventsFileSystemWatcher* m_watcher;
// The event flags that the event handler
// desires to be notified of.
int m_watcherEventFlags;
// The filespec that the event handler
// desires to be notified of.
wxString m_filespec;
private:
wxDECLARE_NO_COPY_CLASS(wxFSEventWatcherContext);
};
// Translate kFSEventStreamEventFlag* flags
// to wxFSW_EVENT_* flags.
// warning and msg are out parameters, filled in when
// there is an error in the stream.
static int wxFSEventsToWatcherFlags(FSEventStreamEventFlags flags,
wxFSWWarningType& warning, wxString& msg)
{
msg.clear();
warning = wxFSW_WARNING_NONE;
// see https://developer.apple.com/library/mac/documentation/Darwin/Reference/FSEvents_Ref/index.html
// for event flag meanings
int ret = 0;
int warnings =
kFSEventStreamEventFlagMustScanSubDirs
| kFSEventStreamEventFlagUserDropped
| kFSEventStreamEventFlagKernelDropped
| kFSEventStreamEventFlagMount
;
int errors = kFSEventStreamEventFlagRootChanged;
// The following flags are not handled:
// kFSEventStreamEventFlagHistoryDone (we never ask for old events)
// kFSEventStreamEventFlagEventIdsWrapped ( we don't keep track nor
// expose event IDs)
int created = kFSEventStreamEventFlagItemCreated;
int deleted = kFSEventStreamEventFlagItemRemoved;
int renamed = kFSEventStreamEventFlagItemRenamed;
int modified = kFSEventStreamEventFlagItemModified;
int attrib = kFSEventStreamEventFlagItemChangeOwner
| kFSEventStreamEventFlagItemFinderInfoMod
| kFSEventStreamEventFlagItemInodeMetaMod
| kFSEventStreamEventFlagItemXattrMod;
if ( created & flags )
{
ret |= wxFSW_EVENT_CREATE;
}
if ( deleted & flags )
{
ret |= wxFSW_EVENT_DELETE;
}
if ( renamed & flags )
{
ret |= wxFSW_EVENT_RENAME;
}
if ( modified & flags )
{
ret |= wxFSW_EVENT_MODIFY;
}
if ( attrib & flags )
{
ret |= wxFSW_EVENT_ATTRIB;
}
if ( kFSEventStreamEventFlagUnmount & flags )
{
ret |= wxFSW_EVENT_UNMOUNT;
}
if ( warnings & flags )
{
warning = wxFSW_WARNING_GENERAL;
ret |= wxFSW_EVENT_WARNING;
if (flags & kFSEventStreamEventFlagMustScanSubDirs)
{
msg += "Must re-scan sub directories.";
}
if (flags & kFSEventStreamEventFlagUserDropped)
{
msg += "User dropped events";
warning = wxFSW_WARNING_OVERFLOW;
}
if (flags & kFSEventStreamEventFlagKernelDropped)
{
msg += "Kernel dropped events";
warning = wxFSW_WARNING_OVERFLOW;
}
if (flags & kFSEventStreamEventFlagMount)
{
msg += "A volume was mounted underneath the watched directory.";
}
}
if ( errors & flags )
{
ret |= wxFSW_EVENT_ERROR;
msg = "Path being watched has been renamed";
}
// don't think that FSEvents tells us about wxFSW_EVENT_ACCESS
return ret;
}
// Fills in eventFileName appropriately based on whether the
// event was on a file or a directory.
static void FileNameFromEvent(wxFileName& eventFileName, char* path,
FSEventStreamEventFlags flags)
{
wxString strPath(path);
if ( flags & kFSEventStreamEventFlagItemIsFile )
{
eventFileName.Assign(strPath);
}
if ( flags & kFSEventStreamEventFlagItemIsDir )
{
eventFileName.AssignDir(strPath);
}
if ( flags & kFSEventStreamEventFlagItemIsSymlink )
{
eventFileName.Assign(strPath);
}
}
// This is the function that the FsEvents API
// will call to notify us that a file has been changed.
static void wxFSEventCallback(ConstFSEventStreamRef WXUNUSED(streamRef), void *clientCallBackInfo,
size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[],
const FSEventStreamEventId WXUNUSED(eventIds)[])
{
wxFSEventWatcherContext* context =
(wxFSEventWatcherContext*) clientCallBackInfo;
char** paths = (char**) eventPaths;
int lastWxEventFlags = 0;
wxFileName lastEventFileName;
wxString msg;
wxFSWWarningType warning = wxFSW_WARNING_NONE;
wxFileName eventFileName;
for ( size_t i = 0; i < numEvents; i++ )
{
FSEventStreamEventFlags flags = eventFlags[i];
FileNameFromEvent(eventFileName, paths[i], flags);
int wxEventFlags = wxFSEventsToWatcherFlags(flags, warning, msg);
if ( context->IsDesiredEvent(eventFileName, wxEventFlags) )
{
// This is a naive way of looking for file renames
// wx presents a renames with a from and to paths
// but fs events events do not give us this (it only
// provides that a file was renamed, not what the new
// name is).
// We deduce the old and new paths by looking for consecutive
// renames. This is very naive and won't catch simulatenous
// renames inside the latency period, nor renames from/to
// a directory that is not inside the watched paths.
if (wxEventFlags == wxFSW_EVENT_RENAME && lastWxEventFlags == wxFSW_EVENT_RENAME)
{
context->m_watcher->PostChange(lastEventFileName, eventFileName, wxEventFlags);
}
else if (flags == kFSEventStreamEventFlagRootChanged)
{
// send two events: the delete event and the error event
context->m_watcher->PostChange(eventFileName, eventFileName, wxFSW_EVENT_DELETE);
context->m_watcher->PostError(msg);
}
else if (wxEventFlags != wxFSW_EVENT_RENAME)
{
context->m_watcher->PostChange(eventFileName, eventFileName, wxEventFlags);
}
else
{
// This is a "rename" event that we only saw once, meaning that
// a file was renamed to somewhere inside the watched tree
// OR a file was renamed to somewhere outside the watched tree.
if (!eventFileName.IsDir())
{
int fileEventType = eventFileName.FileExists() ? wxFSW_EVENT_CREATE : wxFSW_EVENT_DELETE;
context->m_watcher->PostChange(eventFileName, eventFileName, fileEventType);
}
if (eventFileName.IsDir())
{
int dirEventType = eventFileName.DirExists() ? wxFSW_EVENT_CREATE : wxFSW_EVENT_DELETE;
context->m_watcher->PostChange(eventFileName, eventFileName, dirEventType);
}
}
if (wxEventFlags & wxFSW_EVENT_WARNING)
{
context->m_watcher->PostWarning(warning, msg);
}
// A single rename (without the second rename) may be due
// to the file being renamed into a directory outside of the
// watch path.
lastWxEventFlags = wxEventFlags;
lastEventFileName = eventFileName;
}
}
}
static void wxDeleteContext(const void* context)
{
wxFSEventWatcherContext* watcherContext =
static_cast<wxFSEventWatcherContext*>(const_cast<void*>(context));
delete watcherContext;
}
WX_DECLARE_STRING_HASH_MAP(FSEventStreamRef, FSEventStreamRefMap);
struct wxFsEventsFileSystemWatcher::PrivateData
{
// map of path => FSEventStreamRef
FSEventStreamRefMap m_streams;
};
wxFsEventsFileSystemWatcher::wxFsEventsFileSystemWatcher()
: wxKqueueFileSystemWatcher(), m_pImpl(new PrivateData)
{
}
wxFsEventsFileSystemWatcher::wxFsEventsFileSystemWatcher(const wxFileName& path,
int events)
: wxKqueueFileSystemWatcher(path, events), m_pImpl(new PrivateData)
{
}
wxFsEventsFileSystemWatcher::~wxFsEventsFileSystemWatcher()
{
delete m_pImpl;
}
bool wxFsEventsFileSystemWatcher::AddTree(const wxFileName& path, int events,
const wxString& filespec)
{
if (!path.DirExists())
{
return false;
}
wxString canonical = GetCanonicalPath(path);
if ( canonical.empty() )
{
return false;
}
CFRunLoopRef cfLoop = CFRunLoopGetCurrent();
wxASSERT_MSG(
cfLoop,
"there must be a current event loop; this file watcher needs it."
);
if ( ! cfLoop )
{
return false;
}
if ( m_pImpl->m_streams.find(canonical) != m_pImpl->m_streams.end() )
{
// How to take into account filespec
// if client adds a watch for /home/*.cpp
// and then on another call wants to add a
// call to /home/*.h
// Ideally we should not create another watch
// however we would need to keep both filespecs
// around, which we don't do now.
return false;
}
// Will need to pass the desired event flags
// and filespec to our callback via the context
// we make sure to give the context a cleanup
// callback.
FSEventStreamContext ctx;
wxFSEventWatcherContext* watcherContext = new wxFSEventWatcherContext(
this, events, filespec.Clone()
);
ctx.version = 0;
ctx.info = watcherContext;
ctx.retain = NULL;
ctx.release = &wxDeleteContext;
ctx.copyDescription = NULL;
CFTimeInterval latency = 0.2;
wxMacUniCharBuffer pathChars(path.GetPath());
CFStringRef pathRef = CFStringCreateWithCharacters(
kCFAllocatorDefault,
pathChars.GetBuffer(),
pathChars.GetChars()
);
CFArrayRef pathRefs = CFArrayCreate(
kCFAllocatorDefault, (const void**)&pathRef, 1, NULL
);
FSEventStreamCreateFlags flags = kFSEventStreamCreateFlagWatchRoot
| kFSEventStreamCreateFlagFileEvents;
FSEventStreamRef stream = FSEventStreamCreate(
kCFAllocatorDefault,
&wxFSEventCallback,
&ctx,
pathRefs, kFSEventStreamEventIdSinceNow,
latency, flags);
bool started = false;
if ( stream )
{
FSEventStreamScheduleWithRunLoop(stream, cfLoop, kCFRunLoopDefaultMode);
started = FSEventStreamStart(stream);
if ( started )
{
m_pImpl->m_streams[canonical] = stream;
}
}
// cleanup the paths, as we own the pointers
CFRelease(pathRef);
CFRelease(pathRefs);
wxASSERT_MSG(stream, "could not create FS stream");
return started;
}
bool wxFsEventsFileSystemWatcher::RemoveTree(const wxFileName& path)
{
wxString canonical = GetCanonicalPath(path);
if ( canonical.empty() )
{
return false;
}
// Remove any kqueue watches created with Add()
// RemoveTree() should remove all watches no matter
// if they are tree watches or single directory watches.
wxArrayString dirsWatched;
wxKqueueFileSystemWatcher::GetWatchedPaths(&dirsWatched);
for ( size_t i = 0; i < dirsWatched.size(); i++ )
{
if (dirsWatched[i].Find(canonical) == 0)
{
wxKqueueFileSystemWatcher::Remove(dirsWatched[i]);
}
}
FSEventStreamRefMap::iterator it = m_pImpl->m_streams.find(canonical);
bool removed = false;
if ( it != m_pImpl->m_streams.end() )
{
FSEventStreamStop(it->second);
FSEventStreamInvalidate(it->second);
FSEventStreamRelease(it->second);
m_pImpl->m_streams.erase(it);
removed = true;
}
return removed;
}
bool wxFsEventsFileSystemWatcher::RemoveAll()
{
// remove all watches created with Add()
bool ret = wxKqueueFileSystemWatcher::RemoveAll();
FSEventStreamRefMap::iterator it = m_pImpl->m_streams.begin();
while ( it != m_pImpl->m_streams.end() )
{
FSEventStreamStop(it->second);
FSEventStreamInvalidate(it->second);
FSEventStreamRelease(it->second);
++it;
ret = true;
}
m_pImpl->m_streams.clear();
return ret;
}
void wxFsEventsFileSystemWatcher::PostChange(const wxFileName& oldFileName,
const wxFileName& newFileName, int event)
{
wxASSERT_MSG(this->GetOwner(), "owner must exist");
if ( !this->GetOwner() )
{
return;
}
// FSEvents flags are a bit mask, but wx FSW events
// are not, meaning that FSEvent flag might be
// kFSEventStreamEventFlagItemCreated | kFSEventStreamEventFlagItemInodeMetaMod
// this means we must send 2 events not 1.
int allEvents[6] = {
wxFSW_EVENT_CREATE,
wxFSW_EVENT_DELETE,
wxFSW_EVENT_RENAME,
wxFSW_EVENT_MODIFY,
wxFSW_EVENT_ACCESS,
wxFSW_EVENT_ATTRIB
};
for ( unsigned i = 0; i < WXSIZEOF(allEvents); i++ )
{
if ( event & allEvents[i] )
{
wxFileSystemWatcherEvent* evt = new wxFileSystemWatcherEvent(
allEvents[i], oldFileName, newFileName
);
wxQueueEvent(this->GetOwner(), evt);
}
}
}
void wxFsEventsFileSystemWatcher::PostWarning(wxFSWWarningType warning,
const wxString& msg)
{
wxFileSystemWatcherEvent* evt = new wxFileSystemWatcherEvent(
wxFSW_EVENT_WARNING, warning, msg
);
wxASSERT_MSG(this->GetOwner(), "owner must exist");
if (this->GetOwner())
{
wxQueueEvent(this->GetOwner(), evt);
}
}
void wxFsEventsFileSystemWatcher::PostError(const wxString& msg)
{
wxFileSystemWatcherEvent* evt = new wxFileSystemWatcherEvent(
wxFSW_EVENT_ERROR, wxFSW_WARNING_NONE, msg
);
wxASSERT_MSG(this->GetOwner(), "owner must exist");
if (this->GetOwner())
{
wxQueueEvent(this->GetOwner(), evt);
}
}
int wxFsEventsFileSystemWatcher::GetWatchedPathsCount() const
{
return m_pImpl->m_streams.size() + wxFileSystemWatcherBase::GetWatchedPathsCount();
}
int wxFsEventsFileSystemWatcher::GetWatchedPaths(wxArrayString* paths) const
{
wxCHECK_MSG( paths != NULL, -1, "Null array passed to retrieve paths");
if ( !paths )
{
return 0;
}
wxFileSystemWatcherBase::GetWatchedPaths(paths);
FSEventStreamRefMap::const_iterator it = m_pImpl->m_streams.begin();
for ( ; it != m_pImpl->m_streams.end(); ++it )
{
paths->push_back(it->first);
}
return paths->size();
}
#endif // wxUSE_FSWATCHER