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       dispatch_queue_t Queue, FSEventStreamRef EventStream,
47       std::function<void(llvm::ArrayRef<DirectoryWatcher::Event>, bool)>
48           Receiver,
49       llvm::StringRef WatchedDirPath)
50       : Queue(Queue), EventStream(EventStream), Receiver(Receiver),
51         WatchedDirPath(WatchedDirPath) {}
52 
53   ~DirectoryWatcherMac() override {
54     // FSEventStreamStop and Invalidate must be called after Start and
55     // SetDispatchQueue to follow FSEvents API contract. The call to Receiver
56     // also uses Queue to not race with the initial scan.
57     dispatch_sync(Queue, ^{
58       stopFSEventStream(EventStream);
59       EventStream = nullptr;
60       Receiver(
61           DirectoryWatcher::Event(
62               DirectoryWatcher::Event::EventKind::WatcherGotInvalidated, ""),
63           false);
64     });
65 
66     // Balance initial creation.
67     dispatch_release(Queue);
68   }
69 
70 private:
71   dispatch_queue_t Queue;
72   FSEventStreamRef EventStream;
73   std::function<void(llvm::ArrayRef<Event>, bool)> Receiver;
74   const std::string WatchedDirPath;
75 };
76 
77 struct EventStreamContextData {
78   std::string WatchedPath;
79   std::function<void(llvm::ArrayRef<DirectoryWatcher::Event>, bool)> Receiver;
80 
81   EventStreamContextData(
82       std::string &&WatchedPath,
83       std::function<void(llvm::ArrayRef<DirectoryWatcher::Event>, bool)>
84           Receiver)
85       : WatchedPath(std::move(WatchedPath)), Receiver(Receiver) {}
86 
87   // Needed for FSEvents
88   static void dispose(const void *ctx) {
89     delete static_cast<const EventStreamContextData *>(ctx);
90   }
91 };
92 } // namespace
93 
94 constexpr const FSEventStreamEventFlags StreamInvalidatingFlags =
95     kFSEventStreamEventFlagUserDropped | kFSEventStreamEventFlagKernelDropped |
96     kFSEventStreamEventFlagMustScanSubDirs;
97 
98 constexpr const FSEventStreamEventFlags ModifyingFileEvents =
99     kFSEventStreamEventFlagItemCreated | kFSEventStreamEventFlagItemRenamed |
100     kFSEventStreamEventFlagItemModified;
101 
102 static void eventStreamCallback(ConstFSEventStreamRef Stream,
103                                 void *ClientCallBackInfo, size_t NumEvents,
104                                 void *EventPaths,
105                                 const FSEventStreamEventFlags EventFlags[],
106                                 const FSEventStreamEventId EventIds[]) {
107   auto *ctx = static_cast<EventStreamContextData *>(ClientCallBackInfo);
108 
109   std::vector<DirectoryWatcher::Event> Events;
110   for (size_t i = 0; i < NumEvents; ++i) {
111     StringRef Path = ((const char **)EventPaths)[i];
112     const FSEventStreamEventFlags Flags = EventFlags[i];
113 
114     if (Flags & StreamInvalidatingFlags) {
115       Events.emplace_back(DirectoryWatcher::Event{
116           DirectoryWatcher::Event::EventKind::WatcherGotInvalidated, ""});
117       break;
118     } else if (!(Flags & kFSEventStreamEventFlagItemIsFile)) {
119       // Subdirectories aren't supported - if some directory got removed it
120       // must've been the watched directory itself.
121       if ((Flags & kFSEventStreamEventFlagItemRemoved) &&
122           Path == ctx->WatchedPath) {
123         Events.emplace_back(DirectoryWatcher::Event{
124             DirectoryWatcher::Event::EventKind::WatchedDirRemoved, ""});
125         Events.emplace_back(DirectoryWatcher::Event{
126             DirectoryWatcher::Event::EventKind::WatcherGotInvalidated, ""});
127         break;
128       }
129       // No support for subdirectories - just ignore everything.
130       continue;
131     } else if (Flags & kFSEventStreamEventFlagItemRemoved) {
132       Events.emplace_back(DirectoryWatcher::Event::EventKind::Removed,
133                           llvm::sys::path::filename(Path));
134       continue;
135     } else if (Flags & ModifyingFileEvents) {
136       if (!getFileStatus(Path).hasValue()) {
137         Events.emplace_back(DirectoryWatcher::Event::EventKind::Removed,
138                             llvm::sys::path::filename(Path));
139       } else {
140         Events.emplace_back(DirectoryWatcher::Event::EventKind::Modified,
141                             llvm::sys::path::filename(Path));
142       }
143       continue;
144     }
145 
146     // default
147     Events.emplace_back(DirectoryWatcher::Event{
148         DirectoryWatcher::Event::EventKind::WatcherGotInvalidated, ""});
149     llvm_unreachable("Unknown FSEvent type.");
150   }
151 
152   if (!Events.empty()) {
153     ctx->Receiver(Events, /*IsInitial=*/false);
154   }
155 }
156 
157 FSEventStreamRef createFSEventStream(
158     StringRef Path,
159     std::function<void(llvm::ArrayRef<DirectoryWatcher::Event>, bool)> Receiver,
160     dispatch_queue_t Queue) {
161   if (Path.empty())
162     return nullptr;
163 
164   CFMutableArrayRef PathsToWatch = [&]() {
165     CFMutableArrayRef PathsToWatch =
166         CFArrayCreateMutable(nullptr, 0, &kCFTypeArrayCallBacks);
167     CFStringRef CfPathStr =
168         CFStringCreateWithBytes(nullptr, (const UInt8 *)Path.data(),
169                                 Path.size(), kCFStringEncodingUTF8, false);
170     CFArrayAppendValue(PathsToWatch, CfPathStr);
171     CFRelease(CfPathStr);
172     return PathsToWatch;
173   }();
174 
175   FSEventStreamContext Context = [&]() {
176     std::string RealPath;
177     {
178       SmallString<128> Storage;
179       StringRef P = llvm::Twine(Path).toNullTerminatedStringRef(Storage);
180       char Buffer[PATH_MAX];
181       if (::realpath(P.begin(), Buffer) != nullptr)
182         RealPath = Buffer;
183       else
184         RealPath = Path.str();
185     }
186 
187     FSEventStreamContext Context;
188     Context.version = 0;
189     Context.info = new EventStreamContextData(std::move(RealPath), Receiver);
190     Context.retain = nullptr;
191     Context.release = EventStreamContextData::dispose;
192     Context.copyDescription = nullptr;
193     return Context;
194   }();
195 
196   FSEventStreamRef Result = FSEventStreamCreate(
197       nullptr, eventStreamCallback, &Context, PathsToWatch,
198       kFSEventStreamEventIdSinceNow, /* latency in seconds */ 0.0,
199       kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagNoDefer);
200   CFRelease(PathsToWatch);
201 
202   return Result;
203 }
204 
205 void stopFSEventStream(FSEventStreamRef EventStream) {
206   if (!EventStream)
207     return;
208   FSEventStreamStop(EventStream);
209   FSEventStreamInvalidate(EventStream);
210   FSEventStreamRelease(EventStream);
211 }
212 
213 llvm::Expected<std::unique_ptr<DirectoryWatcher>> clang::DirectoryWatcher::create(
214     StringRef Path,
215     std::function<void(llvm::ArrayRef<DirectoryWatcher::Event>, bool)> Receiver,
216     bool WaitForInitialSync) {
217   dispatch_queue_t Queue =
218       dispatch_queue_create("DirectoryWatcher", DISPATCH_QUEUE_SERIAL);
219 
220   if (Path.empty())
221     llvm::report_fatal_error(
222         "DirectoryWatcher::create can not accept an empty Path.");
223 
224   auto EventStream = createFSEventStream(Path, Receiver, Queue);
225   assert(EventStream && "EventStream expected to be non-null");
226 
227   std::unique_ptr<DirectoryWatcher> Result =
228       std::make_unique<DirectoryWatcherMac>(Queue, EventStream, Receiver, Path);
229 
230   // We need to copy the data so the lifetime is ok after a const copy is made
231   // for the block.
232   const std::string CopiedPath = Path.str();
233 
234   auto InitWork = ^{
235     // We need to start watching the directory before we start scanning in order
236     // to not miss any event. By dispatching this on the same serial Queue as
237     // the FSEvents will be handled we manage to start watching BEFORE the
238     // inital scan and handling events ONLY AFTER the scan finishes.
239     FSEventStreamSetDispatchQueue(EventStream, Queue);
240     FSEventStreamStart(EventStream);
241     Receiver(getAsFileEvents(scanDirectory(CopiedPath)), /*IsInitial=*/true);
242   };
243 
244   if (WaitForInitialSync) {
245     dispatch_sync(Queue, InitWork);
246   } else {
247     dispatch_async(Queue, InitWork);
248   }
249 
250   return Result;
251 }
252