chromium/components/variations/cros_evaluate_seed/evaluate_seed.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/variations/cros_evaluate_seed/evaluate_seed.h"

#include "base/check.h"
#include "base/containers/flat_set.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/callback.h"
#include "base/json/json_file_value_serializer.h"
#include "base/logging.h"
#include "base/metrics/field_trial.h"
#include "base/system/sys_info.h"
#include "build/branding_buildflags.h"
#include "chromeos/ash/components/dbus/featured/featured.pb.h"
#include "chromeos/crosapi/cpp/channel_to_enum.h"
#include "chromeos/crosapi/cpp/crosapi_constants.h"
#include "components/metrics/metrics_service.h"
#include "components/metrics/metrics_state_manager.h"
#include "components/prefs/in_memory_pref_store.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/pref_service_factory.h"
#include "components/variations/cros_evaluate_seed/cros_safe_seed_manager.h"
#include "components/variations/cros_evaluate_seed/cros_variations_field_trial_creator.h"
#include "components/variations/cros_evaluate_seed/early_boot_enabled_state_provider.h"
#include "components/variations/cros_evaluate_seed/early_boot_feature_visitor.h"
#include "components/variations/cros_evaluate_seed/early_boot_safe_seed.h"
#include "components/variations/platform_field_trials.h"
#include "components/variations/proto/study.pb.h"
#include "components/variations/service/variations_service.h"
#include "components/variations/synthetic_trial_registry.h"
#include "components/variations/variations_ids_provider.h"
#include "components/variations/variations_safe_seed_store_local_state.h"
#include "components/variations/variations_seed_store.h"
#include "components/variations/variations_switches.h"
#include "components/version_info/version_info.h"
#include "third_party/protobuf/src/google/protobuf/repeated_field.h"

namespace variations::cros_early_boot::evaluate_seed {

namespace {

constexpr char kDefaultLocalStatePath[] = "/home/chronos/Local State";

bool DetermineTrialState(std::unique_ptr<PrefService> local_state,
                         SafeSeed&& safe_seed,
                         featured::ComputedState* computed_state,
                         GetCrOSVariationsFieldTrialCreator get_creator) {
  // In the null seed case, featured just won't exec() evaluate_seed.
  SeedType seed_type =
      safe_seed.use_safe_seed ? SeedType::kSafeSeed : SeedType::kRegularSeed;
  CrOSSafeSeedManager safe_seed_manager(seed_type);

  std::optional<featured::SeedDetails> safe_seed_details;
  if (seed_type == SeedType::kSafeSeed) {
    safe_seed_details = std::move(safe_seed.seed_data);
  }

  CrosVariationsServiceClient client;

  std::unique_ptr<CrOSVariationsFieldTrialCreator> field_trial_creator =
      std::move(get_creator).Run(local_state.get(), &client, safe_seed_details);

  EarlyBootEnabledStateProvider enabled_state_provider;

  std::unique_ptr<metrics::MetricsStateManager> metrics_state_manager =
      metrics::MetricsStateManager::Create(
          local_state.get(), &enabled_state_provider,
          /*backup_registry_key=*/std::wstring(),
          // Don't use a separate directory for safe mode prefs.
          /*user_data_dir=*/base::FilePath(),
          metrics::StartupVisibility::kForeground);
  metrics_state_manager->InstantiateFieldTrialList();

  auto feature_list = std::make_unique<base::FeatureList>();

  variations::VariationsIdsProvider::Create(
      variations::VariationsIdsProvider::Mode::kDontSendSignedInVariations);

  variations::PlatformFieldTrials platform_field_trials;
  variations::SyntheticTrialRegistry synthetic_trial_registry;

  // Despite documentation, SetUpFieldTrials returns false if it didn't use the
  // seed (e.g. if it used fieldtrial_testing_config), *not* just on failures.
  bool used_seed = field_trial_creator->SetUpFieldTrials(
      // TODO(http://b/297251107): implement overrides via chrome://flags.
      /*variation_ids=*/std::vector<std::string>(),
      base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
          variations::switches::kForceVariationIds),
      /*extra_overrides=*/
      std::vector<base::FeatureList::FeatureOverrideInfo>(),
      std::move(feature_list), metrics_state_manager.get(),
      &synthetic_trial_registry, &platform_field_trials, &safe_seed_manager,
      /*add_entropy_source_to_variations_ids=*/false);

  if (used_seed) {
    if (seed_type == SeedType::kRegularSeed) {
      // We use the safe seed manager here because the
      // CrOSVariationsFieldTrialCreator (in the parent class's
      // CreateTrialsFromSeed) calls CrOSSafeSeedManager::SetActiveSeedState
      // when it marks a seed as active (NOT when it marks a seed as safe). This
      // is just to retrieve that active state, and doesn't necessarily indicate
      // that the seed is safe yet (we wait for ash to start to determine that).
      std::optional<featured::SeedDetails> details =
          safe_seed_manager.GetUsedSeed();
      if (details.has_value()) {
        computed_state->mutable_used_seed()->CopyFrom(details.value());
      } else {
        LOG(ERROR) << "Couldn't retrieve seed details; proceeding without them";
      }
    } else {
      // In this case, CrOSSafeSeedManager::SetActiveSeedState is never called,
      // so use the seed we requested to be used.
      CHECK_EQ(seed_type, SeedType::kSafeSeed);
      // Set above, at start of function.
      CHECK(safe_seed_details.has_value());
      computed_state->mutable_used_seed()->CopyFrom(safe_seed_details.value());
    }
  }

  // TODO(b/297870545): serialize correctly.
  // ideally, we want to add:
  // * (Early-boot?) trials that do not have features associated (e.g. for
  //   uniformity trials)
  // We also do not want to mark the trial as active yet, but given that
  // evaluate_seed will not report anything to UMA that isn't urgent.
  EarlyBootFeatureVisitor feature_visitor;
  base::FeatureList::VisitFeaturesAndParams(feature_visitor);
  google::protobuf::RepeatedPtrField<featured::FeatureOverride> overrides =
      feature_visitor.release_overrides();
  computed_state->mutable_overrides()->Assign(overrides.begin(),
                                              overrides.end());

  return true;
}
}  // namespace

// CreateLocalState creates an instance of a PrefService based on the json
// contents of |local_state_path|.
std::unique_ptr<PrefService> CreateLocalState(
    const base::FilePath& local_state_path) {
  auto pref_registry = base::MakeRefCounted<PrefRegistrySimple>();

  metrics::MetricsService::RegisterPrefs(pref_registry.get());
  ::variations::VariationsService::RegisterPrefs(pref_registry.get());

  int error_code = 0;
  std::string error_msg;
  JSONFileValueDeserializer deserializer(local_state_path);
  std::unique_ptr<base::Value> all_prefs_val =
      deserializer.Deserialize(&error_code, &error_msg);
  if (all_prefs_val == nullptr) {
    LOG(ERROR) << "Error deserializing local state: (code " << error_code
               << ") " << error_msg;
    return nullptr;
  }

  if (!all_prefs_val->is_dict()) {
    LOG(ERROR) << "Unexpected type: want dict, got " << all_prefs_val->type();
    return nullptr;
  }
  base::Value::Dict prefs_dict = std::move(*all_prefs_val).TakeDict();

  PrefServiceFactory pref_service_factory;
  auto local_state_pref_store = base::MakeRefCounted<InMemoryPrefStore>();

  for (auto [key, val] : prefs_dict) {
    local_state_pref_store->SetValue(
        key, std::move(val), WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);
  }

  pref_service_factory.set_user_prefs(local_state_pref_store);

  return pref_service_factory.Create(pref_registry);
}

base::Version CrosVariationsServiceClient::GetVersionForSimulation() {
  // TODO(mutexlox): Get the version that will be used on restart instead of
  // the current version IF this is necessary. (We may not need simulations for
  // early-boot experiments.)
  // See ChromeVariationsServiceClient::GetVersionForSimulation.
  return version_info::GetVersion();
}

scoped_refptr<network::SharedURLLoaderFactory>
CrosVariationsServiceClient::GetURLLoaderFactory() {
  // Do not load any data on CrOS early boot. This function is only called to
  // fetch a new seed, and we should not fetch new seeds in evaluate_seed.
  return nullptr;
}

network_time::NetworkTimeTracker*
CrosVariationsServiceClient::GetNetworkTimeTracker() {
  // Do not load any data on CrOS early boot; evaluate_seed should not load new
  // seeds.
  return nullptr;
}

bool CrosVariationsServiceClient::OverridesRestrictParameter(
    std::string* parameter) {
  // TODO(b/263975722): Implement.
  return false;
}

// Get the active channel, if applicable.
version_info::Channel CrosVariationsServiceClient::GetChannel() {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
  std::string channel;
  if (base::SysInfo::GetLsbReleaseValue(crosapi::kChromeOSReleaseTrack,
                                        &channel)) {
    return crosapi::ChannelToEnum(channel);
  }
#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
  return version_info::Channel::UNKNOWN;
}

bool CrosVariationsServiceClient::IsEnterprise() {
  return base::CommandLine::ForCurrentProcess()->HasSwitch(
      kEnterpriseEnrolledSwitch);
}

std::optional<SafeSeed> GetSafeSeedData(FILE* stream) {
  featured::SeedDetails safe_seed;
  if (base::CommandLine::ForCurrentProcess()->HasSwitch(kSafeSeedSwitch)) {
    // Read safe seed from |stream|.
    std::string safe_seed_data;
    if (!base::ReadStreamToString(stream, &safe_seed_data)) {
      PLOG(ERROR) << "Failed to read from stream:";
      return std::nullopt;
    }
    // Parse safe seed.
    if (!safe_seed.ParseFromString(safe_seed_data)) {
      LOG(ERROR) << "Failed to parse proto from input";
      return std::nullopt;
    }
    return SafeSeed{true, std::move(safe_seed)};
  }
  return SafeSeed{false, std::move(safe_seed)};
}

std::unique_ptr<CrOSVariationsFieldTrialCreator> GetFieldTrialCreator(
    PrefService* local_state,
    CrosVariationsServiceClient* client,
    const std::optional<featured::SeedDetails>& safe_seed_details) {
  std::unique_ptr<VariationsSafeSeedStore> safe_seed;
  if (safe_seed_details.has_value()) {
    safe_seed = std::make_unique<EarlyBootSafeSeed>(safe_seed_details.value());
  } else {
    safe_seed =
        std::make_unique<VariationsSafeSeedStoreLocalState>(local_state);
  }
  auto seed_store =
      std::make_unique<VariationsSeedStore>(local_state, std::move(safe_seed));

  return std::make_unique<CrOSVariationsFieldTrialCreator>(
      client, std::move(seed_store));
}

int EvaluateSeedMain(FILE* in_stream, FILE* out_stream) {
  return EvaluateSeedMain(in_stream, out_stream,
                          base::BindOnce(&GetFieldTrialCreator));
}

int EvaluateSeedMain(FILE* in_stream,
                     FILE* out_stream,
                     GetCrOSVariationsFieldTrialCreator get_creator) {
  std::optional<SafeSeed> safe_seed = GetSafeSeedData(in_stream);
  if (!safe_seed.has_value()) {
    LOG(ERROR) << "Failed to read seed from stdin";
    return EXIT_FAILURE;
  }

  // TODO(b/303882431): Set this up properly without races.
  base::FilePath local_state_path =
      base::CommandLine::ForCurrentProcess()->GetSwitchValuePath(
          kLocalStatePathSwitch);
  if (local_state_path.empty()) {
    local_state_path = base::FilePath(kDefaultLocalStatePath);
  }

  std::unique_ptr<PrefService> local_state = CreateLocalState(local_state_path);
  if (!local_state) {
    LOG(ERROR) << "Failed to create local_state";
    return EXIT_FAILURE;
  }

  featured::ComputedState computed_state;
  if (!DetermineTrialState(std::move(local_state), std::move(safe_seed.value()),
                           &computed_state, std::move(get_creator))) {
    LOG(ERROR) << "Failed to determine trial state; will use defaults";
    return EXIT_FAILURE;
  }

  base::File out = base::FILEToFile(out_stream);
  if (!out.IsValid()) {
    LOG(ERROR) << "Failed to open output";
    return EXIT_FAILURE;
  }

  std::string out_str;
  if (!computed_state.SerializeToString(&out_str)) {
    LOG(ERROR) << "Failed to serialize state";
    return EXIT_FAILURE;
  }

  if (!out.WriteAtCurrentPosAndCheck(base::as_byte_span(out_str))) {
    LOG(ERROR) << "Failed to write to output";
    return EXIT_FAILURE;
  }

  return EXIT_SUCCESS;
}

}  // namespace variations::cros_early_boot::evaluate_seed