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
19using namespace llvm;
20using namespace clang;
21
22#if TARGET_OS_OSX
23
24static void stopFSEventStream(FSEventStreamRef);
25
26namespace {
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
46class DirectoryWatcherMac : public clang::DirectoryWatcher {
47public:
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
73private:
74  dispatch_queue_t Queue;
75  FSEventStreamRef EventStream;
76  std::function<void(llvm::ArrayRef<Event>, bool)> Receiver;
77  const std::string WatchedDirPath;
78};
79
80struct 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
97constexpr const FSEventStreamEventFlags StreamInvalidatingFlags =
98    kFSEventStreamEventFlagUserDropped | kFSEventStreamEventFlagKernelDropped |
99    kFSEventStreamEventFlagMustScanSubDirs;
100
101constexpr const FSEventStreamEventFlags ModifyingFileEvents =
102    kFSEventStreamEventFlagItemCreated | kFSEventStreamEventFlagItemRenamed |
103    kFSEventStreamEventFlagItemModified;
104
105static 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
160FSEventStreamRef 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
208void stopFSEventStream(FSEventStreamRef EventStream) {
209  if (!EventStream)
210    return;
211  FSEventStreamStop(EventStream);
212  FSEventStreamInvalidate(EventStream);
213  FSEventStreamRelease(EventStream);
214}
215
216llvm::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
258llvm::Expected<std::unique_ptr<DirectoryWatcher>>
259clang::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