mirror of
				https://github.com/qemu/qemu.git
				synced 2025-10-30 19:15:42 +00:00 
			
		
		
		
	 ff1a5810f6
			
		
	
	
		ff1a5810f6
		
	
	
	
	
		
			
			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>
		
			
				
	
	
		
			458 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			458 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| /*
 | |
|  * 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);
 | |
| }
 |