chromium/chromeos/ash/components/dbus/device_management/install_attributes_client.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 "chromeos/ash/components/dbus/device_management/install_attributes_client.h"

#include <memory>
#include <utility>

#include <google/protobuf/message_lite.h>

#include "base/functional/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "chromeos/ash/components/dbus/device_management/fake_install_attributes_client.h"
#include "chromeos/dbus/common/blocking_method_caller.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/device_management/dbus-constants.h"

namespace ash {
namespace {

// The default timeout for all method call within InstallAttributes interface.
// Note that it is known that device_managementd could be slow to respond to calls
// in certain conditions. D-Bus call blocking for as long as 2 minutes have been
// observed in testing conditions/CQ.
constexpr int kInstallAttributesDefaultTimeoutMS = 5 * 60 * 1000;

InstallAttributesClient* g_instance = nullptr;

// Tries to parse a proto message from |response| into |proto|.
// Returns false if |response| is nullptr or the message cannot be parsed.
bool ParseProto(dbus::Response* response,
                google::protobuf::MessageLite* proto) {
  if (!response) {
    LOG(ERROR) << "Failed to call device_managementd";
    return false;
  }

  dbus::MessageReader reader(response);
  if (!reader.PopArrayOfBytesAsProto(proto)) {
    LOG(ERROR) << "Failed to parse response message from device_managementd";
    return false;
  }

  return true;
}

// "Real" implementation of InstallAttributesClient talking to the device_managementd's
// InstallAttributes interface on the Chrome OS side.
class InstallAttributesClientImpl : public InstallAttributesClient {
 public:
  InstallAttributesClientImpl() = default;
  ~InstallAttributesClientImpl() override = default;

  // Not copyable or movable.
  InstallAttributesClientImpl(const InstallAttributesClientImpl&) = delete;
  InstallAttributesClientImpl& operator=(const InstallAttributesClientImpl&) =
      delete;

  void Init(dbus::Bus* bus) {
    proxy_ = bus->GetObjectProxy(
        ::device_management::kDeviceManagementServiceName,
        dbus::ObjectPath(::device_management::kDeviceManagementServicePath));
    blocking_method_caller_ =
        std::make_unique<chromeos::BlockingMethodCaller>(bus, proxy_);
  }

  // InstallAttributesClient override:

  void WaitForServiceToBeAvailable(
      chromeos::WaitForServiceToBeAvailableCallback callback) override {
    proxy_->WaitForServiceToBeAvailable(std::move(callback));
  }

  void InstallAttributesGet(
      const ::device_management::InstallAttributesGetRequest& request,
      InstallAttributesGetCallback callback) override {
    CallProtoMethod(::device_management::kInstallAttributesGet,
                    ::device_management::kDeviceManagementInterface, request,
                    std::move(callback));
  }

  void InstallAttributesFinalize(
      const ::device_management::InstallAttributesFinalizeRequest& request,
      InstallAttributesFinalizeCallback callback) override {
    CallProtoMethod(::device_management::kInstallAttributesFinalize,
                    ::device_management::kDeviceManagementInterface, request,
                    std::move(callback));
  }

  void InstallAttributesGetStatus(
      const ::device_management::InstallAttributesGetStatusRequest& request,
      InstallAttributesGetStatusCallback callback) override {
    CallProtoMethod(::device_management::kInstallAttributesGetStatus,
                    ::device_management::kDeviceManagementInterface, request,
                    std::move(callback));
  }

  void RemoveFirmwareManagementParameters(
      const ::device_management::RemoveFirmwareManagementParametersRequest&
          request,
      RemoveFirmwareManagementParametersCallback callback) override {
    CallProtoMethod(::device_management::kRemoveFirmwareManagementParameters,
                    ::device_management::kDeviceManagementInterface, request,
                    std::move(callback));
  }

  void SetFirmwareManagementParameters(
      const ::device_management::SetFirmwareManagementParametersRequest& request,
      SetFirmwareManagementParametersCallback callback) override {
    CallProtoMethod(::device_management::kSetFirmwareManagementParameters,
                    ::device_management::kDeviceManagementInterface, request,
                    std::move(callback));
  }

  void GetFirmwareManagementParameters(
      const ::device_management::GetFirmwareManagementParametersRequest& request,
      GetFirmwareManagementParametersCallback callback) override {
    CallProtoMethod(::device_management::kGetFirmwareManagementParameters,
                    ::device_management::kDeviceManagementInterface, request,
                    std::move(callback));
  }

  std::optional<::device_management::InstallAttributesGetReply>
  BlockingInstallAttributesGet(
      const ::device_management::InstallAttributesGetRequest& request) override {
    return BlockingCallProtoMethod<::device_management::InstallAttributesGetReply>(
        ::device_management::kInstallAttributesGet,
        ::device_management::kDeviceManagementInterface, request);
  }

  std::optional<::device_management::InstallAttributesSetReply>
  BlockingInstallAttributesSet(
      const ::device_management::InstallAttributesSetRequest& request) override {
    return BlockingCallProtoMethod<::device_management::InstallAttributesSetReply>(
        ::device_management::kInstallAttributesSet,
        ::device_management::kDeviceManagementInterface, request);
  }

  std::optional<::device_management::InstallAttributesFinalizeReply>
  BlockingInstallAttributesFinalize(
      const ::device_management::InstallAttributesFinalizeRequest& request)
      override {
    return BlockingCallProtoMethod<
        ::device_management::InstallAttributesFinalizeReply>(
        ::device_management::kInstallAttributesFinalize,
        ::device_management::kDeviceManagementInterface, request);
  }

  std::optional<::device_management::InstallAttributesGetStatusReply>
  BlockingInstallAttributesGetStatus(
      const ::device_management::InstallAttributesGetStatusRequest& request)
      override {
    return BlockingCallProtoMethod<
        ::device_management::InstallAttributesGetStatusReply>(
        ::device_management::kInstallAttributesGetStatus,
        ::device_management::kDeviceManagementInterface, request);
  }

 private:
  // Calls device_managementd's |method_name| method in |interface_name| interface,
  // passing in |request| as input with |timeout_ms|. Once the (asynchronous)
  // call finishes, |callback| is called with the response proto.
  template <typename RequestType, typename ReplyType>
  void CallProtoMethodWithTimeout(
      const char* method_name,
      const char* interface_name,
      int timeout_ms,
      const RequestType& request,
      chromeos::DBusMethodCallback<ReplyType> callback) {
    dbus::MethodCall method_call(interface_name, method_name);
    dbus::MessageWriter writer(&method_call);
    if (!writer.AppendProtoAsArrayOfBytes(request)) {
      LOG(ERROR)
          << "Failed to append protobuf when calling InstallAttributes method "
          << method_name;
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(callback), std::nullopt));
      return;
    }
    // Bind with the weak pointer of |this| so the response is not
    // handled once |this| is already destroyed.
    proxy_->CallMethod(
        &method_call, timeout_ms,
        base::BindOnce(&InstallAttributesClientImpl::HandleResponse<ReplyType>,
                       weak_factory_.GetWeakPtr(), std::move(callback)));
  }

  // Calls device_managementd's |method_name| method in |interface_name| interface,
  // passing in |request| as input with the default InstallAttributes timeout.
  // Once the (asynchronous) call finishes, |callback| is called with the
  // response proto.
  template <typename RequestType, typename ReplyType>
  void CallProtoMethod(const char* method_name,
                       const char* interface_name,
                       const RequestType& request,
                       chromeos::DBusMethodCallback<ReplyType> callback) {
    CallProtoMethodWithTimeout(method_name, interface_name,
                               kInstallAttributesDefaultTimeoutMS, request,
                               std::move(callback));
  }

  // Parses the response proto message from |response| and calls |callback| with
  // the decoded message. Calls |callback| with std::nullopt on error, including
  // timeout.
  template <typename ReplyType>
  void HandleResponse(chromeos::DBusMethodCallback<ReplyType> callback,
                      dbus::Response* response) {
    ReplyType reply_proto;
    if (!ParseProto(response, &reply_proto)) {
      LOG(ERROR)
          << "Failed to parse reply protobuf from InstallAttributes method";
      std::move(callback).Run(std::nullopt);
      return;
    }
    std::move(callback).Run(reply_proto);
  }

  template <typename ReplyType, typename RequestType>
  std::optional<ReplyType> BlockingCallProtoMethod(const char* method_name,
                                                   const char* interface_name,
                                                   const RequestType& request) {
    dbus::MethodCall method_call(interface_name, method_name);
    dbus::MessageWriter writer(&method_call);
    if (!writer.AppendProtoAsArrayOfBytes(request)) {
      LOG(ERROR) << "Failed to append protobuf when calling InstallAttributes "
                    "method (blocking) "
                 << method_name;
      return std::nullopt;
    }

    std::unique_ptr<dbus::Response> response(
        blocking_method_caller_->CallMethodAndBlock(&method_call)
            .value_or(nullptr));

    if (!response) {
      LOG(ERROR) << "DBus call failed for InstallAttributes method (blocking) "
                 << method_name;
      return std::nullopt;
    }

    ReplyType reply_proto;
    if (!ParseProto(response.get(), &reply_proto)) {
      LOG(ERROR)
          << "Failed to parse proto from InstallAttributes method (blocking) "
          << method_name;
      return std::nullopt;
    }

    return reply_proto;
  }

  // D-Bus proxy for device_managementd, not owned.
  raw_ptr<dbus::ObjectProxy> proxy_ = nullptr;

  // For making blocking dbus calls.
  std::unique_ptr<chromeos::BlockingMethodCaller> blocking_method_caller_;

  base::WeakPtrFactory<InstallAttributesClientImpl> weak_factory_{this};
};

}  // namespace

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

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

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

// static
void InstallAttributesClient::InitializeFake() {
  new FakeInstallAttributesClient();
}

// static
void InstallAttributesClient::Shutdown() {
  CHECK(g_instance);
  delete g_instance;
  // The destructor resets |g_instance|.
  DCHECK(!g_instance);
}

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

}  // namespace ash