blob: fe7fcdecb6f1a9c82bf891d753ddfc8128ffc81d [file] [log] [blame]
/*
* QEMU DBus display
*
* Copyright (c) 2021 Marc-André Lureau <marcandre.lureau@redhat.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include "qemu/osdep.h"
#include "qemu/dbus.h"
#include "qemu/error-report.h"
#include "qemu/main-loop.h"
#include "qom/object_interfaces.h"
#include "sysemu/sysemu.h"
#include "qapi/error.h"
#include "trace.h"
#include "dbus.h"
#define MIME_TEXT_PLAIN_UTF8 "text/plain;charset=utf-8"
static void
dbus_clipboard_complete_request(
DBusDisplay *dpy,
GDBusMethodInvocation *invocation,
QemuClipboardInfo *info,
QemuClipboardType type)
{
GVariant *v_data = g_variant_new_from_data(
G_VARIANT_TYPE("ay"),
info->types[type].data,
info->types[type].size,
TRUE,
(GDestroyNotify)qemu_clipboard_info_unref,
qemu_clipboard_info_ref(info));
qemu_dbus_display1_clipboard_complete_request(
dpy->clipboard, invocation,
MIME_TEXT_PLAIN_UTF8, v_data);
}
static void
dbus_clipboard_update_info(DBusDisplay *dpy, QemuClipboardInfo *info)
{
bool self_update = info->owner == &dpy->clipboard_peer;
const char *mime[QEMU_CLIPBOARD_TYPE__COUNT + 1] = { 0, };
DBusClipboardRequest *req;
int i = 0;
if (info->owner == NULL) {
if (dpy->clipboard_proxy) {
qemu_dbus_display1_clipboard_call_release(
dpy->clipboard_proxy,
info->selection,
G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
}
return;
}
if (self_update || !info->has_serial) {
return;
}
req = &dpy->clipboard_request[info->selection];
if (req->invocation && info->types[req->type].data) {
dbus_clipboard_complete_request(dpy, req->invocation, info, req->type);
g_clear_object(&req->invocation);
g_source_remove(req->timeout_id);
req->timeout_id = 0;
return;
}
if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) {
mime[i++] = MIME_TEXT_PLAIN_UTF8;
}
if (i > 0) {
if (dpy->clipboard_proxy) {
qemu_dbus_display1_clipboard_call_grab(
dpy->clipboard_proxy,
info->selection,
info->serial,
mime,
G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
}
}
}
static void
dbus_clipboard_reset_serial(DBusDisplay *dpy)
{
if (dpy->clipboard_proxy) {
qemu_dbus_display1_clipboard_call_register(
dpy->clipboard_proxy,
G_DBUS_CALL_FLAGS_NONE,
-1, NULL, NULL, NULL);
}
}
static void
dbus_clipboard_notify(Notifier *notifier, void *data)
{
DBusDisplay *dpy =
container_of(notifier, DBusDisplay, clipboard_peer.notifier);
QemuClipboardNotify *notify = data;
switch (notify->type) {
case QEMU_CLIPBOARD_UPDATE_INFO:
dbus_clipboard_update_info(dpy, notify->info);
return;
case QEMU_CLIPBOARD_RESET_SERIAL:
dbus_clipboard_reset_serial(dpy);
return;
}
}
static void
dbus_clipboard_qemu_request(QemuClipboardInfo *info,
QemuClipboardType type)
{
DBusDisplay *dpy = container_of(info->owner, DBusDisplay, clipboard_peer);
g_autofree char *mime = NULL;
g_autoptr(GVariant) v_data = NULL;
g_autoptr(GError) err = NULL;
const char *data = NULL;
const char *mimes[] = { MIME_TEXT_PLAIN_UTF8, NULL };
size_t n;
if (type != QEMU_CLIPBOARD_TYPE_TEXT) {
/* unsupported atm */
return;
}
if (dpy->clipboard_proxy) {
if (!qemu_dbus_display1_clipboard_call_request_sync(
dpy->clipboard_proxy,
info->selection,
mimes,
G_DBUS_CALL_FLAGS_NONE, -1, &mime, &v_data, NULL, &err)) {
error_report("Failed to request clipboard: %s", err->message);
return;
}
if (g_strcmp0(mime, MIME_TEXT_PLAIN_UTF8)) {
error_report("Unsupported returned MIME: %s", mime);
return;
}
data = g_variant_get_fixed_array(v_data, &n, 1);
qemu_clipboard_set_data(&dpy->clipboard_peer, info, type,
n, data, true);
}
}
static void
dbus_clipboard_request_cancelled(DBusClipboardRequest *req)
{
if (!req->invocation) {
return;
}
g_dbus_method_invocation_return_error(
req->invocation,
DBUS_DISPLAY_ERROR,
DBUS_DISPLAY_ERROR_FAILED,
"Cancelled clipboard request");
g_clear_object(&req->invocation);
g_source_remove(req->timeout_id);
req->timeout_id = 0;
}
static void
dbus_clipboard_unregister_proxy(DBusDisplay *dpy)
{
const char *name = NULL;
int i;
for (i = 0; i < G_N_ELEMENTS(dpy->clipboard_request); ++i) {
dbus_clipboard_request_cancelled(&dpy->clipboard_request[i]);
}
if (!dpy->clipboard_proxy) {
return;
}
name = g_dbus_proxy_get_name(G_DBUS_PROXY(dpy->clipboard_proxy));
trace_dbus_clipboard_unregister(name);
g_clear_object(&dpy->clipboard_proxy);
}
static gboolean
dbus_clipboard_register(
DBusDisplay *dpy,
GDBusMethodInvocation *invocation)
{
g_autoptr(GError) err = NULL;
const char *name = NULL;
GDBusConnection *connection = g_dbus_method_invocation_get_connection(invocation);
if (dpy->clipboard_proxy) {
g_dbus_method_invocation_return_error(
invocation,
DBUS_DISPLAY_ERROR,
DBUS_DISPLAY_ERROR_FAILED,
"Clipboard peer already registered!");
return DBUS_METHOD_INVOCATION_HANDLED;
}
dpy->clipboard_proxy =
qemu_dbus_display1_clipboard_proxy_new_sync(
connection,
G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
g_dbus_method_invocation_get_sender(invocation),
"/org/qemu/Display1/Clipboard",
NULL,
&err);
if (!dpy->clipboard_proxy) {
g_dbus_method_invocation_return_error(
invocation,
DBUS_DISPLAY_ERROR,
DBUS_DISPLAY_ERROR_FAILED,
"Failed to setup proxy: %s", err->message);
return DBUS_METHOD_INVOCATION_HANDLED;
}
name = g_dbus_proxy_get_name(G_DBUS_PROXY(dpy->clipboard_proxy));
trace_dbus_clipboard_register(name);
g_object_connect(dpy->clipboard_proxy,
"swapped-signal::notify::g-name-owner",
dbus_clipboard_unregister_proxy, dpy,
NULL);
g_object_connect(connection,
"swapped-signal::closed",
dbus_clipboard_unregister_proxy, dpy,
NULL);
qemu_clipboard_reset_serial();
qemu_dbus_display1_clipboard_complete_register(dpy->clipboard, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
static gboolean
dbus_clipboard_check_caller(DBusDisplay *dpy, GDBusMethodInvocation *invocation)
{
if (!dpy->clipboard_proxy ||
g_strcmp0(g_dbus_proxy_get_name(G_DBUS_PROXY(dpy->clipboard_proxy)),
g_dbus_method_invocation_get_sender(invocation))) {
g_dbus_method_invocation_return_error(
invocation,
DBUS_DISPLAY_ERROR,
DBUS_DISPLAY_ERROR_FAILED,
"Unregistered caller");
return FALSE;
}
return TRUE;
}
static gboolean
dbus_clipboard_unregister(
DBusDisplay *dpy,
GDBusMethodInvocation *invocation)
{
if (!dbus_clipboard_check_caller(dpy, invocation)) {
return DBUS_METHOD_INVOCATION_HANDLED;
}
dbus_clipboard_unregister_proxy(dpy);
qemu_dbus_display1_clipboard_complete_unregister(
dpy->clipboard, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
static gboolean
dbus_clipboard_grab(
DBusDisplay *dpy,
GDBusMethodInvocation *invocation,
gint arg_selection,
guint arg_serial,
const gchar *const *arg_mimes)
{
QemuClipboardSelection s = arg_selection;
g_autoptr(QemuClipboardInfo) info = NULL;
if (!dbus_clipboard_check_caller(dpy, invocation)) {
return DBUS_METHOD_INVOCATION_HANDLED;
}
if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) {
g_dbus_method_invocation_return_error(
invocation,
DBUS_DISPLAY_ERROR,
DBUS_DISPLAY_ERROR_FAILED,
"Invalid clipboard selection: %d", arg_selection);
return DBUS_METHOD_INVOCATION_HANDLED;
}
info = qemu_clipboard_info_new(&dpy->clipboard_peer, s);
if (g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8)) {
info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true;
}
info->serial = arg_serial;
info->has_serial = true;
if (qemu_clipboard_check_serial(info, true)) {
qemu_clipboard_update(info);
} else {
trace_dbus_clipboard_grab_failed();
}
qemu_dbus_display1_clipboard_complete_grab(dpy->clipboard, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
static gboolean
dbus_clipboard_release(
DBusDisplay *dpy,
GDBusMethodInvocation *invocation,
gint arg_selection)
{
if (!dbus_clipboard_check_caller(dpy, invocation)) {
return DBUS_METHOD_INVOCATION_HANDLED;
}
qemu_clipboard_peer_release(&dpy->clipboard_peer, arg_selection);
qemu_dbus_display1_clipboard_complete_release(dpy->clipboard, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
static gboolean
dbus_clipboard_request_timeout(gpointer user_data)
{
dbus_clipboard_request_cancelled(user_data);
return G_SOURCE_REMOVE;
}
static gboolean
dbus_clipboard_request(
DBusDisplay *dpy,
GDBusMethodInvocation *invocation,
gint arg_selection,
const gchar *const *arg_mimes)
{
QemuClipboardSelection s = arg_selection;
QemuClipboardType type = QEMU_CLIPBOARD_TYPE_TEXT;
QemuClipboardInfo *info = NULL;
if (!dbus_clipboard_check_caller(dpy, invocation)) {
return DBUS_METHOD_INVOCATION_HANDLED;
}
if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) {
g_dbus_method_invocation_return_error(
invocation,
DBUS_DISPLAY_ERROR,
DBUS_DISPLAY_ERROR_FAILED,
"Invalid clipboard selection: %d", arg_selection);
return DBUS_METHOD_INVOCATION_HANDLED;
}
if (dpy->clipboard_request[s].invocation) {
g_dbus_method_invocation_return_error(
invocation,
DBUS_DISPLAY_ERROR,
DBUS_DISPLAY_ERROR_FAILED,
"Pending request");
return DBUS_METHOD_INVOCATION_HANDLED;
}
info = qemu_clipboard_info(s);
if (!info || !info->owner || info->owner == &dpy->clipboard_peer) {
g_dbus_method_invocation_return_error(
invocation,
DBUS_DISPLAY_ERROR,
DBUS_DISPLAY_ERROR_FAILED,
"Empty clipboard");
return DBUS_METHOD_INVOCATION_HANDLED;
}
if (!g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8) ||
!info->types[type].available) {
g_dbus_method_invocation_return_error(
invocation,
DBUS_DISPLAY_ERROR,
DBUS_DISPLAY_ERROR_FAILED,
"Unhandled MIME types requested");
return DBUS_METHOD_INVOCATION_HANDLED;
}
if (info->types[type].data) {
dbus_clipboard_complete_request(dpy, invocation, info, type);
} else {
qemu_clipboard_request(info, type);
dpy->clipboard_request[s].invocation = g_object_ref(invocation);
dpy->clipboard_request[s].type = type;
dpy->clipboard_request[s].timeout_id =
g_timeout_add_seconds(5, dbus_clipboard_request_timeout,
&dpy->clipboard_request[s]);
}
return DBUS_METHOD_INVOCATION_HANDLED;
}
void
dbus_clipboard_init(DBusDisplay *dpy)
{
g_autoptr(GDBusObjectSkeleton) clipboard = NULL;
assert(!dpy->clipboard);
clipboard = g_dbus_object_skeleton_new(DBUS_DISPLAY1_ROOT "/Clipboard");
dpy->clipboard = qemu_dbus_display1_clipboard_skeleton_new();
g_object_connect(dpy->clipboard,
"swapped-signal::handle-register",
dbus_clipboard_register, dpy,
"swapped-signal::handle-unregister",
dbus_clipboard_unregister, dpy,
"swapped-signal::handle-grab",
dbus_clipboard_grab, dpy,
"swapped-signal::handle-release",
dbus_clipboard_release, dpy,
"swapped-signal::handle-request",
dbus_clipboard_request, dpy,
NULL);
g_dbus_object_skeleton_add_interface(
G_DBUS_OBJECT_SKELETON(clipboard),
G_DBUS_INTERFACE_SKELETON(dpy->clipboard));
g_dbus_object_manager_server_export(dpy->server, clipboard);
dpy->clipboard_peer.name = "dbus";
dpy->clipboard_peer.notifier.notify = dbus_clipboard_notify;
dpy->clipboard_peer.request = dbus_clipboard_qemu_request;
qemu_clipboard_peer_register(&dpy->clipboard_peer);
}