| // Copyright (c) 2009 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 "chrome/browser/file_watcher.h" |
| |
| #include <CoreServices/CoreServices.h> |
| |
| #include "base/file_path.h" |
| #include "base/file_util.h" |
| #include "base/logging.h" |
| #include "base/scoped_cftyperef.h" |
| #include "base/time.h" |
| |
| namespace { |
| |
| const CFAbsoluteTime kEventLatencySeconds = 0.3; |
| |
| class FileWatcherImpl : public FileWatcher::PlatformDelegate { |
| public: |
| FileWatcherImpl() {} |
| ~FileWatcherImpl() { |
| if (!path_.value().empty()) { |
| FSEventStreamStop(fsevent_stream_); |
| FSEventStreamInvalidate(fsevent_stream_); |
| FSEventStreamRelease(fsevent_stream_); |
| } |
| } |
| |
| virtual bool Watch(const FilePath& path, FileWatcher::Delegate* delegate) { |
| FilePath parent_dir = path.DirName(); |
| if (!file_util::AbsolutePath(&parent_dir)) |
| return false; |
| |
| // Jump back to the UI thread because FSEventStreamScheduleWithRunLoop |
| // requires a UI thread. |
| if (!ChromeThread::CurrentlyOn(ChromeThread::UI)) { |
| ChromeThread::PostTask(ChromeThread::UI, FROM_HERE, |
| NewRunnableMethod(this, &FileWatcherImpl::WatchImpl, path, delegate)); |
| } else { |
| LOG(INFO) << "Adding FileWatcher watch."; |
| // During unittests, there is only one thread and it is both the UI |
| // thread and the file thread. |
| WatchImpl(path, delegate); |
| } |
| return true; |
| } |
| |
| bool WatchImpl(const FilePath& path, FileWatcher::Delegate* delegate); |
| |
| void OnFSEventsCallback(const FilePath& event_path) { |
| DCHECK(ChromeThread::CurrentlyOn(ChromeThread::FILE)); |
| DCHECK(!path_.value().empty()); |
| FilePath absolute_event_path = event_path; |
| if (!file_util::AbsolutePath(&absolute_event_path)) |
| return; |
| |
| file_util::FileInfo file_info; |
| bool file_exists = file_util::GetFileInfo(path_, &file_info); |
| if (file_exists && (last_modified_.is_null() || |
| last_modified_ != file_info.last_modified)) { |
| last_modified_ = file_info.last_modified; |
| delegate_->OnFileChanged(path_); |
| } else if (file_exists && (base::Time::Now() - last_modified_ < |
| base::TimeDelta::FromSeconds(2))) { |
| // Since we only have a resolution of 1s, if we get a callback within |
| // 2s of the file having changed, go ahead and notify our observer. This |
| // might be from a different file change, but it's better to notify too |
| // much rather than miss a notification. |
| delegate_->OnFileChanged(path_); |
| } else if (!file_exists && !last_modified_.is_null()) { |
| last_modified_ = base::Time(); |
| delegate_->OnFileChanged(path_); |
| } |
| } |
| |
| private: |
| // Delegate to notify upon changes. |
| FileWatcher::Delegate* delegate_; |
| |
| // Path we're watching (passed to delegate). |
| FilePath path_; |
| |
| // Backend stream we receive event callbacks from (strong reference). |
| FSEventStreamRef fsevent_stream_; |
| |
| // Keep track of the last modified time of the file. We use nulltime |
| // to represent the file not existing. |
| base::Time last_modified_; |
| |
| DISALLOW_COPY_AND_ASSIGN(FileWatcherImpl); |
| }; |
| |
| void FSEventsCallback(ConstFSEventStreamRef stream, |
| void* event_watcher, size_t num_events, |
| void* event_paths, const FSEventStreamEventFlags flags[], |
| const FSEventStreamEventId event_ids[]) { |
| char** paths = reinterpret_cast<char**>(event_paths); |
| FileWatcherImpl* watcher = |
| reinterpret_cast<FileWatcherImpl*>(event_watcher); |
| for (size_t i = 0; i < num_events; i++) { |
| if (!ChromeThread::CurrentlyOn(ChromeThread::FILE)) { |
| ChromeThread::PostTask(ChromeThread::FILE, FROM_HERE, |
| NewRunnableMethod(watcher, &FileWatcherImpl::OnFSEventsCallback, |
| FilePath(paths[i]))); |
| } else { |
| LOG(INFO) << "FileWatcher event callback for " << paths[i]; |
| // During unittests, there is only one thread and it is both the UI |
| // thread and the file thread. |
| watcher->OnFSEventsCallback(FilePath(paths[i])); |
| } |
| } |
| } |
| |
| bool FileWatcherImpl::WatchImpl(const FilePath& path, |
| FileWatcher::Delegate* delegate) { |
| DCHECK(path_.value().empty()); // Can only watch one path. |
| DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); |
| |
| file_util::FileInfo file_info; |
| if (file_util::GetFileInfo(path, &file_info)) |
| last_modified_ = file_info.last_modified; |
| |
| path_ = path; |
| delegate_ = delegate; |
| |
| scoped_cftyperef<CFStringRef> cf_path(CFStringCreateWithCString( |
| NULL, path.DirName().value().c_str(), kCFStringEncodingMacHFS)); |
| CFStringRef path_for_array = cf_path.get(); |
| scoped_cftyperef<CFArrayRef> watched_paths(CFArrayCreate( |
| NULL, reinterpret_cast<const void**>(&path_for_array), 1, |
| &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, |
| kFSEventStreamEventIdSinceNow, |
| kEventLatencySeconds, |
| kFSEventStreamCreateFlagNone); |
| FSEventStreamScheduleWithRunLoop(fsevent_stream_, CFRunLoopGetCurrent(), |
| kCFRunLoopDefaultMode); |
| FSEventStreamStart(fsevent_stream_); |
| |
| return true; |
| } |
| |
| } // namespace |
| |
| FileWatcher::FileWatcher() { |
| impl_ = new FileWatcherImpl(); |
| } |