269 lines · cpp
1//===- DirectoryWatcher-mac.cpp - Mac-platform directory watching ---------===//2//3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.4// See https://llvm.org/LICENSE.txt for license information.5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception6//7//===----------------------------------------------------------------------===//8 9#include "DirectoryScanner.h"10#include "clang/DirectoryWatcher/DirectoryWatcher.h"11 12#include "llvm/ADT/STLExtras.h"13#include "llvm/ADT/StringRef.h"14#include "llvm/Support/Error.h"15#include "llvm/Support/Path.h"16#include <CoreServices/CoreServices.h>17#include <TargetConditionals.h>18 19using namespace llvm;20using namespace clang;21 22#if TARGET_OS_OSX23 24static void stopFSEventStream(FSEventStreamRef);25 26namespace {27 28/// This implementation is based on FSEvents API which implementation is29/// aggressively coallescing events. This can manifest as duplicate events.30///31/// For example this scenario has been observed:32///33/// create foo/bar34/// sleep 5 s35/// create DirectoryWatcherMac for dir foo36/// receive notification: bar EventKind::Modified37/// sleep 5 s38/// modify foo/bar39/// receive notification: bar EventKind::Modified40/// receive notification: bar EventKind::Modified41/// sleep 5 s42/// delete foo/bar43/// receive notification: bar EventKind::Modified44/// receive notification: bar EventKind::Modified45/// receive notification: bar EventKind::Removed46class DirectoryWatcherMac : public clang::DirectoryWatcher {47public:48 DirectoryWatcherMac(49 dispatch_queue_t Queue, FSEventStreamRef EventStream,50 std::function<void(llvm::ArrayRef<DirectoryWatcher::Event>, bool)>51 Receiver,52 llvm::StringRef WatchedDirPath)53 : Queue(Queue), EventStream(EventStream), Receiver(Receiver),54 WatchedDirPath(WatchedDirPath) {}55 56 ~DirectoryWatcherMac() override {57 // FSEventStreamStop and Invalidate must be called after Start and58 // SetDispatchQueue to follow FSEvents API contract. The call to Receiver59 // also uses Queue to not race with the initial scan.60 dispatch_sync(Queue, ^{61 stopFSEventStream(EventStream);62 EventStream = nullptr;63 Receiver(64 DirectoryWatcher::Event(65 DirectoryWatcher::Event::EventKind::WatcherGotInvalidated, ""),66 false);67 });68 69 // Balance initial creation.70 dispatch_release(Queue);71 }72 73private:74 dispatch_queue_t Queue;75 FSEventStreamRef EventStream;76 std::function<void(llvm::ArrayRef<Event>, bool)> Receiver;77 const std::string WatchedDirPath;78};79 80struct EventStreamContextData {81 std::string WatchedPath;82 std::function<void(llvm::ArrayRef<DirectoryWatcher::Event>, bool)> Receiver;83 84 EventStreamContextData(85 std::string &&WatchedPath,86 std::function<void(llvm::ArrayRef<DirectoryWatcher::Event>, bool)>87 Receiver)88 : WatchedPath(std::move(WatchedPath)), Receiver(Receiver) {}89 90 // Needed for FSEvents91 static void dispose(const void *ctx) {92 delete static_cast<const EventStreamContextData *>(ctx);93 }94};95} // namespace96 97constexpr const FSEventStreamEventFlags StreamInvalidatingFlags =98 kFSEventStreamEventFlagUserDropped | kFSEventStreamEventFlagKernelDropped |99 kFSEventStreamEventFlagMustScanSubDirs;100 101constexpr const FSEventStreamEventFlags ModifyingFileEvents =102 kFSEventStreamEventFlagItemCreated | kFSEventStreamEventFlagItemRenamed |103 kFSEventStreamEventFlagItemModified;104 105static void eventStreamCallback(ConstFSEventStreamRef Stream,106 void *ClientCallBackInfo, size_t NumEvents,107 void *EventPaths,108 const FSEventStreamEventFlags EventFlags[],109 const FSEventStreamEventId EventIds[]) {110 auto *ctx = static_cast<EventStreamContextData *>(ClientCallBackInfo);111 112 std::vector<DirectoryWatcher::Event> Events;113 for (size_t i = 0; i < NumEvents; ++i) {114 StringRef Path = ((const char **)EventPaths)[i];115 const FSEventStreamEventFlags Flags = EventFlags[i];116 117 if (Flags & StreamInvalidatingFlags) {118 Events.emplace_back(DirectoryWatcher::Event{119 DirectoryWatcher::Event::EventKind::WatcherGotInvalidated, ""});120 break;121 } else if (!(Flags & kFSEventStreamEventFlagItemIsFile)) {122 // Subdirectories aren't supported - if some directory got removed it123 // must've been the watched directory itself.124 if ((Flags & kFSEventStreamEventFlagItemRemoved) &&125 Path == ctx->WatchedPath) {126 Events.emplace_back(DirectoryWatcher::Event{127 DirectoryWatcher::Event::EventKind::WatchedDirRemoved, ""});128 Events.emplace_back(DirectoryWatcher::Event{129 DirectoryWatcher::Event::EventKind::WatcherGotInvalidated, ""});130 break;131 }132 // No support for subdirectories - just ignore everything.133 continue;134 } else if (Flags & kFSEventStreamEventFlagItemRemoved) {135 Events.emplace_back(DirectoryWatcher::Event::EventKind::Removed,136 llvm::sys::path::filename(Path));137 continue;138 } else if (Flags & ModifyingFileEvents) {139 if (!getFileStatus(Path).has_value()) {140 Events.emplace_back(DirectoryWatcher::Event::EventKind::Removed,141 llvm::sys::path::filename(Path));142 } else {143 Events.emplace_back(DirectoryWatcher::Event::EventKind::Modified,144 llvm::sys::path::filename(Path));145 }146 continue;147 }148 149 // default150 Events.emplace_back(DirectoryWatcher::Event{151 DirectoryWatcher::Event::EventKind::WatcherGotInvalidated, ""});152 llvm_unreachable("Unknown FSEvent type.");153 }154 155 if (!Events.empty()) {156 ctx->Receiver(Events, /*IsInitial=*/false);157 }158}159 160FSEventStreamRef createFSEventStream(161 StringRef Path,162 std::function<void(llvm::ArrayRef<DirectoryWatcher::Event>, bool)> Receiver,163 dispatch_queue_t Queue) {164 if (Path.empty())165 return nullptr;166 167 CFMutableArrayRef PathsToWatch = [&]() {168 CFMutableArrayRef PathsToWatch =169 CFArrayCreateMutable(nullptr, 0, &kCFTypeArrayCallBacks);170 CFStringRef CfPathStr =171 CFStringCreateWithBytes(nullptr, (const UInt8 *)Path.data(),172 Path.size(), kCFStringEncodingUTF8, false);173 CFArrayAppendValue(PathsToWatch, CfPathStr);174 CFRelease(CfPathStr);175 return PathsToWatch;176 }();177 178 FSEventStreamContext Context = [&]() {179 std::string RealPath;180 {181 SmallString<128> Storage;182 StringRef P = llvm::Twine(Path).toNullTerminatedStringRef(Storage);183 char Buffer[PATH_MAX];184 if (::realpath(P.begin(), Buffer) != nullptr)185 RealPath = Buffer;186 else187 RealPath = Path.str();188 }189 190 FSEventStreamContext Context;191 Context.version = 0;192 Context.info = new EventStreamContextData(std::move(RealPath), Receiver);193 Context.retain = nullptr;194 Context.release = EventStreamContextData::dispose;195 Context.copyDescription = nullptr;196 return Context;197 }();198 199 FSEventStreamRef Result = FSEventStreamCreate(200 nullptr, eventStreamCallback, &Context, PathsToWatch,201 kFSEventStreamEventIdSinceNow, /* latency in seconds */ 0.0,202 kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagNoDefer);203 CFRelease(PathsToWatch);204 205 return Result;206}207 208void stopFSEventStream(FSEventStreamRef EventStream) {209 if (!EventStream)210 return;211 FSEventStreamStop(EventStream);212 FSEventStreamInvalidate(EventStream);213 FSEventStreamRelease(EventStream);214}215 216llvm::Expected<std::unique_ptr<DirectoryWatcher>> clang::DirectoryWatcher::create(217 StringRef Path,218 std::function<void(llvm::ArrayRef<DirectoryWatcher::Event>, bool)> Receiver,219 bool WaitForInitialSync) {220 dispatch_queue_t Queue =221 dispatch_queue_create("DirectoryWatcher", DISPATCH_QUEUE_SERIAL);222 223 if (Path.empty())224 llvm::report_fatal_error(225 "DirectoryWatcher::create can not accept an empty Path.");226 227 auto EventStream = createFSEventStream(Path, Receiver, Queue);228 assert(EventStream && "EventStream expected to be non-null");229 230 std::unique_ptr<DirectoryWatcher> Result =231 std::make_unique<DirectoryWatcherMac>(Queue, EventStream, Receiver, Path);232 233 // We need to copy the data so the lifetime is ok after a const copy is made234 // for the block.235 const std::string CopiedPath = Path.str();236 237 auto InitWork = ^{238 // We need to start watching the directory before we start scanning in order239 // to not miss any event. By dispatching this on the same serial Queue as240 // the FSEvents will be handled we manage to start watching BEFORE the241 // inital scan and handling events ONLY AFTER the scan finishes.242 FSEventStreamSetDispatchQueue(EventStream, Queue);243 FSEventStreamStart(EventStream);244 Receiver(getAsFileEvents(scanDirectory(CopiedPath)), /*IsInitial=*/true);245 };246 247 if (WaitForInitialSync) {248 dispatch_sync(Queue, InitWork);249 } else {250 dispatch_async(Queue, InitWork);251 }252 253 return Result;254}255 256#else // TARGET_OS_OSX257 258llvm::Expected<std::unique_ptr<DirectoryWatcher>>259clang::DirectoryWatcher::create(260 StringRef Path,261 std::function<void(llvm::ArrayRef<DirectoryWatcher::Event>, bool)> Receiver,262 bool WaitForInitialSync) {263 return llvm::make_error<llvm::StringError>(264 "DirectoryWatcher is not implemented for this platform!",265 llvm::inconvertibleErrorCode());266}267 268#endif // TARGET_OS_OSX269