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 | |