// 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 <dispatch/dispatch.h> #include <list> #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/macros.h" #include "base/message_loop/message_loop.h" #include "base/strings/stringprintf.h" #include "base/threading/thread_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<FilePath::StringType> component_vector; path.GetComponents(&component_vector); std::list<FilePath::StringType> 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<FilePath::StringType> 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) { } bool FilePathWatcherFSEvents::Watch(const FilePath& path, bool recursive, const FilePathWatcher::Callback& callback) { DCHECK(MessageLoopForIO::current()); 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(ThreadTaskRunnerHandle::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 object, and this method is called from the destructor, // execute the block synchronously. dispatch_sync(queue_, ^{ CancelOnMessageLoopThread(); }); } // 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<FilePathWatcherFSEvents*>(event_watcher); bool root_changed = watcher->ResolveTargetPath(); std::vector<FilePath> 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<char**>(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 post a task to do the reset. dispatch_async(watcher->queue_, ^{ watcher->UpdateEventStream(root_change_at); }); } watcher->OnFilePathsChanged(paths); } FilePathWatcherFSEvents::~FilePathWatcherFSEvents() { // This method may be called on either the libdispatch or task_runner() // thread. Checking callback_ on the libdispatch thread here is safe because // it is executing in a task posted by Cancel() which first reset callback_. // PostTask forms a sufficient memory barrier to ensure that the value is // consistent on the target thread. DCHECK(callback_.is_null()) << "Cancel() must be called before FilePathWatcher is destroyed."; } void FilePathWatcherFSEvents::OnFilePathsChanged( const std::vector<FilePath>& paths) { DCHECK(!resolved_target_.empty()); task_runner()->PostTask( FROM_HERE, Bind(&FilePathWatcherFSEvents::DispatchEvents, this, paths, target_, resolved_target_)); } void FilePathWatcherFSEvents::DispatchEvents(const std::vector<FilePath>& paths, const FilePath& target, const FilePath& resolved_target) { DCHECK(task_runner()->RunsTasksOnCurrentThread()); // 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::CancelOnMessageLoopThread() { // For all other implementations, the "message loop thread" is the IO thread, // as returned by task_runner(). This implementation, however, needs to // cancel pending work on the Dispatch Queue thread. if (fsevent_stream_) { DestroyEventStream(); target_.clear(); resolved_target_.clear(); } } 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<CFStringRef> cf_path(CFStringCreateWithCString( NULL, resolved_target_.value().c_str(), kCFStringEncodingMacHFS)); ScopedCFTypeRef<CFStringRef> cf_dir_path(CFStringCreateWithCString( NULL, resolved_target_.DirName().value().c_str(), kCFStringEncodingMacHFS)); CFStringRef paths_array[] = { cf_path.get(), cf_dir_path.get() }; ScopedCFTypeRef<CFArrayRef> watched_paths(CFArrayCreate( NULL, reinterpret_cast<const void**>(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, this, 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, this, target_)); } return changed; } void FilePathWatcherFSEvents::ReportError(const FilePath& target) { DCHECK(task_runner()->RunsTasksOnCurrentThread()); 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