blob: cd7ad6c4e02b7a5296b29f13b3567ba448e72a8a [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_ASH_CROSAPI_BROWSER_DATA_BACK_MIGRATOR_H_
#define CHROME_BROWSER_ASH_CROSAPI_BROWSER_DATA_BACK_MIGRATOR_H_
#include "base/files/file_path.h"
#include "base/functional/callback.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "chrome/browser/ash/crosapi/browser_data_migrator_util.h"
#include "chrome/browser/ash/crosapi/browser_util.h"
#include "components/account_id/account_id.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
class PrefService;
namespace ash {
namespace browser_data_back_migrator {
// Temporary directory for back migration.
constexpr char kTmpDir[] = "back_migrator_tmp";
} // namespace browser_data_back_migrator
constexpr char kFinalStatusUMA[] = "Ash.BrowserDataBackMigrator.FinalStatus";
constexpr char kPosixErrnoUMA[] = "Ash.BrowserDataBackMigrator.PosixErrno.";
constexpr char kSuccessfulMigrationTimeUMA[] =
"Ash.BrowserDataBackMigrator.SuccessfulMigrationTime";
// Injects the restart function called from
// `BrowserDataBackMigrator::AttemptRestart()` in RAII manner.
class ScopedBackMigratorRestartAttemptForTesting {
public:
explicit ScopedBackMigratorRestartAttemptForTesting(
base::RepeatingClosure callback);
~ScopedBackMigratorRestartAttemptForTesting();
};
class BrowserDataBackMigrator {
public:
// Represents a result status.
enum class Result {
kSucceeded,
kFailed,
};
using BackMigrationFinishedCallback =
base::OnceCallback<void(BrowserDataBackMigrator::Result)>;
using BackMigrationProgressCallback = base::RepeatingCallback<void(int)>;
explicit BrowserDataBackMigrator(const base::FilePath& ash_profile_dir,
const std::string& user_id_hash,
PrefService* local_state);
BrowserDataBackMigrator(const BrowserDataBackMigrator&) = delete;
BrowserDataBackMigrator& operator=(const BrowserDataBackMigrator&) = delete;
~BrowserDataBackMigrator();
// Calls `chrome::AttemptRestart()` unless
// `ScopedBackMigratorRestartAttemptForTesting` is in scope.
static void AttemptRestart();
// Migrate performs the Lacros -> Ash migration.
// progress_callback is called repeatedly with the current progress.
// finished_callback is called when migration completes successfully or with
// an error.
// Migrate can only be called once.
void Migrate(BackMigrationProgressCallback progress_callback,
BackMigrationFinishedCallback finished_callback);
// IsBackMigrationEnabled determines if the feature is enabled.
// It checks the following in order:
// 1. The kForceBrowserDataBackwardMigration debug flag.
// 2. The LacrosDataBackwardMigrationMode policy.
// 3. The kLacrosProfileBackwardMigration feature flag.
// The policy value is cached at the beginning of the session and not
// updated.
static bool IsBackMigrationEnabled(
crosapi::browser_util::PolicyInitState policy_init_state);
// MaybeRestartToMigrateBack checks if backward migration should be
// triggered. Migration is started by adding extra flags to Chrome using
// session_manager and then restarting.
// Returns true if Chrome needs to restart to trigger backward migration.
// May block to check if the lacros folder is present.
static bool MaybeRestartToMigrateBack(
const AccountId& account_id,
const std::string& user_id_hash,
crosapi::browser_util::PolicyInitState policy_init_state);
private:
FRIEND_TEST_ALL_PREFIXES(BrowserDataBackMigratorTest, PreMigrationCleanUp);
FRIEND_TEST_ALL_PREFIXES(BrowserDataBackMigratorTest,
MergeCommonExtensionsDataFiles);
FRIEND_TEST_ALL_PREFIXES(BrowserDataBackMigratorFilesSetupTest,
MergeCommonIndexedDB);
FRIEND_TEST_ALL_PREFIXES(BrowserDataBackMigratorFilesSetupTest,
MergeLocalStorageLevelDB);
FRIEND_TEST_ALL_PREFIXES(BrowserDataBackMigratorFilesSetupTest,
MergeStateStoreLevelDB);
FRIEND_TEST_ALL_PREFIXES(BrowserDataBackMigratorFilesSetupTest,
DeletesLacrosItemsFromAshDirCorrectly);
FRIEND_TEST_ALL_PREFIXES(BrowserDataBackMigratorFilesSetupTest,
MovesLacrosItemsToAshDirCorrectly);
FRIEND_TEST_ALL_PREFIXES(BrowserDataBackMigratorUMATest, RecordFinalStatus);
FRIEND_TEST_ALL_PREFIXES(BrowserDataBackMigratorUMATest,
RecordPosixErrnoIfAvailable);
FRIEND_TEST_ALL_PREFIXES(BrowserDataBackMigratorUMATest,
RecordMigrationTimeIfSuccessful);
FRIEND_TEST_ALL_PREFIXES(BrowserDataBackMigratorUMATest, TaskStatusToString);
FRIEND_TEST_ALL_PREFIXES(BrowserDataBackMigratorTest,
MergesAshOnlyPreferencesCorrectly);
FRIEND_TEST_ALL_PREFIXES(BrowserDataBackMigratorTest,
MergesDictSplitPreferencesCorrectly);
FRIEND_TEST_ALL_PREFIXES(BrowserDataBackMigratorTest,
MergesListSplitPreferencesCorrectly);
FRIEND_TEST_ALL_PREFIXES(BrowserDataBackMigratorTest,
MergesLacrosPreferencesCorrectly);
// A list of all the possible results of migration, including success and all
// failure types in each step of the migration.
//
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
//
// When adding new cases to this enum, also update the following:
// - `BrowserDataBackMigrator::ToResult()`
// - `BrowserDataBackMigrator::TaskStatusToString()`
// - `BrowserDataBackMigratorUMATest.TaskStatusToString`
// - `Ash.BrowserDataBackMigrator.PosixErrno.{TaskStatus}` histogram
// - `BrowserDataBackMigratorFinalStatus` histogram enum
enum class TaskStatus {
kSucceeded = 0,
kPreMigrationCleanUpDeleteTmpDirFailed = 1,
kMergeSplitItemsCreateTmpDirFailed = 2,
kMergeSplitItemsCopyExtensionsFailed = 3,
kMergeSplitItemsCopyExtensionStorageFailed = 4,
kMergeSplitItemsCreateDirFailed = 5,
kMergeSplitItemsMergeIndexedDBFailed = 6,
kMergeSplitItemsMergePrefsFailed = 7,
kMergeSplitItemsMergeLocalStorageLevelDBFailed = 8,
kMergeSplitItemsMergeStateStoreLevelDBFailed = 9,
kMergeSplitItemsMergeSyncDataFailed = 10,
kDeleteAshItemsDeleteExtensionsFailed = 11,
kDeleteAshItemsDeleteLacrosItemFailed = 12,
kDeleteTmpDirDeleteFailed = 13,
kDeleteLacrosDirDeleteFailed = 14,
kMoveLacrosItemsToAshDirFailed = 15,
kMoveMergedItemsBackToAshMoveFileFailed = 16,
kMoveMergedItemsBackToAshCopyDirectoryFailed = 17,
kMaxValue = kMoveMergedItemsBackToAshCopyDirectoryFailed,
};
enum class MigrationStep {
kStart = 0,
kPreMigrationCleanUp = 1,
kMergeSplitItems = 2,
kDeleteAshItems = 3,
kMoveLacrosItemsToAshDir = 4,
kMoveMergedItemsBackToAsh = 5,
kDeleteLacrosDir = 6,
kDeleteTmpDir = 7,
kMarkMigrationComplete = 8,
kDone = 9,
kMaxValue = kDone,
};
struct TaskResult {
TaskStatus status;
// Value of `errno` set after a task has failed.
absl::optional<int> posix_errno;
};
bool running_ = false;
BackMigrationProgressCallback progress_callback_;
BackMigrationFinishedCallback finished_callback_;
void SetProgress(MigrationStep step);
// Creates `kTmpDir` and deletes its contents if it already exists. Deletes
// ash and lacros `ItemType::kDeletable` items to free up extra space but this
// does not affect `PreMigrationCleanUpResult::success`.
static TaskResult PreMigrationCleanUp(
const base::FilePath& ash_profile_dir,
const base::FilePath& lacros_profile_dir);
// Called as a reply to `PreMigrationCleanUp()`.
void OnPreMigrationCleanUp(TaskResult result);
// Merges items that were split between Ash and Lacros and puts them into
// the temporary directory created in `PreMigrationCleanUp()`.
static TaskResult MergeSplitItems(const base::FilePath& ash_profile_dir);
// Called as a reply to `MergeSplitItems()`.
void OnMergeSplitItems(TaskResult result);
// Deletes Ash items that will be overwritten by either Lacros items or items
// merged in `MergeSplitItems()`. This prevents conflicts during the calls to
// `MoveLacrosItemsToAshDir()` and `MoveMergedItemsBackToAsh()`.
static TaskResult DeleteAshItems(const base::FilePath& ash_profile_dir);
// Called as a reply to `DeleteAshItems()`.
void OnDeleteAshItems(TaskResult result);
// Moves Lacros-only items back into the Ash profile directory.
static TaskResult MoveLacrosItemsToAshDir(
const base::FilePath& ash_profile_dir);
// Called as a reply to `MoveLacrosItemsToAshDir()`.
void OnMoveLacrosItemsToAshDir(TaskResult result);
// Moves the temporary directory into the Ash profile directory.
static TaskResult MoveMergedItemsBackToAsh(
const base::FilePath& ash_profile_dir);
// Called as a reply to `MoveMergedItemsBackToAsh()`.
void OnMoveMergedItemsBackToAsh(TaskResult result);
// Deletes the Lacros profile directory.
static TaskResult DeleteLacrosDir(const base::FilePath& ash_profile_dir);
// Called as a reply to `DeleteLacrosDir()`.
void OnDeleteLacrosDir(TaskResult result);
// Deletes the temporary directory.
static TaskResult DeleteTmpDir(const base::FilePath& ash_profile_dir);
// Called as a reply to `DeleteTmpDir()`.
void OnDeleteTmpDir(TaskResult result);
// Marks backward migration as complete.
TaskResult MarkMigrationComplete();
// Called as a reply to `MarkMigrationComplete()`.
void OnMarkMigrationComplete();
// For `target_dir` copy subdirectories belonging to extensions that are in
// both Chromes from `lacros_profile_dir` to `tmp_user_dir`.
static bool MergeCommonExtensionsDataFiles(
const base::FilePath& ash_profile_dir,
const base::FilePath& lacros_profile_dir,
const base::FilePath& tmp_user_dir,
const std::string& target_dir);
// For `target_dir` delete the subdirectories belonging to extensions from
// `ash_profile_dir` so that there are no conflicts when `tmp_user_dir` is
// moved to `ash_profile_dir`.
static bool RemoveAshCommonExtensionsDataFiles(
const base::FilePath& ash_profile_dir,
const std::string& target_dir);
// Merge IndexedDB objects for extensions that are both in Ash and Lacros.
// If both exists, delete Ash version and move Lacros version to its place.
// If only Ash exists, do not delete it, i.e. do nothing.
// If only Lacros exists, move to the expected Ash location.
// If neither exists, do nothing.
static bool MergeCommonIndexedDB(const base::FilePath& ash_profile_dir,
const base::FilePath& lacros_profile_dir,
const char* extension_id);
// Merge Preferences.
static bool MergePreferences(const base::FilePath& ash_pref_path,
const base::FilePath& lacros_pref_path,
const base::FilePath& tmp_pref_path);
// For Lacros preferences that were neither split nor ash-only,
// simply prefer them over the ones that are currently in Ash.
// Traverse all JSON dotted paths in Lacros preferences using
// depth-first search and merge them into |ash_root_dict|.
static bool MergeLacrosPreferences(base::Value::Dict& ash_root_dict,
std::string& current_path,
const base::Value& current_value,
unsigned int recursion_depth);
// Decides whether preferences for the given `extension_id` should be migrated
// back from Lacros to Ash.
static bool IsLacrosOnlyExtension(const base::StringPiece extension_id);
// Copy the LevelDB database from Lacros to the temporary directory to be used
// as basis for the merge.
static bool CopyLevelDBBase(const base::FilePath& lacros_leveldb_dir,
const base::FilePath& tmp_leveldb_dir);
// Overwrite some parts of the LevelDB database copied from Lacros with keys
// and values from Ash.
static bool MergeLevelDB(
const base::FilePath& ash_db_path,
const base::FilePath& tmp_db_path,
const browser_data_migrator_util::LevelDBType leveldb_type);
// Create the Sync Data LevelDB that will be used bu Ash upon backward
// migration. If only Ash or only Lacros Sync database exists, copy that file
// directly to the temporary directory. If both databases exist, then perform
// a full merge by opening both databases, getting the corresponding sync data
// from each and merging the results into the temporary directory database.
static bool MergeSyncDataLevelDB(const base::FilePath& ash_db_path,
const base::FilePath& lacros_db_path,
const base::FilePath& tmp_db_path);
// Go through all top-level items in the directory. If they are files move
// them directly. If they are directories, call the same function recursively.
static bool MoveFilesToAshDirectory(const base::FilePath& source_dir,
const base::FilePath& dest_dir,
unsigned int recursion_depth);
// IsBackMigrationForceEnabled checks if backward migration has been force
// enabled using the kLacrosProfileBackwardMigration flag.
static bool IsBackMigrationForceEnabled();
// ShouldMigrateBack determines if backward migration should run.
// Called by MaybeRestartToMigrateBack.
// May block to check if the lacros folder is present.
static bool ShouldMigrateBack(
const AccountId& account_id,
const std::string& user_id_hash,
crosapi::browser_util::PolicyInitState policy_init_state);
// RestartToMigrateBack triggers a Chrome restart to start backward migration.
// Called by MaybeRestartToMigrateBack.
static bool RestartToMigrateBack(const AccountId& account_id);
// Transforms `TaskResult` to `Result`, which is then returned to the caller.
static Result ToResult(TaskResult result);
// Records UMA metrics and calls `finished_callback_`. This function gets
// called once regardless of whether the migration succeeded or not.
void InvokeCallback(TaskResult);
// Records the final status of the migration in `kFinalStatusUMA`.
static void RecordFinalStatus(TaskResult result);
// Records Ash.BrowserDataBackMigrator.PosixErrno.{result.status} UMA with the
// value of `result.posix_errno` if the migration failed.
static void RecordPosixErrnoIfAvailable(TaskResult result);
// Records `kSuccessfulMigrationTimeUMA` UMA with the elapsed time since
// starting backward migration. Only recorded if migration was successful.
static void RecordMigrationTimeIfSuccessful(
TaskResult result,
base::TimeTicks migration_start_time);
// Converts `TaskStatus` to string.
static std::string TaskStatusToString(TaskStatus task_status);
// Path to the ash profile directory.
const base::FilePath ash_profile_dir_;
// A hash string of the profile user ID.
const std::string user_id_hash_;
// Local state prefs, not owned.
PrefService* local_state_ = nullptr;
// Used to record how long the migration takes in UMA.
base::TimeTicks migration_start_time_;
base::WeakPtrFactory<BrowserDataBackMigrator> weak_factory_{this};
};
} // namespace ash
#endif // CHROME_BROWSER_ASH_CROSAPI_BROWSER_DATA_BACK_MIGRATOR_H_