// Copyright 2020 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_loader.h"
#include <utility>
#include "ash/constants/ash_switches.h"
#include "base/barrier_callback.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/ranges/algorithm.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/values.h"
#include "chrome/browser/ash/crosapi/lacros_selection_loader.h"
#include "chrome/browser/ash/crosapi/lacros_selection_loader_factory.h"
#include "chrome/browser/ash/crosapi/rootfs_lacros_loader.h"
#include "chrome/browser/ash/crosapi/stateful_lacros_loader.h"
#include "chrome/browser/browser_process.h"
#include "chromeos/ash/components/cryptohome/system_salt_getter.h"
#include "components/component_updater/ash/component_manager_ash.h"
namespace crosapi {
namespace {
// There are 2 lacros selections, rootfs lacros and stateful lacros.
constexpr size_t kLacrosSelectionTypes = 2;
class LacrosSelectionLoaderFactoryImpl : public LacrosSelectionLoaderFactory {
public:
explicit LacrosSelectionLoaderFactoryImpl(
scoped_refptr<component_updater::ComponentManagerAsh> manager)
: component_manager_(manager) {}
LacrosSelectionLoaderFactoryImpl(const LacrosSelectionLoaderFactoryImpl&) =
delete;
LacrosSelectionLoaderFactoryImpl& operator=(
const LacrosSelectionLoaderFactoryImpl&) = delete;
~LacrosSelectionLoaderFactoryImpl() override = default;
std::unique_ptr<LacrosSelectionLoader> CreateRootfsLacrosLoader() override {
return std::make_unique<RootfsLacrosLoader>();
}
std::unique_ptr<LacrosSelectionLoader> CreateStatefulLacrosLoader() override {
return std::make_unique<StatefulLacrosLoader>(component_manager_);
}
private:
scoped_refptr<component_updater::ComponentManagerAsh> component_manager_;
};
bool IsUnloading(LacrosSelectionLoader* loader) {
return loader && loader->IsUnloading();
}
} // namespace
BrowserLoader::BrowserLoader(
scoped_refptr<component_updater::ComponentManagerAsh> manager)
: factory_(std::make_unique<LacrosSelectionLoaderFactoryImpl>(manager)) {}
BrowserLoader::BrowserLoader(
std::unique_ptr<LacrosSelectionLoaderFactory> factory)
: factory_(std::move(factory)) {}
BrowserLoader::~BrowserLoader() = default;
BrowserLoader::LacrosSelectionVersion::LacrosSelectionVersion(
LacrosSelection selection,
base::Version version)
: selection(selection), version(std::move(version)) {
CHECK_NE(selection, LacrosSelection::kDeployedLocally);
}
// static.
bool BrowserLoader::WillLoadStatefulComponentBuilds() {
// If the lacros chrome path is specified BrowserLoader will always attempt to
// load lacros from this path and component manager builds are ignored.
const base::FilePath lacros_chrome_path =
base::CommandLine::ForCurrentProcess()->GetSwitchValuePath(
ash::switches::kLacrosChromePath);
if (!lacros_chrome_path.empty())
return false;
// If the lacros selection is forced by the user or by policy to rootfs it
// will always be loaded and stateful component manager builds are ignored.
std::optional<ash::standalone_browser::LacrosSelection> lacros_selection =
ash::standalone_browser::DetermineLacrosSelection();
if (lacros_selection == ash::standalone_browser::LacrosSelection::kRootfs) {
return false;
}
return true;
}
void BrowserLoader::SelectRootfsLacros(LoadCompletionCallback callback,
LacrosSelectionSource source) {
CHECK(rootfs_lacros_loader_);
LOG(WARNING) << "rootfs lacros is selected by " << source;
rootfs_lacros_loader_->Load(
base::BindOnce(&BrowserLoader::OnLoadComplete, weak_factory_.GetWeakPtr(),
std::move(callback), LacrosSelection::kRootfs),
source == LacrosSelectionSource::kForced);
}
void BrowserLoader::SelectStatefulLacros(LoadCompletionCallback callback,
LacrosSelectionSource source) {
CHECK(stateful_lacros_loader_);
LOG(WARNING) << "stateful lacros is selected by " << source;
stateful_lacros_loader_->Load(
base::BindOnce(&BrowserLoader::OnLoadComplete, weak_factory_.GetWeakPtr(),
std::move(callback), LacrosSelection::kStateful),
source == LacrosSelectionSource::kForced);
// Unmount the rootfs lacros-chrome when using stateful lacros-chrome.
// This will keep stateful lacros-chrome only mounted and not hold the rootfs
// lacros-chrome mount until an `Unload`.
if (rootfs_lacros_loader_) {
rootfs_lacros_loader_->Unload(
base::BindOnce(&BrowserLoader::OnUnloadCompleted,
weak_factory_.GetWeakPtr(), LacrosSelection::kRootfs));
}
}
void BrowserLoader::Load(LoadCompletionCallback callback) {
// Load should NOT be called after Unload is requested to BrowserLoader.
CHECK(!is_unload_requested_);
// If either of rootfs or stateful lacros loader is still unloading, wait
// until the unload completion.
if (IsUnloading(rootfs_lacros_loader_.get()) ||
IsUnloading(stateful_lacros_loader_.get())) {
LOG(WARNING) << "Wait load until unload completes";
callback_on_unload_completion_ =
base::BindOnce(&BrowserLoader::LoadNow, weak_factory_.GetWeakPtr(),
std::move(callback));
return;
}
LoadNow(std::move(callback));
}
void BrowserLoader::LoadNow(LoadCompletionCallback callback) {
// Reset lacros selection loaders since it may be already initialized one if
// this is reloading.
// TODO(elkurin): We should call Unload before reloading if these loaders
// exist, then we can remove `reset` here.
rootfs_lacros_loader_.reset();
stateful_lacros_loader_.reset();
lacros_start_load_time_ = base::TimeTicks::Now();
// TODO(crbug.com/40689435): Remove non-error logging from this class.
LOG(WARNING) << "Starting lacros component load.";
// If the user has specified a path for the lacros-chrome binary, use that
// rather than component manager.
base::FilePath lacros_chrome_path =
base::CommandLine::ForCurrentProcess()->GetSwitchValuePath(
ash::switches::kLacrosChromePath);
if (!lacros_chrome_path.empty()) {
OnLoadComplete(std::move(callback), LacrosSelection::kDeployedLocally,
base::Version(), lacros_chrome_path);
return;
}
// If the LacrosSelection policy or the user have specified to force using
// stateful or rootfs lacros-chrome binary, force the selection. Otherwise,
// load the newest available binary.
if (std::optional<ash::standalone_browser::LacrosSelection> lacros_selection =
ash::standalone_browser::DetermineLacrosSelection()) {
// TODO(crbug.com/40213424): We should check the version compatibility here,
// too.
switch (lacros_selection.value()) {
case ash::standalone_browser::LacrosSelection::kRootfs:
rootfs_lacros_loader_ = factory_->CreateRootfsLacrosLoader();
SelectRootfsLacros(std::move(callback), LacrosSelectionSource::kForced);
return;
case ash::standalone_browser::LacrosSelection::kStateful:
stateful_lacros_loader_ = factory_->CreateStatefulLacrosLoader();
SelectStatefulLacros(std::move(callback),
LacrosSelectionSource::kForced);
return;
case ash::standalone_browser::LacrosSelection::kDeployedLocally:
NOTREACHED_IN_MIGRATION();
std::move(callback).Run(base::FilePath(),
LacrosSelection::kDeployedLocally,
base::Version());
return;
}
}
rootfs_lacros_loader_ = factory_->CreateRootfsLacrosLoader();
stateful_lacros_loader_ = factory_->CreateStatefulLacrosLoader();
// Proceed to load/mount the stateful lacros-chrome binary.
// In the case that the stateful lacros-chrome binary wasn't installed, this
// might take some time.
auto barrier_callback = base::BarrierCallback<LacrosSelectionVersion>(
kLacrosSelectionTypes,
base::BindOnce(&BrowserLoader::OnLoadVersions, weak_factory_.GetWeakPtr(),
std::move(callback)));
rootfs_lacros_loader_->GetVersion(
base::BindOnce(&BrowserLoader::OnGetVersion, weak_factory_.GetWeakPtr(),
LacrosSelection::kRootfs, barrier_callback));
stateful_lacros_loader_->GetVersion(
base::BindOnce(&BrowserLoader::OnGetVersion, weak_factory_.GetWeakPtr(),
LacrosSelection::kStateful, barrier_callback));
}
void BrowserLoader::OnGetVersion(
LacrosSelection selection,
base::OnceCallback<void(LacrosSelectionVersion)> barrier_callback,
const base::Version& version) {
std::move(barrier_callback).Run(LacrosSelectionVersion(selection, version));
}
void BrowserLoader::OnLoadVersions(
LoadCompletionCallback callback,
std::vector<LacrosSelectionVersion> versions) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK_EQ(versions.size(), kLacrosSelectionTypes);
if (is_unload_requested_) {
LOG(WARNING) << "Unload is requested during collecting Lacros version.";
std::move(callback).Run(base::FilePath(), LacrosSelection::kStateful,
base::Version());
return;
}
// Compare the rootfs vs stateful lacros-chrome binary versions.
// If the rootfs lacros-chrome is greater than lacros-chrome version,
// prioritize using the rootfs lacros-chrome. If the stateful lacros-chrome is
// not installed, let stateful lacros-chrome load in the background.
auto selected = base::ranges::max_element(
versions,
[](const LacrosSelectionVersion& lhs, const LacrosSelectionVersion& rhs) {
if (!lhs.version.IsValid()) {
return true;
}
if (!rhs.version.IsValid()) {
return false;
}
if (lhs.version != rhs.version) {
return lhs.version < rhs.version;
}
// If the versions are the same, stateful lacros-chrome should be
// prioritized, so considers LacrosSelectionVersion with kRootfs to be
// smaller. Note that this comparison only happens between kRootfs and
// kStateful.
return lhs.selection == LacrosSelection::kRootfs;
});
if (!selected->version.IsValid()) {
// Neither rootfs lacros nor stateful lacros are available.
// Returning an empty file path to notify error.
LOG(ERROR) << "No lacros is available";
std::move(callback).Run(base::FilePath(), LacrosSelection::kStateful,
base::Version());
return;
}
// Selected lacros may be older than the one which was running in a previous
// sessions, accidentally. For experiment, now we intentionally ignore
// the case and forcibly load the selected one, which is the best we could do
// at this moment.
// TODO(crbug.com/40213424): Check the condition and report it via UMA stats.
switch (selected->selection) {
case LacrosSelection::kRootfs: {
SelectRootfsLacros(std::move(callback),
LacrosSelectionSource::kCompatibilityCheck);
break;
}
case LacrosSelection::kStateful: {
SelectStatefulLacros(std::move(callback),
LacrosSelectionSource::kCompatibilityCheck);
break;
}
case LacrosSelection::kDeployedLocally: {
NOTREACHED_IN_MIGRATION();
std::move(callback).Run(
base::FilePath(), LacrosSelection::kDeployedLocally, base::Version());
return;
}
}
}
void BrowserLoader::Unload() {
is_unload_requested_ = true;
// Can be called even if Lacros isn't enabled, to clean up the old install.
// Unmount the rootfs/stateful lacros-chrome if it was mounted.
if (rootfs_lacros_loader_) {
rootfs_lacros_loader_->Unload(
base::BindOnce(&BrowserLoader::OnUnloadCompleted,
weak_factory_.GetWeakPtr(), LacrosSelection::kRootfs));
}
if (stateful_lacros_loader_) {
stateful_lacros_loader_->Unload(
base::BindOnce(&BrowserLoader::OnUnloadCompleted,
weak_factory_.GetWeakPtr(), LacrosSelection::kStateful));
}
}
void BrowserLoader::OnUnloadCompleted(LacrosSelection selection) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
switch (selection) {
case LacrosSelection::kRootfs:
CHECK(rootfs_lacros_loader_->IsUnloaded());
rootfs_lacros_loader_.reset();
break;
case LacrosSelection::kStateful:
CHECK(stateful_lacros_loader_->IsUnloaded());
stateful_lacros_loader_.reset();
break;
case LacrosSelection::kDeployedLocally:
NOTREACHED_IN_MIGRATION();
break;
}
// If either of rootfs or stateful lacros loader is still in the process of
// unload, wait running completion callback.
if (IsUnloading(rootfs_lacros_loader_.get()) ||
IsUnloading(stateful_lacros_loader_.get())) {
return;
}
// If both of the rootfs and stateful lacros load completed unloading, run the
// stored callback if exists.
if (callback_on_unload_completion_) {
std::move(callback_on_unload_completion_).Run();
}
}
base::FilePath DetermineLacrosBinaryPath(const base::FilePath& path) {
// Interpret path as directory. If that fails, interpret it as the executable.
base::FilePath expanded =
path.Append(LacrosSelectionLoader::kLacrosChromeBinary);
if (base::PathExists(expanded)) {
return expanded;
}
if (base::PathExists(path)) {
return path;
}
return {};
}
void BrowserLoader::OnLoadComplete(LoadCompletionCallback callback,
LacrosSelection selection,
base::Version version,
const base::FilePath& path) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (is_unload_requested_) {
LOG(WARNING) << "Unload is requested during loading.";
std::move(callback).Run(base::FilePath(), LacrosSelection::kStateful,
base::Version());
return;
}
// Bail out on empty `path` which implies there was an error on loading
// lacros.
if (path.empty()) {
std::move(callback).Run(base::FilePath(), selection, base::Version());
return;
}
// Fail early if the chrome binary still doesn't exist, such that
// (1) we end up with an error message in Ash's log, and
// (2) BrowserManager doesn't endlessly try to spawn Lacros.
// For example, in the past there have been issues with mounting rootfs Lacros
// that resulted in /run/lacros being empty at this point.
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()},
base::BindOnce(&DetermineLacrosBinaryPath, path),
base::BindOnce(&BrowserLoader::FinishOnLoadComplete,
weak_factory_.GetWeakPtr(), std::move(callback), path,
selection, std::move(version)));
}
void BrowserLoader::FinishOnLoadComplete(LoadCompletionCallback callback,
const base::FilePath& path,
LacrosSelection selection,
base::Version version,
const base::FilePath& lacros_binary) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (is_unload_requested_) {
LOG(WARNING) << "Unload is requested during determining lacros path.";
std::move(callback).Run(base::FilePath(), LacrosSelection::kStateful,
base::Version());
return;
}
if (lacros_binary.empty()) {
LOG(ERROR) << "Failed to find binary at " << path;
std::move(callback).Run(base::FilePath(), selection, base::Version());
return;
}
base::UmaHistogramMediumTimes(
"ChromeOS.Lacros.LoadTime",
base::TimeTicks::Now() - lacros_start_load_time_);
// Log the path on success.
LOG(WARNING) << "Loaded lacros image with binary " << lacros_binary;
std::move(callback).Run(lacros_binary, selection, std::move(version));
}
std::ostream& operator<<(std::ostream& ostream,
BrowserLoader::LacrosSelectionSource source) {
switch (source) {
case BrowserLoader::LacrosSelectionSource::kUnknown:
return ostream << "Unknown";
case BrowserLoader::LacrosSelectionSource::kCompatibilityCheck:
return ostream << "CompatibilityCheck";
case BrowserLoader::LacrosSelectionSource::kForced:
return ostream << "Forced";
case BrowserLoader::LacrosSelectionSource::kDeployedPath:
return ostream << "DeployedPath";
}
}
} // namespace crosapi