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