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 <dispatch/dispatch.h>
8
9 #include <list>
10
11 #include "base/bind.h"
12 #include "base/files/file_util.h"
13 #include "base/lazy_instance.h"
14 #include "base/logging.h"
15 #include "base/mac/scoped_cftyperef.h"
16 #include "base/strings/stringprintf.h"
17 #include "base/threading/sequenced_task_runner_handle.h"
18
19 namespace base {
20
21 namespace {
22
23 // The latency parameter passed to FSEventsStreamCreate().
24 const CFAbsoluteTime kEventLatencySeconds = 0.3;
25
26 // Resolve any symlinks in the path.
ResolvePath(const FilePath & path)27 FilePath ResolvePath(const FilePath& path) {
28 const unsigned kMaxLinksToResolve = 255;
29
30 std::vector<FilePath::StringType> component_vector;
31 path.GetComponents(&component_vector);
32 std::list<FilePath::StringType>
33 components(component_vector.begin(), component_vector.end());
34
35 FilePath result;
36 unsigned resolve_count = 0;
37 while (resolve_count < kMaxLinksToResolve && !components.empty()) {
38 FilePath component(*components.begin());
39 components.pop_front();
40
41 FilePath current;
42 if (component.IsAbsolute()) {
43 current = component;
44 } else {
45 current = result.Append(component);
46 }
47
48 FilePath target;
49 if (ReadSymbolicLink(current, &target)) {
50 if (target.IsAbsolute())
51 result.clear();
52 std::vector<FilePath::StringType> target_components;
53 target.GetComponents(&target_components);
54 components.insert(components.begin(), target_components.begin(),
55 target_components.end());
56 resolve_count++;
57 } else {
58 result = current;
59 }
60 }
61
62 if (resolve_count >= kMaxLinksToResolve)
63 result.clear();
64 return result;
65 }
66
67 } // namespace
68
FilePathWatcherFSEvents()69 FilePathWatcherFSEvents::FilePathWatcherFSEvents()
70 : queue_(dispatch_queue_create(
71 base::StringPrintf("org.chromium.base.FilePathWatcher.%p", this)
72 .c_str(),
73 DISPATCH_QUEUE_SERIAL)),
74 fsevent_stream_(nullptr),
75 weak_factory_(this) {}
76
~FilePathWatcherFSEvents()77 FilePathWatcherFSEvents::~FilePathWatcherFSEvents() {
78 DCHECK(!task_runner() || task_runner()->RunsTasksInCurrentSequence());
79 DCHECK(callback_.is_null())
80 << "Cancel() must be called before FilePathWatcher is destroyed.";
81 }
82
Watch(const FilePath & path,bool recursive,const FilePathWatcher::Callback & callback)83 bool FilePathWatcherFSEvents::Watch(const FilePath& path,
84 bool recursive,
85 const FilePathWatcher::Callback& callback) {
86 DCHECK(!callback.is_null());
87 DCHECK(callback_.is_null());
88
89 // This class could support non-recursive watches, but that is currently
90 // left to FilePathWatcherKQueue.
91 if (!recursive)
92 return false;
93
94 set_task_runner(SequencedTaskRunnerHandle::Get());
95 callback_ = callback;
96
97 FSEventStreamEventId start_event = FSEventsGetCurrentEventId();
98 // The block runtime would implicitly capture the reference, not the object
99 // it's referencing. Copy the path into a local, so that the value is
100 // captured by the block's scope.
101 const FilePath path_copy(path);
102
103 dispatch_async(queue_, ^{
104 StartEventStream(start_event, path_copy);
105 });
106 return true;
107 }
108
Cancel()109 void FilePathWatcherFSEvents::Cancel() {
110 set_cancelled();
111 callback_.Reset();
112
113 // Switch to the dispatch queue to tear down the event stream. As the queue is
114 // owned by |this|, and this method is called from the destructor, execute the
115 // block synchronously.
116 dispatch_sync(queue_, ^{
117 if (fsevent_stream_) {
118 DestroyEventStream();
119 target_.clear();
120 resolved_target_.clear();
121 }
122 });
123 }
124
125 // static
FSEventsCallback(ConstFSEventStreamRef stream,void * event_watcher,size_t num_events,void * event_paths,const FSEventStreamEventFlags flags[],const FSEventStreamEventId event_ids[])126 void FilePathWatcherFSEvents::FSEventsCallback(
127 ConstFSEventStreamRef stream,
128 void* event_watcher,
129 size_t num_events,
130 void* event_paths,
131 const FSEventStreamEventFlags flags[],
132 const FSEventStreamEventId event_ids[]) {
133 FilePathWatcherFSEvents* watcher =
134 reinterpret_cast<FilePathWatcherFSEvents*>(event_watcher);
135 bool root_changed = watcher->ResolveTargetPath();
136 std::vector<FilePath> paths;
137 FSEventStreamEventId root_change_at = FSEventStreamGetLatestEventId(stream);
138 for (size_t i = 0; i < num_events; i++) {
139 if (flags[i] & kFSEventStreamEventFlagRootChanged)
140 root_changed = true;
141 if (event_ids[i])
142 root_change_at = std::min(root_change_at, event_ids[i]);
143 paths.push_back(FilePath(
144 reinterpret_cast<char**>(event_paths)[i]).StripTrailingSeparators());
145 }
146
147 // Reinitialize the event stream if we find changes to the root. This is
148 // necessary since FSEvents doesn't report any events for the subtree after
149 // the directory to be watched gets created.
150 if (root_changed) {
151 // Resetting the event stream from within the callback fails (FSEvents spews
152 // bad file descriptor errors), so do the reset asynchronously.
153 //
154 // We can't dispatch_async a call to UpdateEventStream() directly because
155 // there would be no guarantee that |watcher| still exists when it runs.
156 //
157 // Instead, bounce on task_runner() and use a WeakPtr to verify that
158 // |watcher| still exists. If it does, dispatch_async a call to
159 // UpdateEventStream(). Because the destructor of |watcher| runs on
160 // task_runner() and calls dispatch_sync, it is guaranteed that |watcher|
161 // still exists when UpdateEventStream() runs.
162 watcher->task_runner()->PostTask(
163 FROM_HERE, Bind(
164 [](WeakPtr<FilePathWatcherFSEvents> weak_watcher,
165 FSEventStreamEventId root_change_at) {
166 if (!weak_watcher)
167 return;
168 FilePathWatcherFSEvents* watcher = weak_watcher.get();
169 dispatch_async(watcher->queue_, ^{
170 watcher->UpdateEventStream(root_change_at);
171 });
172 },
173 watcher->weak_factory_.GetWeakPtr(), root_change_at));
174 }
175
176 watcher->OnFilePathsChanged(paths);
177 }
178
OnFilePathsChanged(const std::vector<FilePath> & paths)179 void FilePathWatcherFSEvents::OnFilePathsChanged(
180 const std::vector<FilePath>& paths) {
181 DCHECK(!resolved_target_.empty());
182 task_runner()->PostTask(
183 FROM_HERE,
184 Bind(&FilePathWatcherFSEvents::DispatchEvents, weak_factory_.GetWeakPtr(),
185 paths, target_, resolved_target_));
186 }
187
DispatchEvents(const std::vector<FilePath> & paths,const FilePath & target,const FilePath & resolved_target)188 void FilePathWatcherFSEvents::DispatchEvents(const std::vector<FilePath>& paths,
189 const FilePath& target,
190 const FilePath& resolved_target) {
191 DCHECK(task_runner()->RunsTasksInCurrentSequence());
192
193 // Don't issue callbacks after Cancel() has been called.
194 if (is_cancelled() || callback_.is_null()) {
195 return;
196 }
197
198 for (const FilePath& path : paths) {
199 if (resolved_target.IsParent(path) || resolved_target == path) {
200 callback_.Run(target, false);
201 return;
202 }
203 }
204 }
205
UpdateEventStream(FSEventStreamEventId start_event)206 void FilePathWatcherFSEvents::UpdateEventStream(
207 FSEventStreamEventId start_event) {
208 // It can happen that the watcher gets canceled while tasks that call this
209 // function are still in flight, so abort if this situation is detected.
210 if (resolved_target_.empty())
211 return;
212
213 if (fsevent_stream_)
214 DestroyEventStream();
215
216 ScopedCFTypeRef<CFStringRef> cf_path(CFStringCreateWithCString(
217 NULL, resolved_target_.value().c_str(), kCFStringEncodingMacHFS));
218 ScopedCFTypeRef<CFStringRef> cf_dir_path(CFStringCreateWithCString(
219 NULL, resolved_target_.DirName().value().c_str(),
220 kCFStringEncodingMacHFS));
221 CFStringRef paths_array[] = { cf_path.get(), cf_dir_path.get() };
222 ScopedCFTypeRef<CFArrayRef> watched_paths(CFArrayCreate(
223 NULL, reinterpret_cast<const void**>(paths_array), arraysize(paths_array),
224 &kCFTypeArrayCallBacks));
225
226 FSEventStreamContext context;
227 context.version = 0;
228 context.info = this;
229 context.retain = NULL;
230 context.release = NULL;
231 context.copyDescription = NULL;
232
233 fsevent_stream_ = FSEventStreamCreate(NULL, &FSEventsCallback, &context,
234 watched_paths,
235 start_event,
236 kEventLatencySeconds,
237 kFSEventStreamCreateFlagWatchRoot);
238 FSEventStreamSetDispatchQueue(fsevent_stream_, queue_);
239
240 if (!FSEventStreamStart(fsevent_stream_)) {
241 task_runner()->PostTask(FROM_HERE,
242 Bind(&FilePathWatcherFSEvents::ReportError,
243 weak_factory_.GetWeakPtr(), target_));
244 }
245 }
246
ResolveTargetPath()247 bool FilePathWatcherFSEvents::ResolveTargetPath() {
248 FilePath resolved = ResolvePath(target_).StripTrailingSeparators();
249 bool changed = resolved != resolved_target_;
250 resolved_target_ = resolved;
251 if (resolved_target_.empty()) {
252 task_runner()->PostTask(FROM_HERE,
253 Bind(&FilePathWatcherFSEvents::ReportError,
254 weak_factory_.GetWeakPtr(), target_));
255 }
256 return changed;
257 }
258
ReportError(const FilePath & target)259 void FilePathWatcherFSEvents::ReportError(const FilePath& target) {
260 DCHECK(task_runner()->RunsTasksInCurrentSequence());
261 if (!callback_.is_null()) {
262 callback_.Run(target, true);
263 }
264 }
265
DestroyEventStream()266 void FilePathWatcherFSEvents::DestroyEventStream() {
267 FSEventStreamStop(fsevent_stream_);
268 FSEventStreamInvalidate(fsevent_stream_);
269 FSEventStreamRelease(fsevent_stream_);
270 fsevent_stream_ = NULL;
271 }
272
StartEventStream(FSEventStreamEventId start_event,const FilePath & path)273 void FilePathWatcherFSEvents::StartEventStream(FSEventStreamEventId start_event,
274 const FilePath& path) {
275 DCHECK(resolved_target_.empty());
276
277 target_ = path;
278 ResolveTargetPath();
279 UpdateEventStream(start_event);
280 }
281
282 } // namespace base
283