qemu/ui/dbus-clipboard.c
<<
>>
Prefs
   1/*
   2 * QEMU DBus display
   3 *
   4 * Copyright (c) 2021 Marc-André Lureau <marcandre.lureau@redhat.com>
   5 *
   6 * Permission is hereby granted, free of charge, to any person obtaining a copy
   7 * of this software and associated documentation files (the "Software"), to deal
   8 * in the Software without restriction, including without limitation the rights
   9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10 * copies of the Software, and to permit persons to whom the Software is
  11 * furnished to do so, subject to the following conditions:
  12 *
  13 * The above copyright notice and this permission notice shall be included in
  14 * all copies or substantial portions of the Software.
  15 *
  16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
  19 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22 * THE SOFTWARE.
  23 */
  24#include "qemu/osdep.h"
  25#include "qemu/dbus.h"
  26#include "qemu/main-loop.h"
  27#include "qom/object_interfaces.h"
  28#include "sysemu/sysemu.h"
  29#include "qapi/error.h"
  30#include "trace.h"
  31
  32#include "dbus.h"
  33
  34#define MIME_TEXT_PLAIN_UTF8 "text/plain;charset=utf-8"
  35
  36static void
  37dbus_clipboard_complete_request(
  38    DBusDisplay *dpy,
  39    GDBusMethodInvocation *invocation,
  40    QemuClipboardInfo *info,
  41    QemuClipboardType type)
  42{
  43    GVariant *v_data = g_variant_new_from_data(
  44        G_VARIANT_TYPE("ay"),
  45        info->types[type].data,
  46        info->types[type].size,
  47        TRUE,
  48        (GDestroyNotify)qemu_clipboard_info_unref,
  49        qemu_clipboard_info_ref(info));
  50
  51    qemu_dbus_display1_clipboard_complete_request(
  52        dpy->clipboard, invocation,
  53        MIME_TEXT_PLAIN_UTF8, v_data);
  54}
  55
  56static void
  57dbus_clipboard_update_info(DBusDisplay *dpy, QemuClipboardInfo *info)
  58{
  59    bool self_update = info->owner == &dpy->clipboard_peer;
  60    const char *mime[QEMU_CLIPBOARD_TYPE__COUNT + 1] = { 0, };
  61    DBusClipboardRequest *req;
  62    int i = 0;
  63
  64    if (info->owner == NULL) {
  65        if (dpy->clipboard_proxy) {
  66            qemu_dbus_display1_clipboard_call_release(
  67                dpy->clipboard_proxy,
  68                info->selection,
  69                G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
  70        }
  71        return;
  72    }
  73
  74    if (self_update || !info->has_serial) {
  75        return;
  76    }
  77
  78    req = &dpy->clipboard_request[info->selection];
  79    if (req->invocation && info->types[req->type].data) {
  80        dbus_clipboard_complete_request(dpy, req->invocation, info, req->type);
  81        g_clear_object(&req->invocation);
  82        g_source_remove(req->timeout_id);
  83        req->timeout_id = 0;
  84        return;
  85    }
  86
  87    if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) {
  88        mime[i++] = MIME_TEXT_PLAIN_UTF8;
  89    }
  90
  91    if (i > 0) {
  92        if (dpy->clipboard_proxy) {
  93            qemu_dbus_display1_clipboard_call_grab(
  94                dpy->clipboard_proxy,
  95                info->selection,
  96                info->serial,
  97                mime,
  98                G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
  99        }
 100    }
 101}
 102
 103static void
 104dbus_clipboard_reset_serial(DBusDisplay *dpy)
 105{
 106    if (dpy->clipboard_proxy) {
 107        qemu_dbus_display1_clipboard_call_register(
 108            dpy->clipboard_proxy,
 109            G_DBUS_CALL_FLAGS_NONE,
 110            -1, NULL, NULL, NULL);
 111    }
 112}
 113
 114static void
 115dbus_clipboard_notify(Notifier *notifier, void *data)
 116{
 117    DBusDisplay *dpy =
 118        container_of(notifier, DBusDisplay, clipboard_peer.notifier);
 119    QemuClipboardNotify *notify = data;
 120
 121    switch (notify->type) {
 122    case QEMU_CLIPBOARD_UPDATE_INFO:
 123        dbus_clipboard_update_info(dpy, notify->info);
 124        return;
 125    case QEMU_CLIPBOARD_RESET_SERIAL:
 126        dbus_clipboard_reset_serial(dpy);
 127        return;
 128    }
 129}
 130
 131static void
 132dbus_clipboard_qemu_request(QemuClipboardInfo *info,
 133                            QemuClipboardType type)
 134{
 135    DBusDisplay *dpy = container_of(info->owner, DBusDisplay, clipboard_peer);
 136    g_autofree char *mime = NULL;
 137    g_autoptr(GVariant) v_data = NULL;
 138    g_autoptr(GError) err = NULL;
 139    const char *data = NULL;
 140    const char *mimes[] = { MIME_TEXT_PLAIN_UTF8, NULL };
 141    size_t n;
 142
 143    if (type != QEMU_CLIPBOARD_TYPE_TEXT) {
 144        /* unsupported atm */
 145        return;
 146    }
 147
 148    if (dpy->clipboard_proxy) {
 149        if (!qemu_dbus_display1_clipboard_call_request_sync(
 150                dpy->clipboard_proxy,
 151                info->selection,
 152                mimes,
 153                G_DBUS_CALL_FLAGS_NONE, -1, &mime, &v_data, NULL, &err)) {
 154            error_report("Failed to request clipboard: %s", err->message);
 155            return;
 156        }
 157
 158        if (g_strcmp0(mime, MIME_TEXT_PLAIN_UTF8)) {
 159            error_report("Unsupported returned MIME: %s", mime);
 160            return;
 161        }
 162
 163        data = g_variant_get_fixed_array(v_data, &n, 1);
 164        qemu_clipboard_set_data(&dpy->clipboard_peer, info, type,
 165                                n, data, true);
 166    }
 167}
 168
 169static void
 170dbus_clipboard_request_cancelled(DBusClipboardRequest *req)
 171{
 172    if (!req->invocation) {
 173        return;
 174    }
 175
 176    g_dbus_method_invocation_return_error(
 177        req->invocation,
 178        DBUS_DISPLAY_ERROR,
 179        DBUS_DISPLAY_ERROR_FAILED,
 180        "Cancelled clipboard request");
 181
 182    g_clear_object(&req->invocation);
 183    g_source_remove(req->timeout_id);
 184    req->timeout_id = 0;
 185}
 186
 187static void
 188dbus_clipboard_unregister_proxy(DBusDisplay *dpy)
 189{
 190    const char *name = NULL;
 191    int i;
 192
 193    for (i = 0; i < G_N_ELEMENTS(dpy->clipboard_request); ++i) {
 194        dbus_clipboard_request_cancelled(&dpy->clipboard_request[i]);
 195    }
 196
 197    if (!dpy->clipboard_proxy) {
 198        return;
 199    }
 200
 201    name = g_dbus_proxy_get_name(G_DBUS_PROXY(dpy->clipboard_proxy));
 202    trace_dbus_clipboard_unregister(name);
 203    g_clear_object(&dpy->clipboard_proxy);
 204}
 205
 206static void
 207dbus_on_clipboard_proxy_name_owner_changed(
 208    DBusDisplay *dpy,
 209    GObject *object,
 210    GParamSpec *pspec)
 211{
 212    dbus_clipboard_unregister_proxy(dpy);
 213}
 214
 215static gboolean
 216dbus_clipboard_register(
 217    DBusDisplay *dpy,
 218    GDBusMethodInvocation *invocation)
 219{
 220    g_autoptr(GError) err = NULL;
 221    const char *name = NULL;
 222
 223    if (dpy->clipboard_proxy) {
 224        g_dbus_method_invocation_return_error(
 225            invocation,
 226            DBUS_DISPLAY_ERROR,
 227            DBUS_DISPLAY_ERROR_FAILED,
 228            "Clipboard peer already registered!");
 229        return DBUS_METHOD_INVOCATION_HANDLED;
 230    }
 231
 232    dpy->clipboard_proxy =
 233        qemu_dbus_display1_clipboard_proxy_new_sync(
 234            g_dbus_method_invocation_get_connection(invocation),
 235            G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
 236            g_dbus_method_invocation_get_sender(invocation),
 237            "/org/qemu/Display1/Clipboard",
 238            NULL,
 239            &err);
 240    if (!dpy->clipboard_proxy) {
 241        g_dbus_method_invocation_return_error(
 242            invocation,
 243            DBUS_DISPLAY_ERROR,
 244            DBUS_DISPLAY_ERROR_FAILED,
 245            "Failed to setup proxy: %s", err->message);
 246        return DBUS_METHOD_INVOCATION_HANDLED;
 247    }
 248
 249    name = g_dbus_proxy_get_name(G_DBUS_PROXY(dpy->clipboard_proxy));
 250    trace_dbus_clipboard_register(name);
 251
 252    g_object_connect(dpy->clipboard_proxy,
 253                     "swapped-signal::notify::g-name-owner",
 254                     dbus_on_clipboard_proxy_name_owner_changed, dpy,
 255                     NULL);
 256    qemu_clipboard_reset_serial();
 257
 258    qemu_dbus_display1_clipboard_complete_register(dpy->clipboard, invocation);
 259    return DBUS_METHOD_INVOCATION_HANDLED;
 260}
 261
 262static gboolean
 263dbus_clipboard_check_caller(DBusDisplay *dpy, GDBusMethodInvocation *invocation)
 264{
 265    if (!dpy->clipboard_proxy ||
 266        g_strcmp0(g_dbus_proxy_get_name(G_DBUS_PROXY(dpy->clipboard_proxy)),
 267                  g_dbus_method_invocation_get_sender(invocation))) {
 268        g_dbus_method_invocation_return_error(
 269            invocation,
 270            DBUS_DISPLAY_ERROR,
 271            DBUS_DISPLAY_ERROR_FAILED,
 272            "Unregistered caller");
 273        return FALSE;
 274    }
 275
 276    return TRUE;
 277}
 278
 279static gboolean
 280dbus_clipboard_unregister(
 281    DBusDisplay *dpy,
 282    GDBusMethodInvocation *invocation)
 283{
 284    if (!dbus_clipboard_check_caller(dpy, invocation)) {
 285        return DBUS_METHOD_INVOCATION_HANDLED;
 286    }
 287
 288    dbus_clipboard_unregister_proxy(dpy);
 289
 290    qemu_dbus_display1_clipboard_complete_unregister(
 291        dpy->clipboard, invocation);
 292
 293    return DBUS_METHOD_INVOCATION_HANDLED;
 294}
 295
 296static gboolean
 297dbus_clipboard_grab(
 298    DBusDisplay *dpy,
 299    GDBusMethodInvocation *invocation,
 300    gint arg_selection,
 301    guint arg_serial,
 302    const gchar *const *arg_mimes)
 303{
 304    QemuClipboardSelection s = arg_selection;
 305    g_autoptr(QemuClipboardInfo) info = NULL;
 306
 307    if (!dbus_clipboard_check_caller(dpy, invocation)) {
 308        return DBUS_METHOD_INVOCATION_HANDLED;
 309    }
 310
 311    if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) {
 312        g_dbus_method_invocation_return_error(
 313            invocation,
 314            DBUS_DISPLAY_ERROR,
 315            DBUS_DISPLAY_ERROR_FAILED,
 316            "Invalid clipboard selection: %d", arg_selection);
 317        return DBUS_METHOD_INVOCATION_HANDLED;
 318    }
 319
 320    info = qemu_clipboard_info_new(&dpy->clipboard_peer, s);
 321    if (g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8)) {
 322        info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true;
 323    }
 324    info->serial = arg_serial;
 325    info->has_serial = true;
 326    if (qemu_clipboard_check_serial(info, true)) {
 327        qemu_clipboard_update(info);
 328    } else {
 329        trace_dbus_clipboard_grab_failed();
 330    }
 331
 332    qemu_dbus_display1_clipboard_complete_grab(dpy->clipboard, invocation);
 333    return DBUS_METHOD_INVOCATION_HANDLED;
 334}
 335
 336static gboolean
 337dbus_clipboard_release(
 338    DBusDisplay *dpy,
 339    GDBusMethodInvocation *invocation,
 340    gint arg_selection)
 341{
 342    if (!dbus_clipboard_check_caller(dpy, invocation)) {
 343        return DBUS_METHOD_INVOCATION_HANDLED;
 344    }
 345
 346    qemu_clipboard_peer_release(&dpy->clipboard_peer, arg_selection);
 347
 348    qemu_dbus_display1_clipboard_complete_release(dpy->clipboard, invocation);
 349    return DBUS_METHOD_INVOCATION_HANDLED;
 350}
 351
 352static gboolean
 353dbus_clipboard_request_timeout(gpointer user_data)
 354{
 355    dbus_clipboard_request_cancelled(user_data);
 356    return G_SOURCE_REMOVE;
 357}
 358
 359static gboolean
 360dbus_clipboard_request(
 361    DBusDisplay *dpy,
 362    GDBusMethodInvocation *invocation,
 363    gint arg_selection,
 364    const gchar *const *arg_mimes)
 365{
 366    QemuClipboardSelection s = arg_selection;
 367    QemuClipboardType type = QEMU_CLIPBOARD_TYPE_TEXT;
 368    QemuClipboardInfo *info = NULL;
 369
 370    if (!dbus_clipboard_check_caller(dpy, invocation)) {
 371        return DBUS_METHOD_INVOCATION_HANDLED;
 372    }
 373
 374    if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) {
 375        g_dbus_method_invocation_return_error(
 376            invocation,
 377            DBUS_DISPLAY_ERROR,
 378            DBUS_DISPLAY_ERROR_FAILED,
 379            "Invalid clipboard selection: %d", arg_selection);
 380        return DBUS_METHOD_INVOCATION_HANDLED;
 381    }
 382
 383    if (dpy->clipboard_request[s].invocation) {
 384        g_dbus_method_invocation_return_error(
 385            invocation,
 386            DBUS_DISPLAY_ERROR,
 387            DBUS_DISPLAY_ERROR_FAILED,
 388            "Pending request");
 389        return DBUS_METHOD_INVOCATION_HANDLED;
 390    }
 391
 392    info = qemu_clipboard_info(s);
 393    if (!info || !info->owner || info->owner == &dpy->clipboard_peer) {
 394        g_dbus_method_invocation_return_error(
 395            invocation,
 396            DBUS_DISPLAY_ERROR,
 397            DBUS_DISPLAY_ERROR_FAILED,
 398            "Empty clipboard");
 399        return DBUS_METHOD_INVOCATION_HANDLED;
 400    }
 401
 402    if (!g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8) ||
 403        !info->types[type].available) {
 404        g_dbus_method_invocation_return_error(
 405            invocation,
 406            DBUS_DISPLAY_ERROR,
 407            DBUS_DISPLAY_ERROR_FAILED,
 408            "Unhandled MIME types requested");
 409        return DBUS_METHOD_INVOCATION_HANDLED;
 410    }
 411
 412    if (info->types[type].data) {
 413        dbus_clipboard_complete_request(dpy, invocation, info, type);
 414    } else {
 415        qemu_clipboard_request(info, type);
 416
 417        dpy->clipboard_request[s].invocation = g_object_ref(invocation);
 418        dpy->clipboard_request[s].type = type;
 419        dpy->clipboard_request[s].timeout_id =
 420            g_timeout_add_seconds(5, dbus_clipboard_request_timeout,
 421                                  &dpy->clipboard_request[s]);
 422    }
 423
 424    return DBUS_METHOD_INVOCATION_HANDLED;
 425}
 426
 427void
 428dbus_clipboard_init(DBusDisplay *dpy)
 429{
 430    g_autoptr(GDBusObjectSkeleton) clipboard = NULL;
 431
 432    assert(!dpy->clipboard);
 433
 434    clipboard = g_dbus_object_skeleton_new(DBUS_DISPLAY1_ROOT "/Clipboard");
 435    dpy->clipboard = qemu_dbus_display1_clipboard_skeleton_new();
 436    g_object_connect(dpy->clipboard,
 437                     "swapped-signal::handle-register",
 438                     dbus_clipboard_register, dpy,
 439                     "swapped-signal::handle-unregister",
 440                     dbus_clipboard_unregister, dpy,
 441                     "swapped-signal::handle-grab",
 442                     dbus_clipboard_grab, dpy,
 443                     "swapped-signal::handle-release",
 444                     dbus_clipboard_release, dpy,
 445                     "swapped-signal::handle-request",
 446                     dbus_clipboard_request, dpy,
 447                     NULL);
 448
 449    g_dbus_object_skeleton_add_interface(
 450        G_DBUS_OBJECT_SKELETON(clipboard),
 451        G_DBUS_INTERFACE_SKELETON(dpy->clipboard));
 452    g_dbus_object_manager_server_export(dpy->server, clipboard);
 453    dpy->clipboard_peer.name = "dbus";
 454    dpy->clipboard_peer.notifier.notify = dbus_clipboard_notify;
 455    dpy->clipboard_peer.request = dbus_clipboard_qemu_request;
 456    qemu_clipboard_peer_register(&dpy->clipboard_peer);
 457}
 458