// 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 "base/bind.h" #include "base/files/file_util.h" #include "base/lazy_instance.h" #include "base/logging.h" #include "base/mac/libdispatch_task_runner.h" #include "base/mac/scoped_cftyperef.h" #include "base/message_loop/message_loop.h" namespace base { namespace { // The latency parameter passed to FSEventsStreamCreate(). const CFAbsoluteTime kEventLatencySeconds = 0.3; class FSEventsTaskRunner : public mac::LibDispatchTaskRunner { public: FSEventsTaskRunner() : mac::LibDispatchTaskRunner("org.chromium.FilePathWatcherFSEvents") { } protected: ~FSEventsTaskRunner() override {} }; static LazyInstance::Leaky g_task_runner = LAZY_INSTANCE_INITIALIZER; // 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; } // The callback passed to FSEventStreamCreate(). void 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); DCHECK(g_task_runner.Get().RunsTasksOnCurrentThread()); 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 post a task to do the reset. g_task_runner.Get().PostTask( FROM_HERE, Bind(&FilePathWatcherFSEvents::UpdateEventStream, watcher, root_change_at)); } watcher->OnFilePathsChanged(paths); } } // namespace FilePathWatcherFSEvents::FilePathWatcherFSEvents() : fsevent_stream_(NULL) { } void FilePathWatcherFSEvents::OnFilePathsChanged( const std::vector& paths) { if (!message_loop()->BelongsToCurrentThread()) { message_loop()->PostTask( FROM_HERE, Bind(&FilePathWatcherFSEvents::OnFilePathsChanged, this, paths)); return; } DCHECK(message_loop()->BelongsToCurrentThread()); if (resolved_target_.empty()) return; for (size_t i = 0; i < paths.size(); i++) { if (resolved_target_.IsParent(paths[i]) || resolved_target_ == paths[i]) { callback_.Run(target_, false); return; } } } bool FilePathWatcherFSEvents::Watch(const FilePath& path, bool recursive, const FilePathWatcher::Callback& callback) { DCHECK(resolved_target_.empty()); DCHECK(MessageLoopForIO::current()); DCHECK(!callback.is_null()); // This class could support non-recursive watches, but that is currently // left to FilePathWatcherKQueue. if (!recursive) return false; set_message_loop(MessageLoopProxy::current()); callback_ = callback; target_ = path; FSEventStreamEventId start_event = FSEventsGetCurrentEventId(); g_task_runner.Get().PostTask( FROM_HERE, Bind(&FilePathWatcherFSEvents::StartEventStream, this, start_event)); return true; } void FilePathWatcherFSEvents::Cancel() { if (callback_.is_null()) { // Watch was never called, so exit. set_cancelled(); return; } // Switch to the dispatch queue thread if necessary, so we can tear down // the event stream. if (!g_task_runner.Get().RunsTasksOnCurrentThread()) { g_task_runner.Get().PostTask( FROM_HERE, Bind(&FilePathWatcherFSEvents::CancelOnMessageLoopThread, this)); } else { CancelOnMessageLoopThread(); } } void FilePathWatcherFSEvents::CancelOnMessageLoopThread() { // For all other implementations, the "message loop thread" is the IO thread, // as returned by message_loop(). This implementation, however, needs to // cancel pending work on the Dipatch Queue thread. DCHECK(g_task_runner.Get().RunsTasksOnCurrentThread()); set_cancelled(); if (fsevent_stream_) { DestroyEventStream(); callback_.Reset(); target_.clear(); resolved_target_.clear(); } } void FilePathWatcherFSEvents::UpdateEventStream( FSEventStreamEventId start_event) { DCHECK(g_task_runner.Get().RunsTasksOnCurrentThread()); // 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 (is_cancelled() || 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_, g_task_runner.Get().GetDispatchQueue()); if (!FSEventStreamStart(fsevent_stream_)) message_loop()->PostTask(FROM_HERE, Bind(callback_, target_, true)); } bool FilePathWatcherFSEvents::ResolveTargetPath() { DCHECK(g_task_runner.Get().RunsTasksOnCurrentThread()); FilePath resolved = ResolvePath(target_).StripTrailingSeparators(); bool changed = resolved != resolved_target_; resolved_target_ = resolved; if (resolved_target_.empty()) message_loop()->PostTask(FROM_HERE, Bind(callback_, target_, true)); return changed; } void FilePathWatcherFSEvents::DestroyEventStream() { FSEventStreamStop(fsevent_stream_); FSEventStreamInvalidate(fsevent_stream_); FSEventStreamRelease(fsevent_stream_); fsevent_stream_ = NULL; } void FilePathWatcherFSEvents::StartEventStream( FSEventStreamEventId start_event) { DCHECK(g_task_runner.Get().RunsTasksOnCurrentThread()); ResolveTargetPath(); UpdateEventStream(start_event); } FilePathWatcherFSEvents::~FilePathWatcherFSEvents() {} } // namespace base