| 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 | Args: "DirectoryWatcher is not implemented for this platform!" , |
| 265 | Args: llvm::inconvertibleErrorCode()); |
| 266 | } |
| 267 | |
| 268 | #endif // TARGET_OS_OSX |
| 269 | |