// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "base/files/file_path_watcher_fsevents.h" #include #include #include "base/bind.h" #include "base/files/file_util.h" #include "base/lazy_instance.h" #include "base/logging.h" #include "base/mac/scoped_cftyperef.h" #include "base/strings/stringprintf.h" #include "base/threading/sequenced_task_runner_handle.h" namespace base { namespace { // The latency parameter passed to FSEventsStreamCreate(). const CFAbsoluteTime kEventLatencySeconds = 0.3; // Resolve any symlinks in the path. FilePath ResolvePath(const FilePath& path) { const unsigned kMaxLinksToResolve = 255; std::vector component_vector; path.GetComponents(&component_vector); std::list components(component_vector.begin(), component_vector.end()); FilePath result; unsigned resolve_count = 0; while (resolve_count < kMaxLinksToResolve && !components.empty()) { FilePath component(*components.begin()); components.pop_front(); FilePath current; if (component.IsAbsolute()) { current = component; } else { current = result.Append(component); } FilePath target; if (ReadSymbolicLink(current, &target)) { if (target.IsAbsolute()) result.clear(); std::vector target_components; target.GetComponents(&target_components); components.insert(components.begin(), target_components.begin(), target_components.end()); resolve_count++; } else { result = current; } } if (resolve_count >= kMaxLinksToResolve) result.clear(); return result; } } // namespace FilePathWatcherFSEvents::FilePathWatcherFSEvents() : queue_(dispatch_queue_create( base::StringPrintf("org.chromium.base.FilePathWatcher.%p", this) .c_str(), DISPATCH_QUEUE_SERIAL)), fsevent_stream_(nullptr), weak_factory_(this) {} FilePathWatcherFSEvents::~FilePathWatcherFSEvents() { DCHECK(!task_runner() || task_runner()->RunsTasksInCurrentSequence()); DCHECK(callback_.is_null()) << "Cancel() must be called before FilePathWatcher is destroyed."; } bool FilePathWatcherFSEvents::Watch(const FilePath& path, bool recursive, const FilePathWatcher::Callback& callback) { DCHECK(!callback.is_null()); DCHECK(callback_.is_null()); // This class could support non-recursive watches, but that is currently // left to FilePathWatcherKQueue. if (!recursive) return false; set_task_runner(SequencedTaskRunnerHandle::Get()); callback_ = callback; FSEventStreamEventId start_event = FSEventsGetCurrentEventId(); // The block runtime would implicitly capture the reference, not the object // it's referencing. Copy the path into a local, so that the value is // captured by the block's scope. const FilePath path_copy(path); dispatch_async(queue_, ^{ StartEventStream(start_event, path_copy); }); return true; } void FilePathWatcherFSEvents::Cancel() { set_cancelled(); callback_.Reset(); // Switch to the dispatch queue to tear down the event stream. As the queue is // owned by |this|, and this method is called from the destructor, execute the // block synchronously. dispatch_sync(queue_, ^{ if (fsevent_stream_) { DestroyEventStream(); target_.clear(); resolved_target_.clear(); } }); } // static void FilePathWatcherFSEvents::FSEventsCallback( ConstFSEventStreamRef stream, void* event_watcher, size_t num_events, void* event_paths, const FSEventStreamEventFlags flags[], const FSEventStreamEventId event_ids[]) { FilePathWatcherFSEvents* watcher = reinterpret_cast(event_watcher); bool root_changed = watcher->ResolveTargetPath(); std::vector paths; FSEventStreamEventId root_change_at = FSEventStreamGetLatestEventId(stream); for (size_t i = 0; i < num_events; i++) { if (flags[i] & kFSEventStreamEventFlagRootChanged) root_changed = true; if (event_ids[i]) root_change_at = std::min(root_change_at, event_ids[i]); paths.push_back(FilePath( reinterpret_cast(event_paths)[i]).StripTrailingSeparators()); } // Reinitialize the event stream if we find changes to the root. This is // necessary since FSEvents doesn't report any events for the subtree after // the directory to be watched gets created. if (root_changed) { // Resetting the event stream from within the callback fails (FSEvents spews // bad file descriptor errors), so do the reset asynchronously. // // We can't dispatch_async a call to UpdateEventStream() directly because // there would be no guarantee that |watcher| still exists when it runs. // // Instead, bounce on task_runner() and use a WeakPtr to verify that // |watcher| still exists. If it does, dispatch_async a call to // UpdateEventStream(). Because the destructor of |watcher| runs on // task_runner() and calls dispatch_sync, it is guaranteed that |watcher| // still exists when UpdateEventStream() runs. watcher->task_runner()->PostTask( FROM_HERE, Bind( [](WeakPtr weak_watcher, FSEventStreamEventId root_change_at) { if (!weak_watcher) return; FilePathWatcherFSEvents* watcher = weak_watcher.get(); dispatch_async(watcher->queue_, ^{ watcher->UpdateEventStream(root_change_at); }); }, watcher->weak_factory_.GetWeakPtr(), root_change_at)); } watcher->OnFilePathsChanged(paths); } void FilePathWatcherFSEvents::OnFilePathsChanged( const std::vector& paths) { DCHECK(!resolved_target_.empty()); task_runner()->PostTask( FROM_HERE, Bind(&FilePathWatcherFSEvents::DispatchEvents, weak_factory_.GetWeakPtr(), paths, target_, resolved_target_)); } void FilePathWatcherFSEvents::DispatchEvents(const std::vector& paths, const FilePath& target, const FilePath& resolved_target) { DCHECK(task_runner()->RunsTasksInCurrentSequence()); // Don't issue callbacks after Cancel() has been called. if (is_cancelled() || callback_.is_null()) { return; } for (const FilePath& path : paths) { if (resolved_target.IsParent(path) || resolved_target == path) { callback_.Run(target, false); return; } } } void FilePathWatcherFSEvents::UpdateEventStream( FSEventStreamEventId start_event) { // It can happen that the watcher gets canceled while tasks that call this // function are still in flight, so abort if this situation is detected. if (resolved_target_.empty()) return; if (fsevent_stream_) DestroyEventStream(); ScopedCFTypeRef cf_path(CFStringCreateWithCString( NULL, resolved_target_.value().c_str(), kCFStringEncodingMacHFS)); ScopedCFTypeRef cf_dir_path(CFStringCreateWithCString( NULL, resolved_target_.DirName().value().c_str(), kCFStringEncodingMacHFS)); CFStringRef paths_array[] = { cf_path.get(), cf_dir_path.get() }; ScopedCFTypeRef watched_paths(CFArrayCreate( NULL, reinterpret_cast(paths_array), arraysize(paths_array), &kCFTypeArrayCallBacks)); FSEventStreamContext context; context.version = 0; context.info = this; context.retain = NULL; context.release = NULL; context.copyDescription = NULL; fsevent_stream_ = FSEventStreamCreate(NULL, &FSEventsCallback, &context, watched_paths, start_event, kEventLatencySeconds, kFSEventStreamCreateFlagWatchRoot); FSEventStreamSetDispatchQueue(fsevent_stream_, queue_); if (!FSEventStreamStart(fsevent_stream_)) { task_runner()->PostTask(FROM_HERE, Bind(&FilePathWatcherFSEvents::ReportError, weak_factory_.GetWeakPtr(), target_)); } } bool FilePathWatcherFSEvents::ResolveTargetPath() { FilePath resolved = ResolvePath(target_).StripTrailingSeparators(); bool changed = resolved != resolved_target_; resolved_target_ = resolved; if (resolved_target_.empty()) { task_runner()->PostTask(FROM_HERE, Bind(&FilePathWatcherFSEvents::ReportError, weak_factory_.GetWeakPtr(), target_)); } return changed; } void FilePathWatcherFSEvents::ReportError(const FilePath& target) { DCHECK(task_runner()->RunsTasksInCurrentSequence()); if (!callback_.is_null()) { callback_.Run(target, true); } } void FilePathWatcherFSEvents::DestroyEventStream() { FSEventStreamStop(fsevent_stream_); FSEventStreamInvalidate(fsevent_stream_); FSEventStreamRelease(fsevent_stream_); fsevent_stream_ = NULL; } void FilePathWatcherFSEvents::StartEventStream(FSEventStreamEventId start_event, const FilePath& path) { DCHECK(resolved_target_.empty()); target_ = path; ResolveTargetPath(); UpdateEventStream(start_event); } } // namespace base