diff options
-rw-r--r-- | chrome/browser/chromeos/drive/file_write_watcher.cc | 225 | ||||
-rw-r--r-- | chrome/browser/chromeos/drive/file_write_watcher.h | 75 | ||||
-rw-r--r-- | chrome/browser/chromeos/drive/file_write_watcher_unittest.cc | 119 | ||||
-rw-r--r-- | chrome/chrome_browser_chromeos.gypi | 2 | ||||
-rw-r--r-- | chrome/chrome_tests_unit.gypi | 1 |
5 files changed, 422 insertions, 0 deletions
diff --git a/chrome/browser/chromeos/drive/file_write_watcher.cc b/chrome/browser/chromeos/drive/file_write_watcher.cc new file mode 100644 index 0000000..97c64d4 --- /dev/null +++ b/chrome/browser/chromeos/drive/file_write_watcher.cc @@ -0,0 +1,225 @@ +// Copyright 2013 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/chromeos/drive/file_write_watcher.h" + +#include <map> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/files/file_path_watcher.h" +#include "base/stl_util.h" +#include "base/timer/timer.h" +#include "chrome/browser/chromeos/drive/file_system/operation_observer.h" +#include "chrome/browser/chromeos/drive/logging.h" +#include "chrome/browser/google_apis/task_util.h" +#include "content/public/browser/browser_thread.h" + +using content::BrowserThread; + +namespace drive { +namespace internal { + +namespace { +const int64 kWriteEventDelayInSeconds = 5; +} // namespace + +// base::FileWatcher needs to live in a thread that is allowed to do File IO +// and has a TYPE_IO message loop: that is, FILE thread. This class bridges the +// UI thread and FILE thread, and does all the main tasks in the FILE thread. +class FileWriteWatcher::FileWriteWatcherImpl { + public: + FileWriteWatcherImpl(); + + // Forwards the call to DestoryOnFileThread(). This method must be used to + // destruct the instance. + void Destroy(); + + // Forwards the call to StartWatchOnFileThread(). |callback| is called back + // on the caller (UI) thread when the watch has started. + // |on_write_event_callback| is called when a write has happened to the path. + void StartWatch(const base::FilePath& path, + const StartWatchCallback& callback, + const base::Closure& on_write_event_callback); + + void set_delay(base::TimeDelta delay) { delay_ = delay; } + + private: + ~FileWriteWatcherImpl(); + + void DestroyOnFileThread(); + + void StartWatchOnFileThread(const base::FilePath& path, + const StartWatchCallback& callback, + const base::Closure& on_write_event_callback); + + void OnWriteEvent(const base::FilePath& path, bool error); + + void InvokeCallback(const base::FilePath& path); + + struct PathWatchInfo { + base::Closure on_write_event_callback; + base::FilePathWatcher watcher; + base::Timer timer; + + explicit PathWatchInfo(const base::Closure& callback) + : on_write_event_callback(callback), + timer(false /* retain_closure_on_reset */, false /* is_repeating */) { + } + }; + + base::TimeDelta delay_; + std::map<base::FilePath, PathWatchInfo*> watchers_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate its weak pointers before any other members are destroyed. + base::WeakPtrFactory<FileWriteWatcherImpl> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(FileWriteWatcherImpl); +}; + +FileWriteWatcher::FileWriteWatcherImpl::FileWriteWatcherImpl() + : delay_(base::TimeDelta::FromSeconds(kWriteEventDelayInSeconds)), + weak_ptr_factory_(this) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); +} + +void FileWriteWatcher::FileWriteWatcherImpl::Destroy() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + + // Just forwarding the call to FILE thread. + BrowserThread::GetMessageLoopProxyForThread(BrowserThread::FILE)->PostTask( + FROM_HERE, + base::Bind(&FileWriteWatcherImpl::DestroyOnFileThread, + base::Unretained(this))); +} + +void FileWriteWatcher::FileWriteWatcherImpl::StartWatch( + const base::FilePath& path, + const StartWatchCallback& callback, + const base::Closure& on_write_event_callback) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + + // Forwarding the call to FILE thread and relaying the |callback|. + BrowserThread::GetMessageLoopProxyForThread(BrowserThread::FILE)->PostTask( + FROM_HERE, + base::Bind(&FileWriteWatcherImpl::StartWatchOnFileThread, + base::Unretained(this), + path, + google_apis::CreateRelayCallback(callback), + google_apis::CreateRelayCallback(on_write_event_callback))); +} + +FileWriteWatcher::FileWriteWatcherImpl::~FileWriteWatcherImpl() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); + + STLDeleteContainerPairSecondPointers(watchers_.begin(), watchers_.end()); +} + +void FileWriteWatcher::FileWriteWatcherImpl::DestroyOnFileThread() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); + + delete this; +} + +void FileWriteWatcher::FileWriteWatcherImpl::StartWatchOnFileThread( + const base::FilePath& path, + const StartWatchCallback& callback, + const base::Closure& on_write_event_callback) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); + util::Log(logging::LOG_INFO, "Started watching modification to %s.", + path.AsUTF8Unsafe().c_str()); + + std::map<base::FilePath, PathWatchInfo*>::iterator it = watchers_.find(path); + if (it != watchers_.end()) { + // Do nothing if we are already watching the path. + callback.Run(true); + return; + } + + // Start watching |path|. + scoped_ptr<PathWatchInfo> info(new PathWatchInfo(on_write_event_callback)); + bool ok = info->watcher.Watch( + path, + false, // recursive + base::Bind(&FileWriteWatcherImpl::OnWriteEvent, + weak_ptr_factory_.GetWeakPtr())); + watchers_[path] = info.release(); + callback.Run(ok); +} + +void FileWriteWatcher::FileWriteWatcherImpl::OnWriteEvent( + const base::FilePath& path, + bool error) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); + util::Log(logging::LOG_INFO, "Detected modification to %s.", + path.AsUTF8Unsafe().c_str()); + + if (error) + return; + + std::map<base::FilePath, PathWatchInfo*>::iterator it = watchers_.find(path); + DCHECK(it != watchers_.end()); + + // Heuristics for detecting the end of successive write operations. + // Delay running on_write_event_callback by |delay_| time, and if OnWriteEvent + // is called again in the period, the timer is reset. In other words, we + // invoke callback when |delay_| has passed after the last OnWriteEvent(). + it->second->timer.Start(FROM_HERE, + delay_, + base::Bind(&FileWriteWatcherImpl::InvokeCallback, + weak_ptr_factory_.GetWeakPtr(), + path)); +} + +void FileWriteWatcher::FileWriteWatcherImpl::InvokeCallback( + const base::FilePath& path) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); + util::Log(logging::LOG_INFO, "Finished watching modification to %s.", + path.AsUTF8Unsafe().c_str()); + + std::map<base::FilePath, PathWatchInfo*>::iterator it = watchers_.find(path); + DCHECK(it != watchers_.end()); + + base::Closure callback = it->second->on_write_event_callback; + delete it->second; + watchers_.erase(it); + + callback.Run(); +} + +FileWriteWatcher::FileWriteWatcher(file_system::OperationObserver* observer) + : watcher_impl_(new FileWriteWatcherImpl), + operation_observer_(observer), + weak_ptr_factory_(this) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); +} + +FileWriteWatcher::~FileWriteWatcher() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); +} + +void FileWriteWatcher::StartWatch(const base::FilePath& file_path, + const std::string& resource_id, + const StartWatchCallback& callback) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + + watcher_impl_->StartWatch(file_path, + callback, + base::Bind(&FileWriteWatcher::OnWriteEvent, + weak_ptr_factory_.GetWeakPtr(), + resource_id)); +} + +void FileWriteWatcher::DisableDelayForTesting() { + watcher_impl_->set_delay(base::TimeDelta()); +} + +void FileWriteWatcher::OnWriteEvent(const std::string& resource_id) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + + operation_observer_->OnCacheFileUploadNeededByOperation(resource_id); +} + +} // namespace internal +} // namespace drive diff --git a/chrome/browser/chromeos/drive/file_write_watcher.h b/chrome/browser/chromeos/drive/file_write_watcher.h new file mode 100644 index 0000000..91c947d --- /dev/null +++ b/chrome/browser/chromeos/drive/file_write_watcher.h @@ -0,0 +1,75 @@ +// Copyright 2013 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. + +#ifndef CHROME_BROWSER_CHROMEOS_DRIVE_FILE_WRITE_WATCHER_H_ +#define CHROME_BROWSER_CHROMEOS_DRIVE_FILE_WRITE_WATCHER_H_ + +#include "base/callback_forward.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "chrome/browser/chromeos/drive/file_system_util.h" + +namespace base { +class FilePath; +} // namespace base + +namespace drive { + +namespace file_system { +class OperationObserver; +} // namespace file_system + +namespace internal { + +typedef base::Callback<void(bool)> StartWatchCallback; + +// The class watches modification to Drive files in the cache directory. +// This is used for returning a local writable snapshot of Drive files from the +// Save-As file dialog, so that the callers of the dialog can save to Drive +// without any special handling about Drive. +class FileWriteWatcher { + public: + explicit FileWriteWatcher(file_system::OperationObserver* observer); + ~FileWriteWatcher(); + + // Starts watching the modification to |path|. When it successfully started + // watching, it runs |callback| by passing true as the argument. Or if it + // failed, the callback is run with false. + // When modification is detected, it is notified to the |observer| passed to + // the constructor by calling OnCacheFileUploadNeededByOperation(resource_id). + // + // Currently, the modification is watched in "one-shot" manner. That is, once + // a modification is notified, the watch is deactivated for freeing system + // resources. As a heuristic to capture the real end of write operations that + // might be done by several chunked writes, the notification is fired after + // 5 seconds has passed after the last write operation is detected. + // + // TODO(kinaba): investigate the possibility to continuously watch the whole + // cache directory. + void StartWatch(const base::FilePath& path, + const std::string& resource_id, + const StartWatchCallback& callback); + + // For testing purpose, stops inserting delay between the write detection and + // notification to the observer. + void DisableDelayForTesting(); + + private: + // Invoked when a modification is observed. + void OnWriteEvent(const std::string& resource_id); + + class FileWriteWatcherImpl; + scoped_ptr<FileWriteWatcherImpl, util::DestroyHelper> watcher_impl_; + file_system::OperationObserver* operation_observer_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate its weak pointers before any other members are destroyed. + base::WeakPtrFactory<FileWriteWatcher> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(FileWriteWatcher); +}; + +} // namespace internal +} // namespace drive + +#endif // CHROME_BROWSER_CHROMEOS_DRIVE_FILE_WRITE_WATCHER_H_ diff --git a/chrome/browser/chromeos/drive/file_write_watcher_unittest.cc b/chrome/browser/chromeos/drive/file_write_watcher_unittest.cc new file mode 100644 index 0000000..23be2a8 --- /dev/null +++ b/chrome/browser/chromeos/drive/file_write_watcher_unittest.cc @@ -0,0 +1,119 @@ +// Copyright 2013 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/chromeos/drive/file_write_watcher.h" + +#include <set> + +#include "base/bind.h" +#include "base/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/run_loop.h" +#include "chrome/browser/chromeos/drive/file_system/operation_observer.h" +#include "content/public/test/test_browser_thread_bundle.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace drive { +namespace internal { + +namespace { + +class TestObserver : public file_system::OperationObserver { + public: + // After all the resource_ids in |expected_upload| are notified for the + // need of uploading, runs |quit_closure|. Also checks that each id is + // notified only once. + TestObserver(const std::set<std::string>& expected_upload, + const base::Closure& quit_closure) + : expected_upload_(expected_upload), + quit_closure_(quit_closure) { + } + + virtual void OnDirectoryChangedByOperation( + const base::FilePath& path) OVERRIDE {} + + virtual void OnCacheFileUploadNeededByOperation( + const std::string& resource_id) OVERRIDE { + EXPECT_EQ(1U, expected_upload_.count(resource_id)) << resource_id; + expected_upload_.erase(resource_id); + if (expected_upload_.empty()) + quit_closure_.Run(); + } + + private: + std::set<std::string> expected_upload_; + base::Closure quit_closure_; +}; + +// Writes something on the file at |path|. +void WriteSomethingAfterStartWatch(const base::FilePath& path, + bool watch_success) { + EXPECT_TRUE(watch_success) << path.value(); + + const char kDummy[] = "hello"; + ASSERT_TRUE(file_util::WriteFile(path, kDummy, arraysize(kDummy))); +} + +class FileWriteWatcherTest : public testing::Test { + protected: + // The test requires UI thread (FileWriteWatcher DCHECKs that its public + // interface is running on UI thread) and FILE thread (Linux version of + // base::FilePathWatcher needs to live on an IOAllowed thread with TYPE_IO, + // which is FILE thread in the production environment). + // + // By using the IO_MAINLOOP test thread bundle, the main thread is used + // both as UI and FILE thread, with TYPE_IO message loop. + FileWriteWatcherTest() + : thread_bundle_(content::TestBrowserThreadBundle::IO_MAINLOOP) { + } + + virtual void SetUp() OVERRIDE { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + } + + base::FilePath GetTempPath(const std::string& name) { + return temp_dir_.path().Append(name); + } + + private: + content::TestBrowserThreadBundle thread_bundle_; + base::ScopedTempDir temp_dir_; +}; + +} // namespace + +TEST_F(FileWriteWatcherTest, WatchThreeFiles) { + std::set<std::string> expected; + expected.insert("1"); + expected.insert("2"); + expected.insert("3"); + + base::RunLoop loop; + TestObserver observer(expected, loop.QuitClosure()); + + // Set up the watcher. + FileWriteWatcher watcher(&observer); + watcher.DisableDelayForTesting(); + + // Start watching and running. + base::FilePath path1 = GetTempPath("foo.txt"); + base::FilePath path2 = GetTempPath("bar.png"); + base::FilePath path3 = GetTempPath("buz.doc"); + base::FilePath path4 = GetTempPath("mya.mp3"); + watcher.StartWatch(path1, "1", + base::Bind(&WriteSomethingAfterStartWatch, path1)); + watcher.StartWatch(path2, "2", + base::Bind(&WriteSomethingAfterStartWatch, path2)); + watcher.StartWatch(path3, "3", + base::Bind(&WriteSomethingAfterStartWatch, path3)); + + // Unwatched write. It shouldn't be notified. + WriteSomethingAfterStartWatch(path4, true); + + // The loop should quit if all the three paths are notified to be written. + loop.Run(); +} + +} // namespace internal +} // namespace drive diff --git a/chrome/chrome_browser_chromeos.gypi b/chrome/chrome_browser_chromeos.gypi index 36eabb0..cd5fba8 100644 --- a/chrome/chrome_browser_chromeos.gypi +++ b/chrome/chrome_browser_chromeos.gypi @@ -275,6 +275,8 @@ 'browser/chromeos/drive/file_task_executor.h', 'browser/chromeos/drive/file_write_helper.cc', 'browser/chromeos/drive/file_write_helper.h', + 'browser/chromeos/drive/file_write_watcher.cc', + 'browser/chromeos/drive/file_write_watcher.h', 'browser/chromeos/drive/fileapi_worker.cc', 'browser/chromeos/drive/fileapi_worker.h', 'browser/chromeos/drive/job_list.cc', diff --git a/chrome/chrome_tests_unit.gypi b/chrome/chrome_tests_unit.gypi index 4168ac6..5de1502 100644 --- a/chrome/chrome_tests_unit.gypi +++ b/chrome/chrome_tests_unit.gypi @@ -629,6 +629,7 @@ 'browser/chromeos/drive/file_system_unittest.cc', 'browser/chromeos/drive/file_system_util_unittest.cc', 'browser/chromeos/drive/file_write_helper_unittest.cc', + 'browser/chromeos/drive/file_write_watcher_unittest.cc', 'browser/chromeos/drive/fileapi_worker_unittest.cc', 'browser/chromeos/drive/job_queue_unittest.cc', 'browser/chromeos/drive/job_scheduler_unittest.cc', |