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