| // 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 "chrome/browser/ash/crosapi/stateful_lacros_loader.h" |
| |
| #include <utility> |
| |
| #include "ash/constants/ash_switches.h" |
| #include "base/command_line.h" |
| #include "base/files/file_enumerator.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/logging.h" |
| #include "base/path_service.h" |
| #include "base/run_loop.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "chrome/browser/ash/crosapi/browser_util.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/component_updater/cros_component_installer_chromeos.h" |
| #include "chromeos/ash/components/channel/channel_info.h" |
| #include "chromeos/ash/components/cryptohome/system_salt_getter.h" |
| #include "chromeos/ash/components/standalone_browser/channel_util.h" |
| #include "components/component_updater/component_updater_paths.h" |
| #include "components/component_updater/component_updater_service.h" |
| |
| namespace crosapi { |
| |
| namespace { |
| |
| // Returns whether lacros-chrome component is registered. |
| bool CheckRegisteredMayBlock( |
| scoped_refptr<component_updater::ComponentManagerAsh> manager, |
| const std::string& lacros_component_name) { |
| return manager->IsRegisteredMayBlock(lacros_component_name); |
| } |
| |
| // Checks the local disk structure to confirm whether a component is installed. |
| // We intentionally avoid going through ComponentManagerAsh since the latter |
| // functions around the idea of "registration" -- but the timing of this method |
| // is prelogin, so the component might exist but not yet be registered. |
| bool IsInstalledMayBlock(const std::string& name) { |
| base::FilePath root; |
| if (!base::PathService::Get(component_updater::DIR_COMPONENT_USER, &root)) { |
| return false; |
| } |
| |
| base::FilePath component_root = |
| root.Append(component_updater::kComponentsRootPath).Append(name); |
| if (!base::PathExists(component_root)) { |
| return false; |
| } |
| |
| // Check for any subdirectory |
| base::FileEnumerator enumerator(component_root, /*recursive=*/false, |
| base::FileEnumerator::DIRECTORIES); |
| base::FilePath path = enumerator.Next(); |
| return !path.empty(); |
| } |
| |
| |
| } // namespace |
| |
| StatefulLacrosLoader::StatefulLacrosLoader( |
| scoped_refptr<component_updater::ComponentManagerAsh> manager) |
| : StatefulLacrosLoader( |
| manager, |
| g_browser_process->component_updater(), |
| ash::standalone_browser::GetLacrosComponentInfo().name) {} |
| |
| StatefulLacrosLoader::StatefulLacrosLoader( |
| scoped_refptr<component_updater::ComponentManagerAsh> manager, |
| component_updater::ComponentUpdateService* updater, |
| const std::string& lacros_component_name) |
| : component_manager_(manager), |
| component_update_service_(updater), |
| lacros_component_name_(lacros_component_name) { |
| DCHECK(component_manager_); |
| } |
| |
| // TODO(elkurin): Maybe we should call Unload or pending_unload at least? |
| StatefulLacrosLoader::~StatefulLacrosLoader() = default; |
| |
| void StatefulLacrosLoader::Load(LoadCompletionCallback callback, bool forced) { |
| CHECK(state_ == State::kNotLoaded || state_ == State::kLoaded) << state_; |
| LOG(WARNING) << "Loading stateful lacros."; |
| |
| // If stateful lacros-chrome is already loaded once, run `callback` |
| // immediately with cached version and path values. |
| // This code path is used in most cases as they are already calculated on |
| // getting version except for the case where BrowserLoader is forced to select |
| // stateful lacros-chrome by lacros selection policy. |
| if (state_ == State::kLoaded) { |
| std::move(callback).Run(version_.value(), path_.value()); |
| return; |
| } |
| |
| state_ = State::kLoading; |
| LoadInternal(std::move(callback), forced); |
| } |
| |
| void StatefulLacrosLoader::Unload(base::OnceClosure callback) { |
| switch (state_) { |
| case State::kNotLoaded: |
| case State::kUnloaded: |
| // Nothing to unload if it's not loaded or already unloaded. |
| state_ = State::kUnloaded; |
| std::move(callback).Run(); |
| break; |
| case State::kLoading: |
| case State::kUnloading: |
| // If loader is busy, wait Unload until the current task has finished. |
| pending_unload_ = |
| base::BindOnce(&StatefulLacrosLoader::Unload, |
| weak_factory_.GetWeakPtr(), std::move(callback)); |
| break; |
| case State::kLoaded: |
| // Start unloading if lacros-chrome is loaded. |
| state_ = State::kUnloading; |
| |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, {base::MayBlock()}, |
| base::BindOnce(&CheckRegisteredMayBlock, component_manager_, |
| lacros_component_name_), |
| base::BindOnce(&StatefulLacrosLoader::OnCheckInstalledToUnload, |
| weak_factory_.GetWeakPtr(), std::move(callback))); |
| break; |
| } |
| } |
| |
| void StatefulLacrosLoader::GetVersion( |
| base::OnceCallback<void(const base::Version&)> callback) { |
| CHECK_EQ(state_, State::kNotLoaded) << state_; |
| |
| state_ = State::kLoading; |
| |
| // TODO(crbug.com/40917231): There's KI that the current implementation |
| // occasionally wrongly identifies there exists. Fix the logic. |
| // If there currently isn't a stateful lacros-chrome binary, set `verison_` |
| // null to proceed to use the rootfs lacros-chrome binary and start the |
| // installation of the stateful lacros-chrome binary in the background. |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, {base::MayBlock()}, |
| base::BindOnce(&IsInstalledMayBlock, lacros_component_name_), |
| base::BindOnce(&StatefulLacrosLoader::OnCheckInstalledToGetVersion, |
| weak_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| bool StatefulLacrosLoader::IsUnloading() const { |
| return state_ == State::kUnloading; |
| } |
| |
| bool StatefulLacrosLoader::IsUnloaded() const { |
| return state_ == State::kUnloaded; |
| } |
| |
| void StatefulLacrosLoader::LoadInternal(LoadCompletionCallback callback, |
| bool forced) { |
| CHECK_EQ(state_, State::kLoading) << state_; |
| |
| // If a compatible installation exists, use that and download any updates in |
| // the background. If not, report just there is no available stateful lacros |
| // unless stateful lacros is forced by policy or about:flag entry. |
| // If stateful lacros is forced, we cannot fallback to rootfs lacros, so wait |
| // until the installation of stateful to be completed. |
| auto update_policy = |
| forced ? component_updater::ComponentManagerAsh::UpdatePolicy::kDontForce |
| : component_updater::ComponentManagerAsh::UpdatePolicy::kSkip; |
| |
| component_manager_->Load( |
| lacros_component_name_, |
| component_updater::ComponentManagerAsh::MountPolicy::kMount, |
| update_policy, |
| // If `callback` is null, means stateful lacros-chrome should be |
| // installed/updated but rootfs lacros-chrome will be used. |
| base::BindOnce(&StatefulLacrosLoader::OnLoad, weak_factory_.GetWeakPtr(), |
| std::move(callback))); |
| } |
| |
| void StatefulLacrosLoader::OnLoad( |
| LoadCompletionCallback callback, |
| component_updater::ComponentManagerAsh::Error error, |
| const base::FilePath& path) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK_EQ(state_, State::kLoading) << state_; |
| state_ = State::kLoaded; |
| |
| if (pending_unload_) { |
| LOG(WARNING) << "Unload is requested during loading stateful."; |
| if (callback) { |
| std::move(callback).Run(base::Version(), base::FilePath()); |
| } |
| std::move(pending_unload_).Run(); |
| return; |
| } |
| |
| bool is_stateful_lacros_available = |
| error == component_updater::ComponentManagerAsh::Error::NONE && |
| !path.empty(); |
| LOG_IF(WARNING, !is_stateful_lacros_available) |
| << "Error loading lacros component image in the " |
| << (callback ? "foreground" : "background") << ": " |
| << static_cast<int>(error) << ", " << path; |
| |
| version_ = is_stateful_lacros_available |
| ? ash::standalone_browser::GetInstalledLacrosComponentVersion( |
| component_update_service_) |
| : base::Version(); |
| path_ = path; |
| |
| if (callback) { |
| std::move(callback).Run(version_.value(), path_.value()); |
| } else { |
| if (is_stateful_lacros_available) { |
| LOG(WARNING) << "stateful lacros-chrome installation completed in the " |
| << "background in " << path << ", version is " |
| << version_.value(); |
| } |
| } |
| } |
| |
| void StatefulLacrosLoader::OnCheckInstalledToGetVersion( |
| base::OnceCallback<void(const base::Version&)> callback, |
| bool is_installed) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK_EQ(state_, State::kLoading) << state_; |
| |
| if (pending_unload_) { |
| LOG(WARNING) << "Unload is requested during getting version of stateful."; |
| state_ = State::kNotLoaded; |
| if (callback) { |
| std::move(callback).Run(base::Version()); |
| } |
| std::move(pending_unload_).Run(); |
| return; |
| } |
| |
| if (!is_installed) { |
| // Run `callback` immediately with empty version and start loading stateful |
| // lacros-chrome in the background. |
| LoadInternal({}, /*forced=*/false); |
| std::move(callback).Run(base::Version()); |
| return; |
| } |
| |
| LoadInternal(base::BindOnce( |
| [](base::OnceCallback<void(const base::Version&)> callback, |
| base::Version version, const base::FilePath&) { |
| std::move(callback).Run(version); |
| }, |
| std::move(callback)), |
| false); |
| } |
| |
| void StatefulLacrosLoader::OnCheckInstalledToUnload(base::OnceClosure callback, |
| bool was_installed) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK_EQ(state_, State::kUnloading) << state_; |
| |
| if (!was_installed) { |
| state_ = State::kUnloaded; |
| std::move(callback).Run(); |
| return; |
| } |
| |
| // Workaround for login crash when the user disables Lacros. |
| // ComponentManagerAsh::Unload() calls into code in MetadataTable that |
| // assumes that system salt is available. This isn't always true when chrome |
| // restarts to apply non-owner flags. It's hard to make MetadataTable async. |
| // Ensure salt is available before unloading. https://crbug.com/1122674 |
| ash::SystemSaltGetter::Get()->GetSystemSalt( |
| base::BindOnce(&StatefulLacrosLoader::UnloadAfterCleanUp, |
| weak_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void StatefulLacrosLoader::UnloadAfterCleanUp(base::OnceClosure callback, |
| const std::string& ignored_salt) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK_EQ(state_, State::kUnloading) << state_; |
| |
| CHECK(ash::SystemSaltGetter::Get()->GetRawSalt()); |
| component_manager_->Unload(lacros_component_name_); |
| |
| state_ = State::kUnloaded; |
| std::move(callback).Run(); |
| } |
| |
| std::ostream& operator<<(std::ostream& ostream, |
| StatefulLacrosLoader::State state) { |
| switch (state) { |
| case StatefulLacrosLoader::State::kNotLoaded: |
| return ostream << "NotLoaded"; |
| case StatefulLacrosLoader::State::kLoading: |
| return ostream << "Loading"; |
| case StatefulLacrosLoader::State::kLoaded: |
| return ostream << "Loaded"; |
| case StatefulLacrosLoader::State::kUnloading: |
| return ostream << "Unloading"; |
| case StatefulLacrosLoader::State::kUnloaded: |
| return ostream << "Unloaded"; |
| } |
| } |
| |
| } // namespace crosapi |