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

// 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();
}

// Called after preloading is finished.
void DonePreloading(component_updater::ComponentManagerAsh::Error error,
                    const base::FilePath& path) {
  LOG(WARNING) << "Done preloading stateful Lacros. " << static_cast<int>(error)
               << " " << path;
}

// Preloads the given component, or does nothing if |component| is empty.
// Must be called on main thread.
void PreloadComponent(
    scoped_refptr<component_updater::ComponentManagerAsh> manager,
    std::string component) {
  if (!component.empty()) {
    LOG(WARNING) << "Preloading stateful lacros. " << component;
    manager->Load(component,
                  component_updater::ComponentManagerAsh::MountPolicy::kMount,
                  component_updater::ComponentManagerAsh::UpdatePolicy::kSkip,
                  base::BindOnce(&DonePreloading));
  }
}

// This method is dispatched pre-login. At this time, we don't know whether
// Lacros is enabled. This method checks to see if the Lacros stateful component
// matching the ash channel is installed -- if it is then Lacros is enabled. At
// which point this method will begin loading stateful lacros.
// Returns the name of the component on success, empty string on failure.
std::string CheckForComponentToPreloadMayBlock() {
  ash::standalone_browser::ComponentInfo info =
      ash::standalone_browser::GetLacrosComponentInfoForChannel(
          ash::GetChannel());
  bool registered = IsInstalledMayBlock(info.name);
  if (registered) {
    return info.name;
  }
  return "";
}

}  // 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) {
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(&CheckForComponentToPreloadMayBlock),
      base::BindOnce(&PreloadComponent, component_manager_));
  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