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 #include <TargetConditionals.h> 18 19 using namespace llvm; 20 using namespace clang; 21 22 #if TARGET_OS_OSX 23 24 static void stopFSEventStream(FSEventStreamRef); 25 26 namespace { 27 28 /// This implementation is based on FSEvents API which implementation is 29 /// aggressively coallescing events. This can manifest as duplicate events. 30 /// 31 /// For example this scenario has been observed: 32 /// 33 /// create foo/bar 34 /// sleep 5 s 35 /// create DirectoryWatcherMac for dir foo 36 /// receive notification: bar EventKind::Modified 37 /// sleep 5 s 38 /// modify foo/bar 39 /// receive notification: bar EventKind::Modified 40 /// receive notification: bar EventKind::Modified 41 /// sleep 5 s 42 /// delete foo/bar 43 /// receive notification: bar EventKind::Modified 44 /// receive notification: bar EventKind::Modified 45 /// receive notification: bar EventKind::Removed 46 class DirectoryWatcherMac : public clang::DirectoryWatcher { 47 public: 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 and 58 // SetDispatchQueue to follow FSEvents API contract. The call to Receiver 59 // 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 73 private: 74 dispatch_queue_t Queue; 75 FSEventStreamRef EventStream; 76 std::function<void(llvm::ArrayRef<Event>, bool)> Receiver; 77 const std::string WatchedDirPath; 78 }; 79 80 struct 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 FSEvents 91 static void dispose(const void *ctx) { 92 delete static_cast<const EventStreamContextData *>(ctx); 93 } 94 }; 95 } // namespace 96 97 constexpr const FSEventStreamEventFlags StreamInvalidatingFlags = 98 kFSEventStreamEventFlagUserDropped | kFSEventStreamEventFlagKernelDropped | 99 kFSEventStreamEventFlagMustScanSubDirs; 100 101 constexpr const FSEventStreamEventFlags ModifyingFileEvents = 102 kFSEventStreamEventFlagItemCreated | kFSEventStreamEventFlagItemRenamed | 103 kFSEventStreamEventFlagItemModified; 104 105 static 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 it 123 // 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 // default 150 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 160 FSEventStreamRef 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 else 187 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 208 void stopFSEventStream(FSEventStreamRef EventStream) { 209 if (!EventStream) 210 return; 211 FSEventStreamStop(EventStream); 212 FSEventStreamInvalidate(EventStream); 213 FSEventStreamRelease(EventStream); 214 } 215 216 llvm::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 made 234 // 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 order 239 // to not miss any event. By dispatching this on the same serial Queue as 240 // the FSEvents will be handled we manage to start watching BEFORE the 241 // 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_OSX 257 258 llvm::Expected<std::unique_ptr<DirectoryWatcher>> 259 clang::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_OSX 269