ref:main
/**
* @file src/platform/linux/qemu/session.cpp
* @brief Definitions for the QEMU D-Bus display session.
*/
// class header include
#include "session.h"
// standard includes
#include <algorithm>
#include <atomic>
#include <charconv>
#include <condition_variable>
#include <cstring>
#include <functional>
#include <future>
#include <map>
#include <mutex>
#include <thread>
#include <utility>
// platform includes
#include <gio/gio.h>
#include <gio/gunixfdlist.h>
#include <sys/socket.h>
#include <unistd.h>
// local includes
#include "src/logging.h"
// generated includes
extern "C" {
#include "qemu/dbus-display1.h"
}
using namespace std::literals;
namespace qemu {
namespace {
constexpr auto bus_name = "org.qemu"; ///< Well-known name QEMU owns on the bus.
constexpr auto vm_path = "/org/qemu/Display1/VM"; ///< Object path of the VM interface.
constexpr auto listener_path = "/org/qemu/Display1/Listener"; ///< Object path QEMU calls on the listener connection.
constexpr auto unix_map_interface = "org.qemu.Display1.Listener.Unix.Map"; ///< Shared memory listener interface.
/**
* @brief Convert a NULL-terminated string vector to a vector of strings.
*
* @param strv String vector, may be null.
* @return Copied strings.
*/
std::vector<std::string> to_strings(const gchar *const *strv) {
std::vector<std::string> result;
for (auto it = strv; it && *it; ++it) {
result.emplace_back(*it);
}
return result;
}
/**
* @brief Read a string property that may be missing on old QEMU releases.
*
* @param value Property value, may be null.
* @return The value, or an empty string.
*/
std::string to_string(const gchar *value) {
return value ? value : "";
}
/**
* @brief Cancels a GCancellable when a deadline passes, for blocking GDBus calls without a timeout.
* @details Connection setup and authentication have no timeout of their own; a socket that
* accepts but never answers would block forever.
*/
class deadline_t {
public:
/**
* @brief Start the watchdog.
*
* @param timeout Time after which the cancellable is cancelled.
*/
explicit deadline_t(std::chrono::milliseconds timeout):
cancellable {g_cancellable_new()} {
watchdog = std::thread([this, timeout]() {
std::unique_lock lock {mutex};
if (!finished.wait_for(lock, timeout, [this]() {
return done;
})) {
g_cancellable_cancel(cancellable);
}
});
}
~deadline_t() {
{
std::lock_guard lock {mutex};
done = true;
}
finished.notify_all();
watchdog.join();
g_object_unref(cancellable);
}
deadline_t(const deadline_t &) = delete;
deadline_t &operator=(const deadline_t &) = delete;
GCancellable *cancellable; ///< Cancelled when the deadline passes.
private:
std::mutex mutex; ///< Guards `done`.
std::condition_variable finished; ///< Signaled when the guarded work finished.
bool done {false}; ///< Whether the guarded work finished.
std::thread watchdog; ///< Thread waiting for the deadline.
};
/**
* @brief Owns a GLib main context and the thread that runs it.
*/
class loop_thread_t {
public:
loop_thread_t():
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);
});
}
~loop_thread_t() {
post([this]() {
g_main_loop_quit(loop);
});
thread.join();
g_main_loop_unref(loop);
g_main_context_unref(context);
}
loop_thread_t(const loop_thread_t &) = delete;
loop_thread_t &operator=(const loop_thread_t &) = delete;
/**
* @brief Run a function on the loop thread and wait for it to finish.
* @details Runs the function inline when called from the loop thread.
*
* @param fn Function to run.
*/
void invoke(const std::function<void()> &fn) {
if (g_main_context_is_owner(context)) {
fn();
return;
}
std::packaged_task<void()> task {fn};
auto done = task.get_future();
post([&task]() {
task();
});
done.get();
}
private:
/**
* @brief Queue a function on the loop without waiting.
*
* @param fn Function to run; must outlive its execution.
*/
void post(std::function<void()> fn) {
auto source = g_idle_source_new();
g_source_set_priority(source, G_PRIORITY_DEFAULT);
g_source_set_callback(
source,
[](gpointer data) -> gboolean {
(*(std::function<void()> *) data)();
return G_SOURCE_REMOVE;
},
new std::function<void()> {std::move(fn)},
[](gpointer data) {
delete (std::function<void()> *) data;
}
);
g_source_attach(source, context);
g_source_unref(source);
}
GMainContext *context; ///< Context owned by the loop thread.
GMainLoop *loop; ///< Loop running on the thread.
std::thread thread; ///< Thread running the loop.
};
class session_impl_t;
/**
* @brief Peer-to-peer listener connection registered on a console.
*/
class listener_impl_t: public listener_registration_t {
public:
/**
* @brief Create an unconnected registration.
*
* @param session Session that owns the loop thread.
* @param listener Receiver for display calls.
*/
listener_impl_t(std::shared_ptr<session_impl_t> session, std::shared_ptr<display_listener_t> listener);
~listener_impl_t() override;
/**
* @brief Complete the peer-to-peer handshake and export the listener objects.
* @details Must run on the listener thread, right after `RegisterListener` returned.
*
* @param socket_fd Our end of the socket pair.
* @param cancellable Cancels the handshake when the deadline passes.
* @return True when the listener is exported.
*/
bool start(fd_t socket_fd, GCancellable *cancellable);
private:
/**
* @brief Unexport the listener objects and close the connection.
* @details Must run on the listener thread.
*/
void stop();
static gboolean on_scanout(QemuDBusDisplay1Listener *object, GDBusMethodInvocation *invocation, guint width, guint height, guint stride, guint format, GVariant *data, gpointer self);
static gboolean on_update(QemuDBusDisplay1Listener *object, GDBusMethodInvocation *invocation, gint x, gint y, gint width, gint height, guint stride, guint format, GVariant *data, gpointer self);
static gboolean on_scanout_dmabuf(QemuDBusDisplay1Listener *object, GDBusMethodInvocation *invocation, GUnixFDList *fd_list, GVariant *dmabuf, guint width, guint height, guint stride, guint fourcc, guint64 modifier, gboolean y0_top, gpointer self);
static gboolean on_update_dmabuf(QemuDBusDisplay1Listener *object, GDBusMethodInvocation *invocation, gint x, gint y, gint width, gint height, gpointer self);
static gboolean on_disable(QemuDBusDisplay1Listener *object, GDBusMethodInvocation *invocation, gpointer self);
static gboolean on_mouse_set(QemuDBusDisplay1Listener *object, GDBusMethodInvocation *invocation, gint x, gint y, gint on, gpointer self);
static gboolean on_cursor_define(QemuDBusDisplay1Listener *object, GDBusMethodInvocation *invocation, gint width, gint height, gint hot_x, gint hot_y, GVariant *data, gpointer self);
static gboolean on_scanout_map(QemuDBusDisplay1ListenerUnixMap *object, GDBusMethodInvocation *invocation, GUnixFDList *fd_list, GVariant *handle, guint offset, guint width, guint height, guint stride, guint format, gpointer self);
static gboolean on_update_map(QemuDBusDisplay1ListenerUnixMap *object, GDBusMethodInvocation *invocation, gint x, gint y, gint width, gint height, gpointer self);
static void on_closed(GDBusConnection *connection, gboolean remote_peer_vanished, GError *err, gpointer self);
std::shared_ptr<session_impl_t> session; ///< Keeps the loop thread alive while registered.
std::shared_ptr<display_listener_t> listener; ///< Receiver for display calls.
GDBusConnection *connection {nullptr}; ///< Peer-to-peer connection to QEMU.
QemuDBusDisplay1Listener *skeleton {nullptr}; ///< Exported Listener interface.
QemuDBusDisplay1ListenerUnixMap *map_skeleton {nullptr}; ///< Exported Listener.Unix.Map interface.
bool closed {false}; ///< Whether the peer closed the connection.
};
/**
* @brief GDBus implementation of the session.
*/
class session_impl_t: public session_t, public std::enable_shared_from_this<session_impl_t> {
public:
/**
* @brief Create a session with its loop thread; call connect_bus() next.
*
* @param timeout Timeout applied to each D-Bus call.
*/
explicit session_impl_t(std::chrono::milliseconds timeout):
timeout {timeout},
timeout_ms {(int) timeout.count()} {
}
~session_impl_t() override {
loop.invoke([this]() {
disconnect_bus();
});
}
/**
* @brief Connect to the bus and read the VM and console properties.
*
* @param address Bus address, or empty for the session bus.
* @return True when QEMU was found.
*/
bool connect_bus(const std::string &address) {
bool ok = false;
deadline_t deadline {timeout};
loop.invoke([&]() {
ok = connect_bus_on_loop(address, deadline.cancellable);
if (!ok) {
disconnect_bus();
}
});
return ok;
}
vm_info_t vm() const override {
vm_info_t info;
loop.invoke([&]() {
info.name = to_string(qemu_dbus_display1_vm_get_name(vm_proxy));
info.uuid = to_string(qemu_dbus_display1_vm_get_uuid(vm_proxy));
for (const auto &[id, proxy] : console_proxies) {
console_info_t console;
console.id = id;
console.label = to_string(qemu_dbus_display1_console_get_label(proxy));
console.type = to_string(qemu_dbus_display1_console_get_type_(proxy));
console.head = qemu_dbus_display1_console_get_head(proxy);
console.width = qemu_dbus_display1_console_get_width(proxy);
console.height = qemu_dbus_display1_console_get_height(proxy);
console.interfaces = to_strings(qemu_dbus_display1_console_get_interfaces(proxy));
info.consoles.emplace_back(std::move(console));
}
});
return info;
}
bool alive() const override {
return is_alive;
}
std::unique_ptr<listener_registration_t> register_listener(std::uint32_t console_id, std::shared_ptr<display_listener_t> listener) override {
auto registration = std::make_unique<listener_impl_t>(shared_from_this(), std::move(listener));
bool ok = false;
deadline_t deadline {timeout};
loop.invoke([&]() {
ok = register_listener_on_loop(console_id, *registration, deadline.cancellable);
});
if (!ok) {
return nullptr;
}
return registration;
}
/**
* @brief Thread that owns the bus connection and makes blocking calls to QEMU.
*/
mutable loop_thread_t loop;
/**
* @brief Thread that dispatches listener connections and never blocks on QEMU.
* @details QEMU makes synchronous calls on a new listener (property fetch, ScanoutMap) right
* after replying to RegisterListener. Answering them from the thread that may be blocked in
* another RegisterListener would deadlock both processes until the call timeout.
*/
loop_thread_t listener_loop;
private:
/**
* @brief Connect and read properties; runs on the loop thread.
*
* @param address Bus address, or empty for the session bus.
* @param cancellable Cancels blocking calls when the deadline passes.
* @return True when QEMU was found.
*/
bool connect_bus_on_loop(const std::string &address, GCancellable *cancellable) {
GError *err = nullptr;
std::string resolved = address;
if (resolved.empty()) {
auto session_address = g_dbus_address_get_for_bus_sync(G_BUS_TYPE_SESSION, cancellable, &err);
if (!session_address) {
BOOST_LOG(error) << "qemu: no session bus address: "sv << err->message;
g_clear_error(&err);
return false;
}
resolved = session_address;
g_free(session_address);
}
connection = g_dbus_connection_new_for_address_sync(resolved.c_str(), (GDBusConnectionFlags) (G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION), nullptr, cancellable, &err);
if (!connection) {
BOOST_LOG(error) << "qemu: couldn't connect to D-Bus address ["sv << resolved << "]: "sv << err->message;
g_clear_error(&err);
return false;
}
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);
if (!vm_proxy) {
BOOST_LOG(error) << "qemu: couldn't create VM proxy: "sv << err->message;
g_clear_error(&err);
return false;
}
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;
}
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; 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);
if (!proxy) {
BOOST_LOG(warning) << "qemu: skipping console "sv << id_values[i] << ": "sv << err->message;
g_clear_error(&err);
continue;
}
g_dbus_proxy_set_default_timeout(G_DBUS_PROXY(proxy), timeout_ms);
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);
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;
return true;
}
/**
* @brief Release every GDBus object; runs on the loop thread.
*/
void disconnect_bus() {
is_alive = false;
if (name_watch) {
g_bus_unwatch_name(name_watch);
name_watch = 0;
}
for (auto &[id, proxy] : console_proxies) {
g_object_unref(proxy);
}
console_proxies.clear();
g_clear_object(&vm_proxy);
if (connection) {
g_signal_handler_disconnect(connection, closed_handler);
g_dbus_connection_close_sync(connection, nullptr, nullptr);
g_clear_object(&connection);
}
}
/**
* @brief Hand one end of a socket pair to `Console.RegisterListener`; runs on the loop thread.
*
* @param console_id Console to register on.
* @param registration Registration to start on success.
* @param cancellable Cancels blocking calls when the deadline passes.
* @return True when the listener is registered.
*/
bool register_listener_on_loop(std::uint32_t console_id, listener_impl_t &registration, GCancellable *cancellable) {
auto it = std::ranges::find_if(console_proxies, [&](const auto &entry) {
return entry.first == console_id;
});
if (it == console_proxies.end() || !is_alive) {
BOOST_LOG(error) << "qemu: console "sv << console_id << " is not available"sv;
return false;
}
int fds[2];
if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, fds) != 0) {
BOOST_LOG(error) << "qemu: socketpair failed: "sv << std::strerror(errno);
return false;
}
fd_t ours {fds[0]};
fd_t theirs {fds[1]};
GError *err = nullptr;
auto fd_list = g_unix_fd_list_new();
auto index = g_unix_fd_list_append(fd_list, theirs.get(), &err);
theirs = fd_t {};
if (index < 0) {
BOOST_LOG(error) << "qemu: couldn't attach listener socket: "sv << err->message;
g_clear_error(&err);
g_object_unref(fd_list);
return false;
}
bool ok = qemu_dbus_display1_console_call_register_listener_sync(it->second, g_variant_new_handle(index), G_DBUS_CALL_FLAGS_NONE, timeout_ms, fd_list, nullptr, cancellable, &err);
g_object_unref(fd_list);
if (!ok) {
BOOST_LOG(error) << "qemu: RegisterListener on console "sv << console_id << " failed: "sv << err->message;
g_clear_error(&err);
return false;
}
bool started = false;
listener_loop.invoke([&]() {
started = registration.start(std::move(ours), cancellable);
});
return started;
}
static void on_bus_closed(GDBusConnection *connection, gboolean remote_peer_vanished, GError *err, gpointer self) {
BOOST_LOG(warning) << "qemu: D-Bus connection closed"sv;
((session_impl_t *) self)->is_alive = false;
}
static void on_name_vanished(GDBusConnection *connection, const gchar *name, gpointer self) {
BOOST_LOG(warning) << "qemu: ["sv << name << "] vanished from the bus"sv;
((session_impl_t *) self)->is_alive = false;
}
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.
GDBusConnection *connection {nullptr}; ///< Bus connection.
gulong closed_handler {0}; ///< Handler id of the connection's "closed" signal.
guint name_watch {0}; ///< Watch on `org.qemu`.
QemuDBusDisplay1VM *vm_proxy {nullptr}; ///< VM proxy.
std::vector<std::pair<std::uint32_t, QemuDBusDisplay1Console *>> console_proxies; ///< Console proxies in ConsoleIDs order.
};
listener_impl_t::listener_impl_t(std::shared_ptr<session_impl_t> session, std::shared_ptr<display_listener_t> listener):
session {std::move(session)},
listener {std::move(listener)} {
}
listener_impl_t::~listener_impl_t() {
session->listener_loop.invoke([this]() {
stop();
});
}
bool listener_impl_t::start(fd_t socket_fd, GCancellable *cancellable) {
GError *err = nullptr;
auto socket = g_socket_new_from_fd(socket_fd.get(), &err);
if (!socket) {
BOOST_LOG(error) << "qemu: couldn't wrap listener socket: "sv << err->message;
g_clear_error(&err);
return false;
}
socket_fd.release();
auto socket_connection = g_socket_connection_factory_create_connection(socket);
g_object_unref(socket);
// QEMU is the authentication server on this socket. Delay message processing so QEMU's first
// calls (property fetch, ScanoutMap) queue until the listener objects are exported.
connection = g_dbus_connection_new_sync(G_IO_STREAM(socket_connection), nullptr, (GDBusConnectionFlags) (G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | G_DBUS_CONNECTION_FLAGS_DELAY_MESSAGE_PROCESSING), nullptr, cancellable, &err);
g_object_unref(socket_connection);
if (!connection) {
BOOST_LOG(error) << "qemu: listener handshake failed: "sv << err->message;
g_clear_error(&err);
return false;
}
g_dbus_connection_set_exit_on_close(connection, FALSE);
skeleton = qemu_dbus_display1_listener_skeleton_new();
const gchar *interfaces[] = {unix_map_interface, nullptr};
qemu_dbus_display1_listener_set_interfaces(skeleton, interfaces);
g_signal_connect(skeleton, "handle-scanout", G_CALLBACK(&listener_impl_t::on_scanout), this);
g_signal_connect(skeleton, "handle-update", G_CALLBACK(&listener_impl_t::on_update), this);
g_signal_connect(skeleton, "handle-scanout-dmabuf", G_CALLBACK(&listener_impl_t::on_scanout_dmabuf), this);
g_signal_connect(skeleton, "handle-update-dmabuf", G_CALLBACK(&listener_impl_t::on_update_dmabuf), this);
g_signal_connect(skeleton, "handle-disable", G_CALLBACK(&listener_impl_t::on_disable), this);
g_signal_connect(skeleton, "handle-mouse-set", G_CALLBACK(&listener_impl_t::on_mouse_set), this);
g_signal_connect(skeleton, "handle-cursor-define", G_CALLBACK(&listener_impl_t::on_cursor_define), this);
map_skeleton = qemu_dbus_display1_listener_unix_map_skeleton_new();
g_signal_connect(map_skeleton, "handle-scanout-map", G_CALLBACK(&listener_impl_t::on_scanout_map), this);
g_signal_connect(map_skeleton, "handle-update-map", G_CALLBACK(&listener_impl_t::on_update_map), this);
if (!g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(skeleton), connection, listener_path, &err) || !g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(map_skeleton), connection, listener_path, &err)) {
BOOST_LOG(error) << "qemu: couldn't export listener: "sv << err->message;
g_clear_error(&err);
stop();
return false;
}
g_signal_connect(connection, "closed", G_CALLBACK(&listener_impl_t::on_closed), this);
g_dbus_connection_start_message_processing(connection);
return true;
}
void listener_impl_t::stop() {
if (skeleton) {
g_signal_handlers_disconnect_by_data(skeleton, this);
g_dbus_interface_skeleton_unexport(G_DBUS_INTERFACE_SKELETON(skeleton));
g_clear_object(&skeleton);
}
if (map_skeleton) {
g_signal_handlers_disconnect_by_data(map_skeleton, this);
g_dbus_interface_skeleton_unexport(G_DBUS_INTERFACE_SKELETON(map_skeleton));
g_clear_object(&map_skeleton);
}
if (connection) {
g_signal_handlers_disconnect_by_data(connection, this);
if (!closed) {
g_dbus_connection_close_sync(connection, nullptr, nullptr);
}
g_clear_object(&connection);
}
}
gboolean listener_impl_t::on_scanout(QemuDBusDisplay1Listener *object, GDBusMethodInvocation *invocation, guint width, guint height, guint stride, guint format, GVariant *data, gpointer self) {
gsize size = 0;
auto bytes = (const std::uint8_t *) g_variant_get_fixed_array(data, &size, 1);
((listener_impl_t *) self)->listener->scanout(width, height, stride, format, {bytes, size});
qemu_dbus_display1_listener_complete_scanout(object, invocation);
return TRUE;
}
gboolean listener_impl_t::on_update(QemuDBusDisplay1Listener *object, GDBusMethodInvocation *invocation, gint x, gint y, gint width, gint height, guint stride, guint format, GVariant *data, gpointer self) {
gsize size = 0;
auto bytes = (const std::uint8_t *) g_variant_get_fixed_array(data, &size, 1);
((listener_impl_t *) self)->listener->update(x, y, width, height, stride, format, {bytes, size});
qemu_dbus_display1_listener_complete_update(object, invocation);
return TRUE;
}
gboolean listener_impl_t::on_scanout_dmabuf(QemuDBusDisplay1Listener *object, GDBusMethodInvocation *invocation, GUnixFDList *fd_list, GVariant *dmabuf, guint width, guint height, guint stride, guint fourcc, guint64 modifier, gboolean y0_top, gpointer self) {
// DMABUF scanouts need a GL display in QEMU and are handled by the vram capture path (REQ-CAP-003).
static std::once_flag logged;
std::call_once(logged, []() {
BOOST_LOG(warning) << "qemu: ignoring DMABUF scanout; start QEMU without gl=on for shared memory capture"sv;
});
qemu_dbus_display1_listener_complete_scanout_dmabuf(object, invocation, nullptr);
return TRUE;
}
gboolean listener_impl_t::on_update_dmabuf(QemuDBusDisplay1Listener *object, GDBusMethodInvocation *invocation, gint x, gint y, gint width, gint height, gpointer self) {
qemu_dbus_display1_listener_complete_update_dmabuf(object, invocation);
return TRUE;
}
gboolean listener_impl_t::on_disable(QemuDBusDisplay1Listener *object, GDBusMethodInvocation *invocation, gpointer self) {
((listener_impl_t *) self)->listener->disable();
qemu_dbus_display1_listener_complete_disable(object, invocation);
return TRUE;
}
gboolean listener_impl_t::on_mouse_set(QemuDBusDisplay1Listener *object, GDBusMethodInvocation *invocation, gint x, gint y, gint on, gpointer self) {
((listener_impl_t *) self)->listener->mouse_set(x, y, on != 0);
qemu_dbus_display1_listener_complete_mouse_set(object, invocation);
return TRUE;
}
gboolean listener_impl_t::on_cursor_define(QemuDBusDisplay1Listener *object, GDBusMethodInvocation *invocation, gint width, gint height, gint hot_x, gint hot_y, GVariant *data, gpointer self) {
gsize size = 0;
auto bytes = (const std::uint8_t *) g_variant_get_fixed_array(data, &size, 1);
((listener_impl_t *) self)->listener->cursor_define(width, height, hot_x, hot_y, {bytes, size});
qemu_dbus_display1_listener_complete_cursor_define(object, invocation);
return TRUE;
}
gboolean listener_impl_t::on_scanout_map(QemuDBusDisplay1ListenerUnixMap *object, GDBusMethodInvocation *invocation, GUnixFDList *fd_list, GVariant *handle, guint offset, guint width, guint height, guint stride, guint format, gpointer self) {
GError *err = nullptr;
int fd = fd_list ? g_unix_fd_list_get(fd_list, g_variant_get_handle(handle), &err) : -1;
if (fd < 0) {
BOOST_LOG(error) << "qemu: ScanoutMap without a valid descriptor"sv;
if (err) {
g_dbus_method_invocation_return_gerror(invocation, err);
g_clear_error(&err);
} else {
g_dbus_method_invocation_return_error_literal(invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "missing descriptor");
}
return TRUE;
}
((listener_impl_t *) self)->listener->scanout_map(fd_t {fd}, offset, width, height, stride, format);
qemu_dbus_display1_listener_unix_map_complete_scanout_map(object, invocation, nullptr);
return TRUE;
}
gboolean listener_impl_t::on_update_map(QemuDBusDisplay1ListenerUnixMap *object, GDBusMethodInvocation *invocation, gint x, gint y, gint width, gint height, gpointer self) {
((listener_impl_t *) self)->listener->update_map(x, y, width, height);
qemu_dbus_display1_listener_unix_map_complete_update_map(object, invocation);
return TRUE;
}
void listener_impl_t::on_closed(GDBusConnection *connection, gboolean remote_peer_vanished, GError *err, gpointer self) {
auto registration = (listener_impl_t *) self;
BOOST_LOG(info) << "qemu: display listener connection closed"sv;
registration->closed = true;
registration->listener->disconnected();
}
} // namespace
fd_t::fd_t(int fd):
fd {fd} {
}
fd_t::fd_t(fd_t &&other) noexcept:
fd {std::exchange(other.fd, -1)} {
}
fd_t &fd_t::operator=(fd_t &&other) noexcept {
if (this != &other) {
if (fd >= 0) {
close(fd);
}
fd = std::exchange(other.fd, -1);
}
return *this;
}
fd_t::~fd_t() {
if (fd >= 0) {
close(fd);
}
}
int fd_t::release() {
return std::exchange(fd, -1);
}
std::shared_ptr<session_t> session_t::connect(const std::string &address, std::chrono::milliseconds timeout) {
auto session = std::make_shared<session_impl_t>(timeout);
if (!session->connect_bus(address)) {
return nullptr;
}
return session;
}
std::shared_ptr<session_t> shared_session(const std::string &address) {
static std::mutex mutex;
static std::weak_ptr<session_t> cached;
static std::string cached_address;
std::lock_guard lock {mutex};
if (auto session = cached.lock(); session && session->alive() && cached_address == address) {
return session;
}
auto session = session_t::connect(address);
cached = session;
cached_address = address;
return session;
}
std::optional<console_info_t> find_console(const vm_info_t &vm, std::string_view name) {
if (name.empty()) {
auto it = std::ranges::find_if(vm.consoles, &console_info_t::is_graphic);
if (it == vm.consoles.end()) {
return std::nullopt;
}
return *it;
}
std::uint32_t id = 0;
auto [end, ec] = std::from_chars(name.data(), name.data() + name.size(), id);
bool is_id = ec == std::errc {} && end == name.data() + name.size();
auto it = std::ranges::find_if(vm.consoles, [&](const console_info_t &console) {
return is_id ? console.id == id : console.label == name;
});
if (it == vm.consoles.end()) {
return std::nullopt;
}
return *it;
}
std::vector<std::string> graphic_console_names(const vm_info_t &vm) {
std::vector<std::string> names;
for (const auto &console : vm.consoles) {
if (console.is_graphic()) {
names.emplace_back(std::to_string(console.id));
}
}
return names;
}
} // namespace qemu