| // Copyright 2021 The ChromiumOS Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "featured/feature_library.h" | |
| #include <memory> | |
| #include <optional> | |
| #include <string> | |
| #include <string_view> | |
| #include <utility> | |
| #include <base/files/file.h> | |
| #include <base/files/file_path.h> | |
| #include <base/functional/bind.h> | |
| #include <base/functional/callback_helpers.h> | |
| #include <base/logging.h> | |
| #include <base/no_destructor.h> | |
| #include <base/strings/escape.h> | |
| #include <base/strings/strcat.h> | |
| #include <base/synchronization/lock.h> | |
| #include <base/synchronization/waitable_event.h> | |
| #include <brillo/dbus/dbus_connection.h> | |
| #include <brillo/dbus/dbus_method_invoker.h> | |
| #include <brillo/errors/error.h> | |
| #include <chromeos/constants/featured.h> | |
| #include <chromeos/dbus/service_constants.h> | |
| #include <featured/proto_bindings/featured.pb.h> | |
| namespace feature { | |
| namespace { | |
| // GetInstanceLock() must be held while using this variable. | |
| PlatformFeatures* g_instance = nullptr; | |
| // TODO(b/289932268): Consider removing this function. | |
| // This blocks both caller thread and D-Bus thread, which is not recommended. | |
| std::unique_ptr<dbus::Response> CallDBusMethod(scoped_refptr<dbus::Bus> bus, | |
| dbus::ObjectProxy* proxy, | |
| dbus::MethodCall* method_call, | |
| int timeout_ms) { | |
| // Core task to call D-Bus IPC. Should be called on D-Bus available thread. | |
| auto task = base::BindOnce( | |
| [](dbus::ObjectProxy* proxy, dbus::MethodCall* method_call, | |
| int timeout_ms) -> std::unique_ptr<dbus::Response> { | |
| return proxy->CallMethodAndBlock(method_call, timeout_ms) | |
| .value_or(nullptr); | |
| }, | |
| base::Unretained(proxy), base::Unretained(method_call), timeout_ms); | |
| if (bus->HasDBusThread() && | |
| !bus->GetDBusTaskRunner()->RunsTasksInCurrentSequence()) { | |
| // If D-Bus thread is available and it is not the current thread, | |
| // post a task to there. | |
| std::unique_ptr<dbus::Response> result; | |
| base::WaitableEvent event(base::WaitableEvent::ResetPolicy::AUTOMATIC, | |
| base::WaitableEvent::InitialState::NOT_SIGNALED); | |
| bus->GetDBusTaskRunner()->PostTask( | |
| FROM_HERE, std::move(task).Then(base::BindOnce( | |
| [](std::unique_ptr<dbus::Response>& out_result, | |
| base::WaitableEvent& event, | |
| std::unique_ptr<dbus::Response> result) { | |
| out_result = std::move(result); | |
| event.Signal(); | |
| }, | |
| std::ref(result), std::ref(event)))); | |
| event.Wait(); | |
| return result; | |
| } | |
| // Otherwise, run the task directly. | |
| return std::move(task).Run(); | |
| } | |
| } // namespace | |
| PlatformFeatures::PlatformFeatures(scoped_refptr<dbus::Bus> bus, | |
| dbus::ObjectProxy* chrome_proxy, | |
| dbus::ObjectProxy* feature_proxy) | |
| : bus_(bus), | |
| chrome_proxy_(chrome_proxy), | |
| feature_proxy_(feature_proxy), | |
| active_trial_file_dir_(kActiveTrialFileDirectory) { | |
| base::AutoLock auto_lock(GetInstanceLock()); | |
| CHECK(!g_instance); | |
| g_instance = this; | |
| } | |
| // static | |
| bool PlatformFeatures::Initialize(scoped_refptr<dbus::Bus> bus) { | |
| auto* chrome_proxy = bus->GetObjectProxy( | |
| chromeos::kChromeFeaturesServiceName, | |
| dbus::ObjectPath(chromeos::kChromeFeaturesServicePath)); | |
| if (!chrome_proxy) { | |
| LOG(ERROR) << "Failed to create object proxy for " | |
| << chromeos::kChromeFeaturesServiceName; | |
| return false; | |
| } | |
| auto* feature_proxy = bus->GetObjectProxy( | |
| kFeatureLibInterface, dbus::ObjectPath(kFeatureLibServicePath)); | |
| if (!feature_proxy) { | |
| LOG(ERROR) << "Failed to create object proxy for " << kFeatureLibInterface; | |
| return false; | |
| } | |
| new PlatformFeatures(bus, chrome_proxy, feature_proxy); | |
| return true; | |
| } | |
| // static | |
| void PlatformFeatures::InitializeForTesting(scoped_refptr<dbus::Bus> bus, | |
| dbus::ObjectProxy* chrome_proxy, | |
| dbus::ObjectProxy* feature_proxy) { | |
| new PlatformFeatures(bus, chrome_proxy, feature_proxy); | |
| } | |
| // static | |
| void PlatformFeatures::ShutdownForTesting() { | |
| base::AutoLock auto_lock(GetInstanceLock()); | |
| if (g_instance) { | |
| delete g_instance; | |
| } | |
| } | |
| // static | |
| PlatformFeatures* PlatformFeatures::Get() { | |
| base::AutoLock auto_lock(GetInstanceLock()); | |
| return g_instance; | |
| } | |
| PlatformFeatures::~PlatformFeatures() { | |
| GetInstanceLock().AssertAcquired(); | |
| CHECK_EQ(this, g_instance); | |
| g_instance = nullptr; | |
| } | |
| void PlatformFeatures::IsEnabled(const VariationsFeature& feature, | |
| IsEnabledCallback callback) { | |
| DCHECK(CheckFeatureIdentity(feature)) << feature.name; | |
| chrome_proxy_->WaitForServiceToBeAvailable(base::BindOnce( | |
| &PlatformFeatures::OnWaitForServiceIsEnabled, | |
| weak_ptr_factory_.GetWeakPtr(), feature, std::move(callback))); | |
| } | |
| bool PlatformFeatures::IsEnabledBlockingWithTimeout( | |
| const VariationsFeature& feature, int timeout_ms) { | |
| DCHECK(CheckFeatureIdentity(feature)) << feature.name; | |
| dbus::MethodCall call(chromeos::kChromeFeaturesServiceInterface, | |
| chromeos::kChromeFeaturesServiceIsFeatureEnabledMethod); | |
| dbus::MessageWriter writer(&call); | |
| writer.AppendString(feature.name); | |
| std::unique_ptr<dbus::Response> response = | |
| CallDBusMethod(bus_, chrome_proxy_, &call, timeout_ms); | |
| if (!response) { | |
| return feature.default_state == FEATURE_ENABLED_BY_DEFAULT; | |
| } | |
| dbus::MessageReader reader(response.get()); | |
| bool feature_enabled = false; | |
| if (!reader.PopBool(&feature_enabled)) { | |
| LOG(ERROR) << "failed to read bool from dbus result; using default value"; | |
| return feature.default_state == FEATURE_ENABLED_BY_DEFAULT; | |
| } | |
| return feature_enabled; | |
| } | |
| void PlatformFeatures::OnWaitForServiceIsEnabled( | |
| const VariationsFeature& feature, | |
| IsEnabledCallback callback, | |
| bool available) { | |
| if (!available) { | |
| std::move(callback).Run(feature.default_state == | |
| FEATURE_ENABLED_BY_DEFAULT); | |
| LOG(ERROR) << "failed to connect to dbus service; using default value"; | |
| return; | |
| } | |
| dbus::MethodCall call(chromeos::kChromeFeaturesServiceInterface, | |
| chromeos::kChromeFeaturesServiceIsFeatureEnabledMethod); | |
| dbus::MessageWriter writer(&call); | |
| writer.AppendString(feature.name); | |
| chrome_proxy_->CallMethod( | |
| &call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, | |
| base::BindOnce(&PlatformFeatures::HandleIsEnabledResponse, | |
| weak_ptr_factory_.GetWeakPtr(), feature, | |
| std::move(callback))); | |
| } | |
| void PlatformFeatures::HandleIsEnabledResponse(const VariationsFeature& feature, | |
| IsEnabledCallback callback, | |
| dbus::Response* response) { | |
| if (!response) { | |
| LOG(ERROR) << "dbus call failed; using default value"; | |
| std::move(callback).Run(feature.default_state == | |
| FEATURE_ENABLED_BY_DEFAULT); | |
| return; | |
| } | |
| dbus::MessageReader reader(response); | |
| bool feature_enabled = false; | |
| if (!reader.PopBool(&feature_enabled)) { | |
| LOG(ERROR) << "failed to read bool from dbus result; using default value"; | |
| std::move(callback).Run(feature.default_state == | |
| FEATURE_ENABLED_BY_DEFAULT); | |
| return; | |
| } | |
| std::move(callback).Run(feature_enabled); | |
| } | |
| void PlatformFeatures::GetParamsAndEnabled( | |
| const std::vector<const VariationsFeature*>& features, | |
| GetParamsCallback callback) { | |
| for (const auto* feature : features) { | |
| DCHECK(CheckFeatureIdentity(*feature)) << feature->name; | |
| } | |
| chrome_proxy_->WaitForServiceToBeAvailable(base::BindOnce( | |
| &PlatformFeatures::OnWaitForServiceGetParams, | |
| weak_ptr_factory_.GetWeakPtr(), features, std::move(callback))); | |
| } | |
| PlatformFeaturesInterface::ParamsResult | |
| PlatformFeatures::GetParamsAndEnabledBlocking( | |
| const std::vector<const VariationsFeature*>& features) { | |
| for (const auto* feature : features) { | |
| DCHECK(CheckFeatureIdentity(*feature)) << feature->name; | |
| } | |
| dbus::MethodCall call(chromeos::kChromeFeaturesServiceInterface, | |
| chromeos::kChromeFeaturesServiceGetFeatureParamsMethod); | |
| dbus::MessageWriter writer(&call); | |
| EncodeGetParamsArgument(&writer, features); | |
| std::unique_ptr<dbus::Response> response = CallDBusMethod( | |
| bus_, chrome_proxy_, &call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT); | |
| return ParseGetParamsResponse(response.get(), features); | |
| } | |
| void PlatformFeatures::EncodeGetParamsArgument( | |
| dbus::MessageWriter* writer, | |
| const std::vector<const VariationsFeature*>& features) { | |
| dbus::MessageWriter array_writer(nullptr); | |
| writer->OpenArray("s", &array_writer); | |
| for (const auto* feature : features) { | |
| array_writer.AppendString(feature->name); | |
| } | |
| writer->CloseContainer(&array_writer); | |
| } | |
| PlatformFeaturesInterface::ParamsResult | |
| PlatformFeatures::CreateDefaultGetParamsAndEnabledResponse( | |
| const std::vector<const VariationsFeature*>& features) { | |
| std::map<std::string, ParamsResultEntry> default_response; | |
| for (const auto* feature : features) { | |
| default_response[feature->name] = ParamsResultEntry{ | |
| .enabled = feature->default_state == FEATURE_ENABLED_BY_DEFAULT, | |
| }; | |
| } | |
| return default_response; | |
| } | |
| void PlatformFeatures::OnWaitForServiceGetParams( | |
| const std::vector<const VariationsFeature*>& features, | |
| GetParamsCallback callback, | |
| bool available) { | |
| if (!available) { | |
| LOG(ERROR) << "failed to connect to dbus service; using default value"; | |
| std::move(callback).Run(CreateDefaultGetParamsAndEnabledResponse(features)); | |
| return; | |
| } | |
| dbus::MethodCall call(chromeos::kChromeFeaturesServiceInterface, | |
| chromeos::kChromeFeaturesServiceGetFeatureParamsMethod); | |
| dbus::MessageWriter writer(&call); | |
| EncodeGetParamsArgument(&writer, features); | |
| chrome_proxy_->CallMethod( | |
| &call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, | |
| base::BindOnce(&PlatformFeatures::HandleGetParamsResponse, | |
| weak_ptr_factory_.GetWeakPtr(), features, | |
| std::move(callback))); | |
| } | |
| void PlatformFeatures::HandleGetParamsResponse( | |
| const std::vector<const VariationsFeature*>& features, | |
| GetParamsCallback callback, | |
| dbus::Response* response) { | |
| std::move(callback).Run(ParseGetParamsResponse(response, features)); | |
| } | |
| PlatformFeatures::ParamsResult PlatformFeatures::ParseGetParamsResponse( | |
| dbus::Response* response, | |
| const std::vector<const VariationsFeature*>& features) { | |
| // Parse the response, which should be an array containing dict entries which | |
| // maps feature names to a struct containing: | |
| // * A bool for whether to use the overridden feature enable state | |
| // * A bool indicating the enable state (only valid if the first bool is true) | |
| // * A (possibly-empty) array of dict entries mapping parameter keys to values | |
| if (!response) { | |
| LOG(ERROR) << "dbus call failed; using default value"; | |
| return CreateDefaultGetParamsAndEnabledResponse(features); | |
| } | |
| dbus::MessageReader reader(response); | |
| dbus::MessageReader array_reader(nullptr); | |
| if (!reader.PopArray(&array_reader)) { | |
| LOG(ERROR) << "failed to read array from dbus result; using default value"; | |
| return CreateDefaultGetParamsAndEnabledResponse(features); | |
| } | |
| std::map<std::string, ParamsResultEntry> result; | |
| while (array_reader.HasMoreData()) { | |
| ParamsResultEntry entry; | |
| dbus::MessageReader feature_entry_reader(nullptr); | |
| if (!array_reader.PopDictEntry(&feature_entry_reader)) { | |
| LOG(ERROR) << "Failed to read dict from dbus result; using default."; | |
| return CreateDefaultGetParamsAndEnabledResponse(features); | |
| } | |
| // Get name | |
| std::string feature_name; | |
| if (!feature_entry_reader.PopString(&feature_name)) { | |
| LOG(ERROR) << "Failed to read string from dbus result; using default " | |
| << "value"; | |
| return CreateDefaultGetParamsAndEnabledResponse(features); | |
| } | |
| dbus::MessageReader struct_reader(nullptr); | |
| if (!feature_entry_reader.PopStruct(&struct_reader)) { | |
| LOG(ERROR) << "Failed to read struct from dbus result; using default " | |
| << "value"; | |
| return CreateDefaultGetParamsAndEnabledResponse(features); | |
| } | |
| // Get override state. | |
| bool use_override = false; | |
| bool override_value = false; | |
| if (!struct_reader.PopBool(&use_override) || | |
| !struct_reader.PopBool(&override_value)) { | |
| LOG(ERROR) << "Failed to pop a bool; using default value"; | |
| return CreateDefaultGetParamsAndEnabledResponse(features); | |
| } else { | |
| if (use_override) { | |
| entry.enabled = override_value; | |
| } else { | |
| // This is mildly inefficient, but the number of features passed to this | |
| // method is expected to be small (human magnitude), so it isn't a | |
| // prohibitive cost. | |
| for (const auto* feature : features) { | |
| if (feature->name == feature_name) { | |
| entry.enabled = | |
| feature->default_state == FEATURE_ENABLED_BY_DEFAULT; | |
| } | |
| } | |
| } | |
| } | |
| // Finally, get params. | |
| std::map<std::string, std::string> params; | |
| dbus::MessageReader params_array_reader(nullptr); | |
| if (!struct_reader.PopArray(¶ms_array_reader)) { | |
| LOG(ERROR) << "Failed to read params array; using default value"; | |
| return CreateDefaultGetParamsAndEnabledResponse(features); | |
| } | |
| while (params_array_reader.HasMoreData()) { | |
| dbus::MessageReader entry_reader(nullptr); | |
| std::string key; | |
| std::string value; | |
| if (!params_array_reader.PopDictEntry(&entry_reader) || | |
| !entry_reader.PopString(&key) || !entry_reader.PopString(&value)) { | |
| LOG(ERROR) << "failed to read dict entry; using default value"; | |
| return CreateDefaultGetParamsAndEnabledResponse(features); | |
| } | |
| params[key] = value; | |
| } | |
| entry.params = std::move(params); | |
| result[feature_name] = entry; | |
| } | |
| return result; | |
| } | |
| bool PlatformFeatures::CheckFeatureIdentity(const VariationsFeature& feature) { | |
| base::AutoLock auto_lock(lock_); | |
| auto it = feature_identity_tracker_.find(feature.name); | |
| if (it == feature_identity_tracker_.end()) { | |
| // If it's not tracked yet, register it. | |
| feature_identity_tracker_[feature.name] = &feature; | |
| return true; | |
| } | |
| // Compare address of |feature| to the existing tracked entry. | |
| return it->second == &feature; | |
| } | |
| void PlatformFeatures::ListenForRefetchNeeded( | |
| base::RepeatingCallback<void(void)> signal_callback, | |
| base::OnceCallback<void(bool)> attached_callback) { | |
| feature_proxy_->ConnectToSignal( | |
| kFeatureLibInterface, kRefetchSignal, | |
| base::IgnoreArgs<dbus::Signal*>(signal_callback), | |
| base::BindOnce(&PlatformFeatures::OnConnectedCallback, | |
| std::move(attached_callback))); | |
| } | |
| // static | |
| void PlatformFeatures::OnConnectedCallback( | |
| base::OnceCallback<void(bool)> attached_callback, | |
| const std::string& interface, | |
| const std::string& signal, | |
| bool success) { | |
| if (!success) { | |
| LOG(ERROR) << "Failed to connect to " << interface << "." << signal; | |
| } | |
| std::move(attached_callback).Run(success); | |
| } | |
| // static | |
| base::Lock& PlatformFeatures::GetInstanceLock() { | |
| static base::NoDestructor<base::Lock> lock; | |
| return *lock; | |
| } | |
| void PlatformFeatures::SetActiveTrialFileDirectoryForTesting( | |
| const base::FilePath& dir) { | |
| active_trial_file_dir_ = dir; | |
| } | |
| void PlatformFeatures::RecordActiveTrial( | |
| const featured::FeatureOverride& trial) { | |
| std::string escaped_trial_name = | |
| base::EscapeAllExceptUnreserved(trial.trial_name()); | |
| std::string escaped_group_name = | |
| base::EscapeAllExceptUnreserved(trial.group_name()); | |
| const base::FilePath filename(base::StrCat( | |
| {escaped_trial_name, kTrialGroupSeparator, escaped_group_name})); | |
| const base::FilePath full_path(active_trial_file_dir_.Append(filename)); | |
| // Create file if it does not exist. | |
| // Note: This file gets automatically cleaned up on reboot if written to | |
| // kActiveTrialFileDirectory. The study is not treated as once-active, | |
| // always-active, across boots. The study will only be active for the rest of | |
| // the current boot unless activated again in a subsequent boot. | |
| base::File active_trial_file(full_path, | |
| base::File::FLAG_CREATE | base::File::FLAG_READ); | |
| // Not an error if the trial file already exists. | |
| if ((!active_trial_file.IsValid()) && | |
| (active_trial_file.error_details() != | |
| base::File::Error::FILE_ERROR_EXISTS)) { | |
| PLOG(ERROR) << "Failed to create " << full_path | |
| << " for trial: " << trial.trial_name() | |
| << ", group: " << trial.group_name(); | |
| return; | |
| } | |
| } | |
| std::optional<bool> PlatformFeatures::IsEarlyBootFeatureActive( | |
| std::string_view feature_name, std::map<std::string, std::string>* params) { | |
| // TODO(b/319289151): Implement this. | |
| return std::nullopt; | |
| } | |
| } // namespace feature |