#include #include #include #include #include #include "../Event.hh" #include "../Backend.hh" #include "./FSEventsBackend.hh" #include "../Watcher.hh" #define CONVERT_TIME(ts) ((uint64_t)ts.tv_sec * 1000000000 + ts.tv_nsec) #define IGNORED_FLAGS (kFSEventStreamEventFlagItemIsHardlink | kFSEventStreamEventFlagItemIsLastHardlink | kFSEventStreamEventFlagItemIsSymlink | kFSEventStreamEventFlagItemIsDir | kFSEventStreamEventFlagItemIsFile) void stopStream(FSEventStreamRef stream, CFRunLoopRef runLoop) { FSEventStreamStop(stream); FSEventStreamUnscheduleFromRunLoop(stream, runLoop, kCFRunLoopDefaultMode); FSEventStreamInvalidate(stream); FSEventStreamRelease(stream); } // macOS has a case insensitive file system by default. In order to detect // file renames that only affect case, we need to get the canonical path // and compare it with the input path to determine if a file was created or deleted. bool pathExists(char *path) { int fd = open(path, O_RDONLY | O_SYMLINK); if (fd == -1) { return false; } char buf[PATH_MAX]; if (fcntl(fd, F_GETPATH, buf) == -1) { close(fd); return false; } bool res = strncmp(path, buf, PATH_MAX) == 0; close(fd); return res; } struct State { FSEventStreamRef stream; std::shared_ptr tree; uint64_t since; }; void FSEventsCallback( ConstFSEventStreamRef streamRef, void *clientCallBackInfo, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[] ) { char **paths = (char **)eventPaths; Watcher *watcher = (Watcher *)clientCallBackInfo; EventList *list = &watcher->mEvents; State *state = (State *)watcher->state; uint64_t since = state->since; bool deletedRoot = false; for (size_t i = 0; i < numEvents; ++i) { bool isCreated = (eventFlags[i] & kFSEventStreamEventFlagItemCreated) == kFSEventStreamEventFlagItemCreated; bool isRemoved = (eventFlags[i] & kFSEventStreamEventFlagItemRemoved) == kFSEventStreamEventFlagItemRemoved; bool isModified = (eventFlags[i] & kFSEventStreamEventFlagItemModified) == kFSEventStreamEventFlagItemModified || (eventFlags[i] & kFSEventStreamEventFlagItemInodeMetaMod) == kFSEventStreamEventFlagItemInodeMetaMod || (eventFlags[i] & kFSEventStreamEventFlagItemFinderInfoMod) == kFSEventStreamEventFlagItemFinderInfoMod || (eventFlags[i] & kFSEventStreamEventFlagItemChangeOwner) == kFSEventStreamEventFlagItemChangeOwner || (eventFlags[i] & kFSEventStreamEventFlagItemXattrMod) == kFSEventStreamEventFlagItemXattrMod; bool isRenamed = (eventFlags[i] & kFSEventStreamEventFlagItemRenamed) == kFSEventStreamEventFlagItemRenamed; bool isDone = (eventFlags[i] & kFSEventStreamEventFlagHistoryDone) == kFSEventStreamEventFlagHistoryDone; bool isDir = (eventFlags[i] & kFSEventStreamEventFlagItemIsDir) == kFSEventStreamEventFlagItemIsDir; if (isDone) { watcher->notify(); break; } auto ignoredFlags = IGNORED_FLAGS; if (__builtin_available(macOS 10.13, *)) { ignoredFlags |= kFSEventStreamEventFlagItemCloned; } // If we don't care about any of the flags that are set, ignore this event. if ((eventFlags[i] & ~ignoredFlags) == 0) { continue; } // FSEvents exclusion paths only apply to files, not directories. if (watcher->isIgnored(paths[i])) { continue; } // Handle unambiguous events first if (isCreated && !(isRemoved || isModified || isRenamed)) { state->tree->add(paths[i], 0, isDir); list->create(paths[i]); } else if (isRemoved && !(isCreated || isModified || isRenamed)) { state->tree->remove(paths[i]); list->remove(paths[i]); if (paths[i] == watcher->mDir) { deletedRoot = true; } } else if (isModified && !(isCreated || isRemoved || isRenamed)) { state->tree->update(paths[i], 0); list->update(paths[i]); } else { // If multiple flags were set, then we need to call `stat` to determine if the file really exists. // This helps disambiguate creates, updates, and deletes. struct stat file; if (!pathExists(paths[i]) || stat(paths[i], &file)) { // File does not exist, so we have to assume it was removed. This is not exact since the // flags set by fsevents get coalesced together (e.g. created & deleted), so there is no way to // know whether the create and delete both happened since our snapshot (in which case // we'd rather ignore this event completely). This will result in some extra delete events // being emitted for files we don't know about, but that is the best we can do. state->tree->remove(paths[i]); list->remove(paths[i]); if (paths[i] == watcher->mDir) { deletedRoot = true; } continue; } // If the file was modified, and existed before, then this is an update, otherwise a create. uint64_t ctime = CONVERT_TIME(file.st_birthtimespec); uint64_t mtime = CONVERT_TIME(file.st_mtimespec); auto existed = !since && state->tree->find(paths[i]); if (isModified && (existed || ctime <= since)) { state->tree->update(paths[i], mtime); list->update(paths[i]); } else { state->tree->add(paths[i], mtime, S_ISDIR(file.st_mode)); list->create(paths[i]); } } } if (watcher->mWatched) { watcher->notify(); } // Stop watching if the root directory was deleted. if (deletedRoot) { stopStream((FSEventStreamRef)streamRef, CFRunLoopGetCurrent()); delete state; watcher->state = NULL; } } void checkWatcher(Watcher &watcher) { struct stat file; if (stat(watcher.mDir.c_str(), &file)) { throw WatcherError(strerror(errno), &watcher); } if (!S_ISDIR(file.st_mode)) { throw WatcherError(strerror(ENOTDIR), &watcher); } } void FSEventsBackend::startStream(Watcher &watcher, FSEventStreamEventId id) { checkWatcher(watcher); CFAbsoluteTime latency = 0.01; CFStringRef fileWatchPath = CFStringCreateWithCString( NULL, watcher.mDir.c_str(), kCFStringEncodingUTF8 ); CFArrayRef pathsToWatch = CFArrayCreate( NULL, (const void **)&fileWatchPath, 1, NULL ); FSEventStreamContext callbackInfo {0, (void *)&watcher, nullptr, nullptr, nullptr}; FSEventStreamRef stream = FSEventStreamCreate( NULL, &FSEventsCallback, &callbackInfo, pathsToWatch, id, latency, kFSEventStreamCreateFlagFileEvents ); CFMutableArrayRef exclusions = CFArrayCreateMutable(NULL, watcher.mIgnore.size(), NULL); for (auto it = watcher.mIgnore.begin(); it != watcher.mIgnore.end(); it++) { CFStringRef path = CFStringCreateWithCString( NULL, it->c_str(), kCFStringEncodingUTF8 ); CFArrayAppendValue(exclusions, (const void *)path); } FSEventStreamSetExclusionPaths(stream, exclusions); FSEventStreamScheduleWithRunLoop(stream, mRunLoop, kCFRunLoopDefaultMode); bool started = FSEventStreamStart(stream); CFRelease(pathsToWatch); CFRelease(fileWatchPath); if (!started) { FSEventStreamRelease(stream); throw WatcherError("Error starting FSEvents stream", &watcher); } State *s = (State *)watcher.state; s->tree = std::make_shared(watcher.mDir); s->stream = stream; } void FSEventsBackend::start() { mRunLoop = CFRunLoopGetCurrent(); CFRetain(mRunLoop); // Unlock once run loop has started. CFRunLoopPerformBlock(mRunLoop, kCFRunLoopDefaultMode, ^ { notifyStarted(); }); CFRunLoopWakeUp(mRunLoop); CFRunLoopRun(); } FSEventsBackend::~FSEventsBackend() { std::unique_lock lock(mMutex); CFRunLoopStop(mRunLoop); CFRelease(mRunLoop); } void FSEventsBackend::writeSnapshot(Watcher &watcher, std::string *snapshotPath) { std::unique_lock lock(mMutex); checkWatcher(watcher); FSEventStreamEventId id = FSEventsGetCurrentEventId(); std::ofstream ofs(*snapshotPath); ofs << id; ofs << "\n"; struct timespec now; clock_gettime(CLOCK_REALTIME, &now); ofs << CONVERT_TIME(now); } void FSEventsBackend::getEventsSince(Watcher &watcher, std::string *snapshotPath) { std::unique_lock lock(mMutex); std::ifstream ifs(*snapshotPath); if (ifs.fail()) { return; } FSEventStreamEventId id; uint64_t since; ifs >> id; ifs >> since; State *s = new State; s->since = since; watcher.state = (void *)s; startStream(watcher, id); watcher.wait(); stopStream(s->stream, mRunLoop); delete s; watcher.state = NULL; } // This function is called by Backend::watch which takes a lock on mMutex void FSEventsBackend::subscribe(Watcher &watcher) { State *s = new State; s->since = 0; watcher.state = (void *)s; startStream(watcher, kFSEventStreamEventIdSinceNow); } // This function is called by Backend::unwatch which takes a lock on mMutex void FSEventsBackend::unsubscribe(Watcher &watcher) { State *s = (State *)watcher.state; if (s != NULL) { stopStream(s->stream, mRunLoop); delete s; watcher.state = NULL; } }