diff options
author | phajdan.jr@chromium.org <phajdan.jr@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-04-30 19:39:15 +0000 |
---|---|---|
committer | phajdan.jr@chromium.org <phajdan.jr@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-04-30 19:39:15 +0000 |
commit | e611fd66e64564d17865ed19658dc0b56e1ee746 (patch) | |
tree | 0d607ea22b3aed344707f3af3f8e622fe9fab6cf /base | |
parent | 022a7edd851ccd3b0e0f5f4e83e44b986085e636 (diff) | |
download | chromium_src-e611fd66e64564d17865ed19658dc0b56e1ee746.zip chromium_src-e611fd66e64564d17865ed19658dc0b56e1ee746.tar.gz chromium_src-e611fd66e64564d17865ed19658dc0b56e1ee746.tar.bz2 |
Add DirectoryWatcher implementation for Mac.
This uses FSEvents and supports both recursive and non-recursive modes.
TEST=Make sure that tests matching DirectoryWatcherTest.* from base_unittests pass on Mac.
http://crbug.com/10967
Review URL: http://codereview.chromium.org/99057
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@14977 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'base')
-rw-r--r-- | base/base.gyp | 7 | ||||
-rw-r--r-- | base/directory_watcher_mac.cc | 121 | ||||
-rw-r--r-- | base/directory_watcher_unittest.cc | 178 |
3 files changed, 232 insertions, 74 deletions
diff --git a/base/base.gyp b/base/base.gyp index 9c259b6..eef97fb 100644 --- a/base/base.gyp +++ b/base/base.gyp @@ -88,6 +88,7 @@ 'debug_util_win.cc', 'directory_watcher.h', 'directory_watcher_inotify.cc', + 'directory_watcher_mac.cc', 'directory_watcher_win.cc', 'event_recorder.cc', 'event_recorder.h', @@ -641,11 +642,7 @@ '../build/linux/system.gyp:nss', ], }], - ['OS == "mac"', { - 'sources!': [ - 'directory_watcher_unittest.cc', - ], - }, { # OS != "mac" + ['OS != "mac"', { 'sources!': [ 'mac_util_unittest.cc', ], diff --git a/base/directory_watcher_mac.cc b/base/directory_watcher_mac.cc new file mode 100644 index 0000000..d4b3082 --- /dev/null +++ b/base/directory_watcher_mac.cc @@ -0,0 +1,121 @@ +// 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 "base/directory_watcher.h" + +#include <CoreServices/CoreServices.h> + +#include "base/file_path.h" +#include "base/file_util.h" +#include "base/logging.h" +#include "base/message_loop.h" +#include "base/scoped_cftyperef.h" + +namespace { + +const CFAbsoluteTime kEventLatencySeconds = 0.3; + +class DirectoryWatcherImpl : public DirectoryWatcher::PlatformDelegate { + public: + DirectoryWatcherImpl() {} + ~DirectoryWatcherImpl() { + if (!path_.value().empty()) { + FSEventStreamStop(fsevent_stream_); + FSEventStreamInvalidate(fsevent_stream_); + FSEventStreamRelease(fsevent_stream_); + } + } + + virtual bool Watch(const FilePath& path, DirectoryWatcher::Delegate* delegate, + bool recursive); + + void OnFSEventsCallback(const FilePath& event_path) { + DCHECK(!path_.value().empty()); + if (!recursive_) { + FilePath absolute_event_path = event_path; + if (!file_util::AbsolutePath(&absolute_event_path)) + return; + if (absolute_event_path != path_) + return; + } + delegate_->OnDirectoryChanged(path_); + } + + private: + // Delegate to notify upon changes. + DirectoryWatcher::Delegate* delegate_; + + // Path we're watching (passed to delegate). + FilePath path_; + + // Indicates recursive watch. + bool recursive_; + + // Backend stream we receive event callbacks from (strong reference). + FSEventStreamRef fsevent_stream_; + + DISALLOW_COPY_AND_ASSIGN(DirectoryWatcherImpl); +}; + +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); + DirectoryWatcherImpl* watcher = + reinterpret_cast<DirectoryWatcherImpl*> (event_watcher); + for (size_t i = 0; i < num_events; i++) { + watcher->OnFSEventsCallback(FilePath(paths[i])); + } +} + +bool DirectoryWatcherImpl::Watch(const FilePath& path, + DirectoryWatcher::Delegate* delegate, + bool recursive) { + DCHECK(path_.value().empty()); // Can only watch one path. + + DCHECK(MessageLoop::current()->type() == MessageLoop::TYPE_UI); + + if (!file_util::PathExists(path)) + return false; + + path_ = path; + if (!file_util::AbsolutePath(&path_)) { + path_ = FilePath(); // Make sure we're marked as not-in-use. + return false; + } + delegate_ = delegate; + recursive_ = recursive; + + scoped_cftyperef<CFStringRef> cf_path(CFStringCreateWithCString( + NULL, path.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 + +DirectoryWatcher::DirectoryWatcher() { + impl_ = new DirectoryWatcherImpl(); +} diff --git a/base/directory_watcher_unittest.cc b/base/directory_watcher_unittest.cc index fabd46f..9ca5cda 100644 --- a/base/directory_watcher_unittest.cc +++ b/base/directory_watcher_unittest.cc @@ -21,12 +21,20 @@ namespace { // For tests where we wait a bit to verify nothing happened -const int kWaitForEventTime = 500; +const int kWaitForEventTime = 1000; -} // namespace +class DirectoryWatcherTest : public testing::Test { + public: + // Implementation of DirectoryWatcher on Mac requires UI loop. + DirectoryWatcherTest() : loop_(MessageLoop::TYPE_UI) { + } + + void OnTestDelegateFirstNotification(const FilePath& path) { + notified_delegates_++; + if (notified_delegates_ >= expected_notified_delegates_) + MessageLoop::current()->Quit(); + } -class DirectoryWatcherTest : public testing::Test, - public DirectoryWatcher::Delegate { protected: virtual void SetUp() { // Name a subdirectory of the temp directory. @@ -37,20 +45,6 @@ class DirectoryWatcherTest : public testing::Test, // Create a fresh, empty copy of this directory. file_util::Delete(test_dir_, true); file_util::CreateDirectory(test_dir_); - - directory_mods_ = 0; - quit_mod_count_ = 0; - - original_thread_id_ = PlatformThread::CurrentId(); - } - - virtual void OnDirectoryChanged(const FilePath& path) { - EXPECT_EQ(original_thread_id_, PlatformThread::CurrentId()); - ++directory_mods_; - // The exact number is verified by VerifyExpectedNumberOfModifications. - // Sometimes we don't want such check, see WaitForFirstNotification. - if (directory_mods_ >= quit_mod_count_) - MessageLoop::current()->Quit(); } virtual void TearDown() { @@ -69,29 +63,31 @@ class DirectoryWatcherTest : public testing::Test, file_util::WriteFile(path, content.c_str(), content.length()); } - void SetExpectedNumberOfModifications(int n) { - quit_mod_count_ = n; + void SetExpectedNumberOfNotifiedDelegates(int n) { + notified_delegates_ = 0; + expected_notified_delegates_ = n; } - void VerifyExpectedNumberOfModifications() { - // Check that we get at least the expected number of notifications. - if (quit_mod_count_ - directory_mods_ > 0) + void VerifyExpectedNumberOfNotifiedDelegates() { + // Check that we get at least the expected number of notified delegates. + if (expected_notified_delegates_ - notified_delegates_ > 0) loop_.Run(); - // Check that we get no more than the expected number of notifications. + // Check that we get no more than the expected number of notified delegates. loop_.PostDelayedTask(FROM_HERE, new MessageLoop::QuitTask, kWaitForEventTime); loop_.Run(); - EXPECT_EQ(quit_mod_count_, directory_mods_); + EXPECT_EQ(expected_notified_delegates_, notified_delegates_); } - // Only use this function if you don't care about getting - // too many notifications. Useful for tests where you get - // different number of notifications on different platforms. - void WaitForFirstNotification() { - directory_mods_ = 0; - SetExpectedNumberOfModifications(1); - loop_.Run(); + // We need this function for reliable tests on Mac OS X. FSEvents API + // has a latency interval and can merge multiple events into one, + // and we need a clear distinction between events triggered by test setup code + // and test code. + void SyncIfPOSIX() { +#if defined(OS_POSIX) + sync(); +#endif // defined(OS_POSIX) } MessageLoop loop_; @@ -99,12 +95,42 @@ class DirectoryWatcherTest : public testing::Test, // The path to a temporary directory used for testing. FilePath test_dir_; - // The number of times a directory modification has been observed. - int directory_mods_; + // The number of test delegates which received their notification. + int notified_delegates_; + + // The number of notified test delegates after which we quit the message loop. + int expected_notified_delegates_; +}; - // The number of directory mods which, when reached, cause us to quit - // our message loop. - int quit_mod_count_; +class TestDelegate : public DirectoryWatcher::Delegate { + public: + TestDelegate(DirectoryWatcherTest* test) + : test_(test), + got_notification_(false), + original_thread_id_(PlatformThread::CurrentId()) { + } + + bool got_notification() const { + return got_notification_; + } + + void reset() { + got_notification_ = false; + } + + virtual void OnDirectoryChanged(const FilePath& path) { + EXPECT_EQ(original_thread_id_, PlatformThread::CurrentId()); + if (!got_notification_) + test_->OnTestDelegateFirstNotification(path); + got_notification_ = true; + } + + private: + // Hold a pointer to current test fixture to inform it on first notification. + DirectoryWatcherTest* test_; + + // Set to true after first notification. + bool got_notification_; // Keep track of original thread id to verify that callbacks are called // on the same thread. @@ -114,63 +140,68 @@ class DirectoryWatcherTest : public testing::Test, // Basic test: add a file and verify we notice it. TEST_F(DirectoryWatcherTest, NewFile) { DirectoryWatcher watcher; - ASSERT_TRUE(watcher.Watch(test_dir_, this, false)); + TestDelegate delegate(this); + ASSERT_TRUE(watcher.Watch(test_dir_, &delegate, false)); - SetExpectedNumberOfModifications(2); + SetExpectedNumberOfNotifiedDelegates(1); WriteTestDirFile(FILE_PATH_LITERAL("test_file"), "some content"); - VerifyExpectedNumberOfModifications(); + VerifyExpectedNumberOfNotifiedDelegates(); } // Verify that modifying a file is caught. TEST_F(DirectoryWatcherTest, ModifiedFile) { - DirectoryWatcher watcher; - ASSERT_TRUE(watcher.Watch(test_dir_, this, false)); - // Write a file to the test dir. - SetExpectedNumberOfModifications(2); WriteTestDirFile(FILE_PATH_LITERAL("test_file"), "some content"); - VerifyExpectedNumberOfModifications(); + + SyncIfPOSIX(); + + DirectoryWatcher watcher; + TestDelegate delegate(this); + ASSERT_TRUE(watcher.Watch(test_dir_, &delegate, false)); // Now make sure we get notified if the file is modified. + SetExpectedNumberOfNotifiedDelegates(1); WriteTestDirFile(FILE_PATH_LITERAL("test_file"), "some new content"); - // Use a more forgiving function to check because on Linux you will get - // 1 notification, and on Windows 2 (and nothing seems to convince it to - // send less notifications). - WaitForFirstNotification(); + VerifyExpectedNumberOfNotifiedDelegates(); } // Verify that letting the watcher go out of scope stops notifications. TEST_F(DirectoryWatcherTest, Unregister) { + TestDelegate delegate(this); + { DirectoryWatcher watcher; - ASSERT_TRUE(watcher.Watch(test_dir_, this, false)); + ASSERT_TRUE(watcher.Watch(test_dir_, &delegate, false)); // And then let it fall out of scope, clearing its watch. } // Write a file to the test dir. - SetExpectedNumberOfModifications(0); + SetExpectedNumberOfNotifiedDelegates(0); WriteTestDirFile(FILE_PATH_LITERAL("test_file"), "some content"); - VerifyExpectedNumberOfModifications(); + VerifyExpectedNumberOfNotifiedDelegates(); } TEST_F(DirectoryWatcherTest, SubDirRecursive) { FilePath subdir(FILE_PATH_LITERAL("SubDir")); ASSERT_TRUE(file_util::CreateDirectory(test_dir_.Append(subdir))); -#if !defined(OS_WIN) +#if defined(OS_LINUX) // TODO(port): Recursive watches are not implemented on Linux. return; #endif // !defined(OS_WIN) + SyncIfPOSIX(); + // Verify that modifications to a subdirectory are noticed by recursive watch. + TestDelegate delegate(this); DirectoryWatcher watcher; - ASSERT_TRUE(watcher.Watch(test_dir_, this, true)); + ASSERT_TRUE(watcher.Watch(test_dir_, &delegate, true)); // Write a file to the subdir. - SetExpectedNumberOfModifications(2); + SetExpectedNumberOfNotifiedDelegates(1); FilePath test_path = subdir.AppendASCII("test_file"); WriteTestDirFile(test_path.value(), "some content"); - VerifyExpectedNumberOfModifications(); + VerifyExpectedNumberOfNotifiedDelegates(); } TEST_F(DirectoryWatcherTest, SubDirNonRecursive) { @@ -189,15 +220,18 @@ TEST_F(DirectoryWatcherTest, SubDirNonRecursive) { FilePath test_path = subdir.AppendASCII("test_file"); WriteTestDirFile(test_path.value(), "some content"); + SyncIfPOSIX(); + // Verify that modifications to a subdirectory are not noticed // by a not-recursive watch. DirectoryWatcher watcher; - ASSERT_TRUE(watcher.Watch(test_dir_, this, false)); + TestDelegate delegate(this); + ASSERT_TRUE(watcher.Watch(test_dir_, &delegate, false)); // Modify the test file. There should be no notifications. - SetExpectedNumberOfModifications(0); + SetExpectedNumberOfNotifiedDelegates(0); WriteTestDirFile(test_path.value(), "some other content"); - VerifyExpectedNumberOfModifications(); + VerifyExpectedNumberOfNotifiedDelegates(); } namespace { @@ -236,37 +270,41 @@ TEST_F(DirectoryWatcherTest, DeleteDuringNotify) { TEST_F(DirectoryWatcherTest, MultipleWatchersSingleFile) { DirectoryWatcher watcher1, watcher2; - ASSERT_TRUE(watcher1.Watch(test_dir_, this, false)); - ASSERT_TRUE(watcher2.Watch(test_dir_, this, false)); + TestDelegate delegate1(this), delegate2(this); + ASSERT_TRUE(watcher1.Watch(test_dir_, &delegate1, false)); + ASSERT_TRUE(watcher2.Watch(test_dir_, &delegate2, false)); - SetExpectedNumberOfModifications(4); // Each watcher should fire twice. + SetExpectedNumberOfNotifiedDelegates(2); WriteTestDirFile(FILE_PATH_LITERAL("test_file"), "some content"); - VerifyExpectedNumberOfModifications(); + VerifyExpectedNumberOfNotifiedDelegates(); } TEST_F(DirectoryWatcherTest, MultipleWatchersDifferentFiles) { const int kNumberOfWatchers = 5; DirectoryWatcher watchers[kNumberOfWatchers]; + TestDelegate delegates[kNumberOfWatchers] = {this, this, this, this, this}; FilePath subdirs[kNumberOfWatchers]; for (int i = 0; i < kNumberOfWatchers; i++) { subdirs[i] = FilePath(FILE_PATH_LITERAL("Dir")).AppendASCII(IntToString(i)); ASSERT_TRUE(file_util::CreateDirectory(test_dir_.Append(subdirs[i]))); - ASSERT_TRUE(watchers[i].Watch(test_dir_.Append(subdirs[i]), this, false)); + ASSERT_TRUE(watchers[i].Watch(test_dir_.Append(subdirs[i]), &delegates[i], + false)); } for (int i = 0; i < kNumberOfWatchers; i++) { // Verify that we only get modifications from one watcher (each watcher has // different directory). - ASSERT_EQ(0, directory_mods_); + for (int j = 0; j < kNumberOfWatchers; j++) + delegates[j].reset(); // Write a file to the subdir. FilePath test_path = subdirs[i].AppendASCII("test_file"); - SetExpectedNumberOfModifications(2); + SetExpectedNumberOfNotifiedDelegates(1); WriteTestDirFile(test_path.value(), "some content"); - VerifyExpectedNumberOfModifications(); + VerifyExpectedNumberOfNotifiedDelegates(); - directory_mods_ = 0; + loop_.RunAllPending(); } } @@ -275,6 +313,8 @@ TEST_F(DirectoryWatcherTest, MultipleWatchersDifferentFiles) { // Basic test: add a file and verify we notice it. TEST_F(DirectoryWatcherTest, NonExistentDirectory) { DirectoryWatcher watcher; - ASSERT_FALSE(watcher.Watch(test_dir_.AppendASCII("does-not-exist"), this, + ASSERT_FALSE(watcher.Watch(test_dir_.AppendASCII("does-not-exist"), NULL, false)); } + +} // namespace |