blob: c6f7552b517fc5809d44be84c03b35de1f964157 [file] [log] [blame]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "vm_tools/cicerone/shadercached_helper.h"
#include <memory>
#include <utility>
#include <base/logging.h>
#include <base/strings/stringprintf.h>
#include <base/synchronization/waitable_event.h>
#include <dbus/message.h>
#include <dbus/error.h>
#include <dbus/shadercached/dbus-constants.h>
#include <dbus/dlcservice/dbus-constants.h>
#include <dlcservice/proto_bindings/dlcservice.pb.h>
#include <shadercached/proto_bindings/shadercached.pb.h>
#include <vm_protos/proto_bindings/container_host.pb.h>
#include <vm_protos/proto_bindings/vm_host.pb.h>
namespace vm_tools::cicerone {
namespace {
void ShaderCacheMountStatusChanged(
std::string* error_out,
base::WaitableEvent* event,
bool expected_mount,
const shadercached::ShaderCacheMountStatus& mount_status,
bool was_replaced) {
if (was_replaced) {
*error_out = "Another garcon call overrode the waiting request";
} else if (!mount_status.error().empty()) {
*error_out = mount_status.error();
} else if (mount_status.mounted() == expected_mount) {
LOG(INFO) << "Shader cache successfully "
<< (expected_mount ? "mounted" : "unmounted");
*error_out = "";
} else {
// |mounted| does not equate to |expected_mount| despite having no error
LOG(WARNING) << "Unexpected mount status mismatch for "
<< mount_status.vm_name();
*error_out =
base::StringPrintf("Unexpected mount status, expected: %d, got %d",
expected_mount, mount_status.mounted());
}
event->Signal();
}
} // namespace
ShadercachedHelper::ShadercachedHelper(dbus::ObjectProxy* shadercached_proxy) {
connected_ = false;
shadercached_proxy->ConnectToSignal(
shadercached::kShaderCacheInterface,
shadercached::kShaderCacheMountStatusChanged,
base::BindRepeating(&ShadercachedHelper::MountStatusChanged,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&ShadercachedHelper::ConnectedToShadercached,
weak_ptr_factory_.GetWeakPtr()));
}
void ShadercachedHelper::ConnectedToShadercached(const std::string& interface,
const std::string& signal,
bool success) {
connected_ = success;
LOG_IF(ERROR, !success)
<< "Failed to create ShadercachedHelper, connection to signal failed";
}
void ShadercachedHelper::InstallShaderCache(
const std::string& owner_id,
const std::string& vm_name,
const vm_tools::container::InstallShaderCacheRequest* request,
std::string* error_out,
base::WaitableEvent* event,
dbus::ObjectProxy* shadercached_proxy) {
LOG(INFO) << "InstallShaderCache called";
if (!connected_) {
*error_out = "Not connected to shadercached signals";
event->Signal();
return;
}
ShadercachedHelper::CallbackCondition condition{
.vm_name = vm_name,
.owner_id = owner_id,
.steam_app_id = request->steam_app_id()};
bool callback_added = false;
if (request->wait()) {
// Only add call back if request is waiting for completion
callback_added =
AddCallback(condition, /*expected_mount=*/true, error_out, event);
if (!callback_added) {
LOG(WARNING) << "Failed to add callback for install, still attempting "
"install/mount";
}
}
dbus::MethodCall method_call(shadercached::kShaderCacheInterface,
shadercached::kInstallMethod);
dbus::MessageWriter shadercached_writer(&method_call);
shadercached::InstallRequest shader_request;
shader_request.set_mount(request->mount());
shader_request.set_steam_app_id(request->steam_app_id());
shader_request.set_vm_name(vm_name);
shader_request.set_vm_owner_id(owner_id);
shadercached_writer.AppendProtoAsArrayOfBytes(shader_request);
base::expected<std::unique_ptr<dbus::Response>, dbus::Error> dbus_response =
shadercached_proxy->CallMethodAndBlock(
&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
if (!dbus_response.has_value()) {
dbus::Error error = std::move(dbus_response.error());
*error_out =
base::StringPrintf("%s %s: %s", shadercached::kShaderCacheInterface,
error.name().c_str(), error.message().c_str());
if (request->wait()) {
mount_callbacks_.erase(condition);
}
event->Signal();
return;
}
if (!dbus_response.value()) {
*error_out = "Failed to get non-null response.";
if (request->wait()) {
mount_callbacks_.erase(condition);
}
event->Signal();
return;
}
if (!request->wait()) {
// Signal if we don't have to wait, we don't have to worry about Install
// response.
event->Signal();
return;
}
shadercached::InstallResponse response;
auto reader = dbus::MessageReader(dbus_response.value().get());
if (!reader.PopArrayOfBytesAsProto(&response)) {
*error_out = "Failed to parse InstallResponse";
event->Signal();
return;
}
if (response.mounted()) {
// If shader cache is already mounted, don't wait for mount signal
if (callback_added) {
mount_callbacks_.erase(condition);
}
event->Signal();
return;
}
if (!callback_added) {
// If callback wasn't added, signal now. If callback's been added,
// signalling will happen on ShaderCacheMountStatusChanged.
*error_out =
"Unable to notify on mount completion, still queued install/mount";
event->Signal();
}
}
void ShadercachedHelper::UninstallShaderCache(
const std::string& owner_id,
const std::string& vm_name,
const vm_tools::container::UninstallShaderCacheRequest* request,
std::string* error_out,
base::WaitableEvent* event,
dbus::ObjectProxy* shadercached_proxy_) {
LOG(INFO) << "UninstallShaderCache called";
if (!connected_) {
*error_out = "Not connected to shadercached signals";
event->Signal();
return;
}
dbus::MethodCall method_call(shadercached::kShaderCacheInterface,
shadercached::kUninstallMethod);
dbus::MessageWriter shadercached_writer(&method_call);
shadercached::UninstallRequest shader_request;
shader_request.set_steam_app_id(request->steam_app_id());
shadercached_writer.AppendProtoAsArrayOfBytes(shader_request);
base::expected<std::unique_ptr<dbus::Response>, dbus::Error> dbus_response =
shadercached_proxy_->CallMethodAndBlock(
&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
if (!dbus_response.has_value()) {
dbus::Error error = std::move(dbus_response.error());
*error_out =
base::StringPrintf("%s %s: %s", shadercached::kShaderCacheInterface,
error.name().c_str(), error.message().c_str());
event->Signal();
return;
}
if (!dbus_response.value()) {
*error_out = "Failed to get non-null response.";
event->Signal();
return;
}
// We do not need to wait for unmount signal here. Unmount was completed in
// the D-Bus call above and unmount errors would have appeared in D-Bus
// errors.
// This does not wait for DLC uninstallation to complete because game
// uninstalls are independent and orthogonal to DLC uninstalls.
*error_out = "";
event->Signal();
}
void ShadercachedHelper::UnmountShaderCache(
const std::string& owner_id,
const std::string& vm_name,
const vm_tools::container::UnmountShaderCacheRequest* request,
std::string* error_out,
base::WaitableEvent* event,
dbus::ObjectProxy* shadercached_proxy) {
LOG(INFO) << "UnmountShaderCache called";
if (!connected_) {
*error_out = "Not connected to shadercached signals";
event->Signal();
return;
}
ShadercachedHelper::CallbackCondition condition{
.vm_name = vm_name,
.owner_id = owner_id,
.steam_app_id = request->steam_app_id()};
if (request->wait() &&
!AddCallback(condition, /*expected_mount=*/false, error_out, event)) {
event->Signal();
return;
}
dbus::MethodCall method_call(shadercached::kShaderCacheInterface,
shadercached::kUnmountMethod);
dbus::MessageWriter shadercached_writer(&method_call);
shadercached::UnmountRequest shader_request;
shader_request.set_steam_app_id(request->steam_app_id());
shader_request.set_vm_name(vm_name);
shader_request.set_vm_owner_id(owner_id);
shadercached_writer.AppendProtoAsArrayOfBytes(shader_request);
base::expected<std::unique_ptr<dbus::Response>, dbus::Error> dbus_response =
shadercached_proxy->CallMethodAndBlock(
&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
if (!dbus_response.has_value()) {
dbus::Error error = std::move(dbus_response.error());
*error_out =
base::StringPrintf("%s %s: %s", shadercached::kShaderCacheInterface,
error.name().c_str(), error.message().c_str());
if (request->wait()) {
mount_callbacks_.erase(condition);
}
event->Signal();
return;
}
if (!dbus_response.value()) {
*error_out = "Failed to get non-null response.";
if (request->wait()) {
mount_callbacks_.erase(condition);
}
event->Signal();
return;
}
if (!request->wait()) {
// Only signal if we don't have to wait. If wait is set, signal will happen
// at ShaderCacheMountStatusChanged.
*error_out = "";
event->Signal();
}
}
bool ShadercachedHelper::AddCallback(const CallbackCondition& condition,
bool expected_mount,
std::string* error_out,
base::WaitableEvent* event_to_notify) {
// If there is already a process waiting for the game to finish downloading,
// send error to the existing process and replace the waiting with the new
// one.
//
// This is to prevent memory increase from misbehaving user (ex. spamming game
// launches) when DLC download is taking time.
//
// On game-launch fossilize, two processes are 'racing' - garcon client that
// waits for shader cache download+mount and on-device foz blob processing.
// If one of them finishes, the other process is killed.
// Game-launch fossilize process may run multiple times in sequence for dx12
// games. This means if garcon client is waiting but on-device processing
// finishes, garcon client is killed and a new one is created.
// Hence, we have to make the client always wait for the latest garcon call
// for the game.
if (mount_callbacks_.find(condition) != mount_callbacks_.end()) {
LOG(WARNING) << "Already installing shader cache for the Steam app, "
<< "replacing the callback";
shadercached::ShaderCacheMountStatus unused_mount_status;
std::move(mount_callbacks_[condition]).Run(unused_mount_status, true);
}
mount_callbacks_[condition] =
base::BindOnce(&ShaderCacheMountStatusChanged, error_out, event_to_notify,
expected_mount);
return true;
}
void ShadercachedHelper::MountStatusChanged(dbus::Signal* signal) {
shadercached::ShaderCacheMountStatus mount_status;
auto reader = dbus::MessageReader(signal);
if (!reader.PopArrayOfBytesAsProto(&mount_status)) {
LOG(WARNING) << "Failed to parse ShaderCacheMountStatus";
return;
}
// Generate the key for this signal and find it
CallbackCondition condition{
.vm_name = mount_status.vm_name(),
.owner_id = mount_status.vm_owner_id(),
.steam_app_id = mount_status.steam_app_id(),
};
if (mount_callbacks_.find(condition) != mount_callbacks_.end()) {
LOG(INFO) << "Notifying shader cache mount callback for VM "
<< mount_status.vm_name();
std::move(mount_callbacks_[condition]).Run(mount_status, false);
mount_callbacks_.erase(condition);
}
}
} // namespace vm_tools::cicerone