1 // Copyright 2014 The Chromium Authors
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 <algorithm>
10 #include <list>
11
12 #include "base/apple/scoped_cftyperef.h"
13 #include "base/check.h"
14 #include "base/files/file_util.h"
15 #include "base/functional/bind.h"
16 #include "base/lazy_instance.h"
17 #include "base/strings/stringprintf.h"
18 #include "base/task/sequenced_task_runner.h"
19 #include "base/threading/scoped_blocking_call.h"
20
21 namespace base {
22
23 namespace {
24
25 // The latency parameter passed to FSEventsStreamCreate().
26 const CFAbsoluteTime kEventLatencySeconds = 0.3;
27
28 // Resolve any symlinks in the path.
ResolvePath(const FilePath & path)29 FilePath ResolvePath(const FilePath& path) {
30 const unsigned kMaxLinksToResolve = 255;
31
32 std::vector<FilePath::StringType> component_vector = path.GetComponents();
33 std::list<FilePath::StringType>
34 components(component_vector.begin(), component_vector.end());
35
36 FilePath result;
37 unsigned resolve_count = 0;
38 while (resolve_count < kMaxLinksToResolve && !components.empty()) {
39 FilePath component(*components.begin());
40 components.pop_front();
41
42 FilePath current;
43 if (component.IsAbsolute()) {
44 current = component;
45 } else {
46 current = result.Append(component);
47 }
48
49 FilePath target;
50 if (ReadSymbolicLink(current, &target)) {
51 if (target.IsAbsolute())
52 result.clear();
53 std::vector<FilePath::StringType> target_components =
54 target.GetComponents();
55 components.insert(components.begin(), target_components.begin(),
56 target_components.end());
57 resolve_count++;
58 } else {
59 result = current;
60 }
61 }
62
63 if (resolve_count >= kMaxLinksToResolve)
64 result.clear();
65 return result;
66 }
67
68 } // namespace
69
FilePathWatcherFSEvents()70 FilePathWatcherFSEvents::FilePathWatcherFSEvents()
71 : queue_(dispatch_queue_create(
72 base::StringPrintf("org.chromium.base.FilePathWatcher.%p", this)
73 .c_str(),
74 DISPATCH_QUEUE_SERIAL)) {}
75
~FilePathWatcherFSEvents()76 FilePathWatcherFSEvents::~FilePathWatcherFSEvents() {
77 DCHECK(!task_runner() || task_runner()->RunsTasksInCurrentSequence());
78 DCHECK(callback_.is_null())
79 << "Cancel() must be called before FilePathWatcher is destroyed.";
80 }
81
Watch(const FilePath & path,Type type,const FilePathWatcher::Callback & callback)82 bool FilePathWatcherFSEvents::Watch(const FilePath& path,
83 Type type,
84 const FilePathWatcher::Callback& callback) {
85 DCHECK(!callback.is_null());
86 DCHECK(callback_.is_null());
87
88 // This class could support non-recursive watches, but that is currently
89 // left to FilePathWatcherKQueue.
90 if (type != Type::kRecursive)
91 return false;
92
93 set_task_runner(SequencedTaskRunner::GetCurrentDefault());
94 callback_ = callback;
95
96 FSEventStreamEventId start_event = FSEventsGetCurrentEventId();
97 // The block runtime would implicitly capture the reference, not the object
98 // it's referencing. Copy the path into a local, so that the value is
99 // captured by the block's scope.
100 const FilePath path_copy(path);
101
102 dispatch_async(queue_.get(), ^{
103 StartEventStream(start_event, path_copy);
104 });
105 return true;
106 }
107
Cancel()108 void FilePathWatcherFSEvents::Cancel() {
109 set_cancelled();
110 callback_.Reset();
111
112 ScopedBlockingCall scoped_blocking_call(FROM_HERE, BlockingType::MAY_BLOCK);
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_.get(), ^{
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, BindOnce(
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_.get(), ^{
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 BindOnce(&FilePathWatcherFSEvents::DispatchEvents,
185 weak_factory_.GetWeakPtr(), 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 apple::ScopedCFTypeRef<CFStringRef> cf_path(CFStringCreateWithCString(
217 NULL, resolved_target_.value().c_str(), kCFStringEncodingMacHFS));
218 apple::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 apple::ScopedCFTypeRef<CFArrayRef> watched_paths(
223 CFArrayCreate(NULL, reinterpret_cast<const void**>(paths_array),
224 std::size(paths_array), &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(
234 NULL, &FSEventsCallback, &context, watched_paths.get(), start_event,
235 kEventLatencySeconds, kFSEventStreamCreateFlagWatchRoot);
236 FSEventStreamSetDispatchQueue(fsevent_stream_, queue_.get());
237
238 if (!FSEventStreamStart(fsevent_stream_)) {
239 task_runner()->PostTask(FROM_HERE,
240 BindOnce(&FilePathWatcherFSEvents::ReportError,
241 weak_factory_.GetWeakPtr(), target_));
242 }
243 }
244
ResolveTargetPath()245 bool FilePathWatcherFSEvents::ResolveTargetPath() {
246 FilePath resolved = ResolvePath(target_).StripTrailingSeparators();
247 bool changed = resolved != resolved_target_;
248 resolved_target_ = resolved;
249 if (resolved_target_.empty()) {
250 task_runner()->PostTask(FROM_HERE,
251 BindOnce(&FilePathWatcherFSEvents::ReportError,
252 weak_factory_.GetWeakPtr(), target_));
253 }
254 return changed;
255 }
256
ReportError(const FilePath & target)257 void FilePathWatcherFSEvents::ReportError(const FilePath& target) {
258 DCHECK(task_runner()->RunsTasksInCurrentSequence());
259 if (!callback_.is_null()) {
260 callback_.Run(target, true);
261 }
262 }
263
DestroyEventStream()264 void FilePathWatcherFSEvents::DestroyEventStream() {
265 FSEventStreamStop(fsevent_stream_);
266 FSEventStreamInvalidate(fsevent_stream_);
267 FSEventStreamRelease(fsevent_stream_);
268 fsevent_stream_ = NULL;
269 }
270
StartEventStream(FSEventStreamEventId start_event,const FilePath & path)271 void FilePathWatcherFSEvents::StartEventStream(FSEventStreamEventId start_event,
272 const FilePath& path) {
273 DCHECK(resolved_target_.empty());
274
275 target_ = path;
276 ResolveTargetPath();
277 UpdateEventStream(start_event);
278 }
279
280 } // namespace base
281