blob: 334ef45c088c02bcc657e2a1a98894fdd52c382d [file] [log] [blame]
// 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/post_task.h"
#include "base/task/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(Study* 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);
// An error most likely means that the value is not present.
if (result != ERROR_SUCCESS || value == 0)
*participation = kNoStudySelected;
else if (value == 1)
*participation = kStudyOne;
else
*participation = kStudyTwo;
return true;
}
bool ExperimentStorage::Lock::WriteParticipation(Study participation) {
DCHECK(participation == kNoStudySelected || participation == kStudyOne ||
participation == kStudyTwo);
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 == kNoStudySelected)
return key.DeleteValue(kRegValueRetentionStudy) == ERROR_SUCCESS;
return key.WriteValue(kRegValueRetentionStudy, participation) ==
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;
experiment->metrics_ = metrics;
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 << ExperimentMetrics::kStateBits),
"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 << ExperimentMetrics::kStateBits),
"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(&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(&value))
return false;
ExperimentLabels experiment_labels(value);
experiment_labels.SetValueForLabel(kExperimentLabelName,
EncodeMetrics(metrics),
base::TimeDelta::FromDays(182));
return GoogleUpdateSettings::SetExperimentLabels(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