1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "base/files/file_path_watcher_fsevents.h"
6
7#include <list>
8
9#include "base/bind.h"
10#include "base/files/file_util.h"
11#include "base/lazy_instance.h"
12#include "base/logging.h"
13#include "base/mac/libdispatch_task_runner.h"
14#include "base/mac/scoped_cftyperef.h"
15#include "base/message_loop/message_loop.h"
16
17namespace base {
18
19namespace {
20
21// The latency parameter passed to FSEventsStreamCreate().
22const CFAbsoluteTime kEventLatencySeconds = 0.3;
23
24class FSEventsTaskRunner : public mac::LibDispatchTaskRunner {
25 public:
26   FSEventsTaskRunner()
27       : mac::LibDispatchTaskRunner("org.chromium.FilePathWatcherFSEvents") {
28   }
29
30 protected:
31   virtual ~FSEventsTaskRunner() {}
32};
33
34static LazyInstance<FSEventsTaskRunner>::Leaky g_task_runner =
35    LAZY_INSTANCE_INITIALIZER;
36
37// Resolve any symlinks in the path.
38FilePath ResolvePath(const FilePath& path) {
39  const unsigned kMaxLinksToResolve = 255;
40
41  std::vector<FilePath::StringType> component_vector;
42  path.GetComponents(&component_vector);
43  std::list<FilePath::StringType>
44      components(component_vector.begin(), component_vector.end());
45
46  FilePath result;
47  unsigned resolve_count = 0;
48  while (resolve_count < kMaxLinksToResolve && !components.empty()) {
49    FilePath component(*components.begin());
50    components.pop_front();
51
52    FilePath current;
53    if (component.IsAbsolute()) {
54      current = component;
55    } else {
56      current = result.Append(component);
57    }
58
59    FilePath target;
60    if (ReadSymbolicLink(current, &target)) {
61      if (target.IsAbsolute())
62        result.clear();
63      std::vector<FilePath::StringType> target_components;
64      target.GetComponents(&target_components);
65      components.insert(components.begin(), target_components.begin(),
66                        target_components.end());
67      resolve_count++;
68    } else {
69      result = current;
70    }
71  }
72
73  if (resolve_count >= kMaxLinksToResolve)
74    result.clear();
75  return result;
76}
77
78// The callback passed to FSEventStreamCreate().
79void FSEventsCallback(ConstFSEventStreamRef stream,
80                      void* event_watcher, size_t num_events,
81                      void* event_paths, const FSEventStreamEventFlags flags[],
82                      const FSEventStreamEventId event_ids[]) {
83  FilePathWatcherFSEvents* watcher =
84      reinterpret_cast<FilePathWatcherFSEvents*>(event_watcher);
85  DCHECK(g_task_runner.Get().RunsTasksOnCurrentThread());
86
87  bool root_changed = watcher->ResolveTargetPath();
88  std::vector<FilePath> paths;
89  FSEventStreamEventId root_change_at = FSEventStreamGetLatestEventId(stream);
90  for (size_t i = 0; i < num_events; i++) {
91    if (flags[i] & kFSEventStreamEventFlagRootChanged)
92      root_changed = true;
93    if (event_ids[i])
94      root_change_at = std::min(root_change_at, event_ids[i]);
95    paths.push_back(FilePath(
96        reinterpret_cast<char**>(event_paths)[i]).StripTrailingSeparators());
97  }
98
99  // Reinitialize the event stream if we find changes to the root. This is
100  // necessary since FSEvents doesn't report any events for the subtree after
101  // the directory to be watched gets created.
102  if (root_changed) {
103    // Resetting the event stream from within the callback fails (FSEvents spews
104    // bad file descriptor errors), so post a task to do the reset.
105    g_task_runner.Get().PostTask(
106        FROM_HERE,
107        Bind(&FilePathWatcherFSEvents::UpdateEventStream, watcher,
108             root_change_at));
109  }
110
111  watcher->OnFilePathsChanged(paths);
112}
113
114}  // namespace
115
116FilePathWatcherFSEvents::FilePathWatcherFSEvents() : fsevent_stream_(NULL) {
117}
118
119void FilePathWatcherFSEvents::OnFilePathsChanged(
120    const std::vector<FilePath>& paths) {
121  if (!message_loop()->BelongsToCurrentThread()) {
122    message_loop()->PostTask(
123        FROM_HERE,
124        Bind(&FilePathWatcherFSEvents::OnFilePathsChanged, this, paths));
125    return;
126  }
127
128  DCHECK(message_loop()->BelongsToCurrentThread());
129  if (resolved_target_.empty())
130    return;
131
132  for (size_t i = 0; i < paths.size(); i++) {
133    if (resolved_target_.IsParent(paths[i]) || resolved_target_ == paths[i]) {
134      callback_.Run(target_, false);
135      return;
136    }
137  }
138}
139
140bool FilePathWatcherFSEvents::Watch(const FilePath& path,
141                                    bool recursive,
142                                    const FilePathWatcher::Callback& callback) {
143  DCHECK(resolved_target_.empty());
144  DCHECK(MessageLoopForIO::current());
145  DCHECK(!callback.is_null());
146
147  // This class could support non-recursive watches, but that is currently
148  // left to FilePathWatcherKQueue.
149  if (!recursive)
150    return false;
151
152  set_message_loop(MessageLoopProxy::current());
153  callback_ = callback;
154  target_ = path;
155
156  FSEventStreamEventId start_event = FSEventsGetCurrentEventId();
157  g_task_runner.Get().PostTask(
158      FROM_HERE,
159      Bind(&FilePathWatcherFSEvents::StartEventStream, this, start_event));
160  return true;
161}
162
163void FilePathWatcherFSEvents::Cancel() {
164  if (callback_.is_null()) {
165    // Watch was never called, so exit.
166    set_cancelled();
167    return;
168  }
169
170  // Switch to the dispatch queue thread if necessary, so we can tear down
171  // the event stream.
172  if (!g_task_runner.Get().RunsTasksOnCurrentThread()) {
173    g_task_runner.Get().PostTask(
174        FROM_HERE,
175        Bind(&FilePathWatcherFSEvents::CancelOnMessageLoopThread, this));
176  } else {
177    CancelOnMessageLoopThread();
178  }
179}
180
181void FilePathWatcherFSEvents::CancelOnMessageLoopThread() {
182  // For all other implementations, the "message loop thread" is the IO thread,
183  // as returned by message_loop(). This implementation, however, needs to
184  // cancel pending work on the Dipatch Queue thread.
185  DCHECK(g_task_runner.Get().RunsTasksOnCurrentThread());
186
187  set_cancelled();
188  if (fsevent_stream_) {
189    DestroyEventStream();
190    callback_.Reset();
191    target_.clear();
192    resolved_target_.clear();
193  }
194}
195
196void FilePathWatcherFSEvents::UpdateEventStream(
197    FSEventStreamEventId start_event) {
198  DCHECK(g_task_runner.Get().RunsTasksOnCurrentThread());
199
200  // It can happen that the watcher gets canceled while tasks that call this
201  // function are still in flight, so abort if this situation is detected.
202  if (is_cancelled() || resolved_target_.empty())
203    return;
204
205  if (fsevent_stream_)
206    DestroyEventStream();
207
208  ScopedCFTypeRef<CFStringRef> cf_path(CFStringCreateWithCString(
209      NULL, resolved_target_.value().c_str(), kCFStringEncodingMacHFS));
210  ScopedCFTypeRef<CFStringRef> cf_dir_path(CFStringCreateWithCString(
211      NULL, resolved_target_.DirName().value().c_str(),
212      kCFStringEncodingMacHFS));
213  CFStringRef paths_array[] = { cf_path.get(), cf_dir_path.get() };
214  ScopedCFTypeRef<CFArrayRef> watched_paths(CFArrayCreate(
215      NULL, reinterpret_cast<const void**>(paths_array), arraysize(paths_array),
216      &kCFTypeArrayCallBacks));
217
218  FSEventStreamContext context;
219  context.version = 0;
220  context.info = this;
221  context.retain = NULL;
222  context.release = NULL;
223  context.copyDescription = NULL;
224
225  fsevent_stream_ = FSEventStreamCreate(NULL, &FSEventsCallback, &context,
226                                        watched_paths,
227                                        start_event,
228                                        kEventLatencySeconds,
229                                        kFSEventStreamCreateFlagWatchRoot);
230  FSEventStreamSetDispatchQueue(fsevent_stream_,
231                                g_task_runner.Get().GetDispatchQueue());
232
233  if (!FSEventStreamStart(fsevent_stream_))
234    message_loop()->PostTask(FROM_HERE, Bind(callback_, target_, true));
235}
236
237bool FilePathWatcherFSEvents::ResolveTargetPath() {
238  DCHECK(g_task_runner.Get().RunsTasksOnCurrentThread());
239  FilePath resolved = ResolvePath(target_).StripTrailingSeparators();
240  bool changed = resolved != resolved_target_;
241  resolved_target_ = resolved;
242  if (resolved_target_.empty())
243    message_loop()->PostTask(FROM_HERE, Bind(callback_, target_, true));
244  return changed;
245}
246
247void FilePathWatcherFSEvents::DestroyEventStream() {
248  FSEventStreamStop(fsevent_stream_);
249  FSEventStreamInvalidate(fsevent_stream_);
250  FSEventStreamRelease(fsevent_stream_);
251  fsevent_stream_ = NULL;
252}
253
254void FilePathWatcherFSEvents::StartEventStream(
255    FSEventStreamEventId start_event) {
256  DCHECK(g_task_runner.Get().RunsTasksOnCurrentThread());
257  ResolveTargetPath();
258  UpdateEventStream(start_event);
259}
260
261FilePathWatcherFSEvents::~FilePathWatcherFSEvents() {}
262
263}  // namespace base
264