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