chromium/chromeos/ash/components/dbus/dlcservice/dlcservice_client.cc

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/components/dbus/dlcservice/dlcservice_client.h"

#include <stdint.h>

#include <algorithm>
#include <deque>
#include <map>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "base/command_line.h"
#include "base/containers/fixed_flat_set.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "chromeos/ash/components/dbus/dlcservice/fake_dlcservice_client.h"
#include "chromeos/dbus/constants/dbus_switches.h"
#include "dbus/bus.h"
#include "dbus/message.h"
#include "dbus/object_path.h"
#include "dbus/object_proxy.h"
#include "third_party/cros_system_api/dbus/service_constants.h"

namespace ash {

namespace {

DlcserviceClient* g_instance = nullptr;

constexpr auto kGetExistingDlcsTimeout = base::Minutes(3);

std::string_view ToDlcServiceError(dbus::ErrorResponse* err_response) {
  const std::string& error_name = err_response->GetErrorName();
  static constexpr auto kErrSet = base::MakeFixedFlatSet<std::string_view>({
      dlcservice::kErrorNone,
      dlcservice::kErrorInternal,
      dlcservice::kErrorBusy,
      dlcservice::kErrorNeedReboot,
      dlcservice::kErrorInvalidDlc,
      dlcservice::kErrorNoImageFound,
  });
  // Lookup the dlcservice error code and provide default on invalid.
  auto itr = kErrSet.find(error_name);
  if (itr == kErrSet.end()) {
    LOG(ERROR) << "Unknown ErrorResponse '" << error_name
               << "', defaulting to kErrorInternal";
    return dlcservice::kErrorInternal;
  }
  return *itr;
}

std::string ToErrorMessage(dbus::ErrorResponse* err_response) {
  std::string err_msg;
  if (!dbus::MessageReader(err_response).PopString(&err_msg)) {
    LOG(ERROR) << "Failed to pop error message from ErrorResponse.";
  }
  return err_msg;
}

std::string_view ParseError(dbus::ErrorResponse* err_response) {
  if (!err_response) {
    LOG(ERROR) << "Failed to parse error, dbus ErrorResponse is null.";
    return dlcservice::kErrorInternal;
  }
  std::string_view err = ToDlcServiceError(err_response);
  std::string err_msg = ToErrorMessage(err_response);
  VLOG(1) << "Handling err=" << err << " err_msg=" << err_msg;
  return err;
}

}  // namespace

// The DlcserviceClient implementation used in production.
class DlcserviceClientImpl : public DlcserviceClient {
 public:
  DlcserviceClientImpl() : dlcservice_proxy_(nullptr) {}

  DlcserviceClientImpl(const DlcserviceClientImpl&) = delete;
  DlcserviceClientImpl& operator=(const DlcserviceClientImpl&) = delete;

  ~DlcserviceClientImpl() override = default;

  void Install(const dlcservice::InstallRequest& install_request,
               InstallCallback install_callback,
               ProgressCallback progress_callback) override {
    const std::string& id = install_request.id();
    VLOG(1) << "DLC install called for: " << id;
    // If another installation for the same DLC ID was already called, go ahead
    // and hold the installation fields.
    if (installation_holder_.find(id) != installation_holder_.end()) {
      LOG(WARNING) << "DLC install is already in progress for: " << id;
      HoldInstallation(install_request, std::move(install_callback),
                       std::move(progress_callback));
      return;
    }
    if (installing_) {
      LOG(WARNING) << "DLC install is getting queued for: " << id;
      EnqueueTask(base::BindOnce(
          &DlcserviceClientImpl::Install, weak_ptr_factory_.GetWeakPtr(),
          std::move(install_request), std::move(install_callback),
          std::move(progress_callback)));
      return;
    }

    TaskStarted();
    dbus::MethodCall method_call(dlcservice::kDlcServiceInterface,
                                 dlcservice::kInstallMethod);
    dbus::MessageWriter writer(&method_call);
    writer.AppendProtoAsArrayOfBytes(install_request);

    VLOG(1) << "Requesting to install DLC(s).";
    // TODO(b/166782419): dlcservice hashes preloadable DLC images which can
    // cause timeouts during preloads. Transitioning into F20 will fix this as
    // preloading will be deprecated.
    constexpr int timeout_ms = 5 * 60 * 1000;
    dlcservice_proxy_->CallMethodWithErrorResponse(
        &method_call, timeout_ms,
        base::BindOnce(&DlcserviceClientImpl::OnInstall,
                       weak_ptr_factory_.GetWeakPtr(), install_request,
                       std::move(install_callback),
                       std::move(progress_callback)));
  }

  void Uninstall(const std::string& dlc_id,
                 UninstallCallback uninstall_callback) override {
    dbus::MethodCall method_call(dlcservice::kDlcServiceInterface,
                                 dlcservice::kUninstallMethod);
    dbus::MessageWriter writer(&method_call);
    writer.AppendString(dlc_id);

    VLOG(1) << "Requesting to uninstall DLC=" << dlc_id;
    dlcservice_proxy_->CallMethodWithErrorResponse(
        &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
        base::BindOnce(&DlcserviceClientImpl::OnUninstall,
                       weak_ptr_factory_.GetWeakPtr(),
                       std::move(uninstall_callback)));
  }

  void Purge(const std::string& dlc_id, PurgeCallback purge_callback) override {
    dbus::MethodCall method_call(dlcservice::kDlcServiceInterface,
                                 dlcservice::kPurgeMethod);
    dbus::MessageWriter writer(&method_call);
    writer.AppendString(dlc_id);

    VLOG(1) << "Requesting to purge DLC=" << dlc_id;
    dlcservice_proxy_->CallMethodWithErrorResponse(
        &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
        base::BindOnce(&DlcserviceClientImpl::OnPurge,
                       weak_ptr_factory_.GetWeakPtr(),
                       std::move(purge_callback)));
  }

  void GetDlcState(const std::string& dlc_id,
                   GetDlcStateCallback callback) override {
    dbus::MethodCall method_call(dlcservice::kDlcServiceInterface,
                                 dlcservice::kGetDlcStateMethod);
    dbus::MessageWriter writer(&method_call);
    writer.AppendString(dlc_id);
    VLOG(1) << "Requesting DLC state of" << dlc_id;
    dlcservice_proxy_->CallMethodWithErrorResponse(
        &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
        base::BindOnce(&DlcserviceClientImpl::OnGetDlcState,
                       weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
  }

  void GetExistingDlcs(GetExistingDlcsCallback callback) override {
    dbus::MethodCall method_call(dlcservice::kDlcServiceInterface,
                                 dlcservice::kGetExistingDlcsMethod);

    VLOG(1) << "Requesting to get existing DLC(s).";
    dlcservice_proxy_->CallMethodWithErrorResponse(
        &method_call, kGetExistingDlcsTimeout.InMilliseconds(),
        base::BindOnce(&DlcserviceClientImpl::OnGetExistingDlcs,
                       weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
  }

  void DlcStateChangedForTest(dbus::Signal* signal) override {
    DlcStateChanged(signal);
  }

  void AddObserver(Observer* observer) override {
    observers_.AddObserver(observer);
  }

  void RemoveObserver(Observer* observer) override {
    observers_.RemoveObserver(observer);
  }

  void Init(dbus::Bus* bus) {
    dlcservice_proxy_ = bus->GetObjectProxy(
        dlcservice::kDlcServiceServiceName,
        dbus::ObjectPath(dlcservice::kDlcServiceServicePath));
    dlcservice_proxy_->ConnectToSignal(
        dlcservice::kDlcServiceInterface, dlcservice::kDlcStateChangedSignal,
        base::BindRepeating(&DlcserviceClientImpl::DlcStateChanged,
                            weak_ptr_factory_.GetWeakPtr()),
        base::BindOnce(&DlcserviceClientImpl::DlcStateChangedConnected,
                       weak_ptr_factory_.GetWeakPtr()));
    dlcservice_proxy_->WaitForServiceToBeAvailable(
        base::BindOnce(&DlcserviceClientImpl::OnServiceAvailable,
                       weak_ptr_factory_.GetWeakPtr()));
  }

 private:
  // Fields related to an installation allowing for multiple installations to be
  // in flight concurrently and handled by this dlcservice client. The callbacks
  // are used to report progress and the final installation.
  struct InstallationHolder {
    InstallCallback install_callback;
    ProgressCallback progress_callback;

    InstallationHolder(InstallCallback install_callback,
                       ProgressCallback progress_callback)
        : install_callback(std::move(install_callback)),
          progress_callback(std::move(progress_callback)) {}
  };

  void OnServiceAvailable(bool service_available) {
    if (service_available) {
      VLOG(1) << "dlcservice is available.";
    } else {
      LOG(ERROR) << "dlcservice is not available.";
    }
    service_available_ = service_available;
  }

  // Set the indication that an install is being performed which was requested
  // from this client (Chrome specifically).
  void TaskStarted() { installing_ = true; }

  // Clears any state an installation had setup while being performed.
  void TaskEnded() { installing_ = false; }

  void HoldInstallation(const dlcservice::InstallRequest& install_request,
                        InstallCallback install_callback,
                        ProgressCallback progress_callback) {
    installation_holder_[install_request.id()].emplace_back(
        std::move(install_callback), std::move(progress_callback));
  }

  void ReleaseInstallation(const std::string& id) {
    installation_holder_.erase(id);
  }

  void EnqueueTask(base::OnceClosure task) {
    pending_tasks_.emplace_back(std::move(task));
  }

  void CheckAndRunPendingTask() {
    // If there are no pending tasks, we can call TaskEnded() now to allow new
    // requests to run immediately.
    if (pending_tasks_.empty()) {
      TaskEnded();
      return;
    }

    // Delay pending tasks and let new tasks get queued to ensure we don't spin
    // the CPU with repeated calls when the DLC installer is busy.
    base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(&DlcserviceClientImpl::DelayedPendingTask,
                       weak_ptr_factory_.GetWeakPtr()),
        base::Seconds(3));
  }

  void DelayedPendingTask() {
    TaskEnded();
    if (!pending_tasks_.empty()) {
      std::move(pending_tasks_.front()).Run();
      pending_tasks_.pop_front();
    }
  }

  void SendProgress(const dlcservice::DlcState& dlc_state) {
    auto id = dlc_state.id();
    auto progress = dlc_state.progress();
    VLOG(2) << "Installation for DLC " << id << " in progress: " << progress;
    for (auto& installation_state : installation_holder_[id]) {
      installation_state.progress_callback.Run(progress);
    }
  }

  void SendCompleted(const dlcservice::DlcState& dlc_state) {
    auto id = dlc_state.id();
    if (dlc_state.state() == dlcservice::DlcState::NOT_INSTALLED) {
      LOG(ERROR) << "Failed to install DLC " << id
                 << " with error code: " << dlc_state.last_error_code();

    } else {
      VLOG(1) << "DLC " << id << " installed successfully.";
      if (dlc_state.last_error_code() != dlcservice::kErrorNone) {
        LOG(WARNING) << "DLC installation was sucessful but non-success "
                     << "error code: " << dlc_state.last_error_code();
      }
    }

    InstallResult result = {
        .error = dlc_state.last_error_code(),
        .dlc_id = id,
        .root_path = dlc_state.root_path(),
    };
    for (auto& installation_state : installation_holder_[id]) {
      std::move(installation_state.install_callback).Run(result);
    }
    ReleaseInstallation(id);
  }

  void DlcStateChanged(dbus::Signal* signal) {
    dlcservice::DlcState dlc_state;
    if (!dbus::MessageReader(signal).PopArrayOfBytesAsProto(&dlc_state)) {
      LOG(ERROR) << "Failed to parse proto as install status.";
      return;
    }

    // Notify all observers of change in the state of this DLC.
    for (Observer& observer : observers_) {
      observer.OnDlcStateChanged(dlc_state);
    }

    // Skip DLCs not installing from this dlcservice client.
    if (installation_holder_.find(dlc_state.id()) ==
        installation_holder_.end()) {
      return;
    }

    switch (dlc_state.state()) {
      case dlcservice::DlcState::NOT_INSTALLED:
      case dlcservice::DlcState::INSTALLED:
        SendCompleted(dlc_state);
        break;
      case dlcservice::DlcState::INSTALLING:
        SendProgress(dlc_state);
        // Need to return here since we don't want to try starting another
        // pending install from the queue (would waste time checking).
        return;
      default:
        NOTREACHED_IN_MIGRATION();
    }

    // Try to run a pending install since we have complete/failed the current
    // install, but do not waste trying to run a pending install when the
    // current install is running at the moment.
    CheckAndRunPendingTask();
  }

  void DlcStateChangedConnected(const std::string& interface,
                                const std::string& signal,
                                bool success) {
    LOG_IF(ERROR, !success) << "Failed to connect to DlcStateChanged signal.";
  }

  void OnInstall(const dlcservice::InstallRequest& install_request,
                 InstallCallback install_callback,
                 ProgressCallback progress_callback,
                 dbus::Response* response,
                 dbus::ErrorResponse* err_response) {
    const std::string& id = install_request.id();
    if (response) {
      HoldInstallation(install_request, std::move(install_callback),
                       std::move(progress_callback));
      return;
    }

    std::string_view err = ParseError(err_response);
    if (err == dlcservice::kErrorBusy) {
      // No need to log here, as it can be inferred from error response handler
      // and the binded callback logging.
      EnqueueTask(base::BindOnce(&DlcserviceClientImpl::Install,
                                 weak_ptr_factory_.GetWeakPtr(),
                                 install_request, std::move(install_callback),
                                 std::move(progress_callback)));
    } else {
      HoldInstallation(install_request, std::move(install_callback),
                       std::move(progress_callback));
      dlcservice::DlcState dlc_state;
      dlc_state.set_id(id);
      dlc_state.set_last_error_code(std::string(err));
      SendCompleted(dlc_state);
    }
    CheckAndRunPendingTask();
  }

  void OnUninstall(UninstallCallback uninstall_callback,
                   dbus::Response* response,
                   dbus::ErrorResponse* err_response) {
    std::move(uninstall_callback)
        .Run(response ? dlcservice::kErrorNone : ParseError(err_response));
  }

  void OnPurge(PurgeCallback purge_callback,
               dbus::Response* response,
               dbus::ErrorResponse* err_response) {
    std::move(purge_callback)
        .Run(response ? dlcservice::kErrorNone : ParseError(err_response));
  }

  void OnGetDlcState(GetDlcStateCallback callback,
                     dbus::Response* response,
                     dbus::ErrorResponse* err_response) {
    dlcservice::DlcState dlc_state;
    if (response &&
        dbus::MessageReader(response).PopArrayOfBytesAsProto(&dlc_state)) {
      std::move(callback).Run(dlcservice::kErrorNone, dlc_state);
    } else {
      std::move(callback).Run(ParseError(err_response), dlcservice::DlcState());
    }
  }

  void OnGetExistingDlcs(GetExistingDlcsCallback callback,
                         dbus::Response* response,
                         dbus::ErrorResponse* err_response) {
    dlcservice::DlcsWithContent dlcs_with_content;
    if (response && dbus::MessageReader(response).PopArrayOfBytesAsProto(
                        &dlcs_with_content)) {
      std::move(callback).Run(dlcservice::kErrorNone, dlcs_with_content);
    } else {
      std::move(callback).Run(ParseError(err_response),
                              dlcservice::DlcsWithContent());
    }
  }

  // DLC ID to `InstallationHolder` mapping.
  std::map<std::string, std::vector<InstallationHolder>> installation_holder_;

  raw_ptr<dbus::ObjectProxy> dlcservice_proxy_;

  // TODO(crbug.com/928805): Once platform dlcservice batches, can be removed.
  // Specifically when platform dlcservice doesn't return a busy status.
  // Whether an install is currently in progress. Can be used to decide whether
  // to queue up incoming install requests.
  bool installing_ = false;

  // A list of postponed installs to dlcservice.
  std::deque<base::OnceClosure> pending_tasks_;

  // A list of observers that are listening on state changes, etc.
  base::ObserverList<Observer> observers_;

  // Indicates if dlcservice daemon is available.
  bool service_available_ = false;

  // Note: This should remain the last member so it'll be destroyed and
  // invalidate its weak pointers before any other members are destroyed.
  base::WeakPtrFactory<DlcserviceClientImpl> weak_ptr_factory_{this};
};

DlcserviceClient::DlcserviceClient() {
  CHECK(!g_instance);
  g_instance = this;
}

DlcserviceClient::~DlcserviceClient() {
  CHECK_EQ(this, g_instance);
  g_instance = nullptr;
}

// static
void DlcserviceClient::Initialize(dbus::Bus* bus) {
  CHECK(bus);
  (new DlcserviceClientImpl())->Init(bus);
}

// static
void DlcserviceClient::InitializeFake() {
  new FakeDlcserviceClient();
}

// static
void DlcserviceClient::Shutdown() {
  CHECK(g_instance);
  delete g_instance;
}

// static
DlcserviceClient* DlcserviceClient::Get() {
  return g_instance;
}

}  // namespace ash