ui/dbus: add clipboard interface

Expose the clipboard API over D-Bus. See the interface documentation for
further details.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Acked-by: Gerd Hoffmann <kraxel@redhat.com>
This commit is contained in:
Marc-André Lureau 2021-07-20 16:02:52 +04:00
parent 739362d420
commit ff1a5810f6
6 changed files with 579 additions and 0 deletions

457
ui/dbus-clipboard.c Normal file
View File

@ -0,0 +1,457 @@
/*
* 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/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 void
dbus_on_clipboard_proxy_name_owner_changed(
DBusDisplay *dpy,
GObject *object,
GParamSpec *pspec)
{
dbus_clipboard_unregister_proxy(dpy);
}
static gboolean
dbus_clipboard_register(
DBusDisplay *dpy,
GDBusMethodInvocation *invocation)
{
g_autoptr(GError) err = NULL;
const char *name = NULL;
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(
g_dbus_method_invocation_get_connection(invocation),
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_on_clipboard_proxy_name_owner_changed, 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);
}

View File

@ -376,6 +376,103 @@
</method>
</interface>
<!--
org.qemu.Display1.Clipboard:
This interface must be implemented by both the client and the server on
``/org/qemu/Display1/Clipboard`` to support clipboard sharing between
the client and the guest.
Once :dbus:meth:`Register`'ed, method calls may be sent and received in both
directions. Unregistered callers will get error replies.
.. _dbus-clipboard-selection:
**Selection values**::
Clipboard = 0
Primary = 1
Secondary = 2
.. _dbus-clipboard-serial:
**Serial counter**
To solve potential clipboard races, clipboard grabs have an associated
serial counter. It is set to 0 on registration, and incremented by 1 for
each grab. The peer with the highest serial is the clipboard grab owner.
When a grab with a lower serial is received, it should be discarded.
When a grab is attempted with the same serial number as the current grab,
the one coming from the client should have higher priority, and the client
should gain clipboard grab ownership.
-->
<interface name="org.qemu.Display1.Clipboard">
<!--
Register:
Register a clipboard session and reinitialize the serial counter.
The client must register itself, and is granted an exclusive
access for handling the clipboard.
The server can reinitialize the session as well (to reset the counter).
-->
<method name="Register"/>
<!--
Unregister:
Unregister the clipboard session.
-->
<method name="Unregister"/>
<!--
Grab:
@selection: a :ref:`selection value<dbus-clipboard-selection>`.
@serial: the current grab :ref:`serial<dbus-clipboard-serial>`.
@mimes: the list of available content MIME types.
Grab the clipboard, claiming current clipboard content.
-->
<method name="Grab">
<arg type="u" name="selection"/>
<arg type="u" name="serial"/>
<arg type="as" name="mimes"/>
</method>
<!--
Release:
@selection: a :ref:`selection value<dbus-clipboard-selection>`.
Release the clipboard (does nothing if not the current owner).
-->
<method name="Release">
<arg type="u" name="selection"/>
</method>
<!--
Request:
@selection: a :ref:`selection value<dbus-clipboard-selection>`
@mimes: requested MIME types (by order of preference).
@reply_mime: the returned data MIME type.
@data: the clipboard data.
Request the clipboard content.
Return an error if the clipboard is empty, or the requested MIME types
are unavailable.
-->
<method name="Request">
<arg type="u" name="selection"/>
<arg type="as" name="mimes"/>
<arg type="s" name="reply_mime" direction="out"/>
<arg type="ay" name="data" direction="out">
<annotation name="org.gtk.GDBus.C.ForceGVariant" value="true"/>
</arg>
</method>
</interface>
<!--
org.qemu.Display1.Audio:

View File

@ -24,6 +24,7 @@
#include "qemu/osdep.h"
#include "qemu/cutils.h"
#include "qemu/dbus.h"
#include "qemu/main-loop.h"
#include "qemu/option.h"
#include "qom/object_interfaces.h"
#include "sysemu/sysemu.h"
@ -70,6 +71,8 @@ dbus_display_init(Object *o)
g_dbus_object_skeleton_add_interface(
vm, G_DBUS_INTERFACE_SKELETON(dd->iface));
g_dbus_object_manager_server_export(dd->server, vm);
dbus_clipboard_init(dd);
}
static void
@ -77,6 +80,9 @@ dbus_display_finalize(Object *o)
{
DBusDisplay *dd = DBUS_DISPLAY(o);
qemu_clipboard_peer_unregister(&dd->clipboard_peer);
g_clear_object(&dd->clipboard);
g_clear_object(&dd->server);
g_clear_pointer(&dd->consoles, g_ptr_array_unref);
if (dd->add_client_cancellable) {
@ -294,6 +300,7 @@ set_audiodev(Object *o, const char *str, Error **errp)
dd->audiodev = g_strdup(str);
}
static int
get_gl_mode(Object *o, Error **errp)
{

View File

@ -27,9 +27,16 @@
#include "qemu/dbus.h"
#include "qom/object.h"
#include "ui/console.h"
#include "ui/clipboard.h"
#include "dbus-display1.h"
typedef struct DBusClipboardRequest {
GDBusMethodInvocation *invocation;
QemuClipboardType type;
guint timeout_id;
} DBusClipboardRequest;
struct DBusDisplay {
Object parent;
@ -44,6 +51,11 @@ struct DBusDisplay {
QemuDBusDisplay1VM *iface;
GPtrArray *consoles;
GCancellable *add_client_cancellable;
QemuClipboardPeer clipboard_peer;
QemuDBusDisplay1Clipboard *clipboard;
QemuDBusDisplay1Clipboard *clipboard_proxy;
DBusClipboardRequest clipboard_request[QEMU_CLIPBOARD_SELECTION__COUNT];
};
#define TYPE_DBUS_DISPLAY "dbus-display"
@ -83,4 +95,6 @@ dbus_display_listener_get_bus_name(DBusDisplayListener *ddl);
extern const DisplayChangeListenerOps dbus_gl_dcl_ops;
extern const DisplayChangeListenerOps dbus_dcl_ops;
void dbus_clipboard_init(DBusDisplay *dpy);
#endif /* UI_DBUS_H_ */

View File

@ -82,6 +82,7 @@ if dbus_display
'--generate-c-code', '@BASENAME@'])
dbus_ss.add(when: [gio, pixman, opengl, 'CONFIG_GIO'],
if_true: [files(
'dbus-clipboard.c',
'dbus-console.c',
'dbus-error.c',
'dbus-listener.c',

View File

@ -147,3 +147,6 @@ dbus_mouse_release(unsigned int button) "button %u"
dbus_mouse_set_pos(unsigned int x, unsigned int y) "x=%u, y=%u"
dbus_mouse_rel_motion(int dx, int dy) "dx=%d, dy=%d"
dbus_update(int x, int y, int w, int h) "x=%d, y=%d, w=%d, h=%d"
dbus_clipboard_grab_failed(void) ""
dbus_clipboard_register(const char *bus_name) "peer %s"
dbus_clipboard_unregister(const char *bus_name) "peer %s"