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 assert(!Path.empty() && "Path.empty()"); 154 155 CFMutableArrayRef PathsToWatch = [&]() { 156 CFMutableArrayRef PathsToWatch = 157 CFArrayCreateMutable(nullptr, 0, &kCFTypeArrayCallBacks); 158 CFStringRef CfPathStr = 159 CFStringCreateWithBytes(nullptr, (const UInt8 *)Path.data(), 160 Path.size(), kCFStringEncodingUTF8, false); 161 CFArrayAppendValue(PathsToWatch, CfPathStr); 162 CFRelease(CfPathStr); 163 return PathsToWatch; 164 }(); 165 166 FSEventStreamContext Context = [&]() { 167 std::string RealPath; 168 { 169 SmallString<128> Storage; 170 StringRef P = llvm::Twine(Path).toNullTerminatedStringRef(Storage); 171 char Buffer[PATH_MAX]; 172 if (::realpath(P.begin(), Buffer) != nullptr) 173 RealPath = Buffer; 174 else 175 RealPath = Path; 176 } 177 178 FSEventStreamContext Context; 179 Context.version = 0; 180 Context.info = new EventStreamContextData(std::move(RealPath), Receiver); 181 Context.retain = nullptr; 182 Context.release = EventStreamContextData::dispose; 183 Context.copyDescription = nullptr; 184 return Context; 185 }(); 186 187 FSEventStreamRef Result = FSEventStreamCreate( 188 nullptr, eventStreamCallback, &Context, PathsToWatch, 189 kFSEventStreamEventIdSinceNow, /* latency in seconds */ 0.0, 190 kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagNoDefer); 191 CFRelease(PathsToWatch); 192 193 return Result; 194 } 195 196 void stopFSEventStream(FSEventStreamRef EventStream) { 197 if (!EventStream) 198 return; 199 FSEventStreamStop(EventStream); 200 FSEventStreamInvalidate(EventStream); 201 FSEventStreamRelease(EventStream); 202 } 203 204 llvm::Expected<std::unique_ptr<DirectoryWatcher>> clang::DirectoryWatcher::create( 205 StringRef Path, 206 std::function<void(llvm::ArrayRef<DirectoryWatcher::Event>, bool)> Receiver, 207 bool WaitForInitialSync) { 208 dispatch_queue_t Queue = 209 dispatch_queue_create("DirectoryWatcher", DISPATCH_QUEUE_SERIAL); 210 211 assert(!Path.empty() && "Path.empty()"); 212 auto EventStream = createFSEventStream(Path, Receiver, Queue); 213 assert(EventStream && "EventStream expected to be non-null"); 214 215 std::unique_ptr<DirectoryWatcher> Result = 216 llvm::make_unique<DirectoryWatcherMac>(EventStream, Receiver, Path); 217 218 // We need to copy the data so the lifetime is ok after a const copy is made 219 // for the block. 220 const std::string CopiedPath = Path; 221 222 auto InitWork = ^{ 223 // We need to start watching the directory before we start scanning in order 224 // to not miss any event. By dispatching this on the same serial Queue as 225 // the FSEvents will be handled we manage to start watching BEFORE the 226 // inital scan and handling events ONLY AFTER the scan finishes. 227 FSEventStreamSetDispatchQueue(EventStream, Queue); 228 FSEventStreamStart(EventStream); 229 // We need to decrement the ref count for Queue as initialize() will return 230 // and FSEvents has incremented it. Since we have to wait for FSEvents to 231 // take ownership it's the easiest to do it here rather than main thread. 232 dispatch_release(Queue); 233 Receiver(getAsFileEvents(scanDirectory(CopiedPath)), /*IsInitial=*/true); 234 }; 235 236 if (WaitForInitialSync) { 237 dispatch_sync(Queue, InitWork); 238 } else { 239 dispatch_async(Queue, InitWork); 240 } 241 242 return Result; 243 } 244