ref:4ffcec4bcc0d10cc3d3a74e9bfc49103d458369b

feat(linux): reach QEMU's display peer to peer through QMP or libvirt

System libvirt starts a private dbus-daemon per domain that only admits the domain's own user (and root from libvirt 11.2), so a Sunshine running as its own user can't use bus mode there. QEMU's -display dbus,p2p=on instead accepts display clients handed to it with QMP add_client protocol=@dbus-display, which libvirt exposes as virDomainOpenGraphicsFD with the socket labelled for the domain. qemu_dbus_address now also takes: - qmp:<socket>: Sunshine sends one end of a socket pair with QMP getfd and add_client, then runs the D-Bus client handshake on the other end (QEMU's pid comes from SO_PEERCRED). - libvirt:<domain>[?uri=<uri>]: libvirt.so.0 is loaded at run time; for p2p='yes' Sunshine calls virDomainOpenGraphicsFD, otherwise it uses the domain's bus address from the live XML (session libvirt). session_t::connect_peer() creates proxies without a bus name, skips the name owner check and name watch, and relies on the connection's closed signal. Every reconnect makes a new add_client connection. Tests: the fake QEMU gained a p2p mode, a fake QMP server exercises the monitor path, and a fake libvirt module (dlopen()ed by path) the libvirt path. Refs #6 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SHA: 4ffcec4bcc0d10cc3d3a74e9bfc49103d458369b
Author: Cole Christensen <cole.christensen@gmail.com>
Date: 2026-09-13 01:15
Parents: 1c97e20
9 files changed +1640 -37
Type
cmake/compile_definitions/linux.cmake +2 −0
@@ -333,6 +333,8 @@
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/input.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/keymap.h"
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/keymap.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/p2p.h"
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/p2p.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/pixel_format.h"
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/pixel_format.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/render_node.h"
src/platform/linux/qemu/p2p.cpp +487 −0
@@ -1,0 +1,487 @@
/**
* @file src/platform/linux/qemu/p2p.cpp
* @brief Definitions for reaching a QEMU D-Bus display without a message bus.
*/
// class header include
#include "p2p.h"
// standard includes
#include <atomic>
#include <cctype>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <map>
#include <memory>
#include <mutex>
// platform includes
#include <dlfcn.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
// lib includes
#include <nlohmann/json.hpp>
using namespace std::literals;
namespace qemu {
namespace {
constexpr auto qmp_prefix = "qmp:"sv; ///< Prefix of QMP socket addresses.
constexpr auto libvirt_prefix = "libvirt:"sv; ///< Prefix of libvirt domain addresses.
constexpr auto uri_option = "?uri="sv; ///< Separates the domain from the libvirt URI.
/**
* @brief Describe the current `errno`.
*
* @return Error text.
*/
std::string errno_text() {
return std::strerror(errno);
}
/**
* @brief A line-oriented QMP client over a Unix socket with a deadline per operation.
*/
class qmp_client_t {
public:
/**
* @brief Prepare a client with a timeout for each operation.
*
* @param timeout Deadline for connecting, each send and each reply.
*/
explicit qmp_client_t(std::chrono::milliseconds timeout):
timeout {timeout} {
}
/**
* @brief Connect to a QMP server socket.
*
* @param path Socket path.
* @param error Set on failure.
* @return True when connected.
*/
bool connect_to(const std::string &path, std::string &error) {
sockaddr_un addr {};
if (path.size() >= sizeof(addr.sun_path)) {
error = "QMP socket path [" + path + "] is too long";
return false;
}
socket = fd_t {::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0)};
if (socket.get() < 0) {
error = "socket(): " + errno_text();
return false;
}
addr.sun_family = AF_UNIX;
std::memcpy(addr.sun_path, path.c_str(), path.size());
if (::connect(socket.get(), (sockaddr *) &addr, sizeof(addr)) != 0) {
error = "couldn't connect to QMP socket [" + path + "]: " + errno_text();
return false;
}
return true;
}
/**
* @brief Read the process id of the server side of the socket.
*
* @return Peer process id, or nothing when unknown.
*/
std::optional<std::uint32_t> peer_pid() const {
ucred credentials {};
socklen_t length = sizeof(credentials);
if (getsockopt(socket.get(), SOL_SOCKET, SO_PEERCRED, &credentials, &length) != 0 || credentials.pid <= 0) {
return std::nullopt;
}
return (std::uint32_t) credentials.pid;
}
/**
* @brief Read the next message that isn't an asynchronous event.
*
* @param error Set on failure.
* @return The message, or nothing on timeout, a closed socket or invalid JSON.
*/
std::optional<nlohmann::json> read_message(std::string &error) {
const auto deadline = std::chrono::steady_clock::now() + timeout;
while (true) {
for (auto end = buffer.find('\n'); end != std::string::npos; end = buffer.find('\n')) {
auto line = buffer.substr(0, end);
buffer.erase(0, end + 1);
if (line.find_first_not_of(" \t\r") == std::string::npos) {
continue;
}
auto message = nlohmann::json::parse(line, nullptr, false);
if (message.is_discarded() || !message.is_object()) {
error = "invalid QMP message: " + line;
return std::nullopt;
}
if (message.contains("event")) {
continue;
}
return message;
}
const auto left = std::chrono::duration_cast<std::chrono::milliseconds>(deadline - std::chrono::steady_clock::now());
if (left <= 0ms) {
error = "no answer within "s + std::to_string(timeout.count()) + " ms";
return std::nullopt;
}
pollfd p {socket.get(), POLLIN, 0};
const auto ready = poll(&p, 1, (int) left.count());
if (ready < 0 && errno == EINTR) {
continue;
}
if (ready <= 0) {
continue;
}
char data[4096];
const auto n = recv(socket.get(), data, sizeof(data), 0);
if (n < 0 && errno == EINTR) {
continue;
}
if (n <= 0) {
error = n == 0 ? "QEMU closed the QMP connection"s : "recv(): " + errno_text();
return std::nullopt;
}
buffer.append(data, (std::size_t) n);
}
}
/**
* @brief Send a command, optionally with a descriptor, and wait for its reply.
*
* @param command Command object.
* @param error Set on failure, to QMP's `desc` for an error reply.
* @param fd Descriptor to send with SCM_RIGHTS, or -1.
* @return True for a `return` reply.
*/
bool execute(const nlohmann::json &command, std::string &error, int fd = -1) {
const auto text = command.dump() + "\n";
msghdr msg {};
iovec iov {(void *) text.data(), text.size()};
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
char control[CMSG_SPACE(sizeof(int))] {};
if (fd >= 0) {
msg.msg_control = control;
msg.msg_controllen = sizeof(control);
auto cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
std::memcpy(CMSG_DATA(cmsg), &fd, sizeof(int));
}
pollfd p {socket.get(), POLLOUT, 0};
if (poll(&p, 1, (int) timeout.count()) <= 0) {
error = "QMP socket not writable";
return false;
}
// QMP commands are short; the socket buffer takes them in one message
if (sendmsg(socket.get(), &msg, MSG_NOSIGNAL) != (ssize_t) text.size()) {
error = "sendmsg(): " + errno_text();
return false;
}
auto reply = read_message(error);
if (!reply) {
return false;
}
if (reply->contains("return")) {
return true;
}
if (auto e = reply->find("error"); e != reply->end() && e->is_object()) {
error = e->value("desc", e->dump());
} else {
error = "unexpected QMP reply: " + reply->dump();
}
return false;
}
std::chrono::milliseconds timeout; ///< Deadline per operation.
fd_t socket; ///< Connected QMP socket.
std::string buffer; ///< Received bytes not yet split into lines.
};
/**
* @brief Decode the XML entities libvirt writes into attribute values.
*
* @param value Attribute value as written.
* @return Decoded value.
*/
std::string xml_unescape(std::string_view value) {
static const std::pair<std::string_view, char> entities[] {{"&amp;"sv, '&'}, {"&apos;"sv, '\''}, {"&quot;"sv, '"'}, {"&lt;"sv, '<'}, {"&gt;"sv, '>'}};
std::string result;
for (std::size_t i = 0; i < value.size();) {
bool decoded = false;
if (value[i] == '&') {
for (const auto &[entity, character] : entities) {
if (value.substr(i, entity.size()) == entity) {
result.push_back(character);
i += entity.size();
decoded = true;
break;
}
}
}
if (!decoded) {
result.push_back(value[i++]);
}
}
return result;
}
/**
* @brief Read one attribute of an XML start tag.
*
* @param tag Start tag text from `<` to `>`.
* @param name Attribute name.
* @return Decoded value, or nothing when the attribute is missing.
*/
std::optional<std::string> xml_attribute(std::string_view tag, std::string_view name) {
for (std::size_t pos = tag.find(name); pos != std::string_view::npos; pos = tag.find(name, pos + 1)) {
if (pos == 0 || !std::isspace((unsigned char) tag[pos - 1])) {
continue;
}
auto cursor = pos + name.size();
while (cursor < tag.size() && std::isspace((unsigned char) tag[cursor])) {
++cursor;
}
if (cursor >= tag.size() || tag[cursor] != '=') {
continue;
}
++cursor;
while (cursor < tag.size() && std::isspace((unsigned char) tag[cursor])) {
++cursor;
}
if (cursor >= tag.size() || (tag[cursor] != '\'' && tag[cursor] != '"')) {
continue;
}
const auto quote = tag[cursor];
const auto end = tag.find(quote, cursor + 1);
if (end == std::string_view::npos) {
return std::nullopt;
}
return xml_unescape(tag.substr(cursor + 1, end - cursor - 1));
}
return std::nullopt;
}
/**
* @brief The libvirt functions Sunshine uses, loaded at run time.
* @details The signatures are libvirt's stable public API; the handles are opaque.
*/
struct libvirt_api_t {
void *(*connect_open)(const char *name) {nullptr}; ///< virConnectOpen.
int (*connect_close)(void *conn) {nullptr}; ///< virConnectClose.
void *(*domain_lookup_by_name)(void *conn, const char *name) {nullptr}; ///< virDomainLookupByName.
int (*domain_free)(void *domain) {nullptr}; ///< virDomainFree.
char *(*domain_get_xml_desc)(void *domain, unsigned int flags) {nullptr}; ///< virDomainGetXMLDesc.
int (*domain_open_graphics_fd)(void *domain, unsigned int idx, unsigned int flags) {nullptr}; ///< virDomainOpenGraphicsFD.
const char *(*get_last_error_message)() {nullptr}; ///< virGetLastErrorMessage.
};
/**
* @brief Load libvirt once per library path; the library is never unloaded.
*
* @param library Library to load.
* @param error Set on failure.
* @return Loaded functions, or nullptr on failure.
*/
const libvirt_api_t *load_libvirt(const std::string &library, std::string &error) {
static std::mutex mutex;
static std::map<std::string, std::unique_ptr<libvirt_api_t>> loaded;
std::lock_guard lock {mutex};
if (auto it = loaded.find(library); it != loaded.end()) {
return it->second.get();
}
auto handle = dlopen(library.c_str(), RTLD_NOW | RTLD_LOCAL);
if (!handle) {
error = "couldn't load " + library + " (is libvirt installed?): " + dlerror();
return nullptr;
}
auto api = std::make_unique<libvirt_api_t>();
const std::pair<void **, const char *> symbols[] {
{(void **) &api->connect_open, "virConnectOpen"},
{(void **) &api->connect_close, "virConnectClose"},
{(void **) &api->domain_lookup_by_name, "virDomainLookupByName"},
{(void **) &api->domain_free, "virDomainFree"},
{(void **) &api->domain_get_xml_desc, "virDomainGetXMLDesc"},
{(void **) &api->domain_open_graphics_fd, "virDomainOpenGraphicsFD"},
{(void **) &api->get_last_error_message, "virGetLastErrorMessage"},
};
for (const auto &[slot, name] : symbols) {
*slot = dlsym(handle, name);
if (!*slot) {
error = library + " has no "s + name + " (libvirt 1.2.8 or newer is needed)";
dlclose(handle);
return nullptr;
}
}
return loaded.emplace(library, std::move(api)).first->second.get();
}
} // namespace
std::optional<display_address_t> parse_display_address(std::string_view address, std::string &error) {
display_address_t result;
if (address.starts_with(qmp_prefix)) {
result.kind = display_address_t::kind_e::qmp;
result.qmp_socket = address.substr(qmp_prefix.size());
if (result.qmp_socket.empty()) {
error = "qemu_dbus_address [qmp:] needs the QMP socket path, for example qmp:/run/sunshine-qemu/vm.qmp";
return std::nullopt;
}
return result;
}
if (address.starts_with(libvirt_prefix)) {
result.kind = display_address_t::kind_e::libvirt;
auto rest = address.substr(libvirt_prefix.size());
if (auto option = rest.find('?'); option != std::string_view::npos) {
if (!rest.substr(option).starts_with(uri_option)) {
error = "qemu_dbus_address [" + std::string {address} + "]: the only option after the domain is ?uri=<libvirt URI>";
return std::nullopt;
}
result.uri = rest.substr(option + uri_option.size());
rest = rest.substr(0, option);
}
result.domain = rest;
if (result.domain.empty()) {
error = "qemu_dbus_address [" + std::string {address} + "] needs a libvirt domain name, for example libvirt:win11";
return std::nullopt;
}
return result;
}
result.bus = address;
return result;
}
std::string describe(const display_address_t &address) {
switch (address.kind) {
case display_address_t::kind_e::qmp:
return "QMP socket [" + address.qmp_socket + ']';
case display_address_t::kind_e::libvirt:
return "libvirt domain [" + address.domain + "] on [" + (address.uri.empty() ? "default URI"s : address.uri) + ']';
case display_address_t::kind_e::bus:
default:
return address.bus.empty() ? "the session bus"s : "D-Bus address [" + address.bus + ']';
}
}
std::optional<peer_socket_t> qmp_add_client(const std::string &socket_path, std::chrono::milliseconds timeout, std::string &error) {
qmp_client_t qmp {timeout};
if (!qmp.connect_to(socket_path, error)) {
return std::nullopt;
}
std::string detail;
auto greeting = qmp.read_message(detail);
if (!greeting || !greeting->contains("QMP")) {
error = "no QMP greeting on [" + socket_path + "] (QEMU serves one QMP client at a time; is another one connected?): " + detail;
return std::nullopt;
}
if (!qmp.execute({{"execute", "qmp_capabilities"}}, detail)) {
error = "QMP capabilities negotiation failed: " + detail;
return std::nullopt;
}
int pair[2] = {-1, -1};
if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, pair) != 0) {
error = "socketpair(): " + errno_text();
return std::nullopt;
}
fd_t ours {pair[0]};
fd_t theirs {pair[1]};
static std::atomic<unsigned> counter {0};
const auto fdname = "sunshine-" + std::to_string(getpid()) + '-' + std::to_string(counter++);
if (!qmp.execute({{"execute", "getfd"}, {"arguments", {{"fdname", fdname}}}}, detail, theirs.get())) {
error = "QMP getfd failed: " + detail;
return std::nullopt;
}
theirs = fd_t {};
if (!qmp.execute({{"execute", "add_client"}, {"arguments", {{"protocol", "@dbus-display"}, {"fdname", fdname}}}}, detail)) {
error = "QMP add_client failed (QEMU needs -display dbus,p2p=on): " + detail;
std::string ignored;
qmp.execute({{"execute", "closefd"}, {"arguments", {{"fdname", fdname}}}}, ignored);
return std::nullopt;
}
return peer_socket_t {std::move(ours), qmp.peer_pid()};
}
std::optional<dbus_graphics_t> find_dbus_graphics(std::string_view domain_xml) {
constexpr auto open = "<graphics"sv;
std::size_t index = 0;
for (auto pos = domain_xml.find(open); pos != std::string_view::npos; pos = domain_xml.find(open, pos + 1)) {
const auto next = pos + open.size();
if (next >= domain_xml.size() || !(std::isspace((unsigned char) domain_xml[next]) || domain_xml[next] == '>' || domain_xml[next] == '/')) {
continue;
}
const auto end = domain_xml.find('>', next);
if (end == std::string_view::npos) {
break;
}
const auto tag = domain_xml.substr(pos, end - pos + 1);
if (xml_attribute(tag, "type") == "dbus") {
dbus_graphics_t graphics;
graphics.index = index;
graphics.p2p = xml_attribute(tag, "p2p") == "yes";
graphics.address = xml_attribute(tag, "address").value_or("");
return graphics;
}
++index;
}
return std::nullopt;
}
std::optional<libvirt_display_t> libvirt_open_display(const std::string &domain, const std::string &uri, std::string &error, const std::string &library) {
auto api = load_libvirt(library, error);
if (!api) {
return std::nullopt;
}
const auto where = uri.empty() ? "libvirt's default URI"s : '[' + uri + ']';
std::unique_ptr<void, int (*)(void *)> conn {api->connect_open(uri.empty() ? nullptr : uri.c_str()), api->connect_close};
if (!conn) {
error = "couldn't connect to " + where + ": " + api->get_last_error_message();
return std::nullopt;
}
std::unique_ptr<void, int (*)(void *)> dom {api->domain_lookup_by_name(conn.get(), domain.c_str()), api->domain_free};
if (!dom) {
error = "couldn't find domain [" + domain + "] on " + where + ": " + api->get_last_error_message();
return std::nullopt;
}
std::unique_ptr<char, void (*)(void *)> xml {api->domain_get_xml_desc(dom.get(), 0), std::free};
if (!xml) {
error = "couldn't read the XML of domain [" + domain + "]: " + api->get_last_error_message();
return std::nullopt;
}
auto graphics = find_dbus_graphics(xml.get());
if (!graphics) {
error = "domain [" + domain + "] has no <graphics type='dbus'> device";
return std::nullopt;
}
libvirt_display_t display;
if (!graphics->p2p) {
if (graphics->address.empty()) {
error = "domain [" + domain + "] has <graphics type='dbus'> without p2p='yes' and no bus address; is it running?";
return std::nullopt;
}
display.bus_address = graphics->address;
return display;
}
const auto fd = api->domain_open_graphics_fd(dom.get(), (unsigned int) graphics->index, 0);
if (fd < 0) {
error = "virDomainOpenGraphicsFD on domain [" + domain + "] failed: " + api->get_last_error_message();
return std::nullopt;
}
display.peer = peer_socket_t {fd_t {fd}, std::nullopt};
return display;
}
} // namespace qemu
src/platform/linux/qemu/p2p.h +122 −0
@@ -1,0 +1,122 @@
/**
* @file src/platform/linux/qemu/p2p.h
* @brief Declarations for reaching a QEMU D-Bus display without a message bus.
* @details QEMU started with `-display dbus,p2p=on` exports its display on connections handed to it
* with QMP `add_client protocol=@dbus-display`. Sunshine gets such a connection either from a QMP
* socket it may use (raw QEMU) or from libvirt's `virDomainOpenGraphicsFD`, which lets a process
* running as another user reach a VM of the system libvirt daemon. libvirt is loaded at run time, so
* Sunshine neither links nor requires it.
*/
#pragma once
// standard includes
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
// local includes
#include "session.h"
namespace qemu {
/**
* @brief How Sunshine reaches QEMU's display, parsed from `qemu_dbus_address`.
*/
struct display_address_t {
/**
* @brief Kind of address.
*/
enum class kind_e {
bus, ///< A D-Bus bus where QEMU owns `org.qemu` (`unix:path=...`, or empty for the session bus).
qmp, ///< `qmp:<socket>`: peer-to-peer connection added through a QMP socket.
libvirt, ///< `libvirt:<domain>[?uri=<uri>]`: the display of a libvirt domain.
};
kind_e kind {kind_e::bus}; ///< Kind of address.
std::string bus; ///< Bus address for `kind_e::bus`; empty means the session bus.
std::string qmp_socket; ///< QMP socket path for `kind_e::qmp`.
std::string domain; ///< Domain name for `kind_e::libvirt`.
std::string uri; ///< libvirt connection URI for `kind_e::libvirt`; empty uses libvirt's default.
};
/**
* @brief Parse a `qemu_dbus_address` value.
*
* @param address Configured value.
* @param error Set to a description when the value is invalid.
* @return Parsed address, or nothing when `qmp:` or `libvirt:` lacks its socket or domain.
*/
std::optional<display_address_t> parse_display_address(std::string_view address, std::string &error);
/**
* @brief Describe an address for log messages.
*
* @param address Parsed address.
* @return Human-readable description.
*/
std::string describe(const display_address_t &address);
/**
* @brief One end of a peer-to-peer connection that QEMU accepted as a display client.
*/
struct peer_socket_t {
fd_t fd; ///< Socket to run the D-Bus client handshake on.
std::optional<std::uint32_t> qemu_pid; ///< Process id of QEMU, when known.
};
/**
* @brief Hand one end of a new socket pair to QEMU with QMP `getfd` and `add_client protocol=@dbus-display`.
* @details QEMU accepts one QMP client at a time on a socket, so the connection is closed as soon
* as the display client is added. QEMU must run with `-display dbus,p2p=on`.
*
* @param socket_path Path of a QMP server socket (`-qmp unix:<path>,server=on,wait=off`).
* @param timeout Deadline for connecting and for each reply.
* @param error Set to a description on failure, including QMP's error message.
* @return The other end of the pair with QEMU's process id, or nothing on failure.
*/
std::optional<peer_socket_t> qmp_add_client(const std::string &socket_path, std::chrono::milliseconds timeout, std::string &error);
/**
* @brief The `<graphics type='dbus'>` device of a libvirt domain.
*/
struct dbus_graphics_t {
std::size_t index {0}; ///< Index among the domain's `<graphics>` devices, as `virDomainOpenGraphicsFD` expects.
bool p2p {false}; ///< Whether the device has `p2p='yes'`.
std::string address; ///< The `address` attribute (live XML of a running bus-mode domain), unescaped.
};
/**
* @brief Find the D-Bus display device in a libvirt domain XML description.
*
* @param domain_xml Output of `virDomainGetXMLDesc`.
* @return The device, or nothing when the domain has no `<graphics type='dbus'>`.
*/
std::optional<dbus_graphics_t> find_dbus_graphics(std::string_view domain_xml);
/**
* @brief What libvirt gave Sunshine for a domain's D-Bus display.
* @details Exactly one of the members is set: a peer-to-peer socket for `p2p='yes'`, else the
* address of the private bus libvirt started for the domain.
*/
struct libvirt_display_t {
std::optional<peer_socket_t> peer; ///< Socket from `virDomainOpenGraphicsFD`, for `p2p='yes'`.
std::string bus_address; ///< Bus address from the live domain XML, for bus mode.
};
/**
* @brief Open the D-Bus display of a running libvirt domain.
* @details Loads libvirt at run time, connects to `uri`, looks up the domain and reads its live XML.
* For `p2p='yes'` it calls `virDomainOpenGraphicsFD`, which needs the `domain:open_graphics`
* permission; libvirt passes the other end to QEMU with `add_client` and labels the socket for
* the domain. Blocks while libvirt answers.
*
* @param domain Domain name.
* @param uri Connection URI, or empty for libvirt's default.
* @param error Set to a description on failure, including libvirt's error message.
* @param library libvirt shared library to load.
* @return The display, or nothing on failure.
*/
std::optional<libvirt_display_t> libvirt_open_display(const std::string &domain, const std::string &uri, std::string &error, const std::string &library = "libvirt.so.0");
} // namespace qemu
src/platform/linux/qemu/session.cpp +141 −15
@@ -25,6 +25,7 @@
#include <unistd.h>
// local includes
#include "p2p.h"
#include "src/logging.h"
// generated includes
@@ -511,6 +512,27 @@
}
/**
* @brief Run the client handshake on a peer-to-peer socket and read the VM and console properties.
*
* @param fd Socket QEMU accepted with `add_client`.
* @param pid Process id of QEMU, when known.
* @return True when QEMU answered.
*/
bool connect_peer(fd_t fd, std::optional<std::uint32_t> pid) {
peer_to_peer = true;
peer_pid = pid;
bool ok = false;
deadline_t deadline {timeout};
loop.invoke([&]() {
ok = connect_peer_on_loop(std::move(fd), deadline.cancellable);
if (!ok) {
disconnect_bus();
}
});
return ok;
}
/**
* @brief Connect to the bus and read the VM and console properties.
*
* @param address Bus address, or empty for the session bus.
@@ -553,6 +575,9 @@
}
std::optional<std::uint32_t> qemu_pid() const override {
if (peer_to_peer) {
return peer_pid;
}
std::optional<std::uint32_t> pid;
loop.invoke([&]() {
if (!connection) {
@@ -636,6 +661,15 @@
}
/**
* @brief Name to address QEMU's objects with.
*
* @return `org.qemu` on a bus, nullptr on a peer-to-peer connection, where names don't exist.
*/
[[nodiscard]] const char *qemu_name() const {
return peer_to_peer ? nullptr : bus_name;
}
/**
* @brief Thread that owns the bus connection and makes blocking calls to QEMU.
*/
mutable loop_thread_t loop;
@@ -677,10 +711,51 @@
g_clear_error(&err);
return false;
}
return discover_on_loop("D-Bus address [" + resolved + ']', cancellable);
}
/**
* @brief Run the client handshake on a peer-to-peer socket; runs on the loop thread.
*
* @param fd Socket QEMU accepted with `add_client`.
* @param cancellable Cancels blocking calls when the deadline passes.
* @return True when QEMU answered.
*/
bool connect_peer_on_loop(fd_t fd, GCancellable *cancellable) {
GError *err = nullptr;
auto socket = g_socket_new_from_fd(fd.get(), &err);
if (!socket) {
BOOST_LOG(error) << "qemu: invalid peer-to-peer socket: "sv << err->message;
g_clear_error(&err);
return false;
}
fd.release();
auto stream = g_socket_connection_factory_create_connection(socket);
g_object_unref(socket);
// QEMU is the authentication server of add_client connections (ui/dbus.c)
connection = g_dbus_connection_new_sync(G_IO_STREAM(stream), nullptr, G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT, nullptr, cancellable, &err);
g_object_unref(stream);
if (!connection) {
BOOST_LOG(error) << "qemu: D-Bus handshake with QEMU on the peer-to-peer connection failed: "sv << err->message;
g_clear_error(&err);
return false;
}
return discover_on_loop("peer-to-peer connection", cancellable);
}
/**
* @brief Create the VM and console proxies on the new connection; runs on the loop thread.
*
* @param where Description of the connection for log messages.
* @param cancellable Cancels blocking calls when the deadline passes.
* @return True when QEMU's VM object answered.
*/
bool discover_on_loop(const std::string &where, GCancellable *cancellable) {
GError *err = nullptr;
g_dbus_connection_set_exit_on_close(connection, FALSE);
closed_handler = g_signal_connect(connection, "closed", G_CALLBACK(&session_impl_t::on_bus_closed), this);
vm_proxy = qemu_dbus_display1_vm_proxy_new_sync(connection, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, bus_name, vm_path, cancellable, &err);
vm_proxy = qemu_dbus_display1_vm_proxy_new_sync(connection, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, qemu_name(), vm_path, cancellable, &err);
if (!vm_proxy) {
BOOST_LOG(error) << "qemu: couldn't create VM proxy: "sv << err->message;
g_clear_error(&err);
@@ -688,23 +763,25 @@
}
g_dbus_proxy_set_default_timeout(G_DBUS_PROXY(vm_proxy), timeout_ms);
auto owner = g_dbus_proxy_get_name_owner(G_DBUS_PROXY(vm_proxy));
if (!owner) {
BOOST_LOG(error) << "qemu: no QEMU owns ["sv << bus_name << "] on ["sv << resolved << ']';
return false;
if (!peer_to_peer) {
auto owner = g_dbus_proxy_get_name_owner(G_DBUS_PROXY(vm_proxy));
if (!owner) {
BOOST_LOG(error) << "qemu: no QEMU owns ["sv << bus_name << "] on "sv << where;
return false;
}
g_free(owner);
}
g_free(owner);
auto ids = qemu_dbus_display1_vm_get_console_ids(vm_proxy);
if (!ids) {
BOOST_LOG(error) << "qemu: VM has no ConsoleIDs property on "sv << where << "; is -display dbus enabled?"sv;
BOOST_LOG(error) << "qemu: VM has no ConsoleIDs property; is -display dbus enabled?"sv;
return false;
}
gsize count = 0;
auto id_values = (const guint32 *) g_variant_get_fixed_array(ids, &count, sizeof(guint32));
for (gsize i = 0; i < count; ++i) {
auto path = "/org/qemu/Display1/Console_" + std::to_string(id_values[i]);
auto proxy = qemu_dbus_display1_console_proxy_new_sync(connection, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, bus_name, path.c_str(), cancellable, &err);
auto proxy = qemu_dbus_display1_console_proxy_new_sync(connection, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, qemu_name(), path.c_str(), cancellable, &err);
if (!proxy) {
BOOST_LOG(warning) << "qemu: skipping console "sv << id_values[i] << ": "sv << err->message;
g_clear_error(&err);
@@ -714,7 +791,10 @@
console_proxies.emplace_back(id_values[i], proxy);
}
name_watch = g_bus_watch_name_on_connection(connection, bus_name, G_BUS_NAME_WATCHER_FLAGS_NONE, nullptr, &session_impl_t::on_name_vanished, this, nullptr);
if (!peer_to_peer) {
// on a peer-to-peer connection only "closed" tells that QEMU went away
name_watch = g_bus_watch_name_on_connection(connection, bus_name, G_BUS_NAME_WATCHER_FLAGS_NONE, nullptr, &session_impl_t::on_name_vanished, this, nullptr);
}
BOOST_LOG(info) << "qemu: connected to VM ["sv << to_string(qemu_dbus_display1_vm_get_name(vm_proxy)) << "] with "sv << console_proxies.size() << " console(s)"sv;
is_alive = true;
@@ -800,7 +880,7 @@
GError *err = nullptr;
if (!audio_proxy) {
audio_proxy = qemu_dbus_display1_audio_proxy_new_sync(connection, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, bus_name, audio_path, cancellable, &err);
audio_proxy = qemu_dbus_display1_audio_proxy_new_sync(connection, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, qemu_name(), audio_path, cancellable, &err);
if (!audio_proxy) {
BOOST_LOG(error) << "qemu: couldn't create Audio proxy: "sv << err->message;
g_clear_error(&err);
@@ -853,6 +933,8 @@
((session_impl_t *) self)->is_alive = false;
}
std::atomic<bool> peer_to_peer {false}; ///< Whether the connection is a peer-to-peer `add_client` connection.
std::optional<std::uint32_t> peer_pid; ///< QEMU's process id given for a peer-to-peer connection.
std::chrono::milliseconds timeout; ///< Deadline for connection setup and registration.
int timeout_ms; ///< Timeout applied to each D-Bus call.
std::atomic<bool> is_alive {false}; ///< Whether QEMU is reachable.
@@ -895,8 +977,9 @@
GError *err = nullptr;
const auto path = "/org/qemu/Display1/Console_" + std::to_string(id);
const auto flags = G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START;
const auto name = session->qemu_name();
console = qemu_dbus_display1_console_proxy_new_sync(connection, flags, bus_name, path.c_str(), cancellable, &err);
console = qemu_dbus_display1_console_proxy_new_sync(connection, flags, name, path.c_str(), cancellable, &err);
if (!console) {
BOOST_LOG(error) << "qemu: couldn't create the console "sv << id << " proxy for input: "sv << err->message;
g_clear_error(&err);
@@ -908,13 +991,13 @@
};
if (has("org.qemu.Display1.Keyboard")) {
keyboard = qemu_dbus_display1_keyboard_proxy_new_sync(connection, flags, name, path.c_str(), cancellable, &err);
keyboard = qemu_dbus_display1_keyboard_proxy_new_sync(connection, flags, bus_name, path.c_str(), cancellable, &err);
}
if (!err && has("org.qemu.Display1.Mouse")) {
mouse = qemu_dbus_display1_mouse_proxy_new_sync(connection, flags, name, path.c_str(), cancellable, &err);
mouse = qemu_dbus_display1_mouse_proxy_new_sync(connection, flags, bus_name, path.c_str(), cancellable, &err);
}
if (!err && has("org.qemu.Display1.MultiTouch")) {
multitouch = qemu_dbus_display1_multi_touch_proxy_new_sync(connection, flags, name, path.c_str(), cancellable, &err);
multitouch = qemu_dbus_display1_multi_touch_proxy_new_sync(connection, flags, bus_name, path.c_str(), cancellable, &err);
}
if (err) {
BOOST_LOG(error) << "qemu: couldn't create the input proxies of console "sv << id << ": "sv << err->message;
@@ -1406,8 +1489,51 @@
}
std::shared_ptr<session_t> session_t::connect(const std::string &address, std::chrono::milliseconds timeout) {
std::string reason;
auto parsed = parse_display_address(address, reason);
if (!parsed) {
BOOST_LOG(error) << "qemu: "sv << reason;
return nullptr;
}
switch (parsed->kind) {
case display_address_t::kind_e::qmp:
{
auto peer = qmp_add_client(parsed->qmp_socket, timeout, reason);
if (!peer) {
BOOST_LOG(error) << "qemu: "sv << reason;
return nullptr;
}
return connect_peer(std::move(peer->fd), timeout, peer->qemu_pid);
}
case display_address_t::kind_e::libvirt:
{
auto display = libvirt_open_display(parsed->domain, parsed->uri, reason);
if (!display) {
BOOST_LOG(error) << "qemu: "sv << reason;
return nullptr;
}
if (display->peer) {
return connect_peer(std::move(display->peer->fd), timeout, display->peer->qemu_pid);
}
BOOST_LOG(info) << "qemu: "sv << describe(*parsed) << " uses the bus ["sv << display->bus_address << ']';
parsed->bus = display->bus_address;
break;
}
case display_address_t::kind_e::bus:
break;
}
auto session = std::make_shared<session_impl_t>(timeout);
if (!session->connect_bus(parsed->bus)) {
if (!session->connect_bus(address)) {
return nullptr;
}
return session;
}
std::shared_ptr<session_t> session_t::connect_peer(fd_t fd, std::chrono::milliseconds timeout, std::optional<std::uint32_t> qemu_pid) {
auto session = std::make_shared<session_impl_t>(timeout);
if (!session->connect_peer(std::move(fd), qemu_pid)) {
return nullptr;
}
return session;
src/platform/linux/qemu/session.h +23 −4
@@ -475,14 +475,32 @@
class session_t {
public:
/**
* @brief Connect to QEMU's display and discover its consoles.
* @details `address` is a D-Bus bus address where QEMU owns `org.qemu` (empty for the session
* bus), `qmp:<socket>` for a QEMU with `-display dbus,p2p=on` whose QMP socket Sunshine may use,
* or `libvirt:<domain>[?uri=<uri>]` for a libvirt domain with `<graphics type='dbus'>` (peer to
* @brief Connect to a bus where QEMU owns `org.qemu` and discover its consoles.
* peer with `p2p='yes'`, else the domain's private bus). Every call makes a new connection.
*
* @param address D-Bus address of the bus, or empty for the session bus.
* @param address Display address, see above.
* @param timeout Timeout applied to each D-Bus call.
* @return Connected session, or nullptr when the bus or QEMU is unreachable.
*/
static std::shared_ptr<session_t> connect(const std::string &address, std::chrono::milliseconds timeout = 5s);
/**
* @brief Run the D-Bus client handshake on a peer-to-peer socket QEMU accepted and discover its consoles.
* @details For QEMU started with `-display dbus,p2p=on`, after the other end of the socket was
* handed to QEMU with QMP `add_client protocol=@dbus-display` (or libvirt's
* `virDomainOpenGraphicsFD`). QEMU serves one such control connection at a time: a newer one
* takes the VM objects away from an older one.
*
* @param fd Socket; ownership moves to the session.
* @param timeout Timeout applied to each D-Bus call.
* @param qemu_pid Process id of QEMU when the caller knows it, reported by `qemu_pid()`.
* @return Connected session, or nullptr when QEMU doesn't answer.
*/
static std::shared_ptr<session_t> connect_peer(fd_t fd, std::chrono::milliseconds timeout = 5s, std::optional<std::uint32_t> qemu_pid = std::nullopt);
virtual ~session_t() = default;
/**
@@ -500,7 +518,8 @@
[[nodiscard]] virtual bool alive() const = 0;
/**
* @brief Process id of the QEMU that owns `org.qemu`, as reported by the bus, or the one given to
* @brief Process id of the QEMU that owns `org.qemu`, as reported by the bus.
* `connect_peer()`.
*
* @return Process id, or nothing when the bus doesn't know it.
*/
@@ -543,7 +562,7 @@
* @details Capture, input, and audio share one session while any of them holds it. A session
* whose QEMU went away is replaced by a new connection attempt.
*
* @param address Display address as for `session_t::connect()`.
* @param address D-Bus address, or empty for the session bus.
* @return Live session, or nullptr when QEMU is unreachable.
*/
std::shared_ptr<session_t> shared_session(const std::string &address);
tests/CMakeLists.txt +10 −0
@@ -186,6 +186,16 @@
${TEST_SOURCES}
${SUNSHINE_SOURCES})
# a stand-in for libvirt.so.0 that the QEMU backend's libvirt tests load by path
if(QEMU_FOUND)
add_library(sunshine_fake_libvirt MODULE
"${CMAKE_SOURCE_DIR}/tests/unit/platform/linux/qemu/fake_libvirt/fake_libvirt.c")
# no coverage instrumentation: the module is dlopen()ed and must not need gcov symbols
target_compile_options(sunshine_fake_libvirt PRIVATE -fno-profile-arcs -fno-test-coverage)
add_dependencies(${PROJECT_NAME} sunshine_fake_libvirt)
list(APPEND TEST_DEFINITIONS FAKE_LIBVIRT_PATH="$<TARGET_FILE:sunshine_fake_libvirt>")
endif()
# Copy files needed for config consistency tests to build directory
# This ensures both CLI and CLion can access the same files relative to the test executable
# Using configure_file ensures files are copied when they change between builds
tests/unit/platform/linux/qemu/fake_libvirt/fake_libvirt.c +108 −0
@@ -1,0 +1,108 @@
/**
* @file tests/unit/platform/linux/qemu/fake_libvirt/fake_libvirt.c
* @brief A stand-in for libvirt.so.0 with the few functions Sunshine loads, for QemuLibvirtTest.
* @details Built as a module next to test_sunshine. The test loads it with dlopen() to configure it
* (`fake_libvirt_setup`) and to read what Sunshine called; Sunshine loads the same file by path.
* The signatures match libvirt's public API (libvirt-host.h, libvirt-domain.h, virterror.h).
*/
#include <stdlib.h>
#include <string.h>
typedef struct fake_connect {
int unused; ///< Placeholder member.
} *virConnectPtr; ///< Opaque connection handle.
typedef struct fake_domain {
int unused; ///< Placeholder member.
} *virDomainPtr; ///< Opaque domain handle.
typedef int (*fake_open_fd_fn)(unsigned int idx, void *user); ///< Supplies the socket for virDomainOpenGraphicsFD.
static struct {
char domain[256]; ///< Name of the only domain.
char *xml; ///< Its XML description.
fake_open_fd_fn open_fd; ///< Called by virDomainOpenGraphicsFD.
void *user; ///< Passed to open_fd.
char last_uri[256]; ///< URI given to the last virConnectOpen, "(null)" for NULL.
int last_index; ///< idx of the last virDomainOpenGraphicsFD.
unsigned int last_flags; ///< flags of the last virDomainOpenGraphicsFD.
int open_connections; ///< Connections opened and not closed.
int open_domains; ///< Domain handles not freed.
const char *error; ///< Last error message.
} state;
static struct fake_connect the_connection;
static struct fake_domain the_domain;
void fake_libvirt_setup(const char *domain, const char *xml, fake_open_fd_fn open_fd, void *user) {
strncpy(state.domain, domain, sizeof(state.domain) - 1);
free(state.xml);
state.xml = strdup(xml);
state.open_fd = open_fd;
state.user = user;
state.last_uri[0] = '\0';
state.last_index = -1;
state.last_flags = 0;
state.open_connections = 0;
state.open_domains = 0;
state.error = NULL;
}
const char *fake_libvirt_last_uri(void) {
return state.last_uri;
}
int fake_libvirt_last_index(void) {
return state.last_index;
}
int fake_libvirt_open_handles(void) {
return state.open_connections + state.open_domains;
}
virConnectPtr virConnectOpen(const char *name) {
strncpy(state.last_uri, name ? name : "(null)", sizeof(state.last_uri) - 1);
if (name && strcmp(name, "fake:///unreachable") == 0) {
state.error = "Failed to connect socket to '/run/libvirt/virtqemud-sock': No such file or directory";
return NULL;
}
state.open_connections += 1;
return &the_connection;
}
int virConnectClose(virConnectPtr conn) {
state.open_connections -= 1;
return 0;
}
virDomainPtr virDomainLookupByName(virConnectPtr conn, const char *name) {
if (strcmp(name, state.domain) != 0) {
state.error = "Domain not found: no domain with matching name";
return NULL;
}
state.open_domains += 1;
return &the_domain;
}
int virDomainFree(virDomainPtr domain) {
state.open_domains -= 1;
return 0;
}
char *virDomainGetXMLDesc(virDomainPtr domain, unsigned int flags) {
return strdup(state.xml);
}
int virDomainOpenGraphicsFD(virDomainPtr domain, unsigned int idx, unsigned int flags) {
state.last_index = (int) idx;
state.last_flags = flags;
int fd = state.open_fd ? state.open_fd(idx, state.user) : -1;
if (fd < 0) {
state.error = "Operation not supported: can't open graphics";
}
return fd;
}
const char *virGetLastErrorMessage(void) {
return state.error ? state.error : "no error";
}
tests/unit/platform/linux/qemu/fake_qemu.h +139 −18
@@ -4,6 +4,8 @@
* @details The fake exports `org.qemu.Display1.VM` and `org.qemu.Display1.Console` the same way
* QEMU's `ui/dbus-display.c` and `ui/dbus-console.c` do: it replies to `RegisterListener`, then
* acts as the authentication server on the peer-to-peer socket and drives the client's
* `org.qemu.Display1.Listener` object through proxies. Constructed with an empty bus address, it
* runs in peer-to-peer mode like QEMU's `-display dbus,p2p=on`: it owns no bus name and exports its
* objects on the connection given to `add_client()`, as QMP `add_client protocol=@dbus-display` does.
* `org.qemu.Display1.Listener` object through proxies.
*/
#pragma once
@@ -133,7 +135,7 @@
/**
* @brief Start the fake and own `org.qemu` on the given bus.
*
* @param address Bus address.
* @param address Bus address, or empty for peer-to-peer mode (see `add_client()`).
* @param vm_name VM name property.
* @param vm_uuid VM UUID property.
* @param consoles Consoles to export.
@@ -150,12 +152,14 @@
invoke([&]() {
GError *error = nullptr;
connection = g_dbus_connection_new_for_address_sync(address.c_str(), (GDBusConnectionFlags) (G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION), nullptr, nullptr, &error);
if (!connection) {
g_clear_error(&error);
return;
if (!address.empty()) {
connection = g_dbus_connection_new_for_address_sync(address.c_str(), (GDBusConnectionFlags) (G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION), nullptr, nullptr, &error);
if (!connection) {
g_clear_error(&error);
return;
}
g_dbus_connection_set_exit_on_close(connection, FALSE);
}
g_dbus_connection_set_exit_on_close(connection, FALSE);
vm = qemu_dbus_display1_vm_skeleton_new();
std::vector<guint32> ids;
@@ -165,7 +169,6 @@
qemu_dbus_display1_vm_set_name(vm, vm_name.c_str());
qemu_dbus_display1_vm_set_uuid(vm, vm_uuid.c_str());
qemu_dbus_display1_vm_set_console_ids(vm, g_variant_new_fixed_array(G_VARIANT_TYPE_UINT32, ids.data(), ids.size(), sizeof(guint32)));
g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(vm), connection, "/org/qemu/Display1/VM", nullptr);
for (const auto &c : consoles) {
auto &state = console_states[c.id];
@@ -179,25 +182,20 @@
const gchar *interfaces[] = {"org.qemu.Display1.Keyboard", "org.qemu.Display1.Mouse", "org.qemu.Display1.MultiTouch", nullptr};
qemu_dbus_display1_console_set_interfaces(state.skeleton, interfaces);
g_signal_connect(state.skeleton, "handle-register-listener", G_CALLBACK(&fake_qemu_t::on_register_listener), &state);
auto path = "/org/qemu/Display1/Console_" + std::to_string(c.id);
g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(state.skeleton), connection, path.c_str(), nullptr);
// input interfaces on the same object, like QEMU's dbus_display_console_new()
state.keyboard = qemu_dbus_display1_keyboard_skeleton_new();
g_signal_connect(state.keyboard, "handle-press", G_CALLBACK(&fake_qemu_t::on_key_press), &state);
g_signal_connect(state.keyboard, "handle-release", G_CALLBACK(&fake_qemu_t::on_key_release), &state);
g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(state.keyboard), connection, path.c_str(), nullptr);
state.mouse = qemu_dbus_display1_mouse_skeleton_new();
qemu_dbus_display1_mouse_set_is_absolute(state.mouse, TRUE);
g_signal_connect(state.mouse, "handle-press", G_CALLBACK(&fake_qemu_t::on_mouse_press), &state);
g_signal_connect(state.mouse, "handle-release", G_CALLBACK(&fake_qemu_t::on_mouse_release), &state);
g_signal_connect(state.mouse, "handle-set-abs-position", G_CALLBACK(&fake_qemu_t::on_set_abs_position), &state);
g_signal_connect(state.mouse, "handle-rel-motion", G_CALLBACK(&fake_qemu_t::on_rel_motion), &state);
g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(state.mouse), connection, path.c_str(), nullptr);
state.multitouch = qemu_dbus_display1_multi_touch_skeleton_new();
qemu_dbus_display1_multi_touch_set_max_slots(state.multitouch, 10);
g_signal_connect(state.multitouch, "handle-send-event", G_CALLBACK(&fake_qemu_t::on_touch_event), &state);
g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(state.multitouch), connection, path.c_str(), nullptr);
}
if (with_audio) {
@@ -205,9 +203,14 @@
audio.skeleton = qemu_dbus_display1_audio_skeleton_new();
qemu_dbus_display1_audio_set_nsamples(audio.skeleton, 480);
g_signal_connect(audio.skeleton, "handle-register-out-listener", G_CALLBACK(&fake_qemu_t::on_register_out_listener), &audio);
g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(audio.skeleton), connection, "/org/qemu/Display1/Audio", nullptr);
}
if (!connection) {
// peer-to-peer mode: objects are exported on each add_client() connection
started = true;
return;
}
export_objects(connection);
auto reply = g_dbus_connection_call_sync(connection, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "RequestName", g_variant_new("(su)", "org.qemu", 4u), G_VARIANT_TYPE("(u)"), G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error);
if (!reply) {
g_clear_error(&error);
@@ -240,17 +243,25 @@
audio.close_timer = nullptr;
}
drop_audio_listener_locked();
if (g_dbus_interface_skeleton_get_connection(G_DBUS_INTERFACE_SKELETON(audio.skeleton))) {
g_dbus_interface_skeleton_unexport(G_DBUS_INTERFACE_SKELETON(audio.skeleton));
g_dbus_interface_skeleton_unexport(G_DBUS_INTERFACE_SKELETON(audio.skeleton));
}
g_object_unref(audio.skeleton);
}
if (vm) {
if (g_dbus_interface_skeleton_get_connection(G_DBUS_INTERFACE_SKELETON(vm))) {
g_dbus_interface_skeleton_unexport(G_DBUS_INTERFACE_SKELETON(vm));
}
g_dbus_interface_skeleton_unexport(G_DBUS_INTERFACE_SKELETON(vm));
g_object_unref(vm);
}
if (connection) {
g_dbus_connection_close_sync(connection, nullptr, nullptr);
g_object_unref(connection);
}
for (auto client : p2p_clients) {
g_dbus_connection_close_sync(client, nullptr, nullptr);
g_object_unref(client);
}
});
auto source = g_idle_source_new();
g_source_set_callback(
@@ -912,6 +923,74 @@
}
/**
* @brief Accept a peer-to-peer client, like QMP `add_client protocol=@dbus-display`.
* @details Like QEMU's `dbus_display_add_client()`, the fake is the authentication server and
* delays message processing until its objects are exported; a new client takes the objects away
* from the previous one (`g_dbus_object_manager_server_set_connection`).
*
* @param fd One end of a connected socket pair; ownership moves to the fake.
*/
void add_client(int fd) {
invoke([&]() {
auto socket = g_socket_new_from_fd(fd, nullptr);
if (!socket) {
close(fd);
return;
}
auto stream = g_socket_connection_factory_create_connection(socket);
g_object_unref(socket);
auto guid = g_dbus_generate_guid();
g_dbus_connection_new(
G_IO_STREAM(stream),
guid,
(GDBusConnectionFlags) (G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_SERVER | G_DBUS_CONNECTION_FLAGS_DELAY_MESSAGE_PROCESSING),
nullptr,
nullptr,
[](GObject *, GAsyncResult *result, gpointer data) {
auto self = (fake_qemu_t *) data;
auto client = g_dbus_connection_new_finish(result, nullptr);
if (!client) {
return;
}
g_dbus_connection_set_exit_on_close(client, FALSE);
if (!self->p2p_clients.empty()) {
self->unexport_objects(self->p2p_clients.back());
}
self->export_objects(client);
g_dbus_connection_start_message_processing(client);
self->p2p_clients.push_back(client);
std::lock_guard lock {self->mutex};
self->accepted_clients += 1;
},
this
);
g_free(guid);
g_object_unref(stream);
});
}
/**
* @brief Count the peer-to-peer clients whose handshake completed.
*
* @return Number of accepted clients.
*/
int clients() {
std::lock_guard lock {mutex};
return accepted_clients;
}
/**
* @brief Close the current peer-to-peer client connection, as when QEMU exits.
*/
void close_client() {
invoke([&]() {
if (!p2p_clients.empty()) {
g_dbus_connection_close_sync(p2p_clients.back(), nullptr, nullptr);
}
});
}
/**
* @brief Give up ownership of `org.qemu`.
*/
void release_name() {
@@ -925,6 +1004,44 @@
private:
/**
* @brief Export the VM, console, input and audio objects on a connection; runs on the fake's thread.
*
* @param target Connection to export on.
*/
void export_objects(GDBusConnection *target) {
g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(vm), target, "/org/qemu/Display1/VM", nullptr);
for (auto &[id, state] : console_states) {
auto path = "/org/qemu/Display1/Console_" + std::to_string(id);
for (auto skeleton : {(GDBusInterfaceSkeleton *) state.skeleton, (GDBusInterfaceSkeleton *) state.keyboard, (GDBusInterfaceSkeleton *) state.mouse, (GDBusInterfaceSkeleton *) state.multitouch}) {
g_dbus_interface_skeleton_export(skeleton, target, path.c_str(), nullptr);
}
}
if (audio.skeleton) {
g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(audio.skeleton), target, "/org/qemu/Display1/Audio", nullptr);
}
}
/**
* @brief Remove every exported object from a connection; runs on the fake's thread.
*
* @param target Connection to unexport from.
*/
void unexport_objects(GDBusConnection *target) {
std::vector<GDBusInterfaceSkeleton *> skeletons {G_DBUS_INTERFACE_SKELETON(vm)};
for (auto &[id, state] : console_states) {
skeletons.insert(skeletons.end(), {(GDBusInterfaceSkeleton *) state.skeleton, (GDBusInterfaceSkeleton *) state.keyboard, (GDBusInterfaceSkeleton *) state.mouse, (GDBusInterfaceSkeleton *) state.multitouch});
}
if (audio.skeleton) {
skeletons.push_back(G_DBUS_INTERFACE_SKELETON(audio.skeleton));
}
for (auto skeleton : skeletons) {
if (g_dbus_interface_skeleton_has_connection(skeleton, target)) {
g_dbus_interface_skeleton_unexport_from_connection(skeleton, target);
}
}
}
/**
* @brief A playback voice the fake announces to new listeners.
*/
struct voice_t {
@@ -1036,7 +1153,9 @@
*/
static gboolean on_register_out_listener(QemuDBusDisplay1Audio *skeleton, GDBusMethodInvocation *invocation, GUnixFDList *fd_list, GVariant *arg_listener, gpointer data) {
auto state = (audio_state_t *) data;
// QEMU names peer-to-peer clients "p2p"
const auto sender_name = g_dbus_method_invocation_get_sender(invocation);
const std::string sender = g_dbus_method_invocation_get_sender(invocation);
const std::string sender = sender_name ? sender_name : "p2p";
{
std::lock_guard lock {state->owner->mutex};
if (state->peer && state->sender == sender) {
@@ -1379,7 +1498,9 @@
GMainContext *context; ///< Fake's main context.
GMainLoop *loop; ///< Fake's main loop.
std::thread thread; ///< Thread running the loop.
GDBusConnection *connection {nullptr}; ///< Bus connection.
GDBusConnection *connection {nullptr}; ///< Bus connection, null in peer-to-peer mode.
std::vector<GDBusConnection *> p2p_clients; ///< Peer-to-peer client connections, newest last.
int accepted_clients {0}; ///< Peer-to-peer clients whose handshake completed.
QemuDBusDisplay1VM *vm {nullptr}; ///< Exported VM object.
std::map<std::uint32_t, console_state_t> console_states; ///< Consoles by id.
audio_state_t audio; ///< Audio object state.
tests/unit/platform/linux/qemu/test_p2p.cpp +608 −0
@@ -1,0 +1,608 @@
/**
* @file tests/unit/platform/linux/qemu/test_p2p.cpp
* @brief Test reaching QEMU's D-Bus display peer to peer: QMP add_client and libvirt OpenGraphicsFD.
*/
#ifdef SUNSHINE_BUILD_QEMU
// test includes
#include "../../../../tests_common.h"
#include "fake_qemu.h"
// standard includes
#include <atomic>
#include <cstring>
#include <filesystem>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
// platform includes
#include <dlfcn.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/un.h>
// local includes
#include <src/platform/linux/qemu/p2p.h>
#include <src/platform/linux/qemu/session.h>
using namespace std::literals;
namespace {
/**
* @brief Listener that counts scanouts.
*/
struct counting_listener_t: qemu::display_listener_t {
std::atomic<int> scanouts {0};
std::atomic<int> disconnects {0};
void scanout(std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::span<const std::uint8_t>) override {
scanouts += 1;
}
void update(std::int32_t, std::int32_t, std::int32_t, std::int32_t, std::uint32_t, std::uint32_t, std::span<const std::uint8_t>) override {
}
void scanout_map(qemu::fd_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t) override {
}
void update_map(std::int32_t, std::int32_t, std::int32_t, std::int32_t) override {
}
void disable() override {
}
void disconnected() override {
disconnects += 1;
}
};
/**
* @brief Create a connected socket pair.
*
* @return Both ends.
*/
std::pair<qemu::fd_t, qemu::fd_t> socket_pair() {
int fds[2] = {-1, -1};
if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, fds) != 0) {
return {};
}
return {qemu::fd_t {fds[0]}, qemu::fd_t {fds[1]}};
}
/**
* @brief Minimal QMP server on a Unix socket, like QEMU's `-qmp unix:<path>,server=on,wait=off`.
* @details Serves one client at a time. `getfd` keeps the descriptor sent with SCM_RIGHTS;
* `add_client protocol=@dbus-display` hands it to the fake QEMU.
*/
class fake_qmp_t {
public:
fake_qmp_t(std::string path, qemu_test::fake_qemu_t *qemu):
path {std::move(path)},
qemu {qemu} {
listen_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
sockaddr_un addr {};
addr.sun_family = AF_UNIX;
std::strncpy(addr.sun_path, this->path.c_str(), sizeof(addr.sun_path) - 1);
unlink(this->path.c_str());
if (bind(listen_fd, (sockaddr *) &addr, sizeof(addr)) != 0 || listen(listen_fd, 4) != 0) {
return;
}
thread = std::thread([this]() {
serve();
});
}
~fake_qmp_t() {
stop = true;
if (thread.joinable()) {
thread.join();
}
close(listen_fd);
unlink(path.c_str());
}
fake_qmp_t(const fake_qmp_t &) = delete;
fake_qmp_t &operator=(const fake_qmp_t &) = delete;
std::vector<std::string> commands() {
std::lock_guard lock {mutex};
return received;
}
std::string path; ///< Socket path.
std::atomic<bool> greet {true}; ///< Whether to send the greeting (false: like a monitor busy with another client).
std::string add_client_error; ///< Error to answer add_client with, empty for success.
private:
void serve() {
while (!stop) {
pollfd p {listen_fd, POLLIN, 0};
if (poll(&p, 1, 50) <= 0) {
continue;
}
int client = accept4(listen_fd, nullptr, nullptr, SOCK_CLOEXEC);
if (client < 0) {
continue;
}
handle(client);
close(client);
}
}
void send_line(int client, const std::string &line) {
auto text = line + "\r\n";
(void) ::send(client, text.data(), text.size(), MSG_NOSIGNAL);
}
void handle(int client) {
if (!greet) {
// hold the connection without a greeting until the client gives up
while (!stop) {
pollfd p {client, POLLIN, 0};
if (poll(&p, 1, 50) > 0) {
char buffer[256];
if (recv(client, buffer, sizeof(buffer), 0) <= 0) {
return;
}
}
}
return;
}
send_line(client, R"({"QMP": {"version": {"qemu": {"micro": 1, "minor": 1, "major": 11}, "package": ""}, "capabilities": ["oob"]}})");
std::string buffer;
int pending_fd = -1;
bool negotiated = false;
while (!stop) {
pollfd p {client, POLLIN, 0};
if (poll(&p, 1, 50) <= 0) {
continue;
}
char data[4096];
iovec iov {data, sizeof(data)};
char control[CMSG_SPACE(sizeof(int))];
msghdr msg {};
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = control;
msg.msg_controllen = sizeof(control);
auto n = recvmsg(client, &msg, MSG_CMSG_CLOEXEC);
if (n <= 0) {
break;
}
for (auto cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) {
if (pending_fd >= 0) {
close(pending_fd);
}
std::memcpy(&pending_fd, CMSG_DATA(cmsg), sizeof(int));
}
}
buffer.append(data, (std::size_t) n);
for (auto end = buffer.find('\n'); end != std::string::npos; end = buffer.find('\n')) {
auto line = buffer.substr(0, end);
buffer.erase(0, end + 1);
{
std::lock_guard lock {mutex};
received.push_back(line);
}
// an event before a reply, as QEMU may send, must be skipped by the client
send_line(client, R"({"timestamp": {"seconds": 1, "microseconds": 2}, "event": "NIC_RX_FILTER_CHANGED"})");
if (line.find("qmp_capabilities") != std::string::npos) {
negotiated = true;
send_line(client, R"({"return": {}})");
} else if (!negotiated) {
send_line(client, R"({"error": {"class": "CommandNotFound", "desc": "Expecting capabilities negotiation with 'qmp_capabilities'"}})");
} else if (line.find("\"getfd\"") != std::string::npos) {
send_line(client, pending_fd >= 0 ? R"({"return": {}})" : R"({"error": {"class": "GenericError", "desc": "No file descriptor supplied via SCM_RIGHTS"}})");
} else if (line.find("\"add_client\"") != std::string::npos && line.find("@dbus-display") != std::string::npos) {
if (!add_client_error.empty()) {
send_line(client, R"({"error": {"class": "GenericError", "desc": ")" + add_client_error + R"("}})");
} else if (pending_fd < 0) {
send_line(client, R"({"error": {"class": "GenericError", "desc": "File descriptor named 'x' not found"}})");
} else {
qemu->add_client(std::exchange(pending_fd, -1));
send_line(client, R"({"return": {}})");
}
} else {
send_line(client, R"({"error": {"class": "GenericError", "desc": "unexpected command"}})");
}
}
}
if (pending_fd >= 0) {
close(pending_fd);
}
}
qemu_test::fake_qemu_t *qemu; ///< Receives added clients.
int listen_fd {-1}; ///< Listening socket.
std::thread thread; ///< Server thread.
std::atomic<bool> stop {false}; ///< Stops the server.
std::mutex mutex; ///< Guards `received`.
std::vector<std::string> received; ///< Command lines received.
};
/**
* @brief Fixture with a fake QEMU in peer-to-peer mode (no bus) with two consoles.
*/
class QemuP2pTest: public BaseTest {
protected:
void SetUp() override {
BaseTest::SetUp();
fake = std::make_unique<qemu_test::fake_qemu_t>(
"",
"p2p-vm",
"00000000-0000-0000-0000-0000000000a2",
std::vector<qemu_test::fake_console_t> {
{0, "VGA", "Graphic", 640, 480},
{1, "virtio-gpu-pci.1", "Graphic", 800, 600},
}
);
ASSERT_TRUE(fake->ok());
dir = std::filesystem::temp_directory_path() / ("sunshine-qemu-p2p-" + std::to_string(getpid()));
std::filesystem::create_directories(dir);
}
void TearDown() override {
fake.reset();
std::error_code ec;
std::filesystem::remove_all(dir, ec);
BaseTest::TearDown();
}
public:
/**
* @brief Hand a new socket pair to the fake, as `add_client` does, and return our end.
*
* @return Our end.
*/
qemu::fd_t add_client() {
auto [ours, theirs] = socket_pair();
fake->add_client(theirs.release());
return std::move(ours);
}
protected:
std::unique_ptr<qemu_test::fake_qemu_t> fake;
std::filesystem::path dir;
};
/**
* @brief Path of the fake libvirt module built next to test_sunshine.
*
* @return Module path.
*/
std::string fake_libvirt_path() {
return FAKE_LIBVIRT_PATH;
}
/**
* @brief Access the control functions of the fake libvirt module.
*/
struct fake_libvirt_t {
fake_libvirt_t() {
handle = dlopen(fake_libvirt_path().c_str(), RTLD_NOW | RTLD_LOCAL);
if (handle) {
setup = (decltype(setup)) dlsym(handle, "fake_libvirt_setup");
last_uri = (decltype(last_uri)) dlsym(handle, "fake_libvirt_last_uri");
last_index = (decltype(last_index)) dlsym(handle, "fake_libvirt_last_index");
open_handles = (decltype(open_handles)) dlsym(handle, "fake_libvirt_open_handles");
}
}
~fake_libvirt_t() {
if (handle) {
dlclose(handle);
}
}
void *handle {nullptr};
void (*setup)(const char *, const char *, int (*)(unsigned int, void *), void *) {nullptr};
const char *(*last_uri)() {nullptr};
int (*last_index)() {nullptr};
int (*open_handles)() {nullptr};
};
constexpr auto p2p_domain_xml = R"(<domain type='kvm' id='7'>
<name>win11</name>
<devices>
<graphics type='spice' autoport='yes'>
<listen type='address'/>
</graphics>
<graphics type='dbus' p2p='yes'>
<audio id='1'/>
</graphics>
<audio id='1' type='dbus'/>
</devices>
</domain>)"; ///< Live XML of a domain with a SPICE display and a p2p D-Bus display.
} // namespace
// @tag requirements: [REQ-DEP-002]
TEST(QemuDisplayAddressTest, ParsesBusQmpAndLibvirtAddresses) {
std::string error;
auto bus = qemu::parse_display_address("unix:path=/run/vm/bus.sock", error);
ASSERT_TRUE(bus);
EXPECT_EQ(bus->kind, qemu::display_address_t::kind_e::bus);
EXPECT_EQ(bus->bus, "unix:path=/run/vm/bus.sock");
auto session_bus = qemu::parse_display_address("", error);
ASSERT_TRUE(session_bus);
EXPECT_EQ(session_bus->kind, qemu::display_address_t::kind_e::bus);
EXPECT_TRUE(session_bus->bus.empty());
auto qmp = qemu::parse_display_address("qmp:/run/sunshine-qemu/win11.qmp", error);
ASSERT_TRUE(qmp);
EXPECT_EQ(qmp->kind, qemu::display_address_t::kind_e::qmp);
EXPECT_EQ(qmp->qmp_socket, "/run/sunshine-qemu/win11.qmp");
auto libvirt = qemu::parse_display_address("libvirt:win11", error);
ASSERT_TRUE(libvirt);
EXPECT_EQ(libvirt->kind, qemu::display_address_t::kind_e::libvirt);
EXPECT_EQ(libvirt->domain, "win11");
EXPECT_TRUE(libvirt->uri.empty());
auto with_uri = qemu::parse_display_address("libvirt:win11?uri=qemu:///system", error);
ASSERT_TRUE(with_uri);
EXPECT_EQ(with_uri->domain, "win11");
EXPECT_EQ(with_uri->uri, "qemu:///system");
EXPECT_EQ(qemu::describe(*with_uri), "libvirt domain [win11] on [qemu:///system]");
}
// @tag requirements: [REQ-DEP-002]
TEST(QemuDisplayAddressTest, RejectsAddressesWithoutSocketOrDomain) {
std::string error;
EXPECT_FALSE(qemu::parse_display_address("qmp:", error));
EXPECT_NE(error.find("qmp:"), std::string::npos);
error.clear();
EXPECT_FALSE(qemu::parse_display_address("libvirt:", error));
EXPECT_NE(error.find("domain"), std::string::npos);
error.clear();
EXPECT_FALSE(qemu::parse_display_address("libvirt:?uri=qemu:///system", error));
EXPECT_FALSE(qemu::parse_display_address("libvirt:win11?url=qemu:///system", error));
EXPECT_NE(error.find("uri="), std::string::npos);
}
// @tag requirements: [REQ-DEP-002]
TEST_F(QemuP2pTest, PeerSessionDiscoversConsolesAndStreams) {
auto session = qemu::session_t::connect_peer(add_client(), 5s, 4242u);
ASSERT_NE(session, nullptr);
EXPECT_TRUE(session->alive());
EXPECT_EQ(session->qemu_pid(), 4242u);
auto vm = session->vm();
EXPECT_EQ(vm.name, "p2p-vm");
ASSERT_EQ(vm.consoles.size(), 2u);
EXPECT_EQ(vm.consoles[1].label, "virtio-gpu-pci.1");
auto listener = std::make_shared<counting_listener_t>();
auto registration = session->register_listener(1, listener);
ASSERT_NE(registration, nullptr);
ASSERT_TRUE(fake->wait_for_listener(1));
ASSERT_TRUE(fake->scanout(1, 2, 2, 8, qemu::pixman_format::x8r8g8b8, std::vector<std::uint8_t>(16, 0xff)));
EXPECT_TRUE(qemu_test::wait_until([&]() {
return listener->scanouts == 1;
}));
auto input = session->open_input(0);
ASSERT_NE(input, nullptr);
input->key(30, true);
input->key(30, false);
EXPECT_TRUE(input->flush(2s));
EXPECT_EQ(fake->input_calls(0), (std::vector<std::string> {"key press 30", "key release 30"}));
}
// @tag requirements: [REQ-DEP-002]
TEST_F(QemuP2pTest, PeerSessionRegistersAudioListener) {
struct silent_audio_t: qemu::audio_out_listener_t {
void init(std::uint64_t, const qemu::pcm_format_t &) override {
}
void fini(std::uint64_t) override {
}
void set_enabled(std::uint64_t, bool) override {
}
void set_volume(std::uint64_t, bool, std::span<const std::uint8_t>) override {
}
void write(std::uint64_t, std::span<const std::uint8_t>) override {
}
void disconnected() override {
}
};
auto session = qemu::session_t::connect_peer(add_client());
ASSERT_NE(session, nullptr);
auto registration = session->register_audio_out_listener(std::make_shared<silent_audio_t>());
EXPECT_NE(registration, nullptr);
EXPECT_EQ(fake->audio_registrations(), 1);
}
// @tag requirements: [REQ-DEP-002]
TEST_F(QemuP2pTest, PeerSessionNotAliveWhenQemuClosesTheConnection) {
auto session = qemu::session_t::connect_peer(add_client());
ASSERT_NE(session, nullptr);
ASSERT_TRUE(qemu_test::wait_until([&]() {
return fake->clients() == 1;
}));
fake->close_client();
EXPECT_TRUE(qemu_test::wait_until([&]() {
return !session->alive();
}));
}
// @tag requirements: [REQ-DEP-002]
TEST_F(QemuP2pTest, PeerSessionFailsWhenNoQemuAnswers) {
auto [ours, theirs] = socket_pair();
const auto start = std::chrono::steady_clock::now();
auto session = qemu::session_t::connect_peer(std::move(ours), 300ms);
EXPECT_EQ(session, nullptr);
EXPECT_LT(std::chrono::steady_clock::now() - start, 3s);
}
// @tag requirements: [REQ-DEP-002]
TEST_F(QemuP2pTest, QmpAddClientConnectsThroughTheMonitor) {
fake_qmp_t qmp {(dir / "qmp.sock").string(), fake.get()};
auto session = qemu::session_t::connect("qmp:" + qmp.path, 5s);
ASSERT_NE(session, nullptr);
EXPECT_EQ(session->vm().name, "p2p-vm");
// the monitor socket belongs to QEMU, here the test process
EXPECT_EQ(session->qemu_pid(), (std::uint32_t) getpid());
auto commands = qmp.commands();
ASSERT_EQ(commands.size(), 3u);
EXPECT_NE(commands[0].find("qmp_capabilities"), std::string::npos);
EXPECT_NE(commands[1].find("\"getfd\""), std::string::npos);
EXPECT_NE(commands[2].find("\"protocol\":\"@dbus-display\""), std::string::npos);
}
// @tag requirements: [REQ-DEP-002]
TEST_F(QemuP2pTest, SharedSessionReconnectsThroughQmpAfterQemuDropsTheClient) {
fake_qmp_t qmp {(dir / "qmp.sock").string(), fake.get()};
const auto address = "qmp:" + qmp.path;
auto first = qemu::shared_session(address);
ASSERT_NE(first, nullptr);
EXPECT_EQ(qemu::shared_session(address), first);
EXPECT_EQ(fake->clients(), 1);
fake->close_client();
ASSERT_TRUE(qemu_test::wait_until([&]() {
return !first->alive();
}));
auto second = qemu::shared_session(address);
ASSERT_NE(second, nullptr);
EXPECT_NE(second, first);
EXPECT_TRUE(second->alive());
EXPECT_EQ(fake->clients(), 2);
}
// @tag requirements: [REQ-DEP-002]
TEST_F(QemuP2pTest, QmpAddClientReportsQemuErrors) {
fake_qmp_t qmp {(dir / "qmp.sock").string(), fake.get()};
qmp.add_client_error = "p2p connections not accepted in bus mode";
std::string error;
auto peer = qemu::qmp_add_client(qmp.path, 2s, error);
EXPECT_FALSE(peer);
EXPECT_NE(error.find("p2p connections not accepted in bus mode"), std::string::npos) << error;
EXPECT_EQ(qemu::session_t::connect("qmp:" + qmp.path, 2s), nullptr);
}
// @tag requirements: [REQ-DEP-002]
TEST_F(QemuP2pTest, QmpAddClientFailsWithoutMonitorOrGreeting) {
std::string error;
EXPECT_FALSE(qemu::qmp_add_client((dir / "missing.sock").string(), 1s, error));
EXPECT_NE(error.find("missing.sock"), std::string::npos) << error;
fake_qmp_t busy {(dir / "busy.sock").string(), fake.get()};
busy.greet = false;
error.clear();
const auto start = std::chrono::steady_clock::now();
EXPECT_FALSE(qemu::qmp_add_client(busy.path, 300ms, error));
EXPECT_LT(std::chrono::steady_clock::now() - start, 3s);
EXPECT_NE(error.find("greeting"), std::string::npos) << error;
}
// @tag requirements: [REQ-DEP-002]
TEST(QemuLibvirtTest, FindsTheDbusGraphicsDeviceInDomainXml) {
auto p2p = qemu::find_dbus_graphics(p2p_domain_xml);
ASSERT_TRUE(p2p);
EXPECT_EQ(p2p->index, 1u);
EXPECT_TRUE(p2p->p2p);
auto bus = qemu::find_dbus_graphics(R"(<domain><devices><graphics type="dbus" address="unix:path=/run/libvirt/qemu/dbus/3-win&amp;11-dbus.sock"><gl enable="no"/></graphics></devices></domain>)");
ASSERT_TRUE(bus);
EXPECT_EQ(bus->index, 0u);
EXPECT_FALSE(bus->p2p);
EXPECT_EQ(bus->address, "unix:path=/run/libvirt/qemu/dbus/3-win&11-dbus.sock");
EXPECT_FALSE(qemu::find_dbus_graphics("<domain><devices><graphics type='vnc'/><graphicsx type='dbus'/></devices></domain>"));
}
// @tag requirements: [REQ-DEP-002]
TEST_F(QemuP2pTest, LibvirtOpensP2pDisplayWithOpenGraphicsFD) {
fake_libvirt_t libvirt;
ASSERT_NE(libvirt.setup, nullptr) << "couldn't load " << fake_libvirt_path();
libvirt.setup(
"win11",
p2p_domain_xml,
[](unsigned int, void *user) -> int {
auto self = (QemuP2pTest *) user;
return self->add_client().release();
},
this
);
std::string error;
auto display = qemu::libvirt_open_display("win11", "qemu:///system", error, fake_libvirt_path());
ASSERT_TRUE(display) << error;
EXPECT_EQ(std::string {libvirt.last_uri()}, "qemu:///system");
EXPECT_EQ(libvirt.last_index(), 1);
EXPECT_EQ(libvirt.open_handles(), 0);
ASSERT_TRUE(display->peer);
EXPECT_TRUE(display->bus_address.empty());
auto session = qemu::session_t::connect_peer(std::move(display->peer->fd));
ASSERT_NE(session, nullptr);
EXPECT_EQ(session->vm().name, "p2p-vm");
}
// @tag requirements: [REQ-DEP-002]
TEST(QemuLibvirtTest, UsesThePrivateBusOfABusModeDomain) {
fake_libvirt_t libvirt;
ASSERT_NE(libvirt.setup, nullptr) << "couldn't load " << fake_libvirt_path();
libvirt.setup("desktop", "<domain><devices><graphics type='dbus' address='unix:path=/run/user/1000/libvirt/qemu/run/dbus/4-desktop-dbus.sock'/></devices></domain>", nullptr, nullptr);
std::string error;
auto display = qemu::libvirt_open_display("desktop", "", error, fake_libvirt_path());
ASSERT_TRUE(display) << error;
EXPECT_FALSE(display->peer);
EXPECT_EQ(display->bus_address, "unix:path=/run/user/1000/libvirt/qemu/run/dbus/4-desktop-dbus.sock");
EXPECT_EQ(std::string {libvirt.last_uri()}, "(null)");
EXPECT_EQ(libvirt.last_index(), -1);
EXPECT_EQ(libvirt.open_handles(), 0);
}
// @tag requirements: [REQ-DEP-002]
TEST(QemuLibvirtTest, ReportsLibvirtAndDomainErrors) {
fake_libvirt_t libvirt;
ASSERT_NE(libvirt.setup, nullptr) << "couldn't load " << fake_libvirt_path();
libvirt.setup("win11", "<domain><devices><graphics type='spice'/></devices></domain>", nullptr, nullptr);
std::string error;
EXPECT_FALSE(qemu::libvirt_open_display("win11", "fake:///unreachable", error, fake_libvirt_path()));
EXPECT_NE(error.find("virtqemud-sock"), std::string::npos) << error;
error.clear();
EXPECT_FALSE(qemu::libvirt_open_display("win10", "", error, fake_libvirt_path()));
EXPECT_NE(error.find("win10"), std::string::npos) << error;
EXPECT_NE(error.find("Domain not found"), std::string::npos) << error;
error.clear();
EXPECT_FALSE(qemu::libvirt_open_display("win11", "", error, fake_libvirt_path()));
EXPECT_NE(error.find("<graphics type='dbus'"), std::string::npos) << error;
EXPECT_EQ(libvirt.open_handles(), 0);
error.clear();
EXPECT_FALSE(qemu::libvirt_open_display("win11", "", error, "libvirt-does-not-exist.so.0"));
EXPECT_NE(error.find("libvirt-does-not-exist.so.0"), std::string::npos) << error;
}
// @tag requirements: [REQ-DEP-002]
TEST(QemuLibvirtTest, ReportsAFailedOpenGraphicsFD) {
fake_libvirt_t libvirt;
ASSERT_NE(libvirt.setup, nullptr) << "couldn't load " << fake_libvirt_path();
libvirt.setup("win11", p2p_domain_xml, nullptr, nullptr);
std::string error;
EXPECT_FALSE(qemu::libvirt_open_display("win11", "", error, fake_libvirt_path()));
EXPECT_NE(error.find("can't open graphics"), std::string::npos) << error;
EXPECT_EQ(libvirt.open_handles(), 0);
}
#endif