text: // Copyright 2019 The Chromium OS Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  #include <memory>
  #include <utility>
  
  #include <base/guid.h>
  #include <base/optional.h>
  #include <base/time/time.h>
  #include <brillo/dbus/dbus_proxy_util.h>
  #include <chromeos/dbus/service_constants.h>
  #include <dbus/bus.h>
  #include <dbus/exported_object.h>
  #include <dbus/message.h>
  #include <dbus/object_proxy.h>
  #include <dbus/scoped_dbus_error.h>
  
  #include <vm_plugin_dispatcher/proto_bindings/vm_plugin_dispatcher.pb.h>
  
  #include "vm_tools/concierge/vmplugin_dispatcher_interface.h"
  
  namespace vm_tools {
  namespace concierge {
  namespace pvm {
  namespace dispatcher {
  namespace {
  
  constexpr char kVmpluginImageDir[] = "/run/pvm-images";
  
  constexpr base::TimeDelta kVmShutdownTimeout = base::TimeDelta::FromMinutes(2);
  constexpr base::TimeDelta kVmSuspendTimeout = base::TimeDelta::FromSeconds(20);
  
  // Native Parallels error codes.
  constexpr int PRL_ERR_SUCCESS = 0;
  constexpr int PRL_ERR_DISP_SHUTDOWN_IN_PROCESS = 0x80000404;
  constexpr int PRL_ERR_LICENSE_NOT_VALID = 0x80011000;
  constexpr int PRL_ERR_LICENSE_EXPIRED = 0x80011001;
  constexpr int PRL_ERR_LICENSE_WRONG_VERSION = 0x80011002;
  constexpr int PRL_ERR_LICENSE_WRONG_PLATFORM = 0x80011004;
  constexpr int PRL_ERR_LICENSE_BETA_KEY_RELEASE_PRODUCT = 0x80011011;
  constexpr int PRL_ERR_LICENSE_RELEASE_KEY_BETA_PRODUCT = 0x80011013;
  constexpr int PRL_ERR_LICENSE_SUBSCR_EXPIRED = 0x80011074;
  constexpr int PRL_ERR_JLIC_WRONG_HWID = 0x80057005;
  constexpr int PRL_ERR_JLIC_LICENSE_DISABLED = 0x80057010;
  constexpr int PRL_ERR_JLIC_WEB_PORTAL_ACCESS_REQUIRED = 0x80057012;
  
  VmOpResult ConvertNativeResult(int result) {
    switch (result) {
      case PRL_ERR_SUCCESS:
        return VmOpResult::SUCCESS;
      case PRL_ERR_DISP_SHUTDOWN_IN_PROCESS:
        return VmOpResult::DISPATCHER_SHUTTING_DOWN;
      case PRL_ERR_LICENSE_NOT_VALID:
      case PRL_ERR_LICENSE_EXPIRED:
      case PRL_ERR_LICENSE_WRONG_VERSION:
      case PRL_ERR_LICENSE_WRONG_PLATFORM:
      case PRL_ERR_LICENSE_BETA_KEY_RELEASE_PRODUCT:
      case PRL_ERR_LICENSE_RELEASE_KEY_BETA_PRODUCT:
      case PRL_ERR_LICENSE_SUBSCR_EXPIRED:
      case PRL_ERR_JLIC_WRONG_HWID:
      case PRL_ERR_JLIC_LICENSE_DISABLED:
      case PRL_ERR_JLIC_WEB_PORTAL_ACCESS_REQUIRED:
        return VmOpResult::DISPATCHER_LICENSE_ERROR;
      default:
        return VmOpResult::DISPATCHER_GENERIC_ERROR;
    }
  }
  
  VmOpResult ConvertDispatcherResult(plugin_dispatcher::VmErrorCode result,
                                     int native_result) {
    switch (result) {
      case plugin_dispatcher::VM_SUCCESS:
        return VmOpResult::SUCCESS;
      case plugin_dispatcher::VM_ERR_NATIVE_RESULT_CODE:
        return ConvertNativeResult(native_result);
      default:
        return VmOpResult::INTERNAL_ERROR;
    }
  }
  
  bool GetVmInfo(scoped_refptr<dbus::Bus> bus,
                 dbus::ObjectProxy* proxy,
                 const VmId& vm_id,
                 base::Optional<vm_tools::plugin_dispatcher::VmInfo>* info) {
    dbus::MethodCall method_call(
        vm_tools::plugin_dispatcher::kVmPluginDispatcherInterface,
        vm_tools::plugin_dispatcher::kListVmsMethod);
    dbus::MessageWriter writer(&method_call);
  
    vm_tools::plugin_dispatcher::ListVmRequest request;
  
    request.set_owner_id(vm_id.owner_id());
    request.set_vm_name_uuid(vm_id.name());
  
    if (!writer.AppendProtoAsArrayOfBytes(request)) {
      LOG(ERROR) << "Failed to encode ListVmRequest protobuf";
      return false;
    }
  
    std::unique_ptr<dbus::Response> dbus_response =
        brillo::dbus_utils::CallDBusMethod(
            bus, proxy, &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
    if (!dbus_response) {
      LOG(ERROR) << "Failed to send ListVm message to dispatcher service";
      return false;
    }
  
    dbus::MessageReader reader(dbus_response.get());
    vm_tools::plugin_dispatcher::ListVmResponse response;
    if (!reader.PopArrayOfBytesAsProto(&response)) {
      LOG(ERROR) << "Failed to parse ListVmResponse protobuf";
      return false;
    }
  
    if (response.error() != vm_tools::plugin_dispatcher::VM_SUCCESS) {
      LOG(ERROR) << "Failed to get VM info: " << std::hex << std::showbase
                 << response.error() << " (" << response.result_code() << ")";
      return false;
    }
  
    *info = base::nullopt;
    for (const auto& vm_info : response.vm_info()) {
      if (vm_info.name() == vm_id.name()) {
        *info = vm_info;
        break;
      }
    }
  
    return info;
  }
  
  }  // namespace
  
  dbus::ObjectProxy* GetServiceProxy(scoped_refptr<dbus::Bus> bus) {
    return bus->GetObjectProxy(
        vm_tools::plugin_dispatcher::kVmPluginDispatcherServiceName,
        dbus::ObjectPath(
            vm_tools::plugin_dispatcher::kVmPluginDispatcherServicePath));
  }
  
  bool RegisterVm(scoped_refptr<dbus::Bus> bus,
                  dbus::ObjectProxy* proxy,
                  const VmId& vm_id,
                  const base::FilePath& image_path) {
    dbus::MethodCall method_call(
        vm_tools::plugin_dispatcher::kVmPluginDispatcherInterface,
        vm_tools::plugin_dispatcher::kRegisterVmMethod);
    dbus::MessageWriter writer(&method_call);
  
    vm_tools::plugin_dispatcher::RegisterVmRequest request;
  
    request.set_owner_id(vm_id.owner_id());
    request.set_new_name(vm_id.name());
    base::FilePath dispatcher_image_path(base::FilePath(kVmpluginImageDir)
                                             .Append(vm_id.owner_id())
                                             .Append(image_path.BaseName()));
    LOG(INFO) << "Registering VM at " << dispatcher_image_path.value();
    request.set_path(dispatcher_image_path.value());
    // We do not track VMs by uuid but rather by their name, so always generate
    // new one.
    request.set_new_uuid(base::GenerateGUID());
    request.set_preserve_uuid(false);
    request.set_regenerate_src_uuid(true);
  
    if (!writer.AppendProtoAsArrayOfBytes(request)) {
      LOG(ERROR) << "Failed to encode RegisterVmRequest protobuf";
      return false;
    }
  
    std::unique_ptr<dbus::Response> dbus_response =
        brillo::dbus_utils::CallDBusMethod(
            bus, proxy, &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
    if (!dbus_response) {
      LOG(ERROR) << "Failed to send RegisterVm message to dispatcher service";
      return false;
    }
  
    dbus::MessageReader reader(dbus_response.get());
    vm_tools::plugin_dispatcher::RegisterVmResponse response;
    if (!reader.PopArrayOfBytesAsProto(&response)) {
      LOG(ERROR) << "Failed to parse RegisterVmResponse protobuf";
      return false;
    }
  
    if (response.error() != vm_tools::plugin_dispatcher::VM_SUCCESS) {
      LOG(ERROR) << "Failed to register VM: " << std::hex << std::showbase
                 << response.error() << " (" << response.result_code() << ")";
      return false;
    }
  
    return true;
  }
  
  bool UnregisterVm(scoped_refptr<dbus::Bus> bus,
                    dbus::ObjectProxy* proxy,
                    const VmId& vm_id) {
    LOG(INFO) << "Unregistering VM " << vm_id;
  
    dbus::MethodCall method_call(
        vm_tools::plugin_dispatcher::kVmPluginDispatcherInterface,
        vm_tools::plugin_dispatcher::kUnregisterVmMethod);
    dbus::MessageWriter writer(&method_call);
  
    vm_tools::plugin_dispatcher::UnregisterVmRequest request;
  
    request.set_owner_id(vm_id.owner_id());
    request.set_vm_name_uuid(vm_id.name());
  
    if (!writer.AppendProtoAsArrayOfBytes(request)) {
      LOG(ERROR) << "Failed to encode UnregisterVmRequest protobuf";
      return false;
    }
  
    std::unique_ptr<dbus::Response> dbus_response =
        brillo::dbus_utils::CallDBusMethod(
            bus, proxy, &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
    if (!dbus_response) {
      LOG(ERROR) << "Failed to send UnregisterVm message to dispatcher service";
      return false;
    }
  
    dbus::MessageReader reader(dbus_response.get());
    vm_tools::plugin_dispatcher::UnregisterVmResponse response;
    if (!reader.PopArrayOfBytesAsProto(&response)) {
      LOG(ERROR) << "Failed to parse UnregisterVmResponse protobuf";
      return false;
    }
  
    if (response.error() != vm_tools::plugin_dispatcher::VM_SUCCESS) {
      LOG(ERROR) << "Failed to unregister VM: " << std::hex << std::showbase
                 << response.error() << " (" << response.result_code() << ")";
      return false;
    }
  
    return true;
  }
  
  bool IsVmRegistered(scoped_refptr<dbus::Bus> bus,
                      dbus::ObjectProxy* proxy,
                      const VmId& vm_id,
                      bool* result) {
    LOG(INFO) << "Checking whether VM " << vm_id << " is registered";
  
    base::Optional<vm_tools::plugin_dispatcher::VmInfo> info;
    if (!GetVmInfo(bus, proxy, vm_id, &info))
      return false;
  
    *result = info.has_value();
    return true;
  }
  
  bool IsVmShutDown(scoped_refptr<dbus::Bus> bus,
                    dbus::ObjectProxy* proxy,
                    const VmId& vm_id,
                    bool* result) {
    LOG(INFO) << "Checking whether VM " << vm_id << " is shut down";
  
    base::Optional<vm_tools::plugin_dispatcher::VmInfo> info;
    if (!GetVmInfo(bus, proxy, vm_id, &info))
      return false;
  
    *result =
        info.has_value() &&
        info.value().state() == vm_tools::plugin_dispatcher::VM_STATE_STOPPED;
    return true;
  }
  
  VmOpResult ShutdownVm(scoped_refptr<dbus::Bus> bus,
                        dbus::ObjectProxy* proxy,
                        const VmId& vm_id) {
    LOG(INFO) << "Shutting down VM " << vm_id;
  
    dbus::MethodCall method_call(
        vm_tools::plugin_dispatcher::kVmPluginDispatcherInterface,
        vm_tools::plugin_dispatcher::kStopVmMethod);
    dbus::MessageWriter writer(&method_call);
  
    vm_tools::plugin_dispatcher::StopVmRequest request;
  
    request.set_owner_id(vm_id.owner_id());
    request.set_vm_name_uuid(vm_id.name());
    // Allow request to fail if VM is busy.
    request.set_noforce(true);
  
    if (!writer.AppendProtoAsArrayOfBytes(request)) {
      LOG(ERROR) << "Failed to encode StopVmRequest protobuf";
      return VmOpResult::INTERNAL_ERROR;
    }
  
    dbus::ScopedDBusError dbus_error;
    std::unique_ptr<dbus::Response> dbus_response =
        brillo::dbus_utils::CallDBusMethodWithErrorResponse(
            bus, proxy, &method_call, kVmShutdownTimeout.InMilliseconds(),
            &dbus_error);
    if (!dbus_response) {
      if (dbus_error.is_set() &&
          strcmp(dbus_error.name(), DBUS_ERROR_SERVICE_UNKNOWN) == 0) {
        LOG(ERROR) << "Failed to send ShutdownVm request to dispatcher: service "
                      "unavailable";
        return VmOpResult::DISPATCHER_NOT_AVAILABLE;
      } else if (dbus_error.is_set() &&
                 strcmp(dbus_error.name(), DBUS_ERROR_NO_REPLY) == 0) {
        LOG(ERROR) << "ShutdownVm request to dispatcher timed out";
        return VmOpResult::DISPATCHER_TIMEOUT;
      } else {
        LOG(ERROR) << "Failed to send ShutdownVm message to dispatcher service";
        return VmOpResult::INTERNAL_ERROR;
      }
    }
  
    dbus::MessageReader reader(dbus_response.get());
    vm_tools::plugin_dispatcher::StopVmResponse response;
    if (!reader.PopArrayOfBytesAsProto(&response)) {
      LOG(ERROR) << "Failed to parse StopVmResponse protobuf";
      return VmOpResult::INTERNAL_ERROR;
    }
  
    return ConvertDispatcherResult(response.error(), response.result_code());
  }
  
  VmOpResult SuspendVm(scoped_refptr<dbus::Bus> bus,
                       dbus::ObjectProxy* proxy,
                       const VmId& vm_id) {
    LOG(INFO) << "Suspending VM " << vm_id;
  
    dbus::MethodCall method_call(
        vm_tools::plugin_dispatcher::kVmPluginDispatcherInterface,
        vm_tools::plugin_dispatcher::kSuspendVmMethod);
    dbus::MessageWriter writer(&method_call);
  
    vm_tools::plugin_dispatcher::SuspendVmRequest request;
  
    request.set_owner_id(vm_id.owner_id());
    request.set_vm_name_uuid(vm_id.name());
  
    if (!writer.AppendProtoAsArrayOfBytes(request)) {
      LOG(ERROR) << "Failed to encode SuspendVmRequest protobuf";
      return VmOpResult::INTERNAL_ERROR;
    }
  
    dbus::ScopedDBusError dbus_error;
    std::unique_ptr<dbus::Response> dbus_response =
        brillo::dbus_utils::CallDBusMethodWithErrorResponse(
            bus, proxy, &method_call, kVmSuspendTimeout.InMilliseconds(),
            &dbus_error);
    if (!dbus_response) {
      if (dbus_error.is_set() &&
          strcmp(dbus_error.name(), DBUS_ERROR_SERVICE_UNKNOWN) == 0) {
        return VmOpResult::DISPATCHER_NOT_AVAILABLE;
      } else if (dbus_error.is_set() &&
                 strcmp(dbus_error.name(), DBUS_ERROR_NO_REPLY) == 0) {
        return VmOpResult::DISPATCHER_TIMEOUT;
      } else {
        LOG(ERROR) << "Failed to send SuspendVm message to dispatcher service";
        return VmOpResult::INTERNAL_ERROR;
      }
    }
  
    dbus::MessageReader reader(dbus_response.get());
    vm_tools::plugin_dispatcher::SuspendVmResponse response;
    if (!reader.PopArrayOfBytesAsProto(&response)) {
      LOG(ERROR) << "Failed to parse SuspendVmResponse protobuf";
      return VmOpResult::INTERNAL_ERROR;
    }
  
    return ConvertDispatcherResult(response.error(), response.result_code());
  }
  
  void RegisterVmToolsChangedCallbacks(
      dbus::ObjectProxy* proxy,
      dbus::ObjectProxy::SignalCallback cb,
      dbus::ObjectProxy::OnConnectedCallback on_connected_cb) {
    proxy->ConnectToSignal(
        vm_tools::plugin_dispatcher::kVmPluginDispatcherServiceName,
        vm_tools::plugin_dispatcher::kVmToolsStateChangedSignal, cb,
        std::move(on_connected_cb));
  }
  
  bool ParseVmToolsChangedSignal(dbus::Signal* signal,
                                 std::string* owner_id,
                                 std::string* vm_name,
                                 bool* running) {
    DCHECK_EQ(signal->GetInterface(),
              vm_tools::plugin_dispatcher::kVmPluginDispatcherInterface);
    DCHECK_EQ(signal->GetMember(),
              vm_tools::plugin_dispatcher::kVmToolsStateChangedSignal);
  
    vm_tools::plugin_dispatcher::VmToolsStateChangedSignal message;
    dbus::MessageReader reader(signal);
    if (!reader.PopArrayOfBytesAsProto(&message)) {
      LOG(ERROR) << "Failed to parse VmToolsStateChangedSignal from DBus Signal";
      return false;
    }
  
    auto state = message.vm_tools_state();
    LOG(INFO) << "Tools raw state: " << state;
  
    *owner_id = message.owner_id();
    *vm_name = message.vm_name();
    *running = state == vm_tools::plugin_dispatcher::VM_TOOLS_STATE_INSTALLED;
    return true;
  }
  
  }  // namespace dispatcher
  }  // namespace pvm
  }  // namespace concierge
  }  // namespace vm_tools
  