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
19using namespace llvm;
20using namespace clang;
21
22#if TARGET_OS_OSX
23
24static void stopFSEventStream(FSEventStreamRef);
25
26namespace {
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
46class DirectoryWatcherMac : public clang::DirectoryWatcher {
47public:
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
73private:
74 dispatch_queue_t Queue;
75 FSEventStreamRef EventStream;
76 std::function<void(llvm::ArrayRef<Event>, bool)> Receiver;
77 const std::string WatchedDirPath;
78};
79
80struct 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
97constexpr const FSEventStreamEventFlags StreamInvalidatingFlags =
98 kFSEventStreamEventFlagUserDropped | kFSEventStreamEventFlagKernelDropped |
99 kFSEventStreamEventFlagMustScanSubDirs;
100
101constexpr const FSEventStreamEventFlags ModifyingFileEvents =
102 kFSEventStreamEventFlagItemCreated | kFSEventStreamEventFlagItemRenamed |
103 kFSEventStreamEventFlagItemModified;
104
105static 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
160FSEventStreamRef 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
208void stopFSEventStream(FSEventStreamRef EventStream) {
209 if (!EventStream)
210 return;
211 FSEventStreamStop(EventStream);
212 FSEventStreamInvalidate(EventStream);
213 FSEventStreamRelease(EventStream);
214}
215
216llvm::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
258llvm::Expected<std::unique_ptr<DirectoryWatcher>>
259clang::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

source code of clang/lib/DirectoryWatcher/mac/DirectoryWatcher-mac.cpp