Index: chrome/installer/util/experiment_storage.cc |
diff --git a/chrome/installer/util/experiment_storage.cc b/chrome/installer/util/experiment_storage.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..6f8fe0e7d1bec16b7ee1b59095f6a02b315cd04b |
--- /dev/null |
+++ b/chrome/installer/util/experiment_storage.cc |
@@ -0,0 +1,479 @@ |
+// Copyright 2017 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/installer/util/experiment_storage.h" |
+ |
+#include <windows.h> |
+ |
+#include <stdint.h> |
+ |
+#include <limits> |
+#include <string> |
+ |
+#include "base/base64.h" |
+#include "base/bind.h" |
+#include "base/bind_helpers.h" |
+#include "base/logging.h" |
+#include "base/memory/ptr_util.h" |
+#include "base/strings/utf_string_conversions.h" |
+#include "base/task_scheduler/post_task.h" |
+#include "base/task_scheduler/task_traits.h" |
+#include "base/time/time.h" |
+#include "base/win/registry.h" |
+#include "base/win/win_util.h" |
+#include "chrome/install_static/install_details.h" |
+#include "chrome/install_static/install_modes.h" |
+#include "chrome/install_static/install_util.h" |
+#include "chrome/installer/util/experiment.h" |
+#include "chrome/installer/util/experiment_labels.h" |
+#include "chrome/installer/util/experiment_metrics.h" |
+#include "chrome/installer/util/google_update_settings.h" |
+#include "chrome/installer/util/shell_util.h" |
+ |
+namespace installer { |
+ |
+namespace { |
+ |
+constexpr base::char16 kExperimentLabelName[] = L"CrExp60"; |
+constexpr wchar_t kRegKeyRetention[] = L"\\Retention"; |
+constexpr wchar_t kRegValueActionDelay[] = L"ActionDelay"; |
+constexpr wchar_t kRegValueFirstDisplayTime[] = L"FirstDisplayTime"; |
+constexpr wchar_t kRegValueGroup[] = L"Group"; |
+constexpr wchar_t kRegValueInactiveDays[] = L"InactiveDays"; |
+constexpr wchar_t kRegValueLatestDisplayTime[] = L"LatestDisplayTime"; |
+constexpr wchar_t kRegValueRetentionStudy[] = L"RetentionStudy"; |
+constexpr wchar_t kRegValueState[] = L"State"; |
+constexpr wchar_t kRegValueToastCount[] = L"ToastCount"; |
+constexpr wchar_t kRegValueToastLocation[] = L"ToastLocation"; |
+constexpr wchar_t kRegValueUserSessionUptime[] = L"UserSessionUptime"; |
+ |
+constexpr int kSessionLengthBucketLowestBit = 0; |
+constexpr int kActionDelayBucketLowestBit = |
+ ExperimentMetrics::kSessionLengthBucketBits + kSessionLengthBucketLowestBit; |
+constexpr int kLastUsedBucketLowestBit = |
+ ExperimentMetrics::kActionDelayBucketBits + kActionDelayBucketLowestBit; |
+constexpr int kToastHourLowestBit = |
+ ExperimentMetrics::kLastUsedBucketBits + kLastUsedBucketLowestBit; |
+constexpr int kFirstToastOffsetLowestBit = |
+ ExperimentMetrics::kToastHourBits + kToastHourLowestBit; |
+constexpr int kToastCountLowestBit = |
+ ExperimentMetrics::kFirstToastOffsetBits + kFirstToastOffsetLowestBit; |
+constexpr int kToastLocationLowestBit = |
+ ExperimentMetrics::kToastCountBits + kToastCountLowestBit; |
+constexpr int kStateLowestBit = |
+ ExperimentMetrics::kToastLocationBits + kToastLocationLowestBit; |
+constexpr int kGroupLowestBit = ExperimentMetrics::kStateBits + kStateLowestBit; |
+constexpr int kLowestUnusedBit = |
+ ExperimentMetrics::kGroupBits + kGroupLowestBit; |
+ |
+// Helper functions ------------------------------------------------------------ |
+ |
+// Returns the name of the global mutex used to protect the storage location. |
+base::string16 GetMutexName() { |
+ base::string16 name(L"Global\\"); |
+ name.append(install_static::kCompanyPathName); |
+ name.append(ShellUtil::GetBrowserModelId(!install_static::IsSystemInstall())); |
+ name.append(L"ExperimentStorageMutex"); |
+ return name; |
+} |
+ |
+// Populates |path| with the path to the registry key in which the current |
+// user's experiment state is stored. Returns false if the path cannot be |
+// determined. |
+bool GetExperimentStateKeyPath(bool system_level, base::string16* path) { |
+ const install_static::InstallDetails& install_details = |
+ install_static::InstallDetails::Get(); |
+ |
+ if (!system_level) { |
+ *path = install_details.GetClientStateKeyPath().append(kRegKeyRetention); |
+ return true; |
+ } |
+ |
+ base::string16 user_sid; |
+ if (base::win::GetUserSidString(&user_sid)) { |
+ *path = install_details.GetClientStateMediumKeyPath() |
+ .append(kRegKeyRetention) |
+ .append(L"\\") |
+ .append(user_sid); |
+ return true; |
+ } |
+ |
+ NOTREACHED(); |
+ return false; |
+} |
+ |
+bool OpenParticipationKey(bool write_access, base::win::RegKey* key) { |
+ const install_static::InstallDetails& details = |
+ install_static::InstallDetails::Get(); |
+ LONG result = key->Open( |
+ details.system_level() ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER, |
+ details.GetClientStateKeyPath().c_str(), |
+ KEY_WOW64_32KEY | (write_access ? KEY_SET_VALUE : KEY_QUERY_VALUE)); |
+ return result == ERROR_SUCCESS; |
+} |
+ |
+// Reads |value_name| into |result|. Returns false if the value is not found or |
+// is out of range. |
+template <class T> |
+bool ReadBoundedDWORD(base::win::RegKey* key, |
+ const wchar_t* value_name, |
+ DWORD min_value, |
+ DWORD max_value, |
+ T* result) { |
+ DWORD dword_value; |
+ if (key->ReadValueDW(value_name, &dword_value) != ERROR_SUCCESS) |
+ return false; |
+ if (dword_value < min_value || dword_value > max_value) |
+ return false; |
+ *result = static_cast<T>(dword_value); |
+ return true; |
+} |
+ |
+// Reads the internal representation of a Time or TimeDelta from |value_name| |
+// into |result|. Returns false if the value is not found or is out of range. |
+template <class T> |
+bool ReadTime(base::win::RegKey* key, const wchar_t* value_name, T* result) { |
+ int64_t qword_value; |
+ if (key->ReadInt64(value_name, &qword_value) != ERROR_SUCCESS) |
+ return false; |
+ *result = T::FromInternalValue(qword_value); |
+ return true; |
+} |
+ |
+void WriteTime(base::win::RegKey* key, |
+ const wchar_t* value_name, |
+ int64_t internal_time_value) { |
+ key->WriteValue(value_name, &internal_time_value, sizeof(internal_time_value), |
+ REG_QWORD); |
+} |
+ |
+} // namespace |
+ |
+// ExperimentStorage::Lock ----------------------------------------------------- |
+ |
+ExperimentStorage::Lock::~Lock() { |
+ BOOL result = ::ReleaseMutex(storage_->mutex_.Get()); |
+ DCHECK(result); |
+} |
+ |
+bool ExperimentStorage::Lock::ReadParticipation(Participation* participation) { |
+ base::win::RegKey key; |
+ // A failure to open the key likely indicates that this isn't running from a |
+ // real install of Chrome. |
+ if (!OpenParticipationKey(false /* !write_access */, &key)) |
+ return false; |
+ |
+ DWORD value = 0; |
+ LONG result = key.ReadValueDW(kRegValueRetentionStudy, &value); |
+ if (result != ERROR_SUCCESS) { |
+ // This likely means that the value is not present. |
+ *participation = Participation::kNotEvaluated; |
+ } else if (value == 0) { |
+ *participation = Participation::kNotParticipating; |
+ } else { |
+ *participation = Participation::kIsParticipating; |
+ } |
+ return true; |
+} |
+ |
+bool ExperimentStorage::Lock::WriteParticipation(Participation participation) { |
+ base::win::RegKey key; |
+ // A failure to open the key likely indicates that this isn't running from a |
+ // real install of Chrome. |
+ if (!OpenParticipationKey(true /* write_access */, &key)) |
+ return false; |
+ |
+ if (participation == Participation::kNotEvaluated) |
+ return key.DeleteValue(kRegValueRetentionStudy) == ERROR_SUCCESS; |
+ const DWORD value = participation == Participation::kIsParticipating ? 1 : 0; |
+ return key.WriteValue(kRegValueRetentionStudy, value) == ERROR_SUCCESS; |
+} |
+ |
+bool ExperimentStorage::Lock::LoadExperiment(Experiment* experiment) { |
+ // This function loads both the experiment metrics and state from the |
+ // registry. |
+ // - If no metrics are found: |experiment| is cleared, and true is returned. |
+ // (Per-user experiment data in the registry is ignored for all users.) |
+ // - If metrics indicate an initial state (prior to a user being elected into |
+ // an experiment group): |experiment| is populated with the metrics and true |
+ // is returned. (Per-user experiment data in the registry is ignored for all |
+ // users.) |
+ // - If metrics indicate an intermediate or terminal state and per-user |
+ // experiment data is in the same state: |experiment| is populated with all |
+ // data from the registry and true is returned. |
+ // Otherwise, the metrics correspond to a different user on the machine, so |
+ // false is returned. |
+ |
+ *experiment = Experiment(); |
+ |
+ ExperimentMetrics metrics; |
+ if (!storage_->LoadMetricsUnsafe(&metrics)) |
+ return false; // Error reading metrics -- do nothing. |
+ |
+ if (metrics.InInitialState()) { |
+ // There should be no per-user experiment data present (ignore it if there |
+ // happens to be somehow). |
+ experiment->InitializeFromMetrics(metrics); |
+ return true; |
+ } |
+ |
+ Experiment temp_experiment; |
+ if (!storage_->LoadStateUnsafe(&temp_experiment)) |
+ return false; |
+ |
+ // Verify that the state matches the metrics. Ignore the state if this is not |
+ // the case, as the metrics are the source of truth. |
+ if (temp_experiment.state() != metrics.state) |
+ return false; |
+ |
+ *experiment = temp_experiment; |
+ return true; |
+} |
+ |
+bool ExperimentStorage::Lock::StoreExperiment(const Experiment& experiment) { |
+ bool ret = storage_->StoreMetricsUnsafe(experiment.metrics()); |
+ return storage_->StoreStateUnsafe(experiment) && ret; |
+} |
+ |
+bool ExperimentStorage::Lock::LoadMetrics(ExperimentMetrics* metrics) { |
+ DCHECK_EQ(ExperimentMetrics::kUninitialized, metrics->state); |
+ return storage_->LoadMetricsUnsafe(metrics); |
+} |
+ |
+bool ExperimentStorage::Lock::StoreMetrics(const ExperimentMetrics& metrics) { |
+ DCHECK_NE(ExperimentMetrics::kUninitialized, metrics.state); |
+ return storage_->StoreMetricsUnsafe(metrics); |
+} |
+ |
+ExperimentStorage::Lock::Lock(ExperimentStorage* storage) : storage_(storage) { |
+ DCHECK(storage); |
+ DWORD result = ::WaitForSingleObject(storage_->mutex_.Get(), INFINITE); |
+ PLOG_IF(FATAL, result == WAIT_FAILED) |
+ << "Failed to lock ExperimentStorage mutex"; |
+} |
+ |
+// ExperimentStorage ----------------------------------------------------------- |
+ |
+ExperimentStorage::ExperimentStorage() |
+ : mutex_(::CreateMutex(nullptr, FALSE, GetMutexName().c_str())) {} |
+ |
+ExperimentStorage::~ExperimentStorage() {} |
+ |
+std::unique_ptr<ExperimentStorage::Lock> ExperimentStorage::AcquireLock() { |
+ return base::WrapUnique(new Lock(this)); |
+} |
+ |
+// static |
+int ExperimentStorage::ReadUint64Bits(uint64_t source, |
+ int bit_length, |
+ int low_bit) { |
+ DCHECK(bit_length > 0 && bit_length <= static_cast<int>(sizeof(int) * 8) && |
+ low_bit + bit_length <= static_cast<int>(sizeof(source) * 8)); |
+ uint64_t bit_mask = (1ULL << bit_length) - 1; |
+ return static_cast<int>((source >> low_bit) & bit_mask); |
+} |
+ |
+// static |
+void ExperimentStorage::SetUint64Bits(int value, |
+ int bit_length, |
+ int low_bit, |
+ uint64_t* target) { |
+ DCHECK(bit_length > 0 && bit_length <= static_cast<int>(sizeof(value) * 8) && |
+ low_bit + bit_length <= static_cast<int>(sizeof(*target) * 8)); |
+ uint64_t bit_mask = (1ULL << bit_length) - 1; |
+ *target |= ((static_cast<uint64_t>(value) & bit_mask) << low_bit); |
+} |
+ |
+bool ExperimentStorage::DecodeMetrics(base::StringPiece16 encoded_metrics, |
+ ExperimentMetrics* metrics) { |
+ std::string metrics_data; |
+ |
+ if (!base::Base64Decode(base::UTF16ToASCII(encoded_metrics), &metrics_data)) |
+ return false; |
+ |
+ if (metrics_data.size() != 6) |
+ return false; |
+ |
+ uint64_t metrics_value = 0; |
+ for (size_t i = 0; i < metrics_data.size(); ++i) |
+ SetUint64Bits(metrics_data[i], 8, 8 * i, &metrics_value); |
+ |
+ ExperimentMetrics result; |
+ result.session_length_bucket = |
+ ReadUint64Bits(metrics_value, ExperimentMetrics::kSessionLengthBucketBits, |
+ kSessionLengthBucketLowestBit); |
+ result.action_delay_bucket = |
+ ReadUint64Bits(metrics_value, ExperimentMetrics::kActionDelayBucketBits, |
+ kActionDelayBucketLowestBit); |
+ result.last_used_bucket = |
+ ReadUint64Bits(metrics_value, ExperimentMetrics::kLastUsedBucketBits, |
+ kLastUsedBucketLowestBit); |
+ result.toast_hour = ReadUint64Bits( |
+ metrics_value, ExperimentMetrics::kToastHourBits, kToastHourLowestBit); |
+ result.first_toast_offset_days = |
+ ReadUint64Bits(metrics_value, ExperimentMetrics::kFirstToastOffsetBits, |
+ kFirstToastOffsetLowestBit); |
+ result.toast_count = ReadUint64Bits( |
+ metrics_value, ExperimentMetrics::kToastCountBits, kToastCountLowestBit); |
+ result.toast_location = static_cast<ExperimentMetrics::ToastLocation>( |
+ ReadUint64Bits(metrics_value, ExperimentMetrics::kToastLocationBits, |
+ kToastLocationLowestBit)); |
+ |
+ static_assert(ExperimentMetrics::State::NUM_STATES <= (1 << 4), |
+ "Too many states for ExperimentMetrics encoding."); |
+ result.state = static_cast<ExperimentMetrics::State>(ReadUint64Bits( |
+ metrics_value, ExperimentMetrics::kStateBits, kStateLowestBit)); |
+ result.group = ReadUint64Bits(metrics_value, ExperimentMetrics::kGroupBits, |
+ kGroupLowestBit); |
+ |
+ if (ReadUint64Bits(metrics_value, |
+ sizeof(metrics_value) * 8 - kLowestUnusedBit, |
+ kLowestUnusedBit)) { |
+ return false; |
+ } |
+ |
+ *metrics = result; |
+ return true; |
+} |
+ |
+// static |
+base::string16 ExperimentStorage::EncodeMetrics( |
+ const ExperimentMetrics& metrics) { |
+ uint64_t metrics_value = 0; |
+ SetUint64Bits(metrics.session_length_bucket, |
+ ExperimentMetrics::kSessionLengthBucketBits, |
+ kSessionLengthBucketLowestBit, &metrics_value); |
+ SetUint64Bits(metrics.action_delay_bucket, |
+ ExperimentMetrics::kActionDelayBucketBits, |
+ kActionDelayBucketLowestBit, &metrics_value); |
+ SetUint64Bits(metrics.last_used_bucket, |
+ ExperimentMetrics::kLastUsedBucketBits, |
+ kLastUsedBucketLowestBit, &metrics_value); |
+ SetUint64Bits(metrics.toast_hour, ExperimentMetrics::kToastHourBits, |
+ kToastHourLowestBit, &metrics_value); |
+ SetUint64Bits(metrics.first_toast_offset_days, |
+ ExperimentMetrics::kFirstToastOffsetBits, |
+ kFirstToastOffsetLowestBit, &metrics_value); |
+ SetUint64Bits(metrics.toast_count, ExperimentMetrics::kToastCountBits, |
+ kToastCountLowestBit, &metrics_value); |
+ SetUint64Bits(metrics.toast_location, ExperimentMetrics::kToastLocationBits, |
+ kToastLocationLowestBit, &metrics_value); |
+ static_assert(ExperimentMetrics::State::NUM_STATES <= (1 << 4), |
+ "Too many states for ExperimentMetrics encoding."); |
+ SetUint64Bits(metrics.state, ExperimentMetrics::kStateBits, kStateLowestBit, |
+ &metrics_value); |
+ SetUint64Bits(metrics.group, ExperimentMetrics::kGroupBits, kGroupLowestBit, |
+ &metrics_value); |
+ |
+ std::string metrics_data(6, '\0'); |
+ for (size_t i = 0; i < metrics_data.size(); ++i) { |
+ metrics_data[i] = |
+ static_cast<char>(ReadUint64Bits(metrics_value, 8, 8 * i)); |
+ } |
+ std::string encoded_metrics; |
+ base::Base64Encode(metrics_data, &encoded_metrics); |
+ return base::ASCIIToUTF16(encoded_metrics); |
+} |
+ |
+bool ExperimentStorage::LoadMetricsUnsafe(ExperimentMetrics* metrics) { |
+ base::string16 value; |
+ |
+ if (!GoogleUpdateSettings::ReadExperimentLabels( |
+ install_static::IsSystemInstall(), &value)) { |
+ return false; |
+ } |
+ |
+ ExperimentLabels experiment_labels(value); |
+ base::StringPiece16 encoded_metrics = |
+ experiment_labels.GetValueForLabel(kExperimentLabelName); |
+ if (encoded_metrics.empty()) { |
+ *metrics = ExperimentMetrics(); |
+ return true; |
+ } |
+ |
+ return DecodeMetrics(encoded_metrics, metrics); |
+} |
+ |
+bool ExperimentStorage::StoreMetricsUnsafe(const ExperimentMetrics& metrics) { |
+ base::string16 value; |
+ if (!GoogleUpdateSettings::ReadExperimentLabels( |
+ install_static::IsSystemInstall(), &value)) { |
+ return false; |
+ } |
+ ExperimentLabels experiment_labels(value); |
+ |
+ experiment_labels.SetValueForLabel(kExperimentLabelName, |
+ EncodeMetrics(metrics), |
+ base::TimeDelta::FromDays(182)); |
+ |
+ return GoogleUpdateSettings::SetExperimentLabels( |
+ install_static::IsSystemInstall(), experiment_labels.value()); |
+} |
+ |
+bool ExperimentStorage::LoadStateUnsafe(Experiment* experiment) { |
+ const bool system_level = install_static::IsSystemInstall(); |
+ |
+ base::string16 path; |
+ if (!GetExperimentStateKeyPath(system_level, &path)) |
+ return false; |
+ |
+ const HKEY root = system_level ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER; |
+ base::win::RegKey key; |
+ if (key.Open(root, path.c_str(), KEY_QUERY_VALUE | KEY_WOW64_32KEY) != |
+ ERROR_SUCCESS) { |
+ return false; |
+ } |
+ |
+ return ReadBoundedDWORD(&key, kRegValueState, 0, |
+ ExperimentMetrics::NUM_STATES, &experiment->state_) && |
+ ReadBoundedDWORD(&key, kRegValueGroup, 0, |
+ ExperimentMetrics::kNumGroups - 1, |
+ &experiment->group_) && |
+ ReadBoundedDWORD(&key, kRegValueToastLocation, 0, 1, |
+ &experiment->toast_location_) && |
+ ReadBoundedDWORD(&key, kRegValueInactiveDays, 0, INT_MAX, |
+ &experiment->inactive_days_) && |
+ ReadBoundedDWORD(&key, kRegValueToastCount, 0, |
+ ExperimentMetrics::kMaxToastCount, |
+ &experiment->toast_count_) && |
+ ReadTime(&key, kRegValueFirstDisplayTime, |
+ &experiment->first_display_time_) && |
+ ReadTime(&key, kRegValueLatestDisplayTime, |
+ &experiment->latest_display_time_) && |
+ ReadTime(&key, kRegValueUserSessionUptime, |
+ &experiment->user_session_uptime_) && |
+ ReadTime(&key, kRegValueActionDelay, &experiment->action_delay_); |
+} |
+ |
+bool ExperimentStorage::StoreStateUnsafe(const Experiment& experiment) { |
+ const bool system_level = install_static::IsSystemInstall(); |
+ |
+ base::string16 path; |
+ if (!GetExperimentStateKeyPath(system_level, &path)) |
+ return false; |
+ |
+ const HKEY root = system_level ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER; |
+ base::win::RegKey key; |
+ if (key.Create(root, path.c_str(), KEY_SET_VALUE | KEY_WOW64_32KEY) != |
+ ERROR_SUCCESS) { |
+ return false; |
+ } |
+ |
+ key.WriteValue(kRegValueState, experiment.state()); |
+ key.WriteValue(kRegValueGroup, experiment.group()); |
+ key.WriteValue(kRegValueToastLocation, experiment.toast_location()); |
+ key.WriteValue(kRegValueInactiveDays, experiment.inactive_days()); |
+ key.WriteValue(kRegValueToastCount, experiment.toast_count()); |
+ WriteTime(&key, kRegValueFirstDisplayTime, |
+ experiment.first_display_time().ToInternalValue()); |
+ WriteTime(&key, kRegValueLatestDisplayTime, |
+ experiment.latest_display_time().ToInternalValue()); |
+ WriteTime(&key, kRegValueUserSessionUptime, |
+ experiment.user_session_uptime().ToInternalValue()); |
+ WriteTime(&key, kRegValueActionDelay, |
+ experiment.action_delay().ToInternalValue()); |
+ return true; |
+} |
+ |
+} // namespace installer |