chromium/chrome/browser/ash/crosapi/browser_data_migrator.cc

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

#include "chrome/browser/ash/crosapi/browser_data_migrator.h"

#include <string>
#include <utility>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "base/command_line.h"
#include "base/debug/dump_without_crashing.h"
#include "base/feature_list.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/scoped_refptr.h"
#include "base/path_service.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "base/version.h"
#include "chrome/browser/ash/crosapi/browser_util.h"
#include "chrome/browser/ash/crosapi/move_migrator.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/lifetime/application_lifetime.h"
#include "chrome/common/chrome_paths.h"
#include "chromeos/ash/components/cryptohome/cryptohome_parameters.h"
#include "chromeos/ash/components/dbus/session_manager/session_manager_client.h"
#include "chromeos/ash/components/standalone_browser/migration_progress_tracker.h"
#include "chromeos/ash/components/standalone_browser/migrator_util.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/user_manager/user_manager.h"
#include "components/version_info/version_info.h"

namespace ash {
namespace {
// Flag values for `switches::kForceBrowserDataMigrationForTesting`.
const char kBrowserDataMigrationForceSkip[] = "force-skip";
const char kBrowserDataMigrationForceMigration[] = "force-migration";

base::RepeatingClosure* g_attempt_restart = nullptr;

// Checks if the disk space is enough to run profile migration.
// Returns the bytes required to be freed. Specifically, on success
// returns 0.
uint64_t DiskCheck(const base::FilePath& profile_data_dir) {
  using browser_data_migrator_util::GetTargetItems;
  using browser_data_migrator_util::ItemType;
  using browser_data_migrator_util::TargetItems;
  TargetItems deletable_items =
      GetTargetItems(profile_data_dir, ItemType::kDeletable);

  const int64_t required_size =
      browser_data_migrator_util::EstimatedExtraBytesCreated(profile_data_dir) -
      deletable_items.total_size;

  return browser_data_migrator_util::ExtraBytesRequiredToBeFreed(
      required_size, profile_data_dir);
}

}  // namespace

ScopedRestartAttemptForTesting::ScopedRestartAttemptForTesting(
    base::RepeatingClosure callback) {
  DCHECK(!g_attempt_restart);
  g_attempt_restart = new base::RepeatingClosure(std::move(callback));
}

ScopedRestartAttemptForTesting::~ScopedRestartAttemptForTesting() {
  DCHECK(g_attempt_restart);
  delete g_attempt_restart;
  g_attempt_restart = nullptr;
}

bool BrowserDataMigratorImpl::MaybeForceResumeMoveMigration(
    PrefService* local_state,
    const AccountId& account_id,
    const std::string& user_id_hash,
    ash::standalone_browser::migrator_util::PolicyInitState policy_init_state) {
  if (!MoveMigrator::ResumeRequired(local_state, user_id_hash)) {
    return false;
  }

  LOG(WARNING) << "Calling RestartToMigrate() to resume move migration.";
  return RestartToMigrate(account_id, user_id_hash, local_state,
                          policy_init_state);
}

// static
void BrowserDataMigratorImpl::AttemptRestart() {
  if (g_attempt_restart) {
    g_attempt_restart->Run();
    return;
  }

  chrome::AttemptRestart();
}

// static
bool BrowserDataMigratorImpl::MaybeRestartToMigrate(
    const AccountId& account_id,
    const std::string& user_id_hash,
    ash::standalone_browser::migrator_util::PolicyInitState policy_init_state) {
  if (!MaybeRestartToMigrateInternal(account_id, user_id_hash,
                                     policy_init_state)) {
    return false;
  }
  return RestartToMigrate(account_id, user_id_hash,
                          user_manager::UserManager::Get()->GetLocalState(),
                          policy_init_state);
}

void BrowserDataMigratorImpl::MaybeRestartToMigrateWithDiskCheck(
    const AccountId& account_id,
    const std::string& user_id_hash,
    base::OnceCallback<void(bool, const std::optional<uint64_t>&)> callback) {
  if (!MaybeRestartToMigrateInternal(account_id, user_id_hash,
                                     ash::standalone_browser::migrator_util::
                                         PolicyInitState::kAfterInit)) {
    std::move(callback).Run(false, std::nullopt);
    return;
  }

  base::FilePath user_data_dir;
  if (!base::PathService::Get(chrome::DIR_USER_DATA, &user_data_dir)) {
    LOG(DFATAL) << "Could not get the original user data dir path.";
    std::move(callback).Run(false, std::nullopt);
    return;
  }

  const base::FilePath profile_data_dir =
      user_data_dir.Append(ProfileHelper::GetUserProfileDir(user_id_hash));
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE,
      {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
       base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
      base::BindOnce(&DiskCheck, profile_data_dir),
      base::BindOnce(&BrowserDataMigratorImpl::
                         MaybeRestartToMigrateWithDiskCheckAfterDiskCheck,
                     account_id, user_id_hash, std::move(callback)));
}

void BrowserDataMigratorImpl::MaybeRestartToMigrateWithDiskCheckAfterDiskCheck(
    const AccountId& account_id,
    const std::string& user_id_hash,
    base::OnceCallback<void(bool, const std::optional<uint64_t>&)> callback,
    uint64_t required_size) {
  if (required_size > 0) {
    LOG(ERROR) << "Failed due to out of disk: " << required_size;
    std::move(callback).Run(false, required_size);
    return;
  }

  bool result = RestartToMigrate(
      account_id, user_id_hash,
      user_manager::UserManager::Get()->GetLocalState(),
      ash::standalone_browser::migrator_util::PolicyInitState::kAfterInit);
  std::move(callback).Run(result, std::nullopt);
}

bool BrowserDataMigratorImpl::MaybeRestartToMigrateInternal(
    const AccountId& account_id,
    const std::string& user_id_hash,
    ash::standalone_browser::migrator_util::PolicyInitState policy_init_state) {
  auto* user_manager = user_manager::UserManager::Get();
  auto* local_state = user_manager->GetLocalState();

  // If `MigrationStep` is not `kCheckStep`, `MaybeRestartToMigrate()` has
  // already moved on to later steps. Namely either in the middle of migration
  // or migration has already run.
  MigrationStep step = GetMigrationStep(local_state);
  if (step != MigrationStep::kCheckStep) {
    switch (step) {
      case MigrationStep::kRestartCalled:
        LOG(ERROR)
            << "RestartToMigrate() was called but Migrate() was not. "
               "This indicates that either "
               "SessionManagerClient::BlockingRequestBrowserDataMigration() "
               "failed or ash crashed before reaching Migrate(). Check "
               "the previous chrome log and the one before.";
        break;
      case MigrationStep::kStarted:
        LOG(ERROR) << "Migrate() was called but "
                      "MigrateInternalFinishedUIThread() was not indicating "
                      "that ash might have crashed during the migration.";
        break;
      case MigrationStep::kEnded:
      default:
        // TODO(crbug.com/40207942): Once `BrowserDataMigrator` stabilises,
        // remove this log message or reduce to VLOG(1).
        if (ash::standalone_browser::migrator_util::
                IsProfileMigrationCompletedForUser(local_state, user_id_hash,
                                                   true /* print_mode */)) {
          LOG(WARNING) << "Migration was attempted and successfully completed.";
        } else {
          LOG(WARNING) << "Migration was attempted but failed or was skipped.";
        }
        break;
    }

    return false;
  }

  // Check if the switch for testing is present.
  const std::string force_migration_switch =
      base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
          switches::kForceBrowserDataMigrationForTesting);
  if (force_migration_switch == kBrowserDataMigrationForceSkip) {
    return false;
  }
  if (force_migration_switch == kBrowserDataMigrationForceMigration) {
    LOG(WARNING) << "`kBrowserDataMigrationForceMigration` switch is present.";
    return true;
  }

  const user_manager::User* user =
      user_manager::UserManager::Get()->FindUser(account_id);
  // Check if user exists i.e. not a guest session.
  if (!user) {
    return false;
  }

  // Migration should not run for secondary users.
  const auto* primary_user = user_manager::UserManager::Get()->GetPrimaryUser();
  // `MaybeRestartToMigrateInternal()` either gets called before profile
  // initialization or after profile initialization. In case of the former, its
  // called from `PreProfileInit()` and this is only called for the primary
  // profile so we can assume that the user is the primary user if `primary_user
  // == nullptr`. If primary_user is not null then we check if `user !=
  // primary_user`.
  if (primary_user && (user != primary_user)) {
    LOG(WARNING) << "Skip migration for secondary users.";
    return false;
  }

  // Check if profile migration is enabled. If not immediately return.
  if (!crosapi::browser_util::IsProfileMigrationEnabled(user,
                                                        policy_init_state)) {
    if (crosapi::browser_util::IsLacrosEnabledForMigration(user,
                                                           policy_init_state) ||
        base::CommandLine::ForCurrentProcess()->HasSwitch(
            switches::kSafeMode)) {
      // Skip clearing prefs if Lacros is enabled or Lacros is disabled due to
      // safe mode. Profile migration can be disabled even if Lacros is enabled
      // by enabling LacrosProfileMigrationForceOff flag. There's another case
      // where Lacros is disabled due to "safe mode" being enabled after Ash
      // crashes. By not clearing prefs in safe mode, we avoid the following
      // scenario: Ash experiences a crash loop due to some experimental flag ->
      // experimental flags get dropped including ones to enable Lacros ->
      // Lacros is disabled and migration completion flags gets cleared -> on
      // next login migration is run and wipes existing user data.
      LOG(WARNING)
          << "Profile migration is disabled but either Lacros is enabled or "
             "safe mode is enabled so skipping clearing prefs.";
      return false;
    }

    // TODO(crbug.com/40207942): Once `BrowserDataMigrator` stabilises, remove
    // this log message.
    LOG(WARNING)
        << "Lacros is disabled. Call ClearMigrationAttemptCountForUser() so "
           "that the migration can be attempted again once migration is "
           "enabled again.";

    // If Lacros is disabled clear the retry count and
    // `kProfileMigrationCompletedForUserPref` so that users may retry profile
    // migration when re-enabling Lacros.
    ash::standalone_browser::migrator_util::ClearMigrationAttemptCountForUser(
        local_state, user_id_hash);
    ash::standalone_browser::migrator_util::
        ClearProfileMigrationCompletedForUser(local_state, user_id_hash);
    MoveMigrator::ClearResumeStepForUser(local_state, user_id_hash);
    MoveMigrator::ClearResumeAttemptCountForUser(local_state, user_id_hash);
    return false;
  }

  if (ash::standalone_browser::migrator_util::
          IsMigrationAttemptLimitReachedForUser(local_state, user_id_hash)) {
    LOG(ERROR) << "Skipping profile migration since maximum migration "
                  "attempt count has been reached.";
    return false;
  }

  if (ash::standalone_browser::migrator_util::
          IsProfileMigrationCompletedForUser(local_state, user_id_hash,
                                             true /* print_mode */)) {
    LOG(WARNING) << "Profile migration is already completed at version "
                 << ash::standalone_browser::migrator_util::GetDataVer(
                        local_state, user_id_hash)
                        .GetString();

    return false;
  }

  return true;
}

// static
bool BrowserDataMigratorImpl::RestartToMigrate(
    const AccountId& account_id,
    const std::string& user_id_hash,
    PrefService* local_state,
    ash::standalone_browser::migrator_util::PolicyInitState policy_init_state) {
  SetMigrationStep(local_state, MigrationStep::kRestartCalled);

  ash::standalone_browser::migrator_util::UpdateMigrationAttemptCountForUser(
      local_state, user_id_hash);

  ash::standalone_browser::migrator_util::ClearProfileMigrationCompletedForUser(
      local_state, user_id_hash);
  crosapi::browser_util::ClearProfileMigrationCompletionTimeForUser(
      local_state, user_id_hash);

  local_state->CommitPendingWrite();

  const user_manager::User* user =
      user_manager::UserManager::Get()->FindUser(account_id);
  // `user` should exist by the time `RestartToMigrate()` is called.
  CHECK(user) << "User could not be found for " << account_id.GetUserEmail()
              << " but RestartToMigrate() was called.";

  // TODO(crbug.com/40207942): Once `BrowserDataMigrator` stabilises, remove
  // this log message.
  LOG(WARNING) << "Making a dbus method call to session_manager";
  bool success =
      SessionManagerClient::Get()->BlockingRequestBrowserDataMigration(
          cryptohome::CreateAccountIdentifierFromAccountId(account_id),
          browser_data_migrator_util::kMoveSwitchValue);

  // TODO(crbug.com/40799062): Add an UMA.
  if (!success) {
    LOG(ERROR) << "SessionManagerClient::BlockingRequestBrowserDataMigration() "
                  "failed.";
    return false;
  }

  AttemptRestart();
  return true;
}

BrowserDataMigratorImpl::BrowserDataMigratorImpl(
    const base::FilePath& original_profile_dir,
    const std::string& user_id_hash,
    const standalone_browser::ProgressCallback& progress_callback,
    PrefService* local_state)
    : original_profile_dir_(original_profile_dir),
      user_id_hash_(user_id_hash),
      progress_tracker_(
          std::make_unique<standalone_browser::MigrationProgressTrackerImpl>(
              progress_callback)),
      cancel_flag_(
          base::MakeRefCounted<browser_data_migrator_util::CancelFlag>()),
      local_state_(local_state) {
  DCHECK(local_state_);
}

BrowserDataMigratorImpl::~BrowserDataMigratorImpl() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}

void BrowserDataMigratorImpl::Migrate(MigrateCallback callback) {
  DCHECK(local_state_);
  DCHECK(completion_callback_.is_null());
  completion_callback_ = std::move(callback);
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // TODO(crbug.com/40169227): Once BrowserDataMigrator stabilises, reduce the
  // log level to VLOG(1).
  LOG(WARNING) << "BrowserDataMigratorImpl::Migrate() is called.";

  DCHECK(GetMigrationStep(local_state_) == MigrationStep::kRestartCalled);
  SetMigrationStep(local_state_, MigrationStep::kStarted);

  LOG(WARNING) << "Initializing MoveMigrator.";
  migrator_delegate_ = std::make_unique<MoveMigrator>(
      original_profile_dir_, user_id_hash_, std::move(progress_tracker_),
      cancel_flag_, local_state_,
      base::BindOnce(&BrowserDataMigratorImpl::MigrateInternalFinishedUIThread,
                     weak_factory_.GetWeakPtr()));

  migrator_delegate_->Migrate();
}

void BrowserDataMigratorImpl::Cancel() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  cancel_flag_->Set();
}

void BrowserDataMigratorImpl::MigrateInternalFinishedUIThread(

    MigrationResult result) {
  DCHECK(local_state_);
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(GetMigrationStep(local_state_) == MigrationStep::kStarted);
  SetMigrationStep(local_state_, MigrationStep::kEnded);

  // TODO(crbug.com/40169227): Once BrowserDataMigrator stabilises, reduce the
  // log level to VLOG(1).
  LOG(WARNING)
      << "MigrateInternalFinishedUIThread() called with results data wipe = "
      << static_cast<int>(result.data_wipe_result) << " and migration "
      << static_cast<int>(result.data_migration_result.kind);

  if (result.data_wipe_result != DataWipeResult::kFailed) {
    // kSkipped means that the directory did not exist so record the current
    // version as the data version.
    ash::standalone_browser::migrator_util::RecordDataVer(
        local_state_, user_id_hash_, version_info::GetVersion());
  }

  switch (result.data_migration_result.kind) {
    case ResultKind::kSucceeded:
      ash::standalone_browser::migrator_util::
          SetProfileMigrationCompletedForUser(
              local_state_, user_id_hash_,
              ash::standalone_browser::migrator_util::MigrationMode::kMove);

      // Profile migration is marked as completed both when the migration is
      // performed (here) and for a new user without actually performing data
      // migration (`ProfileImpl::OnLocaleReady`). The timestamp of completed
      // migration is only recorded when the migration is actually performed.
      crosapi::browser_util::SetProfileMigrationCompletionTimeForUser(
          local_state_, user_id_hash_);

      ash::standalone_browser::migrator_util::ClearMigrationAttemptCountForUser(
          local_state_, user_id_hash_);
      break;
    case ResultKind::kFailed:
      LOG(ERROR) << "Migration failed for some reason. Look at logs from "
                    "move_migrator.cc for details.";
      // This should not happen often. Send a crash report for debugging.
      base::debug::DumpWithoutCrashing();
      break;
    case ResultKind::kCancelled:
      LOG(WARNING) << "Migration was cancelled by the user.";
      break;
  }

  local_state_->CommitPendingWrite();

  std::move(completion_callback_).Run(result.data_migration_result);
}

// static
void BrowserDataMigratorImpl::RegisterLocalStatePrefs(
    PrefRegistrySimple* registry) {
  registry->RegisterIntegerPref(kMigrationStep,
                                static_cast<int>(MigrationStep::kCheckStep));
  // Register prefs for move migration.
  MoveMigrator::RegisterLocalStatePrefs(registry);
}

// static
void BrowserDataMigratorImpl::SetMigrationStep(
    PrefService* local_state,
    BrowserDataMigratorImpl::MigrationStep step) {
  local_state->SetInteger(kMigrationStep, static_cast<int>(step));
}

// static
void BrowserDataMigratorImpl::ClearMigrationStep(PrefService* local_state) {
  local_state->ClearPref(kMigrationStep);
}

// static
bool BrowserDataMigratorImpl::IsFirstLaunchAfterMigration(
    const PrefService* local_state) {
  return GetMigrationStep(local_state) == MigrationStep::kEnded;
}

// static
void BrowserDataMigratorImpl::SetFirstLaunchAfterMigrationForTesting(
    PrefService* local_state) {
  SetMigrationStep(local_state, MigrationStep::kEnded);
}

// static
BrowserDataMigratorImpl::MigrationStep
BrowserDataMigratorImpl::GetMigrationStep(const PrefService* local_state) {
  return static_cast<MigrationStep>(local_state->GetInteger(kMigrationStep));
}

}  // namespace ash