| // 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 |