ref:main
/**
* @file tests/unit/platform/linux/qemu/fake_qemu.h
* @brief In-process fake of QEMU's D-Bus display, running on a private dbus-daemon.
* @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.
*/
#pragma once
#ifdef SUNSHINE_BUILD_QEMU
// standard includes
#include <chrono>
#include <condition_variable>
#include <csignal>
#include <cstdint>
#include <functional>
#include <future>
#include <map>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <vector>
// platform includes
#include <gio/gio.h>
#include <gio/gunixfdlist.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
// generated includes
extern "C" {
#include "qemu/dbus-display1.h"
}
namespace qemu_test {
using namespace std::literals;
/**
* @brief Poll a condition until it holds or a timeout expires.
*
* @param predicate Condition to wait for.
* @param timeout Maximum time to wait.
* @return True when the condition held before the timeout.
*/
inline bool wait_until(const std::function<bool()> &predicate, std::chrono::milliseconds timeout = 5s) {
const auto deadline = std::chrono::steady_clock::now() + timeout;
while (std::chrono::steady_clock::now() < deadline) {
if (predicate()) {
return true;
}
std::this_thread::sleep_for(5ms);
}
return predicate();
}
/**
* @brief A dbus-daemon spawned for one test.
*/
class private_bus_t {
public:
private_bus_t() {
const gchar *argv[] = {"dbus-daemon", "--session", "--nofork", "--nopidfile", "--print-address=1", nullptr};
gint out_fd = -1;
GError *error = nullptr;
if (!g_spawn_async_with_pipes(nullptr, (gchar **) argv, nullptr, (GSpawnFlags) (G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD), nullptr, nullptr, &pid, nullptr, &out_fd, nullptr, &error)) {
g_clear_error(&error);
return;
}
char c;
while (read(out_fd, &c, 1) == 1 && c != '\n') {
bus_address.push_back(c);
}
close(out_fd);
}
~private_bus_t() {
if (pid > 0) {
kill(pid, SIGTERM);
waitpid(pid, nullptr, 0);
g_spawn_close_pid(pid);
}
}
private_bus_t(const private_bus_t &) = delete;
private_bus_t &operator=(const private_bus_t &) = delete;
/**
* @brief Report whether the daemon started.
*
* @return True when the bus address is known.
*/
[[nodiscard]] bool ok() const {
return !bus_address.empty();
}
/**
* @brief Access the bus address.
*
* @return D-Bus address clients connect to.
*/
[[nodiscard]] const std::string &address() const {
return bus_address;
}
private:
GPid pid {0}; ///< Daemon process id.
std::string bus_address; ///< Address printed by the daemon.
};
/**
* @brief Description of a console the fake exports.
*/
struct fake_console_t {
std::uint32_t id; ///< Console id.
std::string label; ///< Console label.
std::string type; ///< "Graphic" or "Text".
std::uint32_t width; ///< Width in pixels.
std::uint32_t height; ///< Height in pixels.
};
/**
* @brief Fake QEMU D-Bus display.
*/
class fake_qemu_t {
public:
/**
* @brief Start the fake and own `org.qemu` on the given bus.
*
* @param address Bus address.
* @param vm_name VM name property.
* @param vm_uuid VM UUID property.
* @param consoles Consoles to export.
*/
fake_qemu_t(const std::string &address, std::string vm_name, std::string vm_uuid, std::vector<fake_console_t> consoles):
context {g_main_context_new()},
loop {g_main_loop_new(context, FALSE)} {
thread = std::thread([this]() {
g_main_context_push_thread_default(context);
g_main_loop_run(loop);
g_main_context_pop_thread_default(context);
});
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;
}
g_dbus_connection_set_exit_on_close(connection, FALSE);
vm = qemu_dbus_display1_vm_skeleton_new();
std::vector<guint32> ids;
for (const auto &c : consoles) {
ids.push_back(c.id);
}
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];
state.owner = this;
state.id = c.id;
state.skeleton = qemu_dbus_display1_console_skeleton_new();
qemu_dbus_display1_console_set_label(state.skeleton, c.label.c_str());
qemu_dbus_display1_console_set_type_(state.skeleton, c.type.c_str());
qemu_dbus_display1_console_set_width(state.skeleton, c.width);
qemu_dbus_display1_console_set_height(state.skeleton, c.height);
const gchar *interfaces[] = {"org.qemu.Display1.Keyboard", "org.qemu.Display1.Mouse", 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);
}
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);
return;
}
g_variant_unref(reply);
started = true;
});
}
~fake_qemu_t() {
invoke([this]() {
for (auto &[id, state] : console_states) {
drop_listener_locked(state);
g_dbus_interface_skeleton_unexport(G_DBUS_INTERFACE_SKELETON(state.skeleton));
g_object_unref(state.skeleton);
}
if (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);
}
});
auto source = g_idle_source_new();
g_source_set_callback(
source,
[](gpointer data) -> gboolean {
g_main_loop_quit((GMainLoop *) data);
return G_SOURCE_REMOVE;
},
loop,
nullptr
);
g_source_attach(source, context);
g_source_unref(source);
thread.join();
g_main_loop_unref(loop);
g_main_context_unref(context);
}
fake_qemu_t(const fake_qemu_t &) = delete;
fake_qemu_t &operator=(const fake_qemu_t &) = delete;
/**
* @brief Report whether the fake owns `org.qemu`.
*
* @return True when startup succeeded.
*/
[[nodiscard]] bool ok() const {
return started;
}
/**
* @brief Wait for a client to register a listener on a console.
*
* @param console_id Console to check.
* @param timeout Maximum time to wait.
* @return True when a listener is registered.
*/
bool wait_for_listener(std::uint32_t console_id, std::chrono::milliseconds timeout = 5s) {
return wait_until(
[&]() {
std::lock_guard lock {mutex};
return console_states[console_id].proxy != nullptr;
},
timeout
);
}
/**
* @brief Read the listener's advertised `Interfaces` property.
*
* @param console_id Console whose listener to query.
* @return Interface names.
*/
std::vector<std::string> listener_interfaces(std::uint32_t console_id) {
std::lock_guard lock {mutex};
return console_states[console_id].listener_interfaces;
}
/**
* @brief Count how many listeners registered on a console so far.
*
* @param console_id Console to check.
* @return Number of successful registrations.
*/
int registrations(std::uint32_t console_id) {
std::lock_guard lock {mutex};
return console_states[console_id].registrations;
}
/**
* @brief Report whether the client closed the listener connection.
*
* @param console_id Console to check.
* @return True when the peer connection closed from the client side.
*/
bool listener_closed_by_peer(std::uint32_t console_id) {
std::lock_guard lock {mutex};
return console_states[console_id].closed_by_peer;
}
/**
* @brief Send a `Scanout` call and wait for the reply.
*
* @param console_id Target console.
* @param width Width in pixels.
* @param height Height in pixels.
* @param stride Bytes per row.
* @param format Pixman format code.
* @param data Pixel data.
* @return True when the client acknowledged the call.
*/
bool scanout(std::uint32_t console_id, std::uint32_t width, std::uint32_t height, std::uint32_t stride, std::uint32_t format, const std::vector<std::uint8_t> &data) {
auto proxy = listener_proxy(console_id);
if (!proxy) {
return false;
}
auto v = g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE, data.data(), data.size(), 1);
bool ok = qemu_dbus_display1_listener_call_scanout_sync(proxy, width, height, stride, format, v, G_DBUS_CALL_FLAGS_NONE, 5000, nullptr, nullptr);
g_object_unref(proxy);
return ok;
}
/**
* @brief Send an `Update` call and wait for the reply.
*
* @param console_id Target console.
* @param x Left edge.
* @param y Top edge.
* @param width Rectangle width.
* @param height Rectangle height.
* @param stride Bytes per row.
* @param format Pixman format code.
* @param data Pixel data for the rectangle.
* @return True when the client acknowledged the call.
*/
bool update(std::uint32_t console_id, std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height, std::uint32_t stride, std::uint32_t format, const std::vector<std::uint8_t> &data) {
auto proxy = listener_proxy(console_id);
if (!proxy) {
return false;
}
auto v = g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE, data.data(), data.size(), 1);
bool ok = qemu_dbus_display1_listener_call_update_sync(proxy, x, y, width, height, stride, format, v, G_DBUS_CALL_FLAGS_NONE, 5000, nullptr, nullptr);
g_object_unref(proxy);
return ok;
}
/**
* @brief Send a `ScanoutMap` call with a shared memory descriptor and wait for the reply.
*
* @param console_id Target console.
* @param fd Descriptor to share; the caller keeps ownership.
* @param offset Offset of the first pixel.
* @param width Width in pixels.
* @param height Height in pixels.
* @param stride Bytes per row.
* @param format Pixman format code.
* @return True when the client acknowledged the call.
*/
bool scanout_map(std::uint32_t console_id, int fd, std::uint32_t offset, std::uint32_t width, std::uint32_t height, std::uint32_t stride, std::uint32_t format) {
auto proxy = map_proxy(console_id);
if (!proxy) {
return false;
}
auto fd_list = g_unix_fd_list_new();
g_unix_fd_list_append(fd_list, fd, nullptr);
bool ok = qemu_dbus_display1_listener_unix_map_call_scanout_map_sync(proxy, g_variant_new_handle(0), offset, width, height, stride, format, G_DBUS_CALL_FLAGS_NONE, 5000, fd_list, nullptr, nullptr, nullptr);
g_object_unref(fd_list);
g_object_unref(proxy);
return ok;
}
/**
* @brief Send an `UpdateMap` call and wait for the reply.
*
* @param console_id Target console.
* @param x Left edge.
* @param y Top edge.
* @param width Rectangle width.
* @param height Rectangle height.
* @return True when the client acknowledged the call.
*/
bool update_map(std::uint32_t console_id, std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) {
auto proxy = map_proxy(console_id);
if (!proxy) {
return false;
}
bool ok = qemu_dbus_display1_listener_unix_map_call_update_map_sync(proxy, x, y, width, height, G_DBUS_CALL_FLAGS_NONE, 5000, nullptr, nullptr);
g_object_unref(proxy);
return ok;
}
/**
* @brief Send a `Disable` call and wait for the reply.
*
* @param console_id Target console.
* @return True when the client acknowledged the call.
*/
bool disable(std::uint32_t console_id) {
auto proxy = listener_proxy(console_id);
if (!proxy) {
return false;
}
bool ok = qemu_dbus_display1_listener_call_disable_sync(proxy, G_DBUS_CALL_FLAGS_NONE, 5000, nullptr, nullptr);
g_object_unref(proxy);
return ok;
}
/**
* @brief Send `MouseSet` and `CursorDefine` calls and wait for the replies.
*
* @param console_id Target console.
* @return True when the client acknowledged both calls.
*/
bool cursor(std::uint32_t console_id) {
auto proxy = listener_proxy(console_id);
if (!proxy) {
return false;
}
std::vector<std::uint8_t> pixels(4 * 2 * 2, 0xff);
auto v = g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE, pixels.data(), pixels.size(), 1);
bool ok = qemu_dbus_display1_listener_call_mouse_set_sync(proxy, 10, 20, 1, G_DBUS_CALL_FLAGS_NONE, 5000, nullptr, nullptr) &&
qemu_dbus_display1_listener_call_cursor_define_sync(proxy, 2, 2, 1, 1, v, G_DBUS_CALL_FLAGS_NONE, 5000, nullptr, nullptr);
g_object_unref(proxy);
return ok;
}
/**
* @brief Send a `ScanoutDMABUF` call, which the client does not support yet.
*
* @param console_id Target console.
* @return True when the client acknowledged the call without an error.
*/
bool scanout_dmabuf(std::uint32_t console_id) {
auto proxy = listener_proxy(console_id);
if (!proxy) {
return false;
}
auto fd_list = g_unix_fd_list_new();
int fd = memfd_create("fake-dmabuf", MFD_CLOEXEC);
g_unix_fd_list_append(fd_list, fd, nullptr);
close(fd);
bool ok = qemu_dbus_display1_listener_call_scanout_dmabuf_sync(proxy, g_variant_new_handle(0), 16, 16, 64, 0x34325258, 0, TRUE, G_DBUS_CALL_FLAGS_NONE, 5000, fd_list, nullptr, nullptr, nullptr);
g_object_unref(fd_list);
g_object_unref(proxy);
return ok;
}
/**
* @brief Close the listener connection from the QEMU side, as when QEMU exits.
*
* @param console_id Target console.
*/
void drop_listener(std::uint32_t console_id) {
invoke([&]() {
std::lock_guard lock {mutex};
drop_listener_locked(console_states[console_id]);
});
}
/**
* @brief Give up ownership of `org.qemu`.
*/
void release_name() {
invoke([&]() {
auto reply = g_dbus_connection_call_sync(connection, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ReleaseName", g_variant_new("(s)", "org.qemu"), G_VARIANT_TYPE("(u)"), G_DBUS_CALL_FLAGS_NONE, -1, nullptr, nullptr);
if (reply) {
g_variant_unref(reply);
}
});
}
private:
/**
* @brief Per-console fake state.
*/
struct console_state_t {
fake_qemu_t *owner {nullptr}; ///< Owning fake.
std::uint32_t id {0}; ///< Console id.
QemuDBusDisplay1Console *skeleton {nullptr}; ///< Exported console object.
GDBusConnection *peer {nullptr}; ///< Listener peer-to-peer connection.
QemuDBusDisplay1Listener *proxy {nullptr}; ///< Listener proxy.
QemuDBusDisplay1ListenerUnixMap *map {nullptr}; ///< Unix.Map proxy, when advertised.
std::vector<std::string> listener_interfaces; ///< Interfaces the listener advertised.
int registrations {0}; ///< Number of successful registrations.
bool closed_by_peer {false}; ///< Whether the client closed the connection.
};
/**
* @brief Run a function on the fake's thread and wait for it.
*
* @param fn Function to run.
*/
void invoke(const std::function<void()> &fn) {
std::packaged_task<void()> task {fn};
auto done = task.get_future();
auto source = g_idle_source_new();
g_source_set_callback(
source,
[](gpointer data) -> gboolean {
(*(std::packaged_task<void()> *) data)();
return G_SOURCE_REMOVE;
},
&task,
nullptr
);
g_source_attach(source, context);
g_source_unref(source);
done.get();
}
/**
* @brief Get a new reference to a console's listener proxy.
*
* @param console_id Console to look up.
* @return Proxy reference, or nullptr.
*/
QemuDBusDisplay1Listener *listener_proxy(std::uint32_t console_id) {
std::lock_guard lock {mutex};
auto proxy = console_states[console_id].proxy;
return proxy ? (QemuDBusDisplay1Listener *) g_object_ref(proxy) : nullptr;
}
/**
* @brief Get a new reference to a console's Unix.Map proxy.
*
* @param console_id Console to look up.
* @return Proxy reference, or nullptr.
*/
QemuDBusDisplay1ListenerUnixMap *map_proxy(std::uint32_t console_id) {
std::lock_guard lock {mutex};
auto proxy = console_states[console_id].map;
return proxy ? (QemuDBusDisplay1ListenerUnixMap *) g_object_ref(proxy) : nullptr;
}
/**
* @brief Close and forget a console's listener connection.
*
* @param state Console state; the caller holds the mutex or runs on the fake thread.
*/
void drop_listener_locked(console_state_t &state) {
if (!state.peer) {
return;
}
g_signal_handlers_disconnect_by_data(state.peer, &state);
g_clear_object(&state.proxy);
g_clear_object(&state.map);
g_dbus_connection_close_sync(state.peer, nullptr, nullptr);
g_clear_object(&state.peer);
}
/**
* @brief Record that the client side closed a listener connection.
*
* @param connection Closed connection.
* @param remote_peer_vanished Whether the peer closed it.
* @param error Close reason.
* @param data Console state.
*/
static void on_peer_closed(GDBusConnection *connection, gboolean remote_peer_vanished, GError *error, gpointer data) {
auto state = (console_state_t *) data;
std::lock_guard lock {state->owner->mutex};
state->closed_by_peer = true;
}
/**
* @brief Handle `RegisterListener` like QEMU's `dbus_console_register_listener`.
*
* @param skeleton Console object.
* @param invocation Method invocation.
* @param fd_list Descriptors attached to the call.
* @param arg_listener Handle of the listener socket.
* @param data Console state.
* @return Always TRUE.
*/
static gboolean on_register_listener(QemuDBusDisplay1Console *skeleton, GDBusMethodInvocation *invocation, GUnixFDList *fd_list, GVariant *arg_listener, gpointer data) {
auto state = (console_state_t *) data;
GError *error = nullptr;
int fd = g_unix_fd_list_get(fd_list, g_variant_get_handle(arg_listener), &error);
if (fd < 0) {
g_dbus_method_invocation_return_gerror(invocation, error);
g_clear_error(&error);
return TRUE;
}
auto socket = g_socket_new_from_fd(fd, nullptr);
auto socket_connection = g_socket_connection_factory_create_connection(socket);
g_object_unref(socket);
qemu_dbus_display1_console_complete_register_listener(skeleton, invocation, nullptr);
auto guid = g_dbus_generate_guid();
auto peer = g_dbus_connection_new_sync(G_IO_STREAM(socket_connection), guid, G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_SERVER, nullptr, nullptr, &error);
g_free(guid);
g_object_unref(socket_connection);
if (!peer) {
g_clear_error(&error);
return TRUE;
}
auto proxy = qemu_dbus_display1_listener_proxy_new_sync(peer, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, nullptr, "/org/qemu/Display1/Listener", nullptr, &error);
if (!proxy) {
g_clear_error(&error);
g_object_unref(peer);
return TRUE;
}
std::vector<std::string> interfaces;
QemuDBusDisplay1ListenerUnixMap *map = nullptr;
if (auto names = qemu_dbus_display1_listener_get_interfaces(proxy)) {
for (auto name = names; *name; ++name) {
interfaces.emplace_back(*name);
}
if (g_strv_contains(names, "org.qemu.Display1.Listener.Unix.Map")) {
map = qemu_dbus_display1_listener_unix_map_proxy_new_sync(peer, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, nullptr, "/org/qemu/Display1/Listener", nullptr, nullptr);
}
}
g_signal_connect(peer, "closed", G_CALLBACK(&fake_qemu_t::on_peer_closed), state);
std::lock_guard lock {state->owner->mutex};
state->owner->drop_listener_locked(*state);
state->peer = peer;
state->proxy = proxy;
state->map = map;
state->listener_interfaces = std::move(interfaces);
state->registrations += 1;
state->closed_by_peer = false;
return TRUE;
}
GMainContext *context; ///< Fake's main context.
GMainLoop *loop; ///< Fake's main loop.
std::thread thread; ///< Thread running the loop.
GDBusConnection *connection {nullptr}; ///< Bus connection.
QemuDBusDisplay1VM *vm {nullptr}; ///< Exported VM object.
std::map<std::uint32_t, console_state_t> console_states; ///< Consoles by id.
std::mutex mutex; ///< Guards listener state read by the test thread.
bool started {false}; ///< Whether startup succeeded.
};
} // namespace qemu_test
#endif