chromium/chrome/browser/ash/crosapi/rootfs_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/rootfs_lacros_loader.h"

#include <string>
#include <utility>
#include <vector>

#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/ash/crosapi/browser_util.h"
#include "chromeos/ash/components/dbus/upstart/upstart_client.h"
#include "components/user_manager/user_manager.h"

namespace crosapi {

namespace {

// The rootfs lacros-chrome binary related files.
constexpr char kLacrosMetadata[] = "metadata.json";

// The rootfs lacros-chrome binary related paths.
// Must be kept in sync with lacros upstart conf files.
constexpr char kRootfsLacrosMountPoint[] = "/run/lacros";
constexpr char kRootfsLacrosPath[] = "/opt/google/lacros";

// Lacros upstart jobs for mounting/unmounting the lacros-chrome image.
// The conversion of upstart job names to dbus object paths is undocumented. See
// function nih_dbus_path in libnih for the implementation.
constexpr char kLacrosMounterUpstartJob[] = "lacros_2dmounter";
constexpr char kLacrosUnmounterUpstartJob[] = "lacros_2dunmounter";

}  // namespace

RootfsLacrosLoader::RootfsLacrosLoader()
    : RootfsLacrosLoader(
          ash::UpstartClient::Get(),
          base::FilePath(kRootfsLacrosPath).Append(kLacrosMetadata)) {}

RootfsLacrosLoader::RootfsLacrosLoader(ash::UpstartClient* upstart_client,
                                       base::FilePath metadata_path)
    : upstart_client_(upstart_client),
      metadata_path_(std::move(metadata_path)) {}

RootfsLacrosLoader::~RootfsLacrosLoader() = default;

void RootfsLacrosLoader::Load(LoadCompletionCallback callback, bool forced) {
  CHECK(state_ == State::kNotLoaded ||
        state_ == State::kVersionReadyButNotLoaded)
      << state_;
  LOG(WARNING) << "Loading rootfs lacros.";

  if (state_ == State::kVersionReadyButNotLoaded) {
    OnVersionReadyToLoad(std::move(callback), version_.value());
    return;
  }

  // Calculate `version_` before start loading.
  // It's not calculated yet in case when lacros selection is defined by
  // selection policy or stateful lacros is not installed.
  state_ = State::kReadingVersion;
  GetVersionInternal(base::BindOnce(&RootfsLacrosLoader::OnVersionReadyToLoad,
                                    weak_factory_.GetWeakPtr(),
                                    std::move(callback)));
}

void RootfsLacrosLoader::Unload(base::OnceClosure callback) {
  switch (state_) {
    case State::kNotLoaded:
    case State::kVersionReadyButNotLoaded:
    case State::kUnloaded:
      // Nothing to unload if it's not loaded or already unloaded.
      state_ = State::kUnloaded;
      std::move(callback).Run();
      break;
    case State::kReadingVersion:
    case State::kLoading:
    case State::kUnloading:
      // If loader is busy, wait Unload until the current task has finished.
      pending_unload_ =
          base::BindOnce(&RootfsLacrosLoader::Unload,
                         weak_factory_.GetWeakPtr(), std::move(callback));
      break;
    case State::kLoaded:
      state_ = State::kUnloading;

      upstart_client_->StartJob(
          kLacrosUnmounterUpstartJob, {},
          base::BindOnce(&RootfsLacrosLoader::OnUnloadCompleted,
                         weak_factory_.GetWeakPtr(), std::move(callback)));
  }
}

void RootfsLacrosLoader::GetVersion(
    base::OnceCallback<void(const base::Version&)> callback) {
  CHECK_EQ(state_, State::kNotLoaded) << state_;
  state_ = State::kReadingVersion;
  GetVersionInternal(std::move(callback));
}

bool RootfsLacrosLoader::IsUnloading() const {
  return state_ == State::kUnloading;
}

bool RootfsLacrosLoader::IsUnloaded() const {
  return state_ == State::kUnloaded;
}

void RootfsLacrosLoader::GetVersionInternal(
    base::OnceCallback<void(const base::Version&)> callback) {
  CHECK_EQ(state_, State::kReadingVersion) << state_;
  CHECK(!version_);

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(&browser_util::GetRootfsLacrosVersionMayBlock,
                     metadata_path_),
      base::BindOnce(&RootfsLacrosLoader::OnGetVersion,
                     weak_factory_.GetWeakPtr(), std::move(callback)));
}

void RootfsLacrosLoader::OnGetVersion(
    base::OnceCallback<void(const base::Version&)> callback,
    base::Version version) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK_EQ(state_, State::kReadingVersion) << state_;

  version_ = version;
  state_ = State::kVersionReadyButNotLoaded;

  if (pending_unload_) {
    LOG(WARNING) << "Unload is requested during getting version of rootfs.";
    std::move(callback).Run(base::Version());
    std::move(pending_unload_).Run();
    return;
  }

  std::move(callback).Run(version_.value());
}

void RootfsLacrosLoader::OnVersionReadyToLoad(LoadCompletionCallback callback,
                                              const base::Version& version) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK_EQ(state_, State::kVersionReadyButNotLoaded) << state_;

  if (pending_unload_) {
    LOG(WARNING) << "Unload is requested during loading rootfs.";
    std::move(callback).Run(base::Version(), base::FilePath());
    std::move(pending_unload_).Run();
    return;
  }

  // `version_` must be already filled by `version`.
  CHECK(version_.has_value() &&
        ((!version_.value().IsValid() && !version.IsValid()) ||
         (version_.value() == version)));

  state_ = State::kLoading;

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(
          &base::PathExists,
          base::FilePath(kRootfsLacrosMountPoint).Append(kLacrosChromeBinary)),
      base::BindOnce(&RootfsLacrosLoader::OnMountCheckToLoad,
                     weak_factory_.GetWeakPtr(), std::move(callback)));
}

void RootfsLacrosLoader::OnMountCheckToLoad(LoadCompletionCallback callback,
                                            bool already_mounted) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK_EQ(state_, State::kLoading) << state_;

  if (pending_unload_) {
    LOG(WARNING) << "Unload is requested during loading rootfs.";
    state_ = State::kVersionReadyButNotLoaded;
    std::move(callback).Run(base::Version(), base::FilePath());
    std::move(pending_unload_).Run();
    return;
  }

  if (already_mounted) {
    OnUpstartLacrosMounter(std::move(callback), true);
    return;
  }

  std::vector<std::string> job_env;
  if (user_manager::UserManager::Get()->IsLoggedInAsGuest()) {
    job_env.emplace_back("USE_SESSION_NAMESPACE=true");
  }

  upstart_client_->StartJob(
      kLacrosMounterUpstartJob, job_env,
      base::BindOnce(&RootfsLacrosLoader::OnUpstartLacrosMounter,
                     weak_factory_.GetWeakPtr(), std::move(callback)));
}

void RootfsLacrosLoader::OnUpstartLacrosMounter(LoadCompletionCallback callback,
                                                bool success) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK_EQ(state_, State::kLoading) << state_;
  state_ = State::kLoaded;

  LOG_IF(WARNING, !success) << "Upstart failed to mount rootfs lacros.";

  if (pending_unload_) {
    LOG(WARNING) << "Unload is requested during loading rootfs.";
    std::move(callback).Run(base::Version(), base::FilePath());
    std::move(pending_unload_).Run();
    return;
  }

  // `version_` must be calculated before coming here.
  CHECK(version_.has_value());
  std::move(callback).Run(
      version_.value(),
      // If mounting wasn't successful, return a empty mount point to indicate
      // failure. `OnLoadComplete` handles empty mount points and forwards the
      // errors on the return callbacks.
      success ? base::FilePath(kRootfsLacrosMountPoint) : base::FilePath());
}

void RootfsLacrosLoader::OnUnloadCompleted(base::OnceClosure callback,
                                           bool success) {
  // Proceed anyway regardless of unload success.
  if (!success) {
    LOG(ERROR) << "Failed to unload rootfs lacros";
  }

  CHECK_EQ(state_, State::kUnloading) << state_;
  state_ = State::kUnloaded;
  std::move(callback).Run();
}

std::ostream& operator<<(std::ostream& ostream,
                         RootfsLacrosLoader::State state) {
  switch (state) {
    case RootfsLacrosLoader::State::kNotLoaded:
      return ostream << "NotLoaded";
    case RootfsLacrosLoader::State::kReadingVersion:
      return ostream << "ReadingVersion";
    case RootfsLacrosLoader::State::kVersionReadyButNotLoaded:
      return ostream << "VersionReadyButNotLoaded";
    case RootfsLacrosLoader::State::kLoading:
      return ostream << "Loading";
    case RootfsLacrosLoader::State::kLoaded:
      return ostream << "Loaded";
    case RootfsLacrosLoader::State::kUnloading:
      return ostream << "Unloading";
    case RootfsLacrosLoader::State::kUnloaded:
      return ostream << "Unloaded";
  }
}

}  // namespace crosapi