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-exception 6 // 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 18 using namespace llvm; 19 using namespace clang; 20 21 static void stopFSEventStream(FSEventStreamRef); 22 23 namespace { 24 25 /// This implementation is based on FSEvents API which implementation is 26 /// aggressively coallescing events. This can manifest as duplicate events. 27 /// 28 /// For example this scenario has been observed: 29 /// 30 /// create foo/bar 31 /// sleep 5 s 32 /// create DirectoryWatcherMac for dir foo 33 /// receive notification: bar EventKind::Modified 34 /// sleep 5 s 35 /// modify foo/bar 36 /// receive notification: bar EventKind::Modified 37 /// receive notification: bar EventKind::Modified 38 /// sleep 5 s 39 /// delete foo/bar 40 /// receive notification: bar EventKind::Modified 41 /// receive notification: bar EventKind::Modified 42 /// receive notification: bar EventKind::Removed 43 class DirectoryWatcherMac : public clang::DirectoryWatcher { 44 public: 45 DirectoryWatcherMac( 46 dispatch_queue_t Queue, FSEventStreamRef EventStream, 47 std::function<void(llvm::ArrayRef<DirectoryWatcher::Event>, bool)> 48 Receiver, 49 llvm::StringRef WatchedDirPath) 50 : Queue(Queue), EventStream(EventStream), Receiver(Receiver), 51 WatchedDirPath(WatchedDirPath) {} 52 53 ~DirectoryWatcherMac() override { 54 // FSEventStreamStop and Invalidate must be called after Start and 55 // SetDispatchQueue to follow FSEvents API contract. The call to Receiver 56 // also uses Queue to not race with the initial scan. 57 dispatch_sync(Queue, ^{ 58 stopFSEventStream(EventStream); 59 EventStream = nullptr; 60 Receiver( 61 DirectoryWatcher::Event( 62 DirectoryWatcher::Event::EventKind::WatcherGotInvalidated, ""), 63 false); 64 }); 65 66 // Balance initial creation. 67 dispatch_release(Queue); 68 } 69 70 private: 71 dispatch_queue_t Queue; 72 FSEventStreamRef EventStream; 73 std::function<void(llvm::ArrayRef<Event>, bool)> Receiver; 74 const std::string WatchedDirPath; 75 }; 76 77 struct EventStreamContextData { 78 std::string WatchedPath; 79 std::function<void(llvm::ArrayRef<DirectoryWatcher::Event>, bool)> Receiver; 80 81 EventStreamContextData( 82 std::string &&WatchedPath, 83 std::function<void(llvm::ArrayRef<DirectoryWatcher::Event>, bool)> 84 Receiver) 85 : WatchedPath(std::move(WatchedPath)), Receiver(Receiver) {} 86 87 // Needed for FSEvents 88 static void dispose(const void *ctx) { 89 delete static_cast<const EventStreamContextData *>(ctx); 90 } 91 }; 92 } // namespace 93 94 constexpr const FSEventStreamEventFlags StreamInvalidatingFlags = 95 kFSEventStreamEventFlagUserDropped | kFSEventStreamEventFlagKernelDropped | 96 kFSEventStreamEventFlagMustScanSubDirs; 97 98 constexpr const FSEventStreamEventFlags ModifyingFileEvents = 99 kFSEventStreamEventFlagItemCreated | kFSEventStreamEventFlagItemRenamed | 100 kFSEventStreamEventFlagItemModified; 101 102 static void eventStreamCallback(ConstFSEventStreamRef Stream, 103 void *ClientCallBackInfo, size_t NumEvents, 104 void *EventPaths, 105 const FSEventStreamEventFlags EventFlags[], 106 const FSEventStreamEventId EventIds[]) { 107 auto *ctx = static_cast<EventStreamContextData *>(ClientCallBackInfo); 108 109 std::vector<DirectoryWatcher::Event> Events; 110 for (size_t i = 0; i < NumEvents; ++i) { 111 StringRef Path = ((const char **)EventPaths)[i]; 112 const FSEventStreamEventFlags Flags = EventFlags[i]; 113 114 if (Flags & StreamInvalidatingFlags) { 115 Events.emplace_back(DirectoryWatcher::Event{ 116 DirectoryWatcher::Event::EventKind::WatcherGotInvalidated, ""}); 117 break; 118 } else if (!(Flags & kFSEventStreamEventFlagItemIsFile)) { 119 // Subdirectories aren't supported - if some directory got removed it 120 // must've been the watched directory itself. 121 if ((Flags & kFSEventStreamEventFlagItemRemoved) && 122 Path == ctx->WatchedPath) { 123 Events.emplace_back(DirectoryWatcher::Event{ 124 DirectoryWatcher::Event::EventKind::WatchedDirRemoved, ""}); 125 Events.emplace_back(DirectoryWatcher::Event{ 126 DirectoryWatcher::Event::EventKind::WatcherGotInvalidated, ""}); 127 break; 128 } 129 // No support for subdirectories - just ignore everything. 130 continue; 131 } else if (Flags & kFSEventStreamEventFlagItemRemoved) { 132 Events.emplace_back(DirectoryWatcher::Event::EventKind::Removed, 133 llvm::sys::path::filename(Path)); 134 continue; 135 } else if (Flags & ModifyingFileEvents) { 136 if (!getFileStatus(Path).hasValue()) { 137 Events.emplace_back(DirectoryWatcher::Event::EventKind::Removed, 138 llvm::sys::path::filename(Path)); 139 } else { 140 Events.emplace_back(DirectoryWatcher::Event::EventKind::Modified, 141 llvm::sys::path::filename(Path)); 142 } 143 continue; 144 } 145 146 // default 147 Events.emplace_back(DirectoryWatcher::Event{ 148 DirectoryWatcher::Event::EventKind::WatcherGotInvalidated, ""}); 149 llvm_unreachable("Unknown FSEvent type."); 150 } 151 152 if (!Events.empty()) { 153 ctx->Receiver(Events, /*IsInitial=*/false); 154 } 155 } 156 157 FSEventStreamRef createFSEventStream( 158 StringRef Path, 159 std::function<void(llvm::ArrayRef<DirectoryWatcher::Event>, bool)> Receiver, 160 dispatch_queue_t Queue) { 161 if (Path.empty()) 162 return nullptr; 163 164 CFMutableArrayRef PathsToWatch = [&]() { 165 CFMutableArrayRef PathsToWatch = 166 CFArrayCreateMutable(nullptr, 0, &kCFTypeArrayCallBacks); 167 CFStringRef CfPathStr = 168 CFStringCreateWithBytes(nullptr, (const UInt8 *)Path.data(), 169 Path.size(), kCFStringEncodingUTF8, false); 170 CFArrayAppendValue(PathsToWatch, CfPathStr); 171 CFRelease(CfPathStr); 172 return PathsToWatch; 173 }(); 174 175 FSEventStreamContext Context = [&]() { 176 std::string RealPath; 177 { 178 SmallString<128> Storage; 179 StringRef P = llvm::Twine(Path).toNullTerminatedStringRef(Storage); 180 char Buffer[PATH_MAX]; 181 if (::realpath(P.begin(), Buffer) != nullptr) 182 RealPath = Buffer; 183 else 184 RealPath = Path.str(); 185 } 186 187 FSEventStreamContext Context; 188 Context.version = 0; 189 Context.info = new EventStreamContextData(std::move(RealPath), Receiver); 190 Context.retain = nullptr; 191 Context.release = EventStreamContextData::dispose; 192 Context.copyDescription = nullptr; 193 return Context; 194 }(); 195 196 FSEventStreamRef Result = FSEventStreamCreate( 197 nullptr, eventStreamCallback, &Context, PathsToWatch, 198 kFSEventStreamEventIdSinceNow, /* latency in seconds */ 0.0, 199 kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagNoDefer); 200 CFRelease(PathsToWatch); 201 202 return Result; 203 } 204 205 void stopFSEventStream(FSEventStreamRef EventStream) { 206 if (!EventStream) 207 return; 208 FSEventStreamStop(EventStream); 209 FSEventStreamInvalidate(EventStream); 210 FSEventStreamRelease(EventStream); 211 } 212 213 llvm::Expected<std::unique_ptr<DirectoryWatcher>> clang::DirectoryWatcher::create( 214 StringRef Path, 215 std::function<void(llvm::ArrayRef<DirectoryWatcher::Event>, bool)> Receiver, 216 bool WaitForInitialSync) { 217 dispatch_queue_t Queue = 218 dispatch_queue_create("DirectoryWatcher", DISPATCH_QUEUE_SERIAL); 219 220 if (Path.empty()) 221 llvm::report_fatal_error( 222 "DirectoryWatcher::create can not accept an empty Path."); 223 224 auto EventStream = createFSEventStream(Path, Receiver, Queue); 225 assert(EventStream && "EventStream expected to be non-null"); 226 227 std::unique_ptr<DirectoryWatcher> Result = 228 std::make_unique<DirectoryWatcherMac>(Queue, EventStream, Receiver, Path); 229 230 // We need to copy the data so the lifetime is ok after a const copy is made 231 // for the block. 232 const std::string CopiedPath = Path.str(); 233 234 auto InitWork = ^{ 235 // We need to start watching the directory before we start scanning in order 236 // to not miss any event. By dispatching this on the same serial Queue as 237 // the FSEvents will be handled we manage to start watching BEFORE the 238 // inital scan and handling events ONLY AFTER the scan finishes. 239 FSEventStreamSetDispatchQueue(EventStream, Queue); 240 FSEventStreamStart(EventStream); 241 Receiver(getAsFileEvents(scanDirectory(CopiedPath)), /*IsInitial=*/true); 242 }; 243 244 if (WaitForInitialSync) { 245 dispatch_sync(Queue, InitWork); 246 } else { 247 dispatch_async(Queue, InitWork); 248 } 249 250 return Result; 251 } 252