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