DirectoryWatcher-mac.cpp revision 360784
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
18using namespace llvm;
19using namespace clang;
20
21static void stopFSEventStream(FSEventStreamRef);
22
23namespace {
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
43class DirectoryWatcherMac : public clang::DirectoryWatcher {
44public:
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
63private:
64  FSEventStreamRef EventStream;
65  std::function<void(llvm::ArrayRef<Event>, bool)> Receiver;
66  const std::string WatchedDirPath;
67};
68
69struct 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
86constexpr const FSEventStreamEventFlags StreamInvalidatingFlags =
87    kFSEventStreamEventFlagUserDropped | kFSEventStreamEventFlagKernelDropped |
88    kFSEventStreamEventFlagMustScanSubDirs;
89
90constexpr const FSEventStreamEventFlags ModifyingFileEvents =
91    kFSEventStreamEventFlagItemCreated | kFSEventStreamEventFlagItemRenamed |
92    kFSEventStreamEventFlagItemModified;
93
94static 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
149FSEventStreamRef 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
197void stopFSEventStream(FSEventStreamRef EventStream) {
198  if (!EventStream)
199    return;
200  FSEventStreamStop(EventStream);
201  FSEventStreamInvalidate(EventStream);
202  FSEventStreamRelease(EventStream);
203}
204
205llvm::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