Introduce a new framework for back-and-forth tracked preference migration
between Protected Preferences and unprotected Preferences.
Migration from unprotected Preferences to Protected Preferences was previously
done after both stores had been initialized. This was inherently incorrect as
some operations (PrefHashFilter::FilterOnLoad) would occur before the values
had been moved to the proper store. It also introduced a weird method in
PrefHashFilter::MigrateValues which required an independent PrefHashFilter
(backed by a copy of the real PrefHashStore). This after-the-fact migration
caused Settings.TrackedPreferenceCleared spikes when changing a value from
being enforced to not being enforced (as we'd have a MAC, but no value yet in
this store when running FilterOnLoad()) and more importantly it also caused
issue 365769 -- both of these issues highlight the incorrectness of the
current approach.
The migration back from Protected Preferences to unprotected Preferences when
enforcement was disabled was using yet another mechanism which would only kick
in when a given pref was written to (ref. old non-const
SegregatedPrefStore::StoreForKey()).
The new framework intercepts PrefFilter::FilterOnLoad() events for both stores
and does the back-and-forth migration in place before it even hands them back
to the PrefFilter::FinalizeFilterOnLoad() which then hands it back to the
JsonPrefStores (so that they are agnostic to the migration; from their point
of view their values were always in their store as they received it).
Furthermore, this new framework will easily allow us to later move MACs out of
Local State into their respective stores (which is a task on our radar which
we currently have no easy way to accomplish).
The new framework also handles read errors better. For example, it was
previously possible for the unprotected->protected migration to result in data
loss if the protected store was somehow read-only from a read error while the
unprotected store wasn't -- resulting in an in-memory migration only flushed
to disk in the store from which the value was deleted... The new framework
handles those cases, preferring temporary data duplication over potential data
loss (duplicated data is cleaned up once confirmation is obtained that the new
authority for this data has been successfully written to disk -- it will even
try again in following Chrome runs if it doesn't succeed in this one).
Note: This CL helped LSAN discover an existing leak in post_task_and_reply_impl.cc, see issue 371974 for details.
BUG=365769, 371974
TEST=
A) Make sure all kTrackedPrefs consistently report
Settings.TrackedPreferenceUnchanged across changes from various enforcement
levels (using --force-fieldtrials).
B) Make sure the prefs are properly migrated to their new store (and
subsequently cleaned up from their old store) when changing the
enforcement_level across multiple runs.
C) Make sure prefs are properly migrated in a quick startup/shutdown with a
new enforcement_level and that their old value is properly cleaned up in a
subsequent startup at the same enforcement_level (or re-migrated at another
enforcement_level).
[email protected], [email protected], [email protected], [email protected]
Initially Committed in: https://src.chromium.org/viewvc/chrome?view=rev&revision=269346
Reverted in: https://src.chromium.org/viewvc/chrome?view=rev&revision=269367
Committed again: https://src.chromium.org/viewvc/chrome?view=rev&revision=269415
Reverted again: https://src.chromium.org/viewvc/chrome?view=rev&revision=269438
Review URL: https://codereview.chromium.org/257003007
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@269735 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/base/prefs/json_pref_store_unittest.cc b/base/prefs/json_pref_store_unittest.cc
index f4e1e519..4c9c847 100644
--- a/base/prefs/json_pref_store_unittest.cc
+++ b/base/prefs/json_pref_store_unittest.cc
@@ -4,10 +4,12 @@
#include "base/prefs/json_pref_store.h"
+#include "base/bind.h"
#include "base/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/ref_counted.h"
#include "base/memory/scoped_ptr.h"
+#include "base/message_loop/message_loop.h"
#include "base/path_service.h"
#include "base/prefs/pref_filter.h"
#include "base/run_loop.h"
@@ -25,6 +27,50 @@
const char kHomePage[] = "homepage";
+// A PrefFilter that will intercept all calls to FilterOnLoad() and hold on
+// to the |prefs| until explicitly asked to release them.
+class InterceptingPrefFilter : public PrefFilter {
+ public:
+ InterceptingPrefFilter();
+ virtual ~InterceptingPrefFilter();
+
+ // PrefFilter implementation:
+ virtual void FilterOnLoad(
+ const PostFilterOnLoadCallback& post_filter_on_load_callback,
+ scoped_ptr<base::DictionaryValue> pref_store_contents) OVERRIDE;
+ virtual void FilterUpdate(const std::string& path) OVERRIDE {}
+ virtual void FilterSerializeData(
+ const base::DictionaryValue* pref_store_contents) OVERRIDE {}
+
+ bool has_intercepted_prefs() const { return intercepted_prefs_ != NULL; }
+
+ // Finalize an intercepted read, handing |intercept_prefs_| back to its
+ // JsonPrefStore.
+ void ReleasePrefs();
+
+ private:
+ PostFilterOnLoadCallback post_filter_on_load_callback_;
+ scoped_ptr<base::DictionaryValue> intercepted_prefs_;
+
+ DISALLOW_COPY_AND_ASSIGN(InterceptingPrefFilter);
+};
+
+InterceptingPrefFilter::InterceptingPrefFilter() {}
+InterceptingPrefFilter::~InterceptingPrefFilter() {}
+
+void InterceptingPrefFilter::FilterOnLoad(
+ const PostFilterOnLoadCallback& post_filter_on_load_callback,
+ scoped_ptr<base::DictionaryValue> pref_store_contents) {
+ post_filter_on_load_callback_ = post_filter_on_load_callback;
+ intercepted_prefs_ = pref_store_contents.Pass();
+}
+
+void InterceptingPrefFilter::ReleasePrefs() {
+ EXPECT_FALSE(post_filter_on_load_callback_.is_null());
+ post_filter_on_load_callback_.Run(intercepted_prefs_.Pass(), false);
+ post_filter_on_load_callback_.Reset();
+}
+
class MockPrefStoreObserver : public PrefStore::Observer {
public:
MOCK_METHOD1(OnPrefValueChanged, void (const std::string&));
@@ -48,6 +94,13 @@
ASSERT_TRUE(PathExists(data_dir_));
}
+ virtual void TearDown() OVERRIDE {
+ // Make sure all pending tasks have been processed (e.g., deleting the
+ // JsonPrefStore may post write tasks).
+ message_loop_.PostTask(FROM_HERE, MessageLoop::QuitWhenIdleClosure());
+ message_loop_.Run();
+ }
+
// The path to temporary directory used to contain the test operations.
base::ScopedTempDir temp_dir_;
// The path to the directory where the test data is stored.
@@ -157,7 +210,7 @@
TEST_F(JsonPrefStoreTest, Basic) {
ASSERT_TRUE(base::CopyFile(data_dir_.AppendASCII("read.json"),
- temp_dir_.path().AppendASCII("write.json")));
+ temp_dir_.path().AppendASCII("write.json")));
// Test that the persistent value can be loaded.
base::FilePath input_file = temp_dir_.path().AppendASCII("write.json");
@@ -167,7 +220,8 @@
message_loop_.message_loop_proxy().get(),
scoped_ptr<PrefFilter>());
ASSERT_EQ(PersistentPrefStore::PREF_READ_ERROR_NONE, pref_store->ReadPrefs());
- ASSERT_FALSE(pref_store->ReadOnly());
+ EXPECT_FALSE(pref_store->ReadOnly());
+ EXPECT_TRUE(pref_store->IsInitializationComplete());
// The JSON file looks like this:
// {
@@ -185,7 +239,7 @@
TEST_F(JsonPrefStoreTest, BasicAsync) {
ASSERT_TRUE(base::CopyFile(data_dir_.AppendASCII("read.json"),
- temp_dir_.path().AppendASCII("write.json")));
+ temp_dir_.path().AppendASCII("write.json")));
// Test that the persistent value can be loaded.
base::FilePath input_file = temp_dir_.path().AppendASCII("write.json");
@@ -208,7 +262,8 @@
RunLoop().RunUntilIdle();
pref_store->RemoveObserver(&mock_observer);
- ASSERT_FALSE(pref_store->ReadOnly());
+ EXPECT_FALSE(pref_store->ReadOnly());
+ EXPECT_TRUE(pref_store->IsInitializationComplete());
}
// The JSON file looks like this:
@@ -301,4 +356,117 @@
EXPECT_FALSE(pref_store->ReadOnly());
}
+TEST_F(JsonPrefStoreTest, ReadWithInterceptor) {
+ ASSERT_TRUE(base::CopyFile(data_dir_.AppendASCII("read.json"),
+ temp_dir_.path().AppendASCII("write.json")));
+
+ // Test that the persistent value can be loaded.
+ base::FilePath input_file = temp_dir_.path().AppendASCII("write.json");
+ ASSERT_TRUE(PathExists(input_file));
+
+ scoped_ptr<InterceptingPrefFilter> intercepting_pref_filter(
+ new InterceptingPrefFilter());
+ InterceptingPrefFilter* raw_intercepting_pref_filter_ =
+ intercepting_pref_filter.get();
+ scoped_refptr<JsonPrefStore> pref_store =
+ new JsonPrefStore(input_file,
+ message_loop_.message_loop_proxy().get(),
+ intercepting_pref_filter.PassAs<PrefFilter>());
+
+ ASSERT_EQ(PersistentPrefStore::PREF_READ_ERROR_ASYNCHRONOUS_TASK_INCOMPLETE,
+ pref_store->ReadPrefs());
+ EXPECT_FALSE(pref_store->ReadOnly());
+
+ // The store shouldn't be considered initialized until the interceptor
+ // returns.
+ EXPECT_TRUE(raw_intercepting_pref_filter_->has_intercepted_prefs());
+ EXPECT_FALSE(pref_store->IsInitializationComplete());
+ EXPECT_FALSE(pref_store->GetValue(kHomePage, NULL));
+
+ raw_intercepting_pref_filter_->ReleasePrefs();
+
+ EXPECT_FALSE(raw_intercepting_pref_filter_->has_intercepted_prefs());
+ EXPECT_TRUE(pref_store->IsInitializationComplete());
+ EXPECT_TRUE(pref_store->GetValue(kHomePage, NULL));
+
+ // The JSON file looks like this:
+ // {
+ // "homepage": "http://www.cnn.com",
+ // "some_directory": "/usr/local/",
+ // "tabs": {
+ // "new_windows_in_tabs": true,
+ // "max_tabs": 20
+ // }
+ // }
+
+ RunBasicJsonPrefStoreTest(
+ pref_store.get(), input_file, data_dir_.AppendASCII("write.golden.json"));
+}
+
+TEST_F(JsonPrefStoreTest, ReadAsyncWithInterceptor) {
+ ASSERT_TRUE(base::CopyFile(data_dir_.AppendASCII("read.json"),
+ temp_dir_.path().AppendASCII("write.json")));
+
+ // Test that the persistent value can be loaded.
+ base::FilePath input_file = temp_dir_.path().AppendASCII("write.json");
+ ASSERT_TRUE(PathExists(input_file));
+
+ scoped_ptr<InterceptingPrefFilter> intercepting_pref_filter(
+ new InterceptingPrefFilter());
+ InterceptingPrefFilter* raw_intercepting_pref_filter_ =
+ intercepting_pref_filter.get();
+ scoped_refptr<JsonPrefStore> pref_store =
+ new JsonPrefStore(input_file,
+ message_loop_.message_loop_proxy().get(),
+ intercepting_pref_filter.PassAs<PrefFilter>());
+
+ MockPrefStoreObserver mock_observer;
+ pref_store->AddObserver(&mock_observer);
+
+ // Ownership of the |mock_error_delegate| is handed to the |pref_store| below.
+ MockReadErrorDelegate* mock_error_delegate = new MockReadErrorDelegate;
+
+ {
+ pref_store->ReadPrefsAsync(mock_error_delegate);
+
+ EXPECT_CALL(mock_observer, OnInitializationCompleted(true)).Times(0);
+ // EXPECT_CALL(*mock_error_delegate,
+ // OnError(PersistentPrefStore::PREF_READ_ERROR_NONE)).Times(0);
+ RunLoop().RunUntilIdle();
+
+ EXPECT_FALSE(pref_store->ReadOnly());
+ EXPECT_TRUE(raw_intercepting_pref_filter_->has_intercepted_prefs());
+ EXPECT_FALSE(pref_store->IsInitializationComplete());
+ EXPECT_FALSE(pref_store->GetValue(kHomePage, NULL));
+ }
+
+ {
+ EXPECT_CALL(mock_observer, OnInitializationCompleted(true)).Times(1);
+ // EXPECT_CALL(*mock_error_delegate,
+ // OnError(PersistentPrefStore::PREF_READ_ERROR_NONE)).Times(0);
+
+ raw_intercepting_pref_filter_->ReleasePrefs();
+
+ EXPECT_FALSE(pref_store->ReadOnly());
+ EXPECT_FALSE(raw_intercepting_pref_filter_->has_intercepted_prefs());
+ EXPECT_TRUE(pref_store->IsInitializationComplete());
+ EXPECT_TRUE(pref_store->GetValue(kHomePage, NULL));
+ }
+
+ pref_store->RemoveObserver(&mock_observer);
+
+ // The JSON file looks like this:
+ // {
+ // "homepage": "http://www.cnn.com",
+ // "some_directory": "/usr/local/",
+ // "tabs": {
+ // "new_windows_in_tabs": true,
+ // "max_tabs": 20
+ // }
+ // }
+
+ RunBasicJsonPrefStoreTest(
+ pref_store.get(), input_file, data_dir_.AppendASCII("write.golden.json"));
+}
+
} // namespace base