All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/7] ui: add vdagent implementation and clipboard support.
@ 2021-02-19 13:13 Gerd Hoffmann
  2021-02-19 13:13 ` [PATCH 1/7] ui: add clipboard infrastructure Gerd Hoffmann
                   ` (7 more replies)
  0 siblings, 8 replies; 20+ messages in thread
From: Gerd Hoffmann @ 2021-02-19 13:13 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Gerd Hoffmann, Marc-André Lureau, Markus Armbruster

Fist sketch of cut+paste support for vnc.  On the guest side we are
going to reuse the spice vdagent, so things should work out-of-the-box
with guests in the wild.  So this patch set brings a qemu implemenation
of the vdagent protocol.

Beside that there is the clipboard infrastructure of course.  For now
only text support is there.  The design allows adding more data types,
so we can add image support and maybe more later on.  So far vdagent,
vnc server and gtk ui are hooked up.

Usage: qemu \
  -chardev vdagent,id=vdagent,clipboard=on \
  -device virtio-serial-pci \
  -device virtserialport,chardev=vdagent,name=com.redhat.spice.0

Enjoy!

Gerd Hoffmann (7):
  ui: add clipboard infrastructure
  ui/vdagent: core infrastructure
  ui/vdagent: add mouse support
  ui/vdagent: add clipboard support
  ui/vnc: clipboard support
  ui/gtk: move struct GtkDisplayState to ui/gtk.h
  ui/gtk: add clipboard support

 include/ui/clipboard.h |  68 +++++
 include/ui/gtk.h       |  66 +++++
 ui/vnc.h               |  24 ++
 chardev/char.c         |   6 +
 ui/clipboard.c         |  92 ++++++
 ui/gtk-clipboard.c     | 124 ++++++++
 ui/gtk.c               |  56 +---
 ui/vdagent.c           | 624 +++++++++++++++++++++++++++++++++++++++++
 ui/vnc-clipboard.c     | 326 +++++++++++++++++++++
 ui/vnc.c               |  20 +-
 qapi/char.json         |  17 ++
 ui/meson.build         |   5 +-
 ui/trace-events        |   9 +
 13 files changed, 1375 insertions(+), 62 deletions(-)
 create mode 100644 include/ui/clipboard.h
 create mode 100644 ui/clipboard.c
 create mode 100644 ui/gtk-clipboard.c
 create mode 100644 ui/vdagent.c
 create mode 100644 ui/vnc-clipboard.c

-- 
2.29.2




^ permalink raw reply	[flat|nested] 20+ messages in thread

* [PATCH 1/7] ui: add clipboard infrastructure
  2021-02-19 13:13 [PATCH 0/7] ui: add vdagent implementation and clipboard support Gerd Hoffmann
@ 2021-02-19 13:13 ` Gerd Hoffmann
  2021-02-19 13:13 ` [PATCH 2/7] ui/vdagent: core infrastructure Gerd Hoffmann
                   ` (6 subsequent siblings)
  7 siblings, 0 replies; 20+ messages in thread
From: Gerd Hoffmann @ 2021-02-19 13:13 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Gerd Hoffmann, Marc-André Lureau, Markus Armbruster

Add some infrastructure to manage the clipboard in qemu.

TODO: Add API docs.

Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
---
 include/ui/clipboard.h | 68 +++++++++++++++++++++++++++++++
 ui/clipboard.c         | 92 ++++++++++++++++++++++++++++++++++++++++++
 ui/meson.build         |  1 +
 3 files changed, 161 insertions(+)
 create mode 100644 include/ui/clipboard.h
 create mode 100644 ui/clipboard.c

diff --git a/include/ui/clipboard.h b/include/ui/clipboard.h
new file mode 100644
index 000000000000..00ff559425ee
--- /dev/null
+++ b/include/ui/clipboard.h
@@ -0,0 +1,68 @@
+#ifndef QEMU_CLIPBOARD_H
+#define QEMU_CLIPBOARD_H
+
+#include "qemu/notify.h"
+
+typedef enum QemuClipboardType QemuClipboardType;
+typedef enum QemuClipboardSelection QemuClipboardSelection;
+typedef struct QemuClipboardPeer QemuClipboardPeer;
+typedef struct QemuClipboardInfo QemuClipboardInfo;
+
+enum QemuClipboardType {
+    QEMU_CLIPBOARD_TYPE_TEXT,  /* text/plain; charset=utf-8 */
+    QEMU_CLIPBOARD_TYPE__COUNT,
+};
+
+/* same as VD_AGENT_CLIPBOARD_SELECTION_* */
+enum QemuClipboardSelection {
+    QEMU_CLIPBOARD_SELECTION_CLIPBOARD,
+    QEMU_CLIPBOARD_SELECTION_PRIMARY,
+    QEMU_CLIPBOARD_SELECTION_SECONDARY,
+    QEMU_CLIPBOARD_SELECTION__COUNT,
+};
+
+struct QemuClipboardPeer {
+    const char *name;
+    Notifier update;
+    void (*request)(QemuClipboardInfo *info,
+                    QemuClipboardType type);
+};
+
+struct QemuClipboardInfo {
+    uint32_t refcount;
+    QemuClipboardPeer *owner;
+    QemuClipboardSelection selection;
+    struct {
+        bool available;
+        bool requested;
+        size_t size;
+        void *data;
+    } types[QEMU_CLIPBOARD_TYPE__COUNT];
+};
+
+struct QemuClipboardData {
+    uint32_t refcount;
+    QemuClipboardInfo *info;
+    QemuClipboardType type;
+};
+
+void qemu_clipboard_peer_register(QemuClipboardPeer *peer);
+void qemu_clipboard_peer_unregister(QemuClipboardPeer *peer);
+
+QemuClipboardInfo *qemu_clipboard_info_new(QemuClipboardPeer *owner,
+                                           QemuClipboardSelection selection);
+QemuClipboardInfo *qemu_clipboard_info_get(QemuClipboardInfo *info);
+void qemu_clipboard_info_put(QemuClipboardInfo *info);
+
+void qemu_clipboard_update(QemuClipboardInfo *info);
+void qemu_clipboard_request(QemuClipboardInfo *info,
+                            QemuClipboardType type);
+
+void qemu_clipboard_set_data(QemuClipboardPeer *peer,
+                             QemuClipboardInfo *info,
+                             QemuClipboardType type,
+                             uint32_t size,
+                             void *data,
+                             bool update);
+
+#endif /* QEMU_CLIPBOARD_H */
diff --git a/ui/clipboard.c b/ui/clipboard.c
new file mode 100644
index 000000000000..556531c578a1
--- /dev/null
+++ b/ui/clipboard.c
@@ -0,0 +1,92 @@
+#include "qemu/osdep.h"
+#include "ui/clipboard.h"
+
+static NotifierList clipboard_notifiers =
+    NOTIFIER_LIST_INITIALIZER(clipboard_notifiers);
+
+void qemu_clipboard_peer_register(QemuClipboardPeer *peer)
+{
+    notifier_list_add(&clipboard_notifiers, &peer->update);
+}
+
+void qemu_clipboard_peer_unregister(QemuClipboardPeer *peer)
+{
+    notifier_remove(&peer->update);
+}
+
+void qemu_clipboard_update(QemuClipboardInfo *info)
+{
+    notifier_list_notify(&clipboard_notifiers, info);
+}
+
+QemuClipboardInfo *qemu_clipboard_info_new(QemuClipboardPeer *owner,
+                                           QemuClipboardSelection selection)
+{
+    QemuClipboardInfo *info = g_new0(QemuClipboardInfo, 1);
+
+    info->owner = owner;
+    info->selection = selection;
+    info->refcount = 1;
+
+    return info;
+}
+
+QemuClipboardInfo *qemu_clipboard_info_get(QemuClipboardInfo *info)
+{
+    info->refcount++;
+    return info;
+}
+
+void qemu_clipboard_info_put(QemuClipboardInfo *info)
+{
+    uint32_t type;
+
+    if (!info) {
+        return;
+    }
+
+    info->refcount--;
+    if (info->refcount > 0) {
+        return;
+    }
+
+    for (type = 0; type < QEMU_CLIPBOARD_TYPE__COUNT; type++) {
+        g_free(info->types[type].data);
+    }
+    g_free(info);
+}
+
+void qemu_clipboard_request(QemuClipboardInfo *info,
+                            QemuClipboardType type)
+{
+    if (info->types[type].data ||
+        info->types[type].requested ||
+        !info->types[type].available ||
+        !info->owner)
+        return;
+
+    info->types[type].requested = true;
+    info->owner->request(info, type);
+}
+
+void qemu_clipboard_set_data(QemuClipboardPeer *peer,
+                             QemuClipboardInfo *info,
+                             QemuClipboardType type,
+                             uint32_t size,
+                             void *data,
+                             bool update)
+{
+    if (!info ||
+        info->owner != peer) {
+        return;
+    }
+
+    g_free(info->types[type].data);
+    info->types[type].data = g_memdup(data, size);
+    info->types[type].size = size;
+    info->types[type].available = true;
+
+    if (update) {
+        qemu_clipboard_update(info);
+    }
+}
diff --git a/ui/meson.build b/ui/meson.build
index 156b600a9998..fbb9a512042a 100644
--- a/ui/meson.build
+++ b/ui/meson.build
@@ -2,6 +2,7 @@ softmmu_ss.add(pixman)
 specific_ss.add(when: ['CONFIG_SOFTMMU'], if_true: pixman)   # for the include path
 
 softmmu_ss.add(files(
+  'clipboard.c',
   'console.c',
   'cursor.c',
   'input-keymap.c',
-- 
2.29.2



^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH 2/7] ui/vdagent: core infrastructure
  2021-02-19 13:13 [PATCH 0/7] ui: add vdagent implementation and clipboard support Gerd Hoffmann
  2021-02-19 13:13 ` [PATCH 1/7] ui: add clipboard infrastructure Gerd Hoffmann
@ 2021-02-19 13:13 ` Gerd Hoffmann
  2021-02-25 18:24   ` Marc-André Lureau
  2021-02-19 13:13 ` [PATCH 3/7] ui/vdagent: add mouse support Gerd Hoffmann
                   ` (5 subsequent siblings)
  7 siblings, 1 reply; 20+ messages in thread
From: Gerd Hoffmann @ 2021-02-19 13:13 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Gerd Hoffmann, Marc-André Lureau, Markus Armbruster

The vdagent protocol allows the guest agent (spice-vdagent) and the
spice client exchange messages to implement features which require
guest cooperation, for example clipboard support.

This is a qemu implementation of the spice client side.  This allows
the spice guest agent talk to qemu directly when not using the spice
protocol.

usage: qemu \
  -chardev vdagent,id=vdagent \
  -device virtserialport,chardev=vdagent,name=com.redhat.spice.0

This patch adds just the protocol basics: initial handshake and
capability negotiation.

Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
---
 ui/vdagent.c    | 240 ++++++++++++++++++++++++++++++++++++++++++++++++
 qapi/char.json  |  13 +++
 ui/meson.build  |   1 +
 ui/trace-events |   7 ++
 4 files changed, 261 insertions(+)
 create mode 100644 ui/vdagent.c

diff --git a/ui/vdagent.c b/ui/vdagent.c
new file mode 100644
index 000000000000..9ef4ed3f4dd8
--- /dev/null
+++ b/ui/vdagent.c
@@ -0,0 +1,240 @@
+#include "qemu/osdep.h"
+#include "include/qemu-common.h"
+#include "chardev/char.h"
+#include "trace.h"
+
+#include "qapi/qapi-types-char.h"
+
+#include "spice/vd_agent.h"
+
+#define MSGSIZE_MAX (sizeof(VDIChunkHeader) + \
+                     sizeof(VDAgentMessage) + \
+                     VD_AGENT_MAX_DATA_SIZE)
+
+struct VDAgentChardev {
+    Chardev parent;
+
+    /* guest vdagent */
+    uint32_t caps;
+    uint8_t msgbuf[MSGSIZE_MAX];
+    uint32_t msgsize;
+};
+typedef struct VDAgentChardev VDAgentChardev;
+
+#define TYPE_CHARDEV_VDAGENT "chardev-vdagent"
+
+DECLARE_INSTANCE_CHECKER(VDAgentChardev, VDAGENT_CHARDEV,
+                         TYPE_CHARDEV_VDAGENT);
+
+/* ------------------------------------------------------------------ */
+/* names, for debug logging                                           */
+
+static const char *cap_name[] = {
+    [VD_AGENT_CAP_MOUSE_STATE]                    = "mouse-state",
+    [VD_AGENT_CAP_MONITORS_CONFIG]                = "monitors-config",
+    [VD_AGENT_CAP_REPLY]                          = "reply",
+    [VD_AGENT_CAP_CLIPBOARD]                      = "clipboard",
+    [VD_AGENT_CAP_DISPLAY_CONFIG]                 = "display-config",
+    [VD_AGENT_CAP_CLIPBOARD_BY_DEMAND]            = "clipboard-by-demand",
+    [VD_AGENT_CAP_CLIPBOARD_SELECTION]            = "clipboard-selection",
+    [VD_AGENT_CAP_SPARSE_MONITORS_CONFIG]         = "sparse-monitors-config",
+    [VD_AGENT_CAP_GUEST_LINEEND_LF]               = "guest-lineend-lf",
+    [VD_AGENT_CAP_GUEST_LINEEND_CRLF]             = "guest-lineend-crlf",
+    [VD_AGENT_CAP_MAX_CLIPBOARD]                  = "max-clipboard",
+    [VD_AGENT_CAP_AUDIO_VOLUME_SYNC]              = "audio-volume-sync",
+    [VD_AGENT_CAP_MONITORS_CONFIG_POSITION]       = "monitors-config-position",
+    [VD_AGENT_CAP_FILE_XFER_DISABLED]             = "file-xfer-disabled",
+    [VD_AGENT_CAP_FILE_XFER_DETAILED_ERRORS]      = "file-xfer-detailed-errors",
+#if 0
+    [VD_AGENT_CAP_GRAPHICS_DEVICE_INFO]           = "graphics-device-info",
+    [VD_AGENT_CAP_CLIPBOARD_NO_RELEASE_ON_REGRAB] = "clipboard-no-release-on-regrab",
+    [VD_AGENT_CAP_CLIPBOARD_GRAB_SERIAL]          = "clipboard-grab-serial",
+#endif
+};
+
+static const char *msg_name[] = {
+    [VD_AGENT_MOUSE_STATE]           = "mouse-state",
+    [VD_AGENT_MONITORS_CONFIG]       = "monitors-config",
+    [VD_AGENT_REPLY]                 = "reply",
+    [VD_AGENT_CLIPBOARD]             = "clipboard",
+    [VD_AGENT_DISPLAY_CONFIG]        = "display-config",
+    [VD_AGENT_ANNOUNCE_CAPABILITIES] = "announce-capabilities",
+    [VD_AGENT_CLIPBOARD_GRAB]        = "clipboard-grab",
+    [VD_AGENT_CLIPBOARD_REQUEST]     = "clipboard-request",
+    [VD_AGENT_CLIPBOARD_RELEASE]     = "clipboard-release",
+    [VD_AGENT_FILE_XFER_START]       = "file-xfer-start",
+    [VD_AGENT_FILE_XFER_STATUS]      = "file-xfer-status",
+    [VD_AGENT_FILE_XFER_DATA]        = "file-xfer-data",
+    [VD_AGENT_CLIENT_DISCONNECTED]   = "client-disconnected",
+    [VD_AGENT_MAX_CLIPBOARD]         = "max-clipboard",
+    [VD_AGENT_AUDIO_VOLUME_SYNC]     = "audio-volume-sync",
+#if 0
+    [VD_AGENT_GRAPHICS_DEVICE_INFO]  = "graphics-device-info",
+#endif
+};
+
+#define GET_NAME(_m, _v) \
+    (((_v) < ARRAY_SIZE(_m) && (_m[_v])) ? (_m[_v]) : "???")
+
+/* ------------------------------------------------------------------ */
+/* send messages                                                      */
+
+static void vdagent_send_buf(VDAgentChardev *vd, void *ptr, uint32_t msgsize)
+{
+    uint8_t *msgbuf = ptr;
+    uint32_t len, pos = 0;
+
+    while (pos < msgsize) {
+        len = qemu_chr_be_can_write(CHARDEV(vd));
+        if (len > msgsize - pos) {
+            len = msgsize - pos;
+        }
+        qemu_chr_be_write(CHARDEV(vd), msgbuf + pos, len);
+        pos += len;
+    }
+}
+
+static void vdagent_send_msg(VDAgentChardev *vd, VDAgentMessage *msg)
+{
+    uint8_t *msgbuf = (void *)msg;
+    uint32_t msgsize = sizeof(VDAgentMessage) + msg->size;
+    VDIChunkHeader chunk;
+
+    trace_vdagent_send(GET_NAME(msg_name, msg->type));
+
+    chunk.port = VDP_CLIENT_PORT;
+    chunk.size = msgsize;
+    vdagent_send_buf(vd, &chunk, sizeof(chunk));
+
+    msg->protocol = VD_AGENT_PROTOCOL;
+    vdagent_send_buf(vd, msgbuf, msgsize);
+    g_free(msg);
+}
+
+static void vdagent_send_caps(VDAgentChardev *vd)
+{
+    VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) +
+                                    sizeof(VDAgentAnnounceCapabilities) +
+                                    sizeof(uint32_t));
+
+    msg->type = VD_AGENT_ANNOUNCE_CAPABILITIES;
+    msg->size = sizeof(VDAgentAnnounceCapabilities) + sizeof(uint32_t);
+
+    vdagent_send_msg(vd, msg);
+}
+
+/* ------------------------------------------------------------------ */
+/* chardev backend                                                    */
+
+static void vdagent_chr_open(Chardev *chr,
+                             ChardevBackend *backend,
+                             bool *be_opened,
+                             Error **errp)
+{
+    *be_opened = true;
+}
+
+static void vdagent_chr_recv_caps(VDAgentChardev *vd, VDAgentMessage *msg)
+{
+    VDAgentAnnounceCapabilities *caps = (void *)msg->data;
+    int i;
+
+    for (i = 0; i < ARRAY_SIZE(cap_name); i++) {
+        if (caps->caps[0] & (1 << i)) {
+            trace_vdagent_peer_cap(GET_NAME(cap_name, i));
+        }
+    }
+
+    vd->caps = caps->caps[0];
+    if (caps->request) {
+        vdagent_send_caps(vd);
+    }
+}
+
+static uint32_t vdagent_chr_recv(VDAgentChardev *vd)
+{
+    VDIChunkHeader *chunk = (void *)vd->msgbuf;
+    VDAgentMessage *msg = (void *)vd->msgbuf + sizeof(VDIChunkHeader);
+
+    if (sizeof(VDIChunkHeader) + chunk->size > vd->msgsize) {
+        return 0;
+    }
+
+    trace_vdagent_recv(GET_NAME(msg_name, msg->type));
+
+    switch (msg->type) {
+    case VD_AGENT_ANNOUNCE_CAPABILITIES:
+        vdagent_chr_recv_caps(vd, msg);
+        break;
+    default:
+        break;
+    }
+
+    return sizeof(VDIChunkHeader) + chunk->size;
+}
+
+static int vdagent_chr_write(Chardev *chr, const uint8_t *buf, int len)
+{
+    VDAgentChardev *vd = VDAGENT_CHARDEV(chr);
+    uint32_t copy, move;
+
+    copy = MSGSIZE_MAX - vd->msgsize;
+    if (copy > len) {
+        copy = len;
+    }
+
+    memcpy(vd->msgbuf + vd->msgsize, buf, copy);
+    vd->msgsize += copy;
+
+    while (vd->msgsize > sizeof(VDIChunkHeader)) {
+        move = vdagent_chr_recv(vd);
+        if (move == 0) {
+            break;
+        }
+
+        memmove(vd->msgbuf, vd->msgbuf + move, vd->msgsize - move);
+        vd->msgsize -= move;
+    }
+
+    return copy;
+}
+
+static void vdagent_chr_set_fe_open(struct Chardev *chr, int fe_open)
+{
+    VDAgentChardev *vd = VDAGENT_CHARDEV(chr);
+
+    if (!fe_open) {
+        trace_vdagent_close();
+        /* reset state */
+        vd->msgsize = 0;
+        vd->caps = 0;
+        return;
+    }
+
+    trace_vdagent_open();
+}
+
+/* ------------------------------------------------------------------ */
+
+static void vdagent_chr_class_init(ObjectClass *oc, void *data)
+{
+    ChardevClass *cc = CHARDEV_CLASS(oc);
+
+    cc->open             = vdagent_chr_open;
+    cc->chr_write        = vdagent_chr_write;
+    cc->chr_set_fe_open  = vdagent_chr_set_fe_open;
+}
+
+static const TypeInfo vdagent_chr_type_info = {
+    .name = TYPE_CHARDEV_VDAGENT,
+    .parent = TYPE_CHARDEV,
+    .instance_size = sizeof(VDAgentChardev),
+    .class_init = vdagent_chr_class_init,
+};
+
+static void register_types(void)
+{
+    type_register_static(&vdagent_chr_type_info);
+}
+
+type_init(register_types);
diff --git a/qapi/char.json b/qapi/char.json
index 58338ed62d43..62e161aea343 100644
--- a/qapi/char.json
+++ b/qapi/char.json
@@ -390,6 +390,17 @@
   'data': { '*size': 'int' },
   'base': 'ChardevCommon' }
 
+##
+# @ChardevVDAgent:
+#
+# Configuration info for vdagent.
+#
+# Since: 6.0
+##
+{ 'struct': 'ChardevVDAgent',
+  'data': { },
+  'base': 'ChardevCommon' }
+
 ##
 # @ChardevBackend:
 #
@@ -417,6 +428,8 @@
                           'if': 'defined(CONFIG_SPICE)' },
             'spiceport': { 'type': 'ChardevSpicePort',
                            'if': 'defined(CONFIG_SPICE)' },
+            'vdagent': { 'type': 'ChardevVDAgent',
+                         'if': 'defined(CONFIG_SPICE)' },
             'vc': 'ChardevVC',
             'ringbuf': 'ChardevRingbuf',
             # next one is just for compatibility
diff --git a/ui/meson.build b/ui/meson.build
index fbb9a512042a..08447ac15c5e 100644
--- a/ui/meson.build
+++ b/ui/meson.build
@@ -14,6 +14,7 @@ softmmu_ss.add(files(
   'qemu-pixman.c',
 ))
 softmmu_ss.add([spice_headers, files('spice-module.c')])
+softmmu_ss.add(when: spice_headers, if_true: files('vdagent.c'))
 
 softmmu_ss.add(when: 'CONFIG_LINUX', if_true: files('input-linux.c'))
 softmmu_ss.add(when: cocoa, if_true: files('cocoa.m'))
diff --git a/ui/trace-events b/ui/trace-events
index 0ffcdb4408a6..1a5bd3861da5 100644
--- a/ui/trace-events
+++ b/ui/trace-events
@@ -108,3 +108,10 @@ xkeymap_extension(const char *name) "extension '%s'"
 xkeymap_vendor(const char *name) "vendor '%s'"
 xkeymap_keycodes(const char *name) "keycodes '%s'"
 xkeymap_keymap(const char *name) "keymap '%s'"
+
+# vdagent.c
+vdagent_open(void) ""
+vdagent_close(void) ""
+vdagent_send(const char *name) "msg %s"
+vdagent_recv(const char *name) "msg %s"
+vdagent_peer_cap(const char *name) "cap %s"
-- 
2.29.2



^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH 3/7] ui/vdagent: add mouse support
  2021-02-19 13:13 [PATCH 0/7] ui: add vdagent implementation and clipboard support Gerd Hoffmann
  2021-02-19 13:13 ` [PATCH 1/7] ui: add clipboard infrastructure Gerd Hoffmann
  2021-02-19 13:13 ` [PATCH 2/7] ui/vdagent: core infrastructure Gerd Hoffmann
@ 2021-02-19 13:13 ` Gerd Hoffmann
  2021-02-19 13:13 ` [PATCH 4/7] ui/vdagent: add clipboard support Gerd Hoffmann
                   ` (4 subsequent siblings)
  7 siblings, 0 replies; 20+ messages in thread
From: Gerd Hoffmann @ 2021-02-19 13:13 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Gerd Hoffmann, Marc-André Lureau, Markus Armbruster

This patch adds support for mouse messages to the vdagent
implementation.  This can be enabled/disabled using the new
'mouse' parameter for the vdagent chardev.  Default is on.

Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
---
 chardev/char.c |   3 ++
 ui/vdagent.c   | 141 +++++++++++++++++++++++++++++++++++++++++++++++++
 qapi/char.json |   4 +-
 3 files changed, 147 insertions(+), 1 deletion(-)

diff --git a/chardev/char.c b/chardev/char.c
index 288efebd1257..ea986dac1bff 100644
--- a/chardev/char.c
+++ b/chardev/char.c
@@ -924,6 +924,9 @@ QemuOptsList qemu_chardev_opts = {
         },{
             .name = "logappend",
             .type = QEMU_OPT_BOOL,
+        },{
+            .name = "mouse",
+            .type = QEMU_OPT_BOOL,
 #ifdef CONFIG_LINUX
         },{
             .name = "tight",
diff --git a/ui/vdagent.c b/ui/vdagent.c
index 9ef4ed3f4dd8..b48b0129b9f2 100644
--- a/ui/vdagent.c
+++ b/ui/vdagent.c
@@ -1,9 +1,14 @@
 #include "qemu/osdep.h"
 #include "include/qemu-common.h"
 #include "chardev/char.h"
+#include "hw/qdev-core.h"
+#include "qemu/option.h"
+#include "ui/console.h"
+#include "ui/input.h"
 #include "trace.h"
 
 #include "qapi/qapi-types-char.h"
+#include "qapi/qapi-types-ui.h"
 
 #include "spice/vd_agent.h"
 
@@ -11,13 +16,25 @@
                      sizeof(VDAgentMessage) + \
                      VD_AGENT_MAX_DATA_SIZE)
 
+#define VDAGENT_MOUSE_DEFAULT true
+
 struct VDAgentChardev {
     Chardev parent;
 
+    /* config */
+    bool mouse;
+
     /* guest vdagent */
     uint32_t caps;
     uint8_t msgbuf[MSGSIZE_MAX];
     uint32_t msgsize;
+
+    /* mouse */
+    DeviceState mouse_dev;
+    uint32_t mouse_x;
+    uint32_t mouse_y;
+    uint32_t mouse_btn;
+    QemuInputHandlerState *mouse_hs;
 };
 typedef struct VDAgentChardev VDAgentChardev;
 
@@ -116,13 +133,105 @@ static void vdagent_send_caps(VDAgentChardev *vd)
     VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) +
                                     sizeof(VDAgentAnnounceCapabilities) +
                                     sizeof(uint32_t));
+    VDAgentAnnounceCapabilities *caps = (void *)msg->data;
 
     msg->type = VD_AGENT_ANNOUNCE_CAPABILITIES;
     msg->size = sizeof(VDAgentAnnounceCapabilities) + sizeof(uint32_t);
+    if (vd->mouse) {
+        caps->caps[0] |= (1 << VD_AGENT_CAP_MOUSE_STATE);
+    }
 
     vdagent_send_msg(vd, msg);
 }
 
+/* ------------------------------------------------------------------ */
+/* mouse events                                                       */
+
+static void vdagent_send_mouse(VDAgentChardev *vd)
+{
+    VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) +
+                                    sizeof(VDAgentMouseState));
+    VDAgentMouseState *mouse = (void *)msg->data;
+
+    msg->type = VD_AGENT_MOUSE_STATE;
+    msg->size = sizeof(VDAgentMouseState);
+
+    mouse->x       = vd->mouse_x;
+    mouse->y       = vd->mouse_y;
+    mouse->buttons = vd->mouse_btn;
+
+    vdagent_send_msg(vd, msg);
+}
+
+static void vdagent_pointer_event(DeviceState *dev, QemuConsole *src,
+                                  InputEvent *evt)
+{
+    static const int bmap[INPUT_BUTTON__MAX] = {
+        [INPUT_BUTTON_LEFT]        = VD_AGENT_LBUTTON_MASK,
+        [INPUT_BUTTON_RIGHT]       = VD_AGENT_RBUTTON_MASK,
+        [INPUT_BUTTON_MIDDLE]      = VD_AGENT_MBUTTON_MASK,
+        [INPUT_BUTTON_WHEEL_UP]    = VD_AGENT_UBUTTON_MASK,
+        [INPUT_BUTTON_WHEEL_DOWN]  = VD_AGENT_DBUTTON_MASK,
+#if 0
+        [INPUT_BUTTON_SIDE]        = VD_AGENT_SBUTTON_MASK,
+        [INPUT_BUTTON_EXTRA]       = VD_AGENT_EBUTTON_MASK,
+#endif
+    };
+
+    VDAgentChardev *vd = container_of(dev, struct VDAgentChardev, mouse_dev);
+    InputMoveEvent *move;
+    InputBtnEvent *btn;
+    uint32_t xres, yres;
+
+    switch (evt->type) {
+    case INPUT_EVENT_KIND_ABS:
+        move = evt->u.abs.data;
+        xres = qemu_console_get_width(src, 1024);
+        yres = qemu_console_get_height(src, 768);
+        if (move->axis == INPUT_AXIS_X) {
+            vd->mouse_x = qemu_input_scale_axis(move->value,
+                                                INPUT_EVENT_ABS_MIN,
+                                                INPUT_EVENT_ABS_MAX,
+                                                0, xres);
+        } else if (move->axis == INPUT_AXIS_Y) {
+            vd->mouse_y = qemu_input_scale_axis(move->value,
+                                                INPUT_EVENT_ABS_MIN,
+                                                INPUT_EVENT_ABS_MAX,
+                                                0, yres);
+        }
+        break;
+
+    case INPUT_EVENT_KIND_BTN:
+        btn = evt->u.btn.data;
+        if (btn->down) {
+            vd->mouse_btn |= bmap[btn->button];
+        } else {
+            vd->mouse_btn &= ~bmap[btn->button];
+        }
+        break;
+
+    default:
+        /* keep gcc happy */
+        break;
+    }
+}
+
+static void vdagent_pointer_sync(DeviceState *dev)
+{
+    VDAgentChardev *vd = container_of(dev, struct VDAgentChardev, mouse_dev);
+
+    if (vd->caps & (1 << VD_AGENT_CAP_MOUSE_STATE)) {
+        vdagent_send_mouse(vd);
+    }
+}
+
+static QemuInputHandler vdagent_mouse_handler = {
+    .name  = "vdagent mouse",
+    .mask  = INPUT_EVENT_MASK_BTN | INPUT_EVENT_MASK_ABS,
+    .event = vdagent_pointer_event,
+    .sync  = vdagent_pointer_sync,
+};
+
 /* ------------------------------------------------------------------ */
 /* chardev backend                                                    */
 
@@ -131,6 +240,19 @@ static void vdagent_chr_open(Chardev *chr,
                              bool *be_opened,
                              Error **errp)
 {
+    VDAgentChardev *vd = VDAGENT_CHARDEV(chr);
+    ChardevVDAgent *cfg = backend->u.vdagent.data;
+
+    vd->mouse = VDAGENT_MOUSE_DEFAULT;
+    if (cfg->has_mouse) {
+        vd->mouse = cfg->mouse;
+    }
+
+    if (vd->mouse) {
+        vd->mouse_hs = qemu_input_handler_register(&vd->mouse_dev,
+                                                   &vdagent_mouse_handler);
+    }
+
     *be_opened = true;
 }
 
@@ -149,6 +271,9 @@ static void vdagent_chr_recv_caps(VDAgentChardev *vd, VDAgentMessage *msg)
     if (caps->request) {
         vdagent_send_caps(vd);
     }
+    if (vd->caps & (1 << VD_AGENT_CAP_MOUSE_STATE) && vd->mouse_hs) {
+        qemu_input_handler_activate(vd->mouse_hs);
+    }
 }
 
 static uint32_t vdagent_chr_recv(VDAgentChardev *vd)
@@ -208,18 +333,34 @@ static void vdagent_chr_set_fe_open(struct Chardev *chr, int fe_open)
         /* reset state */
         vd->msgsize = 0;
         vd->caps = 0;
+        if (vd->mouse_hs) {
+            qemu_input_handler_deactivate(vd->mouse_hs);
+        }
         return;
     }
 
     trace_vdagent_open();
 }
 
+static void vdagent_chr_parse(QemuOpts *opts, ChardevBackend *backend,
+                              Error **errp)
+{
+    ChardevVDAgent *cfg;
+
+    backend->type = CHARDEV_BACKEND_KIND_VDAGENT;
+    cfg = backend->u.vdagent.data = g_new0(ChardevVDAgent, 1);
+    qemu_chr_parse_common(opts, qapi_ChardevVDAgent_base(cfg));
+    cfg->has_mouse = true;
+    cfg->mouse = qemu_opt_get_bool(opts, "mouse", VDAGENT_MOUSE_DEFAULT);
+}
+
 /* ------------------------------------------------------------------ */
 
 static void vdagent_chr_class_init(ObjectClass *oc, void *data)
 {
     ChardevClass *cc = CHARDEV_CLASS(oc);
 
+    cc->parse            = vdagent_chr_parse;
     cc->open             = vdagent_chr_open;
     cc->chr_write        = vdagent_chr_write;
     cc->chr_set_fe_open  = vdagent_chr_set_fe_open;
diff --git a/qapi/char.json b/qapi/char.json
index 62e161aea343..d8e96b772523 100644
--- a/qapi/char.json
+++ b/qapi/char.json
@@ -395,10 +395,12 @@
 #
 # Configuration info for vdagent.
 #
+# @mouse: enable/disable mouse, default is enabled.
+#
 # Since: 6.0
 ##
 { 'struct': 'ChardevVDAgent',
-  'data': { },
+  'data': { '*mouse'    : 'bool' },
   'base': 'ChardevCommon' }
 
 ##
-- 
2.29.2



^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH 4/7] ui/vdagent: add clipboard support
  2021-02-19 13:13 [PATCH 0/7] ui: add vdagent implementation and clipboard support Gerd Hoffmann
                   ` (2 preceding siblings ...)
  2021-02-19 13:13 ` [PATCH 3/7] ui/vdagent: add mouse support Gerd Hoffmann
@ 2021-02-19 13:13 ` Gerd Hoffmann
  2021-02-25 18:37   ` Marc-André Lureau
  2021-02-19 13:13 ` [PATCH 5/7] ui/vnc: " Gerd Hoffmann
                   ` (3 subsequent siblings)
  7 siblings, 1 reply; 20+ messages in thread
From: Gerd Hoffmann @ 2021-02-19 13:13 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Gerd Hoffmann, Marc-André Lureau, Markus Armbruster

This patch adds support for clipboard messages to the qemu vdagent
implementation, which allows the guest exchange clipboard data with
qemu.  Clipboard support can be enabled/disabled using the new
'clipboard' parameter for the vdagent chardev.  Default is off.

Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
---
 chardev/char.c  |   3 +
 ui/vdagent.c    | 243 ++++++++++++++++++++++++++++++++++++++++++++++++
 qapi/char.json  |   4 +-
 ui/trace-events |   2 +
 4 files changed, 251 insertions(+), 1 deletion(-)

diff --git a/chardev/char.c b/chardev/char.c
index ea986dac1bff..7f3ee2c11f9d 100644
--- a/chardev/char.c
+++ b/chardev/char.c
@@ -927,6 +927,9 @@ QemuOptsList qemu_chardev_opts = {
         },{
             .name = "mouse",
             .type = QEMU_OPT_BOOL,
+        },{
+            .name = "clipboard",
+            .type = QEMU_OPT_BOOL,
 #ifdef CONFIG_LINUX
         },{
             .name = "tight",
diff --git a/ui/vdagent.c b/ui/vdagent.c
index b48b0129b9f2..ee058688027e 100644
--- a/ui/vdagent.c
+++ b/ui/vdagent.c
@@ -3,6 +3,7 @@
 #include "chardev/char.h"
 #include "hw/qdev-core.h"
 #include "qemu/option.h"
+#include "ui/clipboard.h"
 #include "ui/console.h"
 #include "ui/input.h"
 #include "trace.h"
@@ -17,12 +18,14 @@
                      VD_AGENT_MAX_DATA_SIZE)
 
 #define VDAGENT_MOUSE_DEFAULT true
+#define VDAGENT_CLIPBOARD_DEFAULT false
 
 struct VDAgentChardev {
     Chardev parent;
 
     /* config */
     bool mouse;
+    bool clipboard;
 
     /* guest vdagent */
     uint32_t caps;
@@ -35,6 +38,11 @@ struct VDAgentChardev {
     uint32_t mouse_y;
     uint32_t mouse_btn;
     QemuInputHandlerState *mouse_hs;
+
+    /* clipboard */
+    QemuClipboardPeer cbpeer;
+    QemuClipboardInfo *cbinfo[QEMU_CLIPBOARD_SELECTION__COUNT];
+    uint32_t cbpending[QEMU_CLIPBOARD_SELECTION__COUNT];
 };
 typedef struct VDAgentChardev VDAgentChardev;
 
@@ -90,6 +98,24 @@ static const char *msg_name[] = {
 #endif
 };
 
+static const char *sel_name[] = {
+    [VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD] = "clipboard",
+    [VD_AGENT_CLIPBOARD_SELECTION_PRIMARY]   = "primary",
+    [VD_AGENT_CLIPBOARD_SELECTION_SECONDARY] = "secondary",
+};
+
+static const char *type_name[] = {
+    [VD_AGENT_CLIPBOARD_NONE]       = "none",
+    [VD_AGENT_CLIPBOARD_UTF8_TEXT]  = "text",
+    [VD_AGENT_CLIPBOARD_IMAGE_PNG]  = "png",
+    [VD_AGENT_CLIPBOARD_IMAGE_BMP]  = "bmp",
+    [VD_AGENT_CLIPBOARD_IMAGE_TIFF] = "tiff",
+    [VD_AGENT_CLIPBOARD_IMAGE_JPG]  = "jpg",
+#if 0
+    [VD_AGENT_CLIPBOARD_FILE_LIST]  = "files",
+#endif
+};
+
 #define GET_NAME(_m, _v) \
     (((_v) < ARRAY_SIZE(_m) && (_m[_v])) ? (_m[_v]) : "???")
 
@@ -140,6 +166,10 @@ static void vdagent_send_caps(VDAgentChardev *vd)
     if (vd->mouse) {
         caps->caps[0] |= (1 << VD_AGENT_CAP_MOUSE_STATE);
     }
+    if (vd->clipboard) {
+        caps->caps[0] |= (1 << VD_AGENT_CAP_CLIPBOARD_BY_DEMAND);
+        caps->caps[0] |= (1 << VD_AGENT_CAP_CLIPBOARD_SELECTION);
+    }
 
     vdagent_send_msg(vd, msg);
 }
@@ -232,6 +262,193 @@ static QemuInputHandler vdagent_mouse_handler = {
     .sync  = vdagent_pointer_sync,
 };
 
+/* ------------------------------------------------------------------ */
+/* clipboard                                                          */
+
+static uint32_t type_qemu_to_vdagent(enum QemuClipboardType type)
+{
+    switch (type) {
+    case QEMU_CLIPBOARD_TYPE_TEXT:
+        return VD_AGENT_CLIPBOARD_UTF8_TEXT;
+    default:
+        return VD_AGENT_CLIPBOARD_NONE;
+    }
+}
+
+static void vdagent_send_clipboard_grab(VDAgentChardev *vd,
+                                        QemuClipboardInfo *info)
+{
+    VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) +
+                                    sizeof(uint32_t) * (QEMU_CLIPBOARD_TYPE__COUNT + 1));
+    uint8_t *s = msg->data;
+    uint32_t *data = (uint32_t *)(msg->data + 4);
+    uint32_t q, v, type;
+
+    for (q = 0, v = 0; q < QEMU_CLIPBOARD_TYPE__COUNT; q++) {
+        type = type_qemu_to_vdagent(q);
+        if (type != VD_AGENT_CLIPBOARD_NONE && info->types[q].available) {
+            data[v++] = type;
+        }
+    }
+
+    *s = info->selection;
+    msg->type = VD_AGENT_CLIPBOARD_GRAB;
+    msg->size = sizeof(uint32_t) * (v + 1);
+
+    vdagent_send_msg(vd, msg);
+}
+
+static void vdagent_send_clipboard_data(VDAgentChardev *vd,
+                                        QemuClipboardInfo *info,
+                                        QemuClipboardType type)
+{
+    VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) +
+                                    sizeof(uint32_t) * 2 +
+                                    info->types[type].size);
+
+    uint8_t *s = msg->data;
+    uint32_t *t = (uint32_t *)(msg->data + 4);
+    uint8_t *d = msg->data + 8;
+
+    *s = info->selection;
+    *t = type_qemu_to_vdagent(type);
+    memcpy(d, info->types[type].data, info->types[type].size);
+
+    msg->type = VD_AGENT_CLIPBOARD;
+    msg->size = sizeof(uint32_t) * 2 + info->types[type].size;
+
+    vdagent_send_msg(vd, msg);
+}
+
+static void vdagent_clipboard_notify(Notifier *notifier, void *data)
+{
+    VDAgentChardev *vd = container_of(notifier, VDAgentChardev, cbpeer.update);
+    QemuClipboardInfo *info = data;
+    QemuClipboardSelection s = info->selection;
+    QemuClipboardType type;
+    bool self_update = info->owner == &vd->cbpeer;
+
+    if (info != vd->cbinfo[s]) {
+        qemu_clipboard_info_put(vd->cbinfo[s]);
+        vd->cbinfo[s] = qemu_clipboard_info_get(info);
+        vd->cbpending[s] = 0;
+        if (!self_update) {
+            vdagent_send_clipboard_grab(vd, info);
+        }
+        return;
+    }
+
+    if (self_update) {
+        return;
+    }
+
+    for (type = 0; type < QEMU_CLIPBOARD_TYPE__COUNT; type++) {
+        if (vd->cbpending[s] & (1 << type)) {
+            vd->cbpending[s] &= ~(1 << type);
+            vdagent_send_clipboard_data(vd, info, type);
+        }
+    }
+}
+
+static void vdagent_clipboard_request(QemuClipboardInfo *info,
+                                      QemuClipboardType qtype)
+{
+    VDAgentChardev *vd = container_of(info->owner, VDAgentChardev, cbpeer);
+    VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) +
+                                    sizeof(uint32_t) * 2);
+    uint32_t type = type_qemu_to_vdagent(qtype);
+    uint8_t *s = msg->data;
+    uint32_t *data = (uint32_t *)(msg->data + 4);
+
+    if (type == VD_AGENT_CLIPBOARD_NONE) {
+        return;
+    }
+
+    *s = info->selection;
+    *data = type;
+    msg->type = VD_AGENT_CLIPBOARD_REQUEST;
+    msg->size = sizeof(uint32_t) * 2;
+
+    vdagent_send_msg(vd, msg);
+}
+
+static void vdagent_chr_recv_clipboard(VDAgentChardev *vd, VDAgentMessage *msg)
+{
+    uint8_t s = VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD;
+    uint32_t size = msg->size;
+    void *data = msg->data;
+    QemuClipboardInfo *info;
+    QemuClipboardType type;
+
+    if (vd->caps & (1 << VD_AGENT_CAP_CLIPBOARD_SELECTION)) {
+        s = *(uint8_t *)data;
+        data += 4;
+        size -= 4;
+    }
+
+    switch (msg->type) {
+    case VD_AGENT_CLIPBOARD_GRAB:
+        trace_vdagent_cb_grab_selection(GET_NAME(sel_name, s));
+        info = qemu_clipboard_info_new(&vd->cbpeer, s);
+        while (size) {
+            trace_vdagent_cb_grab_type(GET_NAME(type_name, *(uint32_t *)data));
+            switch (*(uint32_t *)data) {
+            case VD_AGENT_CLIPBOARD_UTF8_TEXT:
+                info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true;
+                break;
+            default:
+                break;
+            }
+            data += 4;
+            size -= 4;
+        }
+        qemu_clipboard_update(info);
+        qemu_clipboard_info_put(info);
+        break;
+    case VD_AGENT_CLIPBOARD_REQUEST:
+        switch (*(uint32_t *)data) {
+        case VD_AGENT_CLIPBOARD_UTF8_TEXT:
+            type = QEMU_CLIPBOARD_TYPE_TEXT;
+            break;
+        default:
+            return;
+        }
+        if (vd->cbinfo[s] &&
+            vd->cbinfo[s]->types[type].available &&
+            vd->cbinfo[s]->owner != &vd->cbpeer) {
+            if (vd->cbinfo[s]->types[type].data) {
+                vdagent_send_clipboard_data(vd, vd->cbinfo[s], type);
+            } else {
+                vd->cbpending[s] |= (1 << type);
+                qemu_clipboard_request(vd->cbinfo[s], type);
+            }
+        }
+        break;
+    case VD_AGENT_CLIPBOARD: /* data */
+        switch (*(uint32_t *)data) {
+        case VD_AGENT_CLIPBOARD_UTF8_TEXT:
+            type = QEMU_CLIPBOARD_TYPE_TEXT;
+            break;
+        default:
+            return;
+        }
+        data += 4;
+        size -= 4;
+        qemu_clipboard_set_data(&vd->cbpeer, vd->cbinfo[s], type,
+                                size, data, true);
+        break;
+    case VD_AGENT_CLIPBOARD_RELEASE: /* data */
+        if (vd->cbinfo[s] &&
+            vd->cbinfo[s]->owner == &vd->cbpeer) {
+            /* set empty clipboard info */
+            info = qemu_clipboard_info_new(NULL, s);
+            qemu_clipboard_update(info);
+            qemu_clipboard_info_put(info);
+        }
+        break;
+    }
+}
+
 /* ------------------------------------------------------------------ */
 /* chardev backend                                                    */
 
@@ -248,6 +465,11 @@ static void vdagent_chr_open(Chardev *chr,
         vd->mouse = cfg->mouse;
     }
 
+    vd->clipboard = VDAGENT_CLIPBOARD_DEFAULT;
+    if (cfg->has_clipboard) {
+        vd->clipboard = cfg->clipboard;
+    }
+
     if (vd->mouse) {
         vd->mouse_hs = qemu_input_handler_register(&vd->mouse_dev,
                                                    &vdagent_mouse_handler);
@@ -274,6 +496,15 @@ static void vdagent_chr_recv_caps(VDAgentChardev *vd, VDAgentMessage *msg)
     if (vd->caps & (1 << VD_AGENT_CAP_MOUSE_STATE) && vd->mouse_hs) {
         qemu_input_handler_activate(vd->mouse_hs);
     }
+    if (vd->caps & (1 << VD_AGENT_CAP_CLIPBOARD_BY_DEMAND) &&
+        vd->caps & (1 << VD_AGENT_CAP_CLIPBOARD_SELECTION) &&
+        vd->clipboard &&
+        vd->cbpeer.update.notify == NULL) {
+        vd->cbpeer.name = "vdagent";
+        vd->cbpeer.update.notify = vdagent_clipboard_notify;
+        vd->cbpeer.request = vdagent_clipboard_request;
+        qemu_clipboard_peer_register(&vd->cbpeer);
+    }
 }
 
 static uint32_t vdagent_chr_recv(VDAgentChardev *vd)
@@ -291,6 +522,12 @@ static uint32_t vdagent_chr_recv(VDAgentChardev *vd)
     case VD_AGENT_ANNOUNCE_CAPABILITIES:
         vdagent_chr_recv_caps(vd, msg);
         break;
+    case VD_AGENT_CLIPBOARD:
+    case VD_AGENT_CLIPBOARD_GRAB:
+    case VD_AGENT_CLIPBOARD_REQUEST:
+    case VD_AGENT_CLIPBOARD_RELEASE:
+        vdagent_chr_recv_clipboard(vd, msg);
+        break;
     default:
         break;
     }
@@ -336,6 +573,10 @@ static void vdagent_chr_set_fe_open(struct Chardev *chr, int fe_open)
         if (vd->mouse_hs) {
             qemu_input_handler_deactivate(vd->mouse_hs);
         }
+        if (vd->cbpeer.update.notify) {
+            qemu_clipboard_peer_unregister(&vd->cbpeer);
+            memset(&vd->cbpeer, 0, sizeof(vd->cbpeer));
+        }
         return;
     }
 
@@ -352,6 +593,8 @@ static void vdagent_chr_parse(QemuOpts *opts, ChardevBackend *backend,
     qemu_chr_parse_common(opts, qapi_ChardevVDAgent_base(cfg));
     cfg->has_mouse = true;
     cfg->mouse = qemu_opt_get_bool(opts, "mouse", VDAGENT_MOUSE_DEFAULT);
+    cfg->has_clipboard = true;
+    cfg->clipboard = qemu_opt_get_bool(opts, "clipboard", VDAGENT_CLIPBOARD_DEFAULT);
 }
 
 /* ------------------------------------------------------------------ */
diff --git a/qapi/char.json b/qapi/char.json
index d8e96b772523..059c9d634b06 100644
--- a/qapi/char.json
+++ b/qapi/char.json
@@ -396,11 +396,13 @@
 # Configuration info for vdagent.
 #
 # @mouse: enable/disable mouse, default is enabled.
+# @clipboard: enable/disable clipboard, default is disabled.
 #
 # Since: 6.0
 ##
 { 'struct': 'ChardevVDAgent',
-  'data': { '*mouse'    : 'bool' },
+  'data': { '*mouse'    : 'bool',
+            '*clipboard': 'bool' },
   'base': 'ChardevCommon' }
 
 ##
diff --git a/ui/trace-events b/ui/trace-events
index 1a5bd3861da5..90191cc1d285 100644
--- a/ui/trace-events
+++ b/ui/trace-events
@@ -115,3 +115,5 @@ vdagent_close(void) ""
 vdagent_send(const char *name) "msg %s"
 vdagent_recv(const char *name) "msg %s"
 vdagent_peer_cap(const char *name) "cap %s"
+vdagent_cb_grab_selection(const char *name) "selection %s"
+vdagent_cb_grab_type(const char *name) "type %s"
-- 
2.29.2



^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH 5/7] ui/vnc: clipboard support
  2021-02-19 13:13 [PATCH 0/7] ui: add vdagent implementation and clipboard support Gerd Hoffmann
                   ` (3 preceding siblings ...)
  2021-02-19 13:13 ` [PATCH 4/7] ui/vdagent: add clipboard support Gerd Hoffmann
@ 2021-02-19 13:13 ` Gerd Hoffmann
  2021-02-25 19:09   ` Marc-André Lureau
  2021-02-19 13:13 ` [PATCH 6/7] ui/gtk: move struct GtkDisplayState to ui/gtk.h Gerd Hoffmann
                   ` (2 subsequent siblings)
  7 siblings, 1 reply; 20+ messages in thread
From: Gerd Hoffmann @ 2021-02-19 13:13 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Gerd Hoffmann, Marc-André Lureau, Markus Armbruster

This patch adds support for cut+paste to the qemu vnc server, which
allows the vnc client exchange clipbaord data with qemu and other peers
like the qemu vdagent implementation.

Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
---
 ui/vnc.h           |  24 ++++
 ui/vnc-clipboard.c | 326 +++++++++++++++++++++++++++++++++++++++++++++
 ui/vnc.c           |  20 ++-
 ui/meson.build     |   1 +
 4 files changed, 365 insertions(+), 6 deletions(-)
 create mode 100644 ui/vnc-clipboard.c

diff --git a/ui/vnc.h b/ui/vnc.h
index 116463d5f099..f611223859ae 100644
--- a/ui/vnc.h
+++ b/ui/vnc.h
@@ -29,6 +29,7 @@
 
 #include "qemu/queue.h"
 #include "qemu/thread.h"
+#include "ui/clipboard.h"
 #include "ui/console.h"
 #include "audio/audio.h"
 #include "qemu/bitmap.h"
@@ -347,6 +348,10 @@ struct VncState
 
     Notifier mouse_mode_notifier;
 
+    QemuClipboardPeer cbpeer;
+    QemuClipboardInfo *cbinfo;
+    uint32_t cbpending;
+
     QTAILQ_ENTRY(VncState) next;
 };
 
@@ -416,6 +421,7 @@ enum {
 #define VNC_ENCODING_XVP                  0XFFFFFECB /* -309 */
 #define VNC_ENCODING_ALPHA_CURSOR         0XFFFFFEC6 /* -314 */
 #define VNC_ENCODING_WMVi                 0x574D5669
+#define VNC_ENCODING_CLIPBOARD_EXT        0xc0a1e5ce
 
 /*****************************************************************************
  *
@@ -457,6 +463,7 @@ enum VncFeatures {
     VNC_FEATURE_ZYWRLE,
     VNC_FEATURE_LED_STATE,
     VNC_FEATURE_XVP,
+    VNC_FEATURE_CLIPBOARD_EXT,
 };
 
 #define VNC_FEATURE_RESIZE_MASK              (1 << VNC_FEATURE_RESIZE)
@@ -473,6 +480,7 @@ enum VncFeatures {
 #define VNC_FEATURE_ZYWRLE_MASK              (1 << VNC_FEATURE_ZYWRLE)
 #define VNC_FEATURE_LED_STATE_MASK           (1 << VNC_FEATURE_LED_STATE)
 #define VNC_FEATURE_XVP_MASK                 (1 << VNC_FEATURE_XVP)
+#define VNC_FEATURE_CLIPBOARD_EXT_MASK       (1 <<  VNC_FEATURE_CLIPBOARD_EXT)
 
 
 /* Client -> Server message IDs */
@@ -534,6 +542,17 @@ enum VncFeatures {
 #define VNC_XVP_ACTION_REBOOT 3
 #define VNC_XVP_ACTION_RESET 4
 
+/* extended clipboard flags  */
+#define VNC_CLIPBOARD_TEXT     (1 << 0)
+#define VNC_CLIPBOARD_RTF      (1 << 1)
+#define VNC_CLIPBOARD_HTML     (1 << 2)
+#define VNC_CLIPBOARD_DIB      (1 << 3)
+#define VNC_CLIPBOARD_FILES    (1 << 4)
+#define VNC_CLIPBOARD_CAPS     (1 << 24)
+#define VNC_CLIPBOARD_REQUEST  (1 << 25)
+#define VNC_CLIPBOARD_PEEK     (1 << 26)
+#define VNC_CLIPBOARD_NOTIFY   (1 << 27)
+#define VNC_CLIPBOARD_PROVIDE  (1 << 28)
 
 /*****************************************************************************
  *
@@ -617,4 +636,9 @@ int vnc_zrle_send_framebuffer_update(VncState *vs, int x, int y, int w, int h);
 int vnc_zywrle_send_framebuffer_update(VncState *vs, int x, int y, int w, int h);
 void vnc_zrle_clear(VncState *vs);
 
+/* vnc-clipboard.c */
+void vnc_server_cut_text_caps(VncState *vs);
+void vnc_client_cut_text(VncState *vs, size_t len, uint8_t *text);
+void vnc_client_cut_text_ext(VncState *vs, int32_t len, uint32_t flags, uint8_t *data);
+
 #endif /* QEMU_VNC_H */
diff --git a/ui/vnc-clipboard.c b/ui/vnc-clipboard.c
new file mode 100644
index 000000000000..e729120ba360
--- /dev/null
+++ b/ui/vnc-clipboard.c
@@ -0,0 +1,326 @@
+/*
+ * QEMU VNC display driver -- clipboard support
+ *
+ * Copyright (C) 2021 Gerd Hoffmann <kraxel@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-common.h"
+#include "vnc.h"
+#include "vnc-jobs.h"
+
+static uint8_t *inflate_buffer(uint8_t *in, uint32_t in_len, uint32_t *size)
+{
+    z_stream stream = {
+        .next_in  = in,
+        .avail_in = in_len,
+        .zalloc   = Z_NULL,
+        .zfree    = Z_NULL,
+    };
+    uint32_t out_len = 8;
+    uint8_t *out = g_malloc(out_len);
+    int ret;
+
+    stream.next_out = out + stream.total_out;
+    stream.avail_out = out_len - stream.total_out;
+
+    ret = inflateInit(&stream);
+    if (ret != Z_OK) {
+        goto err;
+    }
+
+    while (stream.avail_in) {
+        ret = inflate(&stream, Z_FINISH);
+        switch (ret) {
+        case Z_OK:
+        case Z_STREAM_END:
+            break;
+        case Z_BUF_ERROR:
+            out_len <<= 1;
+            if (out_len > (1 << 20)) {
+                goto err_end;
+            }
+            out = g_realloc(out, out_len);
+            stream.next_out = out + stream.total_out;
+            stream.avail_out = out_len - stream.total_out;
+            break;
+        default:
+            goto err_end;
+        }
+    }
+
+    *size = stream.total_out;
+    inflateEnd(&stream);
+
+    return out;
+
+err_end:
+    inflateEnd(&stream);
+err:
+    g_free(out);
+    return NULL;
+}
+
+static uint8_t *deflate_buffer(uint8_t *in, uint32_t in_len, uint32_t *size)
+{
+    z_stream stream = {
+        .next_in  = in,
+        .avail_in = in_len,
+        .zalloc   = Z_NULL,
+        .zfree    = Z_NULL,
+    };
+    uint32_t out_len = 8;
+    uint8_t *out = g_malloc(out_len);
+    int ret;
+
+    stream.next_out = out + stream.total_out;
+    stream.avail_out = out_len - stream.total_out;
+
+    ret = deflateInit(&stream, Z_DEFAULT_COMPRESSION);
+    if (ret != Z_OK) {
+        goto err;
+    }
+
+    while (ret != Z_STREAM_END) {
+        ret = deflate(&stream, Z_FINISH);
+        switch (ret) {
+        case Z_OK:
+        case Z_STREAM_END:
+            break;
+        case Z_BUF_ERROR:
+            out_len <<= 1;
+            if (out_len > (1 << 20)) {
+                goto err_end;
+            }
+            out = g_realloc(out, out_len);
+            stream.next_out = out + stream.total_out;
+            stream.avail_out = out_len - stream.total_out;
+            break;
+        default:
+            goto err_end;
+        }
+    }
+
+    *size = stream.total_out;
+    deflateEnd(&stream);
+
+    return out;
+
+err_end:
+    deflateEnd(&stream);
+err:
+    g_free(out);
+    return NULL;
+}
+
+static void vnc_clipboard_send(VncState *vs, uint32_t count, uint32_t *dwords)
+{
+    int i;
+
+    vnc_lock_output(vs);
+    vnc_write_u8(vs, VNC_MSG_SERVER_CUT_TEXT);
+    vnc_write_u8(vs, 0);
+    vnc_write_u8(vs, 0);
+    vnc_write_u8(vs, 0);
+    vnc_write_s32(vs, -(count * sizeof(uint32_t)));  /* -(message length) */
+    for (i = 0; i < count; i++) {
+        vnc_write_u32(vs, dwords[i]);
+    }
+    vnc_unlock_output(vs);
+    vnc_flush(vs);
+}
+
+static void vnc_clipboard_provide(VncState *vs,
+                                  QemuClipboardInfo *info,
+                                  QemuClipboardType type)
+{
+    uint32_t flags = 0;
+    uint8_t *buf;
+    void *zbuf;
+    uint32_t zsize;
+
+    switch (type) {
+    case QEMU_CLIPBOARD_TYPE_TEXT:
+        flags |= VNC_CLIPBOARD_TEXT;
+        break;
+    default:
+        return;
+    }
+    flags |= VNC_CLIPBOARD_PROVIDE;
+
+    buf = g_malloc(info->types[type].size + 4);
+    buf[0] = (info->types[type].size >> 24) & 0xff;
+    buf[1] = (info->types[type].size >> 16) & 0xff;
+    buf[2] = (info->types[type].size >>  8) & 0xff;
+    buf[3] = (info->types[type].size >>  0) & 0xff;
+    memcpy(buf + 4, info->types[type].data, info->types[type].size);
+    zbuf = deflate_buffer(buf, info->types[type].size + 4, &zsize);
+    g_free(buf);
+
+    if (!zbuf) {
+        return;
+    }
+
+    vnc_lock_output(vs);
+    vnc_write_u8(vs, VNC_MSG_SERVER_CUT_TEXT);
+    vnc_write_u8(vs, 0);
+    vnc_write_u8(vs, 0);
+    vnc_write_u8(vs, 0);
+    vnc_write_s32(vs, -(sizeof(uint32_t) + zsize));  /* -(message length) */
+    vnc_write_u32(vs, flags);
+    vnc_write(vs, zbuf, zsize);
+    vnc_unlock_output(vs);
+    vnc_flush(vs);
+}
+
+static void vnc_clipboard_notify(Notifier *notifier, void *data)
+{
+    VncState *vs = container_of(notifier, VncState, cbpeer.update);
+    QemuClipboardInfo *info = data;
+    QemuClipboardType type;
+    bool self_update = info->owner == &vs->cbpeer;
+    uint32_t flags = 0;
+
+    if (info != vs->cbinfo) {
+        qemu_clipboard_info_put(vs->cbinfo);
+        vs->cbinfo = qemu_clipboard_info_get(info);
+        vs->cbpending = 0;
+        if (!self_update) {
+            if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) {
+                flags |= VNC_CLIPBOARD_TEXT;
+            }
+            flags |= VNC_CLIPBOARD_NOTIFY;
+            vnc_clipboard_send(vs, 1, &flags);
+        }
+        return;
+    }
+
+    if (self_update) {
+        return;
+    }
+
+    for (type = 0; type < QEMU_CLIPBOARD_TYPE__COUNT; type++) {
+        if (vs->cbpending & (1 << type)) {
+            vs->cbpending &= ~(1 << type);
+            vnc_clipboard_provide(vs, info, type);
+        }
+    }
+}
+
+static void vnc_clipboard_request(QemuClipboardInfo *info,
+                                  QemuClipboardType type)
+{
+    VncState *vs = container_of(info->owner, VncState, cbpeer);
+    uint32_t flags = 0;
+
+    if (type == QEMU_CLIPBOARD_TYPE_TEXT) {
+        flags |= VNC_CLIPBOARD_TEXT;
+    }
+    if (!flags) {
+        return;
+    }
+    flags |= VNC_CLIPBOARD_REQUEST;
+
+    vnc_clipboard_send(vs, 1, &flags);
+}
+
+void vnc_client_cut_text_ext(VncState *vs, int32_t len, uint32_t flags, uint8_t *data)
+{
+    if (flags & VNC_CLIPBOARD_CAPS) {
+        /* need store caps somewhere ? */
+        return;
+    }
+
+    if (flags & VNC_CLIPBOARD_NOTIFY) {
+        QemuClipboardInfo *info =
+            qemu_clipboard_info_new(&vs->cbpeer, QEMU_CLIPBOARD_SELECTION_CLIPBOARD);
+        if (flags & VNC_CLIPBOARD_TEXT) {
+            info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true;
+        }
+        qemu_clipboard_update(info);
+        qemu_clipboard_info_put(info);
+        return;
+    }
+
+    if (flags & VNC_CLIPBOARD_PROVIDE &&
+        vs->cbinfo &&
+        vs->cbinfo->owner == &vs->cbpeer) {
+        uint32_t size = 0;
+        uint8_t *buf = inflate_buffer(data, len - 4, &size);
+        if ((flags & VNC_CLIPBOARD_TEXT) &&
+            buf && size >= 4) {
+            uint32_t tsize = read_u32(buf, 0);
+            uint8_t *tbuf = buf + 4;
+            if (tsize < size) {
+                qemu_clipboard_set_data(&vs->cbpeer, vs->cbinfo,
+                                        QEMU_CLIPBOARD_TYPE_TEXT,
+                                        tsize, tbuf, true);
+            }
+        }
+        g_free(buf);
+    }
+
+    if (flags & VNC_CLIPBOARD_REQUEST &&
+        vs->cbinfo &&
+        vs->cbinfo->owner != &vs->cbpeer) {
+        if ((flags & VNC_CLIPBOARD_TEXT) &&
+            vs->cbinfo->types[QEMU_CLIPBOARD_TYPE_TEXT].available) {
+            if (vs->cbinfo->types[QEMU_CLIPBOARD_TYPE_TEXT].data) {
+                vnc_clipboard_provide(vs, vs->cbinfo, QEMU_CLIPBOARD_TYPE_TEXT);
+            } else {
+                vs->cbpending |= (1 << QEMU_CLIPBOARD_TYPE_TEXT);
+                qemu_clipboard_request(vs->cbinfo, QEMU_CLIPBOARD_TYPE_TEXT);
+            }
+        }
+    }
+}
+
+void vnc_client_cut_text(VncState *vs, size_t len, uint8_t *text)
+{
+    QemuClipboardInfo *info =
+        qemu_clipboard_info_new(&vs->cbpeer, QEMU_CLIPBOARD_SELECTION_CLIPBOARD);
+
+    qemu_clipboard_set_data(&vs->cbpeer, info, QEMU_CLIPBOARD_TYPE_TEXT,
+                            len, text, true);
+    qemu_clipboard_info_put(info);
+}
+
+void vnc_server_cut_text_caps(VncState *vs)
+{
+    uint32_t caps[2];
+
+    if (!vnc_has_feature(vs, VNC_FEATURE_CLIPBOARD_EXT)) {
+        return;
+    }
+
+    caps[0] = (VNC_CLIPBOARD_PROVIDE |
+               VNC_CLIPBOARD_NOTIFY  |
+               VNC_CLIPBOARD_REQUEST |
+               VNC_CLIPBOARD_CAPS    |
+               VNC_CLIPBOARD_TEXT);
+    caps[1] = 0;
+    vnc_clipboard_send(vs, 2, caps);
+
+    vs->cbpeer.name = "vnc";
+    vs->cbpeer.update.notify = vnc_clipboard_notify;
+    vs->cbpeer.request = vnc_clipboard_request;
+    qemu_clipboard_peer_register(&vs->cbpeer);
+}
diff --git a/ui/vnc.c b/ui/vnc.c
index 16bb3be770b2..91ec51c7c67d 100644
--- a/ui/vnc.c
+++ b/ui/vnc.c
@@ -25,6 +25,7 @@
  */
 
 #include "qemu/osdep.h"
+#include "qemu-common.h"
 #include "vnc.h"
 #include "vnc-jobs.h"
 #include "trace.h"
@@ -1309,6 +1310,9 @@ void vnc_disconnect_finish(VncState *vs)
         /* last client gone */
         vnc_update_server_surface(vs->vd);
     }
+    if (vs->cbpeer.update.notify) {
+        qemu_clipboard_peer_unregister(&vs->cbpeer);
+    }
 
     vnc_unlock_output(vs);
 
@@ -1734,10 +1738,6 @@ uint32_t read_u32(uint8_t *data, size_t offset)
             (data[offset + 2] << 8) | data[offset + 3]);
 }
 
-static void client_cut_text(VncState *vs, size_t len, uint8_t *text)
-{
-}
-
 static void check_pointer_type_change(Notifier *notifier, void *data)
 {
     VncState *vs = container_of(notifier, VncState, mouse_mode_notifier);
@@ -2179,6 +2179,10 @@ static void set_encodings(VncState *vs, int32_t *encodings, size_t n_encodings)
                 send_xvp_message(vs, VNC_XVP_CODE_INIT);
             }
             break;
+        case VNC_ENCODING_CLIPBOARD_EXT:
+            vs->features |= VNC_FEATURE_CLIPBOARD_EXT_MASK;
+            vnc_server_cut_text_caps(vs);
+            break;
         case VNC_ENCODING_COMPRESSLEVEL0 ... VNC_ENCODING_COMPRESSLEVEL0 + 9:
             vs->tight->compression = (enc & 0x0F);
             break;
@@ -2395,7 +2399,7 @@ static int protocol_client_msg(VncState *vs, uint8_t *data, size_t len)
             return 8;
         }
         if (len == 8) {
-            uint32_t dlen = read_u32(data, 4);
+            uint32_t dlen = abs(read_s32(data, 4));
             if (dlen > (1 << 20)) {
                 error_report("vnc: client_cut_text msg payload has %u bytes"
                              " which exceeds our limit of 1MB.", dlen);
@@ -2407,7 +2411,11 @@ static int protocol_client_msg(VncState *vs, uint8_t *data, size_t len)
             }
         }
 
-        client_cut_text(vs, read_u32(data, 4), data + 8);
+        if (read_s32(data, 4) < 0) {
+            vnc_client_cut_text_ext(vs, abs(read_s32(data, 4)), read_u32(data, 8), data + 12);
+            break;
+        }
+        vnc_client_cut_text(vs, read_u32(data, 4), data + 8);
         break;
     case VNC_MSG_CLIENT_XVP:
         if (!(vs->features & VNC_FEATURE_XVP)) {
diff --git a/ui/meson.build b/ui/meson.build
index 08447ac15c5e..a98f89b48978 100644
--- a/ui/meson.build
+++ b/ui/meson.build
@@ -30,6 +30,7 @@ vnc_ss.add(files(
   'vnc-auth-vencrypt.c',
   'vnc-ws.c',
   'vnc-jobs.c',
+  'vnc-clipboard.c',
 ))
 vnc_ss.add(zlib, png, jpeg, gnutls)
 vnc_ss.add(when: sasl, if_true: files('vnc-auth-sasl.c'))
-- 
2.29.2



^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH 6/7] ui/gtk: move struct GtkDisplayState to ui/gtk.h
  2021-02-19 13:13 [PATCH 0/7] ui: add vdagent implementation and clipboard support Gerd Hoffmann
                   ` (4 preceding siblings ...)
  2021-02-19 13:13 ` [PATCH 5/7] ui/vnc: " Gerd Hoffmann
@ 2021-02-19 13:13 ` Gerd Hoffmann
  2021-02-19 13:13 ` [PATCH 7/7] ui/gtk: add clipboard support Gerd Hoffmann
  2021-02-19 13:36 ` [PATCH 0/7] ui: add vdagent implementation and " no-reply
  7 siblings, 0 replies; 20+ messages in thread
From: Gerd Hoffmann @ 2021-02-19 13:13 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Gerd Hoffmann, Marc-André Lureau, Markus Armbruster

Want place gtk clipboard code in a separate C file, which in turn
requires GtkDisplayState being in a header file.  So move it.  No
functional change.

Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
---
 include/ui/gtk.h | 57 ++++++++++++++++++++++++++++++++++++++++++++++++
 ui/gtk.c         | 55 ----------------------------------------------
 2 files changed, 57 insertions(+), 55 deletions(-)

diff --git a/include/ui/gtk.h b/include/ui/gtk.h
index 3c1cd98db8b1..55319843758d 100644
--- a/include/ui/gtk.h
+++ b/include/ui/gtk.h
@@ -18,12 +18,15 @@
 #include <gdk/gdkwayland.h>
 #endif
 
+#include "ui/console.h"
 #include "ui/kbd-state.h"
 #if defined(CONFIG_OPENGL)
 #include "ui/egl-helpers.h"
 #include "ui/egl-context.h"
 #endif
 
+#define MAX_VCS 10
+
 typedef struct GtkDisplayState GtkDisplayState;
 
 typedef struct VirtualGfxConsole {
@@ -83,6 +86,60 @@ typedef struct VirtualConsole {
     };
 } VirtualConsole;
 
+struct GtkDisplayState {
+    GtkWidget *window;
+
+    GtkWidget *menu_bar;
+
+    GtkAccelGroup *accel_group;
+
+    GtkWidget *machine_menu_item;
+    GtkWidget *machine_menu;
+    GtkWidget *pause_item;
+    GtkWidget *reset_item;
+    GtkWidget *powerdown_item;
+    GtkWidget *quit_item;
+
+    GtkWidget *view_menu_item;
+    GtkWidget *view_menu;
+    GtkWidget *full_screen_item;
+    GtkWidget *copy_item;
+    GtkWidget *zoom_in_item;
+    GtkWidget *zoom_out_item;
+    GtkWidget *zoom_fixed_item;
+    GtkWidget *zoom_fit_item;
+    GtkWidget *grab_item;
+    GtkWidget *grab_on_hover_item;
+
+    int nb_vcs;
+    VirtualConsole vc[MAX_VCS];
+
+    GtkWidget *show_tabs_item;
+    GtkWidget *untabify_item;
+    GtkWidget *show_menubar_item;
+
+    GtkWidget *vbox;
+    GtkWidget *notebook;
+    int button_mask;
+    gboolean last_set;
+    int last_x;
+    int last_y;
+    int grab_x_root;
+    int grab_y_root;
+    VirtualConsole *kbd_owner;
+    VirtualConsole *ptr_owner;
+
+    gboolean full_screen;
+
+    GdkCursor *null_cursor;
+    Notifier mouse_mode_notifier;
+    gboolean free_scale;
+
+    bool external_pause_update;
+
+    DisplayOptions *opts;
+};
+
 extern bool gtk_use_gl_area;
 
 /* ui/gtk.c */
diff --git a/ui/gtk.c b/ui/gtk.c
index 79dc2401203a..7b412dd4fe0b 100644
--- a/ui/gtk.c
+++ b/ui/gtk.c
@@ -60,7 +60,6 @@
 #include "chardev/char.h"
 #include "qom/object.h"
 
-#define MAX_VCS 10
 #define VC_WINDOW_X_MIN  320
 #define VC_WINDOW_Y_MIN  240
 #define VC_TERM_X_MIN     80
@@ -119,60 +118,6 @@
 static const guint16 *keycode_map;
 static size_t keycode_maplen;
 
-struct GtkDisplayState {
-    GtkWidget *window;
-
-    GtkWidget *menu_bar;
-
-    GtkAccelGroup *accel_group;
-
-    GtkWidget *machine_menu_item;
-    GtkWidget *machine_menu;
-    GtkWidget *pause_item;
-    GtkWidget *reset_item;
-    GtkWidget *powerdown_item;
-    GtkWidget *quit_item;
-
-    GtkWidget *view_menu_item;
-    GtkWidget *view_menu;
-    GtkWidget *full_screen_item;
-    GtkWidget *copy_item;
-    GtkWidget *zoom_in_item;
-    GtkWidget *zoom_out_item;
-    GtkWidget *zoom_fixed_item;
-    GtkWidget *zoom_fit_item;
-    GtkWidget *grab_item;
-    GtkWidget *grab_on_hover_item;
-
-    int nb_vcs;
-    VirtualConsole vc[MAX_VCS];
-
-    GtkWidget *show_tabs_item;
-    GtkWidget *untabify_item;
-    GtkWidget *show_menubar_item;
-
-    GtkWidget *vbox;
-    GtkWidget *notebook;
-    int button_mask;
-    gboolean last_set;
-    int last_x;
-    int last_y;
-    int grab_x_root;
-    int grab_y_root;
-    VirtualConsole *kbd_owner;
-    VirtualConsole *ptr_owner;
-
-    gboolean full_screen;
-
-    GdkCursor *null_cursor;
-    Notifier mouse_mode_notifier;
-    gboolean free_scale;
-
-    bool external_pause_update;
-
-    DisplayOptions *opts;
-};
-
 struct VCChardev {
     Chardev parent;
     VirtualConsole *console;
-- 
2.29.2



^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH 7/7] ui/gtk: add clipboard support
  2021-02-19 13:13 [PATCH 0/7] ui: add vdagent implementation and clipboard support Gerd Hoffmann
                   ` (5 preceding siblings ...)
  2021-02-19 13:13 ` [PATCH 6/7] ui/gtk: move struct GtkDisplayState to ui/gtk.h Gerd Hoffmann
@ 2021-02-19 13:13 ` Gerd Hoffmann
  2021-02-25 19:45   ` Marc-André Lureau
  2021-02-19 13:36 ` [PATCH 0/7] ui: add vdagent implementation and " no-reply
  7 siblings, 1 reply; 20+ messages in thread
From: Gerd Hoffmann @ 2021-02-19 13:13 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Gerd Hoffmann, Marc-André Lureau, Markus Armbruster

This patch adds clipboard support to the qemu gtk ui.

Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
---
 include/ui/gtk.h   |   9 ++++
 ui/gtk-clipboard.c | 124 +++++++++++++++++++++++++++++++++++++++++++++
 ui/gtk.c           |   1 +
 ui/meson.build     |   2 +-
 4 files changed, 135 insertions(+), 1 deletion(-)
 create mode 100644 ui/gtk-clipboard.c

diff --git a/include/ui/gtk.h b/include/ui/gtk.h
index 55319843758d..08999f8835e6 100644
--- a/include/ui/gtk.h
+++ b/include/ui/gtk.h
@@ -18,6 +18,7 @@
 #include <gdk/gdkwayland.h>
 #endif
 
+#include "ui/clipboard.h"
 #include "ui/console.h"
 #include "ui/kbd-state.h"
 #if defined(CONFIG_OPENGL)
@@ -137,6 +138,11 @@ struct GtkDisplayState {
 
     bool external_pause_update;
 
+    QemuClipboardPeer cbpeer;
+    QemuClipboardInfo *cbinfo[QEMU_CLIPBOARD_SELECTION__COUNT];
+    uint32_t cbpending[QEMU_CLIPBOARD_SELECTION__COUNT];
+    GtkClipboard *gtkcb[QEMU_CLIPBOARD_SELECTION__COUNT];
+
     DisplayOptions *opts;
 };
 
@@ -208,4 +214,7 @@ QEMUGLContext gd_gl_area_get_current_context(DisplayChangeListener *dcl);
 int gd_gl_area_make_current(DisplayChangeListener *dcl,
                             QEMUGLContext ctx);
 
+/* gtk-clipboard.c */
+void gd_clipboard_init(GtkDisplayState *gd);
+
 #endif /* UI_GTK_H */
diff --git a/ui/gtk-clipboard.c b/ui/gtk-clipboard.c
new file mode 100644
index 000000000000..4a7f44b25818
--- /dev/null
+++ b/ui/gtk-clipboard.c
@@ -0,0 +1,124 @@
+/*
+ * GTK UI -- clipboard support
+ *
+ * Copyright (C) 2021 Gerd Hoffmann <kraxel@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "qemu/osdep.h"
+#include "qemu-common.h"
+
+#include "ui/gtk.h"
+
+static void gd_clipboard_notify(Notifier *notifier, void *data)
+{
+    GtkDisplayState *gd = container_of(notifier, GtkDisplayState, cbpeer.update);
+    QemuClipboardInfo *info = data;
+    QemuClipboardSelection s = info->selection;
+    bool self_update = info->owner == &gd->cbpeer;
+
+    if (info != gd->cbinfo[s]) {
+        qemu_clipboard_info_put(gd->cbinfo[s]);
+        gd->cbinfo[s] = qemu_clipboard_info_get(info);
+        gd->cbpending[s] = 0;
+        if (!self_update) {
+            if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) {
+                qemu_clipboard_request(info, QEMU_CLIPBOARD_TYPE_TEXT);
+            }
+        }
+        return;
+    }
+
+    if (self_update) {
+        return;
+    }
+
+    if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available &&
+        info->types[QEMU_CLIPBOARD_TYPE_TEXT].data) {
+        gtk_clipboard_set_text(gd->gtkcb[s],
+                               info->types[QEMU_CLIPBOARD_TYPE_TEXT].data,
+                               info->types[QEMU_CLIPBOARD_TYPE_TEXT].size);
+    }
+}
+
+static void gd_clipboard_request(QemuClipboardInfo *info,
+                                 QemuClipboardType type)
+{
+    GtkDisplayState *gd = container_of(info->owner, GtkDisplayState, cbpeer);
+    char *text;
+
+    switch (type) {
+    case QEMU_CLIPBOARD_TYPE_TEXT:
+        text = gtk_clipboard_wait_for_text(gd->gtkcb[info->selection]);
+        qemu_clipboard_set_data(&gd->cbpeer, info, type,
+                                strlen(text), text, true);
+        break;
+    default:
+        break;
+    }
+}
+
+static QemuClipboardSelection gd_find_selection(GtkDisplayState *gd,
+                                                GtkClipboard *clipboard)
+{
+    QemuClipboardSelection s;
+
+    for (s = 0; s < QEMU_CLIPBOARD_SELECTION__COUNT; s++) {
+        if (gd->gtkcb[s] == clipboard) {
+            return s;
+        }
+    }
+    return QEMU_CLIPBOARD_SELECTION_CLIPBOARD;
+}
+
+static void gd_owner_change(GtkClipboard *clipboard,
+                            GdkEvent *event,
+                            gpointer data)
+{
+    GtkDisplayState *gd = data;
+    QemuClipboardSelection s = gd_find_selection(gd, clipboard);
+    QemuClipboardInfo *info;
+
+    info = qemu_clipboard_info_new(&gd->cbpeer, s);
+    if (gtk_clipboard_wait_is_text_available(clipboard)) {
+        info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true;
+    }
+
+    qemu_clipboard_update(info);
+    qemu_clipboard_info_put(info);
+}
+
+void gd_clipboard_init(GtkDisplayState *gd)
+{
+    gd->cbpeer.name = "gtk";
+    gd->cbpeer.update.notify = gd_clipboard_notify;
+    gd->cbpeer.request = gd_clipboard_request;
+    qemu_clipboard_peer_register(&gd->cbpeer);
+
+    gd->gtkcb[QEMU_CLIPBOARD_SELECTION_CLIPBOARD] =
+        gtk_clipboard_get(gdk_atom_intern("CLIPBOARD", FALSE));
+    gd->gtkcb[QEMU_CLIPBOARD_SELECTION_PRIMARY] =
+        gtk_clipboard_get(gdk_atom_intern("PRIMARY", FALSE));
+    gd->gtkcb[QEMU_CLIPBOARD_SELECTION_SECONDARY] =
+        gtk_clipboard_get(gdk_atom_intern("SECONDARY", FALSE));
+
+    g_signal_connect(gd->gtkcb[QEMU_CLIPBOARD_SELECTION_CLIPBOARD],
+                     "owner-change", G_CALLBACK(gd_owner_change), gd);
+    g_signal_connect(gd->gtkcb[QEMU_CLIPBOARD_SELECTION_PRIMARY],
+                     "owner-change", G_CALLBACK(gd_owner_change), gd);
+    g_signal_connect(gd->gtkcb[QEMU_CLIPBOARD_SELECTION_SECONDARY],
+                     "owner-change", G_CALLBACK(gd_owner_change), gd);
+}
diff --git a/ui/gtk.c b/ui/gtk.c
index 7b412dd4fe0b..0ae3ec20f594 100644
--- a/ui/gtk.c
+++ b/ui/gtk.c
@@ -2252,6 +2252,7 @@ static void gtk_display_init(DisplayState *ds, DisplayOptions *opts)
         opts->u.gtk.grab_on_hover) {
         gtk_menu_item_activate(GTK_MENU_ITEM(s->grab_on_hover_item));
     }
+    gd_clipboard_init(s);
 }
 
 static void early_gtk_display_init(DisplayOptions *opts)
diff --git a/ui/meson.build b/ui/meson.build
index a98f89b48978..3ea969a6210b 100644
--- a/ui/meson.build
+++ b/ui/meson.build
@@ -64,7 +64,7 @@ if gtk.found()
   softmmu_ss.add(when: 'CONFIG_WIN32', if_true: files('win32-kbd-hook.c'))
 
   gtk_ss = ss.source_set()
-  gtk_ss.add(gtk, vte, pixman, files('gtk.c'))
+  gtk_ss.add(gtk, vte, pixman, files('gtk.c', 'gtk-clipboard.c'))
   gtk_ss.add(when: x11, if_true: files('x_keymap.c'))
   gtk_ss.add(when: [opengl, 'CONFIG_OPENGL'], if_true: files('gtk-egl.c', 'gtk-gl-area.c'))
   ui_modules += {'gtk' : gtk_ss}
-- 
2.29.2



^ permalink raw reply related	[flat|nested] 20+ messages in thread

* Re: [PATCH 0/7] ui: add vdagent implementation and clipboard support.
  2021-02-19 13:13 [PATCH 0/7] ui: add vdagent implementation and clipboard support Gerd Hoffmann
                   ` (6 preceding siblings ...)
  2021-02-19 13:13 ` [PATCH 7/7] ui/gtk: add clipboard support Gerd Hoffmann
@ 2021-02-19 13:36 ` no-reply
  7 siblings, 0 replies; 20+ messages in thread
From: no-reply @ 2021-02-19 13:36 UTC (permalink / raw)
  To: kraxel; +Cc: pbonzini, armbru, qemu-devel, marcandre.lureau, kraxel

Patchew URL: https://patchew.org/QEMU/20210219131349.3993192-1-kraxel@redhat.com/



Hi,

This series seems to have some coding style problems. See output below for
more information:

Type: series
Message-id: 20210219131349.3993192-1-kraxel@redhat.com
Subject: [PATCH 0/7] ui: add vdagent implementation and clipboard support.

=== TEST SCRIPT BEGIN ===
#!/bin/bash
git rev-parse base > /dev/null || exit 0
git config --local diff.renamelimit 0
git config --local diff.renames True
git config --local diff.algorithm histogram
./scripts/checkpatch.pl --mailback base..
=== TEST SCRIPT END ===

Updating 3c8cf5a9c21ff8782164d1def7f44bd888713384
From https://github.com/patchew-project/qemu
 * [new tag]         patchew/20210219131349.3993192-1-kraxel@redhat.com -> patchew/20210219131349.3993192-1-kraxel@redhat.com
Switched to a new branch 'test'
c2b97c6 ui/gtk: add clipboard support
79fe669 ui/gtk: move struct GtkDisplayState to ui/gtk.h
067e7e7 ui/vnc: clipboard support
e79dd8a ui/vdagent: add clipboard support
f83e621 ui/vdagent: add mouse support
55faac0 ui/vdagent: core infrastructure
4670677 ui: add clipboard infrastructure

=== OUTPUT BEGIN ===
1/7 Checking commit 4670677501e1 (ui: add clipboard infrastructure)
WARNING: added, moved or deleted file(s), does MAINTAINERS need updating?
#20: 
new file mode 100644

total: 0 errors, 1 warnings, 167 lines checked

Patch 1/7 has style problems, please review.  If any of these errors
are false positives report them to the maintainer, see
CHECKPATCH in MAINTAINERS.
2/7 Checking commit 55faac09768e (ui/vdagent: core infrastructure)
WARNING: added, moved or deleted file(s), does MAINTAINERS need updating?
#90: 
new file mode 100644

ERROR: if this code is redundant consider removing it
#142: FILE: ui/vdagent.c:48:
+#if 0

WARNING: line over 80 characters
#144: FILE: ui/vdagent.c:50:
+    [VD_AGENT_CAP_CLIPBOARD_NO_RELEASE_ON_REGRAB] = "clipboard-no-release-on-regrab",

ERROR: if this code is redundant consider removing it
#165: FILE: ui/vdagent.c:71:
+#if 0

total: 2 errors, 2 warnings, 282 lines checked

Patch 2/7 has style problems, please review.  If any of these errors
are false positives report them to the maintainer, see
CHECKPATCH in MAINTAINERS.

3/7 Checking commit f83e621420e6 (ui/vdagent: add mouse support)
ERROR: if this code is redundant consider removing it
#139: FILE: ui/vdagent.c:175:
+#if 0

total: 1 errors, 0 warnings, 229 lines checked

Patch 3/7 has style problems, please review.  If any of these errors
are false positives report them to the maintainer, see
CHECKPATCH in MAINTAINERS.

4/7 Checking commit e79dd8a43aef (ui/vdagent: add clipboard support)
ERROR: if this code is redundant consider removing it
#120: FILE: ui/vdagent.c:114:
+#if 0

WARNING: line over 80 characters
#159: FILE: ui/vdagent.c:282:
+                                    sizeof(uint32_t) * (QEMU_CLIPBOARD_TYPE__COUNT + 1));

WARNING: line over 80 characters
#390: FILE: ui/vdagent.c:597:
+    cfg->clipboard = qemu_opt_get_bool(opts, "clipboard", VDAGENT_CLIPBOARD_DEFAULT);

total: 1 errors, 2 warnings, 343 lines checked

Patch 4/7 has style problems, please review.  If any of these errors
are false positives report them to the maintainer, see
CHECKPATCH in MAINTAINERS.

5/7 Checking commit 067e7e7026e2 (ui/vnc: clipboard support)
WARNING: added, moved or deleted file(s), does MAINTAINERS need updating?
#33: 
new file mode 100644

WARNING: line over 80 characters
#282: FILE: ui/vnc-clipboard.c:245:
+void vnc_client_cut_text_ext(VncState *vs, int32_t len, uint32_t flags, uint8_t *data)

WARNING: line over 80 characters
#291: FILE: ui/vnc-clipboard.c:254:
+            qemu_clipboard_info_new(&vs->cbpeer, QEMU_CLIPBOARD_SELECTION_CLIPBOARD);

WARNING: line over 80 characters
#336: FILE: ui/vnc-clipboard.c:299:
+        qemu_clipboard_info_new(&vs->cbpeer, QEMU_CLIPBOARD_SELECTION_CLIPBOARD);

ERROR: line over 90 characters
#423: FILE: ui/vnc.c:2415:
+            vnc_client_cut_text_ext(vs, abs(read_s32(data, 4)), read_u32(data, 8), data + 12);

WARNING: line over 80 characters
#502: FILE: ui/vnc.h:642:
+void vnc_client_cut_text_ext(VncState *vs, int32_t len, uint32_t flags, uint8_t *data);

total: 1 errors, 5 warnings, 453 lines checked

Patch 5/7 has style problems, please review.  If any of these errors
are false positives report them to the maintainer, see
CHECKPATCH in MAINTAINERS.

6/7 Checking commit 79fe669da11d (ui/gtk: move struct GtkDisplayState to ui/gtk.h)
7/7 Checking commit c2b97c6a3c68 (ui/gtk: add clipboard support)
WARNING: added, moved or deleted file(s), does MAINTAINERS need updating?
#51: 
new file mode 100644

WARNING: line over 80 characters
#83: FILE: ui/gtk-clipboard.c:28:
+    GtkDisplayState *gd = container_of(notifier, GtkDisplayState, cbpeer.update);

total: 0 errors, 2 warnings, 164 lines checked

Patch 7/7 has style problems, please review.  If any of these errors
are false positives report them to the maintainer, see
CHECKPATCH in MAINTAINERS.
=== OUTPUT END ===

Test command exited with code: 1


The full log is available at
http://patchew.org/logs/20210219131349.3993192-1-kraxel@redhat.com/testing.checkpatch/?type=message.
---
Email generated automatically by Patchew [https://patchew.org/].
Please send your feedback to patchew-devel@redhat.com

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH 2/7] ui/vdagent: core infrastructure
  2021-02-19 13:13 ` [PATCH 2/7] ui/vdagent: core infrastructure Gerd Hoffmann
@ 2021-02-25 18:24   ` Marc-André Lureau
  0 siblings, 0 replies; 20+ messages in thread
From: Marc-André Lureau @ 2021-02-25 18:24 UTC (permalink / raw)
  To: Gerd Hoffmann; +Cc: Paolo Bonzini, QEMU, Markus Armbruster

[-- Attachment #1: Type: text/plain, Size: 11844 bytes --]

Hi

On Fri, Feb 19, 2021 at 5:17 PM Gerd Hoffmann <kraxel@redhat.com> wrote:

> The vdagent protocol allows the guest agent (spice-vdagent) and the
> spice client exchange messages to implement features which require
> guest cooperation, for example clipboard support.
>
> This is a qemu implementation of the spice client side.  This allows
> the spice guest agent talk to qemu directly when not using the spice
> protocol.
>
> usage: qemu \
>   -chardev vdagent,id=vdagent \
>   -device virtserialport,chardev=vdagent,name=com.redhat.spice.0
>
> This patch adds just the protocol basics: initial handshake and
> capability negotiation.
>
> Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
> ---
>  ui/vdagent.c    | 240 ++++++++++++++++++++++++++++++++++++++++++++++++
>  qapi/char.json  |  13 +++
>  ui/meson.build  |   1 +
>  ui/trace-events |   7 ++
>  4 files changed, 261 insertions(+)
>  create mode 100644 ui/vdagent.c
>
> diff --git a/ui/vdagent.c b/ui/vdagent.c
> new file mode 100644
> index 000000000000..9ef4ed3f4dd8
> --- /dev/null
> +++ b/ui/vdagent.c
> @@ -0,0 +1,240 @@
> +#include "qemu/osdep.h"
> +#include "include/qemu-common.h"
> +#include "chardev/char.h"
> +#include "trace.h"
> +
> +#include "qapi/qapi-types-char.h"
> +
> +#include "spice/vd_agent.h"
> +
> +#define MSGSIZE_MAX (sizeof(VDIChunkHeader) + \
> +                     sizeof(VDAgentMessage) + \
> +                     VD_AGENT_MAX_DATA_SIZE)
> +
> +struct VDAgentChardev {
> +    Chardev parent;
> +
> +    /* guest vdagent */
> +    uint32_t caps;
> +    uint8_t msgbuf[MSGSIZE_MAX];
> +    uint32_t msgsize;
> +};
> +typedef struct VDAgentChardev VDAgentChardev;
> +
> +#define TYPE_CHARDEV_VDAGENT "chardev-vdagent"
> +
> +DECLARE_INSTANCE_CHECKER(VDAgentChardev, VDAGENT_CHARDEV,
> +                         TYPE_CHARDEV_VDAGENT);
> +
> +/* ------------------------------------------------------------------ */
> +/* names, for debug logging                                           */
> +
> +static const char *cap_name[] = {
> +    [VD_AGENT_CAP_MOUSE_STATE]                    = "mouse-state",
> +    [VD_AGENT_CAP_MONITORS_CONFIG]                = "monitors-config",
> +    [VD_AGENT_CAP_REPLY]                          = "reply",
> +    [VD_AGENT_CAP_CLIPBOARD]                      = "clipboard",
> +    [VD_AGENT_CAP_DISPLAY_CONFIG]                 = "display-config",
> +    [VD_AGENT_CAP_CLIPBOARD_BY_DEMAND]            = "clipboard-by-demand",
> +    [VD_AGENT_CAP_CLIPBOARD_SELECTION]            = "clipboard-selection",
> +    [VD_AGENT_CAP_SPARSE_MONITORS_CONFIG]         =
> "sparse-monitors-config",
> +    [VD_AGENT_CAP_GUEST_LINEEND_LF]               = "guest-lineend-lf",
> +    [VD_AGENT_CAP_GUEST_LINEEND_CRLF]             = "guest-lineend-crlf",
> +    [VD_AGENT_CAP_MAX_CLIPBOARD]                  = "max-clipboard",
> +    [VD_AGENT_CAP_AUDIO_VOLUME_SYNC]              = "audio-volume-sync",
> +    [VD_AGENT_CAP_MONITORS_CONFIG_POSITION]       =
> "monitors-config-position",
> +    [VD_AGENT_CAP_FILE_XFER_DISABLED]             = "file-xfer-disabled",
> +    [VD_AGENT_CAP_FILE_XFER_DETAILED_ERRORS]      =
> "file-xfer-detailed-errors",
> +#if 0
> +    [VD_AGENT_CAP_GRAPHICS_DEVICE_INFO]           =
> "graphics-device-info",
> +    [VD_AGENT_CAP_CLIPBOARD_NO_RELEASE_ON_REGRAB] =
> "clipboard-no-release-on-regrab",
> +    [VD_AGENT_CAP_CLIPBOARD_GRAB_SERIAL]          =
> "clipboard-grab-serial",
> +#endif
> +};
> +
> +static const char *msg_name[] = {
> +    [VD_AGENT_MOUSE_STATE]           = "mouse-state",
> +    [VD_AGENT_MONITORS_CONFIG]       = "monitors-config",
> +    [VD_AGENT_REPLY]                 = "reply",
> +    [VD_AGENT_CLIPBOARD]             = "clipboard",
> +    [VD_AGENT_DISPLAY_CONFIG]        = "display-config",
> +    [VD_AGENT_ANNOUNCE_CAPABILITIES] = "announce-capabilities",
> +    [VD_AGENT_CLIPBOARD_GRAB]        = "clipboard-grab",
> +    [VD_AGENT_CLIPBOARD_REQUEST]     = "clipboard-request",
> +    [VD_AGENT_CLIPBOARD_RELEASE]     = "clipboard-release",
> +    [VD_AGENT_FILE_XFER_START]       = "file-xfer-start",
> +    [VD_AGENT_FILE_XFER_STATUS]      = "file-xfer-status",
> +    [VD_AGENT_FILE_XFER_DATA]        = "file-xfer-data",
> +    [VD_AGENT_CLIENT_DISCONNECTED]   = "client-disconnected",
> +    [VD_AGENT_MAX_CLIPBOARD]         = "max-clipboard",
> +    [VD_AGENT_AUDIO_VOLUME_SYNC]     = "audio-volume-sync",
> +#if 0
> +    [VD_AGENT_GRAPHICS_DEVICE_INFO]  = "graphics-device-info",
> +#endif
> +};
> +
> +#define GET_NAME(_m, _v) \
> +    (((_v) < ARRAY_SIZE(_m) && (_m[_v])) ? (_m[_v]) : "???")
> +
> +/* ------------------------------------------------------------------ */
> +/* send messages                                                      */
> +
> +static void vdagent_send_buf(VDAgentChardev *vd, void *ptr, uint32_t
> msgsize)
> +{
> +    uint8_t *msgbuf = ptr;
> +    uint32_t len, pos = 0;
> +
> +    while (pos < msgsize) {
> +        len = qemu_chr_be_can_write(CHARDEV(vd));
> +        if (len > msgsize - pos) {
> +            len = msgsize - pos;
> +        }
> +        qemu_chr_be_write(CHARDEV(vd), msgbuf + pos, len);
> +        pos += len;
> +    }
> +}
> +
> +static void vdagent_send_msg(VDAgentChardev *vd, VDAgentMessage *msg)
> +{
> +    uint8_t *msgbuf = (void *)msg;
> +    uint32_t msgsize = sizeof(VDAgentMessage) + msg->size;
> +    VDIChunkHeader chunk;
> +
> +    trace_vdagent_send(GET_NAME(msg_name, msg->type));
> +
> +    chunk.port = VDP_CLIENT_PORT;
> +    chunk.size = msgsize;
> +    vdagent_send_buf(vd, &chunk, sizeof(chunk));
> +
> +    msg->protocol = VD_AGENT_PROTOCOL;
> +    vdagent_send_buf(vd, msgbuf, msgsize);
> +    g_free(msg);
> +}
> +
> +static void vdagent_send_caps(VDAgentChardev *vd)
> +{
> +    VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) +
> +                                    sizeof(VDAgentAnnounceCapabilities) +
> +                                    sizeof(uint32_t));
> +
> +    msg->type = VD_AGENT_ANNOUNCE_CAPABILITIES;
> +    msg->size = sizeof(VDAgentAnnounceCapabilities) + sizeof(uint32_t);
> +
> +    vdagent_send_msg(vd, msg);
> +}
> +
> +/* ------------------------------------------------------------------ */
> +/* chardev backend                                                    */
> +
> +static void vdagent_chr_open(Chardev *chr,
> +                             ChardevBackend *backend,
> +                             bool *be_opened,
> +                             Error **errp)
> +{
> +    *be_opened = true;
> +}
> +
> +static void vdagent_chr_recv_caps(VDAgentChardev *vd, VDAgentMessage *msg)
> +{
> +    VDAgentAnnounceCapabilities *caps = (void *)msg->data;
>

It's missing some boundary checks for vd->msgsize (-
sizeof(VDIChunkHeader)), since vdagent_chr_recv() doesn't check for >=
sizeof(VDAgentAnnounceCapabilities) + sizeof(uint32).

In the following patches, there are similar concerns for other messages.

+    int i;
> +
> +    for (i = 0; i < ARRAY_SIZE(cap_name); i++) {
> +        if (caps->caps[0] & (1 << i)) {
> +            trace_vdagent_peer_cap(GET_NAME(cap_name, i));
> +        }
> +    }
> +
> +    vd->caps = caps->caps[0];
> +    if (caps->request) {
> +        vdagent_send_caps(vd);
> +    }
> +}
> +
> +static uint32_t vdagent_chr_recv(VDAgentChardev *vd)
> +{
> +    VDIChunkHeader *chunk = (void *)vd->msgbuf;
> +    VDAgentMessage *msg = (void *)vd->msgbuf + sizeof(VDIChunkHeader);
> +
> +    if (sizeof(VDIChunkHeader) + chunk->size > vd->msgsize) {
> +        return 0;
> +    }
> +
> +    trace_vdagent_recv(GET_NAME(msg_name, msg->type));
> +
> +    switch (msg->type) {
> +    case VD_AGENT_ANNOUNCE_CAPABILITIES:
> +        vdagent_chr_recv_caps(vd, msg);
> +        break;
> +    default:
> +        break;
> +    }
> +
> +    return sizeof(VDIChunkHeader) + chunk->size;
> +}
> +
> +static int vdagent_chr_write(Chardev *chr, const uint8_t *buf, int len)
> +{
> +    VDAgentChardev *vd = VDAGENT_CHARDEV(chr);
> +    uint32_t copy, move;
> +
> +    copy = MSGSIZE_MAX - vd->msgsize;
> +    if (copy > len) {
> +        copy = len;
> +    }
> +
> +    memcpy(vd->msgbuf + vd->msgsize, buf, copy);
> +    vd->msgsize += copy;
> +
> +    while (vd->msgsize > sizeof(VDIChunkHeader)) {
> +        move = vdagent_chr_recv(vd);
> +        if (move == 0) {
> +            break;
> +        }
> +
> +        memmove(vd->msgbuf, vd->msgbuf + move, vd->msgsize - move);
> +        vd->msgsize -= move;
> +    }
> +
> +    return copy;
> +}
> +
> +static void vdagent_chr_set_fe_open(struct Chardev *chr, int fe_open)
> +{
> +    VDAgentChardev *vd = VDAGENT_CHARDEV(chr);
> +
> +    if (!fe_open) {
> +        trace_vdagent_close();
> +        /* reset state */
> +        vd->msgsize = 0;
> +        vd->caps = 0;
> +        return;
> +    }
> +
> +    trace_vdagent_open();
> +}
> +
> +/* ------------------------------------------------------------------ */
> +
> +static void vdagent_chr_class_init(ObjectClass *oc, void *data)
> +{
> +    ChardevClass *cc = CHARDEV_CLASS(oc);
> +
> +    cc->open             = vdagent_chr_open;
> +    cc->chr_write        = vdagent_chr_write;
> +    cc->chr_set_fe_open  = vdagent_chr_set_fe_open;
> +}
> +
> +static const TypeInfo vdagent_chr_type_info = {
> +    .name = TYPE_CHARDEV_VDAGENT,
> +    .parent = TYPE_CHARDEV,
> +    .instance_size = sizeof(VDAgentChardev),
> +    .class_init = vdagent_chr_class_init,
> +};
> +
> +static void register_types(void)
> +{
> +    type_register_static(&vdagent_chr_type_info);
> +}
> +
> +type_init(register_types);
> diff --git a/qapi/char.json b/qapi/char.json
> index 58338ed62d43..62e161aea343 100644
> --- a/qapi/char.json
> +++ b/qapi/char.json
> @@ -390,6 +390,17 @@
>    'data': { '*size': 'int' },
>    'base': 'ChardevCommon' }
>
> +##
> +# @ChardevVDAgent:
> +#
> +# Configuration info for vdagent.
> +#
> +# Since: 6.0
> +##
> +{ 'struct': 'ChardevVDAgent',
> +  'data': { },
> +  'base': 'ChardevCommon' }
> +
>  ##
>  # @ChardevBackend:
>  #
> @@ -417,6 +428,8 @@
>                            'if': 'defined(CONFIG_SPICE)' },
>              'spiceport': { 'type': 'ChardevSpicePort',
>                             'if': 'defined(CONFIG_SPICE)' },
> +            'vdagent': { 'type': 'ChardevVDAgent',
> +                         'if': 'defined(CONFIG_SPICE)' },
>              'vc': 'ChardevVC',
>              'ringbuf': 'ChardevRingbuf',
>              # next one is just for compatibility
> diff --git a/ui/meson.build b/ui/meson.build
> index fbb9a512042a..08447ac15c5e 100644
> --- a/ui/meson.build
> +++ b/ui/meson.build
> @@ -14,6 +14,7 @@ softmmu_ss.add(files(
>    'qemu-pixman.c',
>  ))
>  softmmu_ss.add([spice_headers, files('spice-module.c')])
> +softmmu_ss.add(when: spice_headers, if_true: files('vdagent.c'))
>
>  softmmu_ss.add(when: 'CONFIG_LINUX', if_true: files('input-linux.c'))
>  softmmu_ss.add(when: cocoa, if_true: files('cocoa.m'))
> diff --git a/ui/trace-events b/ui/trace-events
> index 0ffcdb4408a6..1a5bd3861da5 100644
> --- a/ui/trace-events
> +++ b/ui/trace-events
> @@ -108,3 +108,10 @@ xkeymap_extension(const char *name) "extension '%s'"
>  xkeymap_vendor(const char *name) "vendor '%s'"
>  xkeymap_keycodes(const char *name) "keycodes '%s'"
>  xkeymap_keymap(const char *name) "keymap '%s'"
> +
> +# vdagent.c
> +vdagent_open(void) ""
> +vdagent_close(void) ""
> +vdagent_send(const char *name) "msg %s"
> +vdagent_recv(const char *name) "msg %s"
> +vdagent_peer_cap(const char *name) "cap %s"
> --
> 2.29.2
>
>
>

-- 
Marc-André Lureau

[-- Attachment #2: Type: text/html, Size: 14868 bytes --]

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH 4/7] ui/vdagent: add clipboard support
  2021-02-19 13:13 ` [PATCH 4/7] ui/vdagent: add clipboard support Gerd Hoffmann
@ 2021-02-25 18:37   ` Marc-André Lureau
  0 siblings, 0 replies; 20+ messages in thread
From: Marc-André Lureau @ 2021-02-25 18:37 UTC (permalink / raw)
  To: Gerd Hoffmann; +Cc: Paolo Bonzini, QEMU, Markus Armbruster

[-- Attachment #1: Type: text/plain, Size: 14158 bytes --]

On Fri, Feb 19, 2021 at 5:19 PM Gerd Hoffmann <kraxel@redhat.com> wrote:

> This patch adds support for clipboard messages to the qemu vdagent
> implementation, which allows the guest exchange clipboard data with
> qemu.  Clipboard support can be enabled/disabled using the new
> 'clipboard' parameter for the vdagent chardev.  Default is off.
>
> Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
> ---
>  chardev/char.c  |   3 +
>  ui/vdagent.c    | 243 ++++++++++++++++++++++++++++++++++++++++++++++++
>  qapi/char.json  |   4 +-
>  ui/trace-events |   2 +
>  4 files changed, 251 insertions(+), 1 deletion(-)
>
> diff --git a/chardev/char.c b/chardev/char.c
> index ea986dac1bff..7f3ee2c11f9d 100644
> --- a/chardev/char.c
> +++ b/chardev/char.c
> @@ -927,6 +927,9 @@ QemuOptsList qemu_chardev_opts = {
>          },{
>              .name = "mouse",
>              .type = QEMU_OPT_BOOL,
> +        },{
> +            .name = "clipboard",
> +            .type = QEMU_OPT_BOOL,
>  #ifdef CONFIG_LINUX
>          },{
>              .name = "tight",
> diff --git a/ui/vdagent.c b/ui/vdagent.c
> index b48b0129b9f2..ee058688027e 100644
> --- a/ui/vdagent.c
> +++ b/ui/vdagent.c
> @@ -3,6 +3,7 @@
>  #include "chardev/char.h"
>  #include "hw/qdev-core.h"
>  #include "qemu/option.h"
> +#include "ui/clipboard.h"
>  #include "ui/console.h"
>  #include "ui/input.h"
>  #include "trace.h"
> @@ -17,12 +18,14 @@
>                       VD_AGENT_MAX_DATA_SIZE)
>
>  #define VDAGENT_MOUSE_DEFAULT true
> +#define VDAGENT_CLIPBOARD_DEFAULT false
>
>  struct VDAgentChardev {
>      Chardev parent;
>
>      /* config */
>      bool mouse;
> +    bool clipboard;
>
>      /* guest vdagent */
>      uint32_t caps;
> @@ -35,6 +38,11 @@ struct VDAgentChardev {
>      uint32_t mouse_y;
>      uint32_t mouse_btn;
>      QemuInputHandlerState *mouse_hs;
> +
> +    /* clipboard */
> +    QemuClipboardPeer cbpeer;
> +    QemuClipboardInfo *cbinfo[QEMU_CLIPBOARD_SELECTION__COUNT];
> +    uint32_t cbpending[QEMU_CLIPBOARD_SELECTION__COUNT];
>  };
>  typedef struct VDAgentChardev VDAgentChardev;
>
> @@ -90,6 +98,24 @@ static const char *msg_name[] = {
>  #endif
>  };
>
> +static const char *sel_name[] = {
> +    [VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD] = "clipboard",
> +    [VD_AGENT_CLIPBOARD_SELECTION_PRIMARY]   = "primary",
> +    [VD_AGENT_CLIPBOARD_SELECTION_SECONDARY] = "secondary",
> +};
> +
> +static const char *type_name[] = {
> +    [VD_AGENT_CLIPBOARD_NONE]       = "none",
> +    [VD_AGENT_CLIPBOARD_UTF8_TEXT]  = "text",
> +    [VD_AGENT_CLIPBOARD_IMAGE_PNG]  = "png",
> +    [VD_AGENT_CLIPBOARD_IMAGE_BMP]  = "bmp",
> +    [VD_AGENT_CLIPBOARD_IMAGE_TIFF] = "tiff",
> +    [VD_AGENT_CLIPBOARD_IMAGE_JPG]  = "jpg",
> +#if 0
> +    [VD_AGENT_CLIPBOARD_FILE_LIST]  = "files",
> +#endif
> +};
> +
>  #define GET_NAME(_m, _v) \
>      (((_v) < ARRAY_SIZE(_m) && (_m[_v])) ? (_m[_v]) : "???")
>
> @@ -140,6 +166,10 @@ static void vdagent_send_caps(VDAgentChardev *vd)
>      if (vd->mouse) {
>          caps->caps[0] |= (1 << VD_AGENT_CAP_MOUSE_STATE);
>      }
> +    if (vd->clipboard) {
> +        caps->caps[0] |= (1 << VD_AGENT_CAP_CLIPBOARD_BY_DEMAND);
> +        caps->caps[0] |= (1 << VD_AGENT_CAP_CLIPBOARD_SELECTION);
> +    }
>
>      vdagent_send_msg(vd, msg);
>  }
> @@ -232,6 +262,193 @@ static QemuInputHandler vdagent_mouse_handler = {
>      .sync  = vdagent_pointer_sync,
>  };
>
> +/* ------------------------------------------------------------------ */
> +/* clipboard                                                          */
> +
> +static uint32_t type_qemu_to_vdagent(enum QemuClipboardType type)
> +{
> +    switch (type) {
> +    case QEMU_CLIPBOARD_TYPE_TEXT:
> +        return VD_AGENT_CLIPBOARD_UTF8_TEXT;
> +    default:
> +        return VD_AGENT_CLIPBOARD_NONE;
> +    }
> +}
> +
> +static void vdagent_send_clipboard_grab(VDAgentChardev *vd,
> +                                        QemuClipboardInfo *info)
> +{
> +    VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) +
> +                                    sizeof(uint32_t) *
> (QEMU_CLIPBOARD_TYPE__COUNT + 1));
> +    uint8_t *s = msg->data;
> +    uint32_t *data = (uint32_t *)(msg->data + 4);
> +    uint32_t q, v, type;
> +
> +    for (q = 0, v = 0; q < QEMU_CLIPBOARD_TYPE__COUNT; q++) {
> +        type = type_qemu_to_vdagent(q);
> +        if (type != VD_AGENT_CLIPBOARD_NONE && info->types[q].available) {
> +            data[v++] = type;
> +        }
> +    }
> +
> +    *s = info->selection;
> +    msg->type = VD_AGENT_CLIPBOARD_GRAB;
> +    msg->size = sizeof(uint32_t) * (v + 1);
> +
> +    vdagent_send_msg(vd, msg);
> +}
> +
> +static void vdagent_send_clipboard_data(VDAgentChardev *vd,
> +                                        QemuClipboardInfo *info,
> +                                        QemuClipboardType type)
> +{
> +    VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) +
> +                                    sizeof(uint32_t) * 2 +
> +                                    info->types[type].size);
> +
> +    uint8_t *s = msg->data;
> +    uint32_t *t = (uint32_t *)(msg->data + 4);
> +    uint8_t *d = msg->data + 8;
> +
> +    *s = info->selection;
> +    *t = type_qemu_to_vdagent(type);
> +    memcpy(d, info->types[type].data, info->types[type].size);
> +
> +    msg->type = VD_AGENT_CLIPBOARD;
> +    msg->size = sizeof(uint32_t) * 2 + info->types[type].size;
> +
> +    vdagent_send_msg(vd, msg);
> +}
> +
> +static void vdagent_clipboard_notify(Notifier *notifier, void *data)
> +{
> +    VDAgentChardev *vd = container_of(notifier, VDAgentChardev,
> cbpeer.update);
> +    QemuClipboardInfo *info = data;
> +    QemuClipboardSelection s = info->selection;
> +    QemuClipboardType type;
> +    bool self_update = info->owner == &vd->cbpeer;
> +
> +    if (info != vd->cbinfo[s]) {
> +        qemu_clipboard_info_put(vd->cbinfo[s]);
> +        vd->cbinfo[s] = qemu_clipboard_info_get(info);
> +        vd->cbpending[s] = 0;
> +        if (!self_update) {
> +            vdagent_send_clipboard_grab(vd, info);
> +        }
> +        return;
> +    }
> +
> +    if (self_update) {
> +        return;
> +    }
> +
> +    for (type = 0; type < QEMU_CLIPBOARD_TYPE__COUNT; type++) {
> +        if (vd->cbpending[s] & (1 << type)) {
> +            vd->cbpending[s] &= ~(1 << type);
> +            vdagent_send_clipboard_data(vd, info, type);
> +        }
> +    }
> +}
> +
> +static void vdagent_clipboard_request(QemuClipboardInfo *info,
> +                                      QemuClipboardType qtype)
> +{
> +    VDAgentChardev *vd = container_of(info->owner, VDAgentChardev,
> cbpeer);
> +    VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) +
> +                                    sizeof(uint32_t) * 2);
> +    uint32_t type = type_qemu_to_vdagent(qtype);
> +    uint8_t *s = msg->data;
> +    uint32_t *data = (uint32_t *)(msg->data + 4);
> +
> +    if (type == VD_AGENT_CLIPBOARD_NONE) {
> +        return;
> +    }
> +
> +    *s = info->selection;
> +    *data = type;
> +    msg->type = VD_AGENT_CLIPBOARD_REQUEST;
> +    msg->size = sizeof(uint32_t) * 2;
> +
> +    vdagent_send_msg(vd, msg);
> +}
> +
> +static void vdagent_chr_recv_clipboard(VDAgentChardev *vd, VDAgentMessage
> *msg)
> +{
> +    uint8_t s = VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD;
> +    uint32_t size = msg->size;
> +    void *data = msg->data;
> +    QemuClipboardInfo *info;
> +    QemuClipboardType type;
> +
> +    if (vd->caps & (1 << VD_AGENT_CAP_CLIPBOARD_SELECTION)) {
> +        s = *(uint8_t *)data;
> +        data += 4;
> +        size -= 4;
> +    }
>

Here are some other concerns about "size" checks,

+
> +    switch (msg->type) {
> +    case VD_AGENT_CLIPBOARD_GRAB:
> +        trace_vdagent_cb_grab_selection(GET_NAME(sel_name, s));
> +        info = qemu_clipboard_info_new(&vd->cbpeer, s);
> +        while (size) {
>

to avoid looping on maxu32 for example,


> +            trace_vdagent_cb_grab_type(GET_NAME(type_name, *(uint32_t
> *)data));
> +            switch (*(uint32_t *)data) {
> +            case VD_AGENT_CLIPBOARD_UTF8_TEXT:
> +                info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true;
> +                break;
> +            default:
> +                break;
> +            }
> +            data += 4;
> +            size -= 4;
> +        }
> +        qemu_clipboard_update(info);
> +        qemu_clipboard_info_put(info);
> +        break;
> +    case VD_AGENT_CLIPBOARD_REQUEST:
> +        switch (*(uint32_t *)data) {
>

or checking that a u32 still fits

+        case VD_AGENT_CLIPBOARD_UTF8_TEXT:
> +            type = QEMU_CLIPBOARD_TYPE_TEXT;
> +            break;
> +        default:
>

Despite what spice-gtk currently seems to do in some code paths, it's
certainly better to reply with  an empty clipboard, rather than not
replying.

+            return;
> +        }
> +        if (vd->cbinfo[s] &&
> +            vd->cbinfo[s]->types[type].available &&
> +            vd->cbinfo[s]->owner != &vd->cbpeer) {
> +            if (vd->cbinfo[s]->types[type].data) {
> +                vdagent_send_clipboard_data(vd, vd->cbinfo[s], type);
> +            } else {
> +                vd->cbpending[s] |= (1 << type);
> +                qemu_clipboard_request(vd->cbinfo[s], type);
> +            }
> +        }
>

similarly

+        break;
> +    case VD_AGENT_CLIPBOARD: /* data */
> +        switch (*(uint32_t *)data) {
> +        case VD_AGENT_CLIPBOARD_UTF8_TEXT:
> +            type = QEMU_CLIPBOARD_TYPE_TEXT;
> +            break;
> +        default:
> +            return;
> +        }
> +        data += 4;
> +        size -= 4;
> +        qemu_clipboard_set_data(&vd->cbpeer, vd->cbinfo[s], type,
> +                                size, data, true);
> +        break;
> +    case VD_AGENT_CLIPBOARD_RELEASE: /* data */
> +        if (vd->cbinfo[s] &&
> +            vd->cbinfo[s]->owner == &vd->cbpeer) {
> +            /* set empty clipboard info */
> +            info = qemu_clipboard_info_new(NULL, s);
> +            qemu_clipboard_update(info);
> +            qemu_clipboard_info_put(info);
> +        }
> +        break;
> +    }
> +}
> +
>  /* ------------------------------------------------------------------ */
>  /* chardev backend                                                    */
>
> @@ -248,6 +465,11 @@ static void vdagent_chr_open(Chardev *chr,
>          vd->mouse = cfg->mouse;
>      }
>
> +    vd->clipboard = VDAGENT_CLIPBOARD_DEFAULT;
> +    if (cfg->has_clipboard) {
> +        vd->clipboard = cfg->clipboard;
> +    }
> +
>      if (vd->mouse) {
>          vd->mouse_hs = qemu_input_handler_register(&vd->mouse_dev,
>
> &vdagent_mouse_handler);
> @@ -274,6 +496,15 @@ static void vdagent_chr_recv_caps(VDAgentChardev *vd,
> VDAgentMessage *msg)
>      if (vd->caps & (1 << VD_AGENT_CAP_MOUSE_STATE) && vd->mouse_hs) {
>          qemu_input_handler_activate(vd->mouse_hs);
>      }
> +    if (vd->caps & (1 << VD_AGENT_CAP_CLIPBOARD_BY_DEMAND) &&
> +        vd->caps & (1 << VD_AGENT_CAP_CLIPBOARD_SELECTION) &&
> +        vd->clipboard &&
> +        vd->cbpeer.update.notify == NULL) {
> +        vd->cbpeer.name = "vdagent";
> +        vd->cbpeer.update.notify = vdagent_clipboard_notify;
> +        vd->cbpeer.request = vdagent_clipboard_request;
> +        qemu_clipboard_peer_register(&vd->cbpeer);
> +    }
>  }
>
>  static uint32_t vdagent_chr_recv(VDAgentChardev *vd)
> @@ -291,6 +522,12 @@ static uint32_t vdagent_chr_recv(VDAgentChardev *vd)
>      case VD_AGENT_ANNOUNCE_CAPABILITIES:
>          vdagent_chr_recv_caps(vd, msg);
>          break;
> +    case VD_AGENT_CLIPBOARD:
> +    case VD_AGENT_CLIPBOARD_GRAB:
> +    case VD_AGENT_CLIPBOARD_REQUEST:
> +    case VD_AGENT_CLIPBOARD_RELEASE:
> +        vdagent_chr_recv_clipboard(vd, msg);
> +        break;
>      default:
>          break;
>      }
> @@ -336,6 +573,10 @@ static void vdagent_chr_set_fe_open(struct Chardev
> *chr, int fe_open)
>          if (vd->mouse_hs) {
>              qemu_input_handler_deactivate(vd->mouse_hs);
>          }
> +        if (vd->cbpeer.update.notify) {
> +            qemu_clipboard_peer_unregister(&vd->cbpeer);
> +            memset(&vd->cbpeer, 0, sizeof(vd->cbpeer));
> +        }
>          return;
>      }
>
> @@ -352,6 +593,8 @@ static void vdagent_chr_parse(QemuOpts *opts,
> ChardevBackend *backend,
>      qemu_chr_parse_common(opts, qapi_ChardevVDAgent_base(cfg));
>      cfg->has_mouse = true;
>      cfg->mouse = qemu_opt_get_bool(opts, "mouse", VDAGENT_MOUSE_DEFAULT);
> +    cfg->has_clipboard = true;
> +    cfg->clipboard = qemu_opt_get_bool(opts, "clipboard",
> VDAGENT_CLIPBOARD_DEFAULT);
>  }
>
>  /* ------------------------------------------------------------------ */
> diff --git a/qapi/char.json b/qapi/char.json
> index d8e96b772523..059c9d634b06 100644
> --- a/qapi/char.json
> +++ b/qapi/char.json
> @@ -396,11 +396,13 @@
>  # Configuration info for vdagent.
>  #
>  # @mouse: enable/disable mouse, default is enabled.
> +# @clipboard: enable/disable clipboard, default is disabled.
>  #
>  # Since: 6.0
>  ##
>  { 'struct': 'ChardevVDAgent',
> -  'data': { '*mouse'    : 'bool' },
> +  'data': { '*mouse'    : 'bool',
> +            '*clipboard': 'bool' },
>    'base': 'ChardevCommon' }
>
>  ##
> diff --git a/ui/trace-events b/ui/trace-events
> index 1a5bd3861da5..90191cc1d285 100644
> --- a/ui/trace-events
> +++ b/ui/trace-events
> @@ -115,3 +115,5 @@ vdagent_close(void) ""
>  vdagent_send(const char *name) "msg %s"
>  vdagent_recv(const char *name) "msg %s"
>  vdagent_peer_cap(const char *name) "cap %s"
> +vdagent_cb_grab_selection(const char *name) "selection %s"
> +vdagent_cb_grab_type(const char *name) "type %s"
> --
> 2.29.2
>
>
>

-- 
Marc-André Lureau

[-- Attachment #2: Type: text/html, Size: 18341 bytes --]

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH 5/7] ui/vnc: clipboard support
  2021-02-19 13:13 ` [PATCH 5/7] ui/vnc: " Gerd Hoffmann
@ 2021-02-25 19:09   ` Marc-André Lureau
  2021-03-03 12:13     ` Gerd Hoffmann
  0 siblings, 1 reply; 20+ messages in thread
From: Marc-André Lureau @ 2021-02-25 19:09 UTC (permalink / raw)
  To: Gerd Hoffmann; +Cc: Paolo Bonzini, QEMU, Markus Armbruster

[-- Attachment #1: Type: text/plain, Size: 17552 bytes --]

On Fri, Feb 19, 2021 at 5:25 PM Gerd Hoffmann <kraxel@redhat.com> wrote:

> This patch adds support for cut+paste to the qemu vnc server, which
> allows the vnc client exchange clipbaord data with qemu and other peers
>

clipboard

like the qemu vdagent implementation.
>
> Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
> ---
>  ui/vnc.h           |  24 ++++
>  ui/vnc-clipboard.c | 326 +++++++++++++++++++++++++++++++++++++++++++++
>  ui/vnc.c           |  20 ++-
>  ui/meson.build     |   1 +
>  4 files changed, 365 insertions(+), 6 deletions(-)
>  create mode 100644 ui/vnc-clipboard.c
>
> diff --git a/ui/vnc.h b/ui/vnc.h
> index 116463d5f099..f611223859ae 100644
> --- a/ui/vnc.h
> +++ b/ui/vnc.h
> @@ -29,6 +29,7 @@
>
>  #include "qemu/queue.h"
>  #include "qemu/thread.h"
> +#include "ui/clipboard.h"
>  #include "ui/console.h"
>  #include "audio/audio.h"
>  #include "qemu/bitmap.h"
> @@ -347,6 +348,10 @@ struct VncState
>
>      Notifier mouse_mode_notifier;
>
> +    QemuClipboardPeer cbpeer;
> +    QemuClipboardInfo *cbinfo;
> +    uint32_t cbpending;
> +
>      QTAILQ_ENTRY(VncState) next;
>  };
>
> @@ -416,6 +421,7 @@ enum {
>  #define VNC_ENCODING_XVP                  0XFFFFFECB /* -309 */
>  #define VNC_ENCODING_ALPHA_CURSOR         0XFFFFFEC6 /* -314 */
>  #define VNC_ENCODING_WMVi                 0x574D5669
> +#define VNC_ENCODING_CLIPBOARD_EXT        0xc0a1e5ce
>
>
>  /*****************************************************************************
>   *
> @@ -457,6 +463,7 @@ enum VncFeatures {
>      VNC_FEATURE_ZYWRLE,
>      VNC_FEATURE_LED_STATE,
>      VNC_FEATURE_XVP,
> +    VNC_FEATURE_CLIPBOARD_EXT,
>  };
>
>  #define VNC_FEATURE_RESIZE_MASK              (1 << VNC_FEATURE_RESIZE)
> @@ -473,6 +480,7 @@ enum VncFeatures {
>  #define VNC_FEATURE_ZYWRLE_MASK              (1 << VNC_FEATURE_ZYWRLE)
>  #define VNC_FEATURE_LED_STATE_MASK           (1 << VNC_FEATURE_LED_STATE)
>  #define VNC_FEATURE_XVP_MASK                 (1 << VNC_FEATURE_XVP)
> +#define VNC_FEATURE_CLIPBOARD_EXT_MASK       (1 <<
> VNC_FEATURE_CLIPBOARD_EXT)
>
>
>  /* Client -> Server message IDs */
> @@ -534,6 +542,17 @@ enum VncFeatures {
>  #define VNC_XVP_ACTION_REBOOT 3
>  #define VNC_XVP_ACTION_RESET 4
>
> +/* extended clipboard flags  */
> +#define VNC_CLIPBOARD_TEXT     (1 << 0)
> +#define VNC_CLIPBOARD_RTF      (1 << 1)
> +#define VNC_CLIPBOARD_HTML     (1 << 2)
> +#define VNC_CLIPBOARD_DIB      (1 << 3)
> +#define VNC_CLIPBOARD_FILES    (1 << 4)
> +#define VNC_CLIPBOARD_CAPS     (1 << 24)
> +#define VNC_CLIPBOARD_REQUEST  (1 << 25)
> +#define VNC_CLIPBOARD_PEEK     (1 << 26)
> +#define VNC_CLIPBOARD_NOTIFY   (1 << 27)
> +#define VNC_CLIPBOARD_PROVIDE  (1 << 28)
>
>
>  /*****************************************************************************
>   *
> @@ -617,4 +636,9 @@ int vnc_zrle_send_framebuffer_update(VncState *vs, int
> x, int y, int w, int h);
>  int vnc_zywrle_send_framebuffer_update(VncState *vs, int x, int y, int w,
> int h);
>  void vnc_zrle_clear(VncState *vs);
>
> +/* vnc-clipboard.c */
> +void vnc_server_cut_text_caps(VncState *vs);
> +void vnc_client_cut_text(VncState *vs, size_t len, uint8_t *text);
> +void vnc_client_cut_text_ext(VncState *vs, int32_t len, uint32_t flags,
> uint8_t *data);
> +
>  #endif /* QEMU_VNC_H */
> diff --git a/ui/vnc-clipboard.c b/ui/vnc-clipboard.c
> new file mode 100644
> index 000000000000..e729120ba360
> --- /dev/null
> +++ b/ui/vnc-clipboard.c
> @@ -0,0 +1,326 @@
> +/*
> + * QEMU VNC display driver -- clipboard support
> + *
> + * Copyright (C) 2021 Gerd Hoffmann <kraxel@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-common.h"
> +#include "vnc.h"
> +#include "vnc-jobs.h"
> +
> +static uint8_t *inflate_buffer(uint8_t *in, uint32_t in_len, uint32_t
> *size)
> +{
> +    z_stream stream = {
> +        .next_in  = in,
> +        .avail_in = in_len,
> +        .zalloc   = Z_NULL,
> +        .zfree    = Z_NULL,
> +    };
> +    uint32_t out_len = 8;
> +    uint8_t *out = g_malloc(out_len);
>

g_autofree ?

+    int ret;
> +
> +    stream.next_out = out + stream.total_out;
> +    stream.avail_out = out_len - stream.total_out;
> +
> +    ret = inflateInit(&stream);
> +    if (ret != Z_OK) {
> +        goto err;
> +    }
> +
> +    while (stream.avail_in) {
> +        ret = inflate(&stream, Z_FINISH);
> +        switch (ret) {
> +        case Z_OK:
> +        case Z_STREAM_END:
> +            break;
> +        case Z_BUF_ERROR:
> +            out_len <<= 1;
> +            if (out_len > (1 << 20)) {
>

1Mb isn't that much, is it? Well, since it handles only text for now it's
probably enough. Would it make sense to make this a #define for clarity ?

+                goto err_end;
> +            }
> +            out = g_realloc(out, out_len);
> +            stream.next_out = out + stream.total_out;
> +            stream.avail_out = out_len - stream.total_out;
> +            break;
> +        default:
> +            goto err_end;
> +        }
> +    }
> +
> +    *size = stream.total_out;
> +    inflateEnd(&stream);
> +
> +    return out;
> +
> +err_end:
> +    inflateEnd(&stream);
> +err:
> +    g_free(out);
> +    return NULL;
> +}
> +
> +static uint8_t *deflate_buffer(uint8_t *in, uint32_t in_len, uint32_t
> *size)
> +{
> +    z_stream stream = {
> +        .next_in  = in,
> +        .avail_in = in_len,
> +        .zalloc   = Z_NULL,
> +        .zfree    = Z_NULL,
> +    };
> +    uint32_t out_len = 8;
> +    uint8_t *out = g_malloc(out_len);
>

same as inflate

+    int ret;
> +
> +    stream.next_out = out + stream.total_out;
> +    stream.avail_out = out_len - stream.total_out;
> +
> +    ret = deflateInit(&stream, Z_DEFAULT_COMPRESSION);
> +    if (ret != Z_OK) {
> +        goto err;
> +    }
> +
> +    while (ret != Z_STREAM_END) {
> +        ret = deflate(&stream, Z_FINISH);
> +        switch (ret) {
> +        case Z_OK:
> +        case Z_STREAM_END:
> +            break;
> +        case Z_BUF_ERROR:
> +            out_len <<= 1;
> +            if (out_len > (1 << 20)) {
> +                goto err_end;
> +            }
> +            out = g_realloc(out, out_len);
> +            stream.next_out = out + stream.total_out;
> +            stream.avail_out = out_len - stream.total_out;
> +            break;
> +        default:
> +            goto err_end;
> +        }
> +    }
> +
> +    *size = stream.total_out;
> +    deflateEnd(&stream);
> +
> +    return out;
> +
> +err_end:
> +    deflateEnd(&stream);
> +err:
> +    g_free(out);
> +    return NULL;
> +}
> +
> +static void vnc_clipboard_send(VncState *vs, uint32_t count, uint32_t
> *dwords)
> +{
> +    int i;
> +
> +    vnc_lock_output(vs);
> +    vnc_write_u8(vs, VNC_MSG_SERVER_CUT_TEXT);
> +    vnc_write_u8(vs, 0);
> +    vnc_write_u8(vs, 0);
> +    vnc_write_u8(vs, 0);
> +    vnc_write_s32(vs, -(count * sizeof(uint32_t)));  /* -(message length)
> */
> +    for (i = 0; i < count; i++) {
> +        vnc_write_u32(vs, dwords[i]);
> +    }
> +    vnc_unlock_output(vs);
> +    vnc_flush(vs);
> +}
> +
> +static void vnc_clipboard_provide(VncState *vs,
> +                                  QemuClipboardInfo *info,
> +                                  QemuClipboardType type)
> +{
> +    uint32_t flags = 0;
> +    uint8_t *buf;
> +    void *zbuf;
> +    uint32_t zsize;
> +
> +    switch (type) {
> +    case QEMU_CLIPBOARD_TYPE_TEXT:
> +        flags |= VNC_CLIPBOARD_TEXT;
> +        break;
> +    default:
> +        return;
> +    }
> +    flags |= VNC_CLIPBOARD_PROVIDE;
> +
> +    buf = g_malloc(info->types[type].size + 4);
> +    buf[0] = (info->types[type].size >> 24) & 0xff;
> +    buf[1] = (info->types[type].size >> 16) & 0xff;
> +    buf[2] = (info->types[type].size >>  8) & 0xff;
> +    buf[3] = (info->types[type].size >>  0) & 0xff;
> +    memcpy(buf + 4, info->types[type].data, info->types[type].size);
> +    zbuf = deflate_buffer(buf, info->types[type].size + 4, &zsize);
> +    g_free(buf);
> +
> +    if (!zbuf) {
> +        return;
> +    }
> +
> +    vnc_lock_output(vs);
> +    vnc_write_u8(vs, VNC_MSG_SERVER_CUT_TEXT);
> +    vnc_write_u8(vs, 0);
> +    vnc_write_u8(vs, 0);
> +    vnc_write_u8(vs, 0);
> +    vnc_write_s32(vs, -(sizeof(uint32_t) + zsize));  /* -(message length)
> */
> +    vnc_write_u32(vs, flags);
> +    vnc_write(vs, zbuf, zsize);
> +    vnc_unlock_output(vs);
> +    vnc_flush(vs);
>

zbuf is leaked, g_autofree is your friend

+}
> +
> +static void vnc_clipboard_notify(Notifier *notifier, void *data)
> +{
> +    VncState *vs = container_of(notifier, VncState, cbpeer.update);
> +    QemuClipboardInfo *info = data;
> +    QemuClipboardType type;
> +    bool self_update = info->owner == &vs->cbpeer;
> +    uint32_t flags = 0;
> +
> +    if (info != vs->cbinfo) {
> +        qemu_clipboard_info_put(vs->cbinfo);
> +        vs->cbinfo = qemu_clipboard_info_get(info);
> +        vs->cbpending = 0;
> +        if (!self_update) {
> +            if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) {
> +                flags |= VNC_CLIPBOARD_TEXT;
> +            }
> +            flags |= VNC_CLIPBOARD_NOTIFY;
> +            vnc_clipboard_send(vs, 1, &flags);
> +        }
> +        return;
> +    }
> +
> +    if (self_update) {
> +        return;
> +    }
> +
> +    for (type = 0; type < QEMU_CLIPBOARD_TYPE__COUNT; type++) {
> +        if (vs->cbpending & (1 << type)) {
> +            vs->cbpending &= ~(1 << type);
> +            vnc_clipboard_provide(vs, info, type);
> +        }
> +    }
> +}
> +
> +static void vnc_clipboard_request(QemuClipboardInfo *info,
> +                                  QemuClipboardType type)
> +{
> +    VncState *vs = container_of(info->owner, VncState, cbpeer);
> +    uint32_t flags = 0;
> +
> +    if (type == QEMU_CLIPBOARD_TYPE_TEXT) {
> +        flags |= VNC_CLIPBOARD_TEXT;
> +    }
> +    if (!flags) {
>

It might be worth noticing an empty clipboard in this case.

+        return;
> +    }
> +    flags |= VNC_CLIPBOARD_REQUEST;
> +
> +    vnc_clipboard_send(vs, 1, &flags);
> +}
> +
> +void vnc_client_cut_text_ext(VncState *vs, int32_t len, uint32_t flags,
> uint8_t *data)
> +{
> +    if (flags & VNC_CLIPBOARD_CAPS) {
> +        /* need store caps somewhere ? */
> +        return;
> +    }
> +
> +    if (flags & VNC_CLIPBOARD_NOTIFY) {
> +        QemuClipboardInfo *info =
> +            qemu_clipboard_info_new(&vs->cbpeer,
> QEMU_CLIPBOARD_SELECTION_CLIPBOARD);
> +        if (flags & VNC_CLIPBOARD_TEXT) {
> +            info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true;
> +        }
> +        qemu_clipboard_update(info);
> +        qemu_clipboard_info_put(info);
> +        return;
> +    }
> +
> +    if (flags & VNC_CLIPBOARD_PROVIDE &&
> +        vs->cbinfo &&
> +        vs->cbinfo->owner == &vs->cbpeer) {
> +        uint32_t size = 0;
> +        uint8_t *buf = inflate_buffer(data, len - 4, &size);
> +        if ((flags & VNC_CLIPBOARD_TEXT) &&
> +            buf && size >= 4) {
> +            uint32_t tsize = read_u32(buf, 0);
> +            uint8_t *tbuf = buf + 4;
> +            if (tsize < size) {
> +                qemu_clipboard_set_data(&vs->cbpeer, vs->cbinfo,
> +                                        QEMU_CLIPBOARD_TYPE_TEXT,
> +                                        tsize, tbuf, true);
> +            }
> +        }
> +        g_free(buf);
> +    }
> +
> +    if (flags & VNC_CLIPBOARD_REQUEST &&
> +        vs->cbinfo &&
> +        vs->cbinfo->owner != &vs->cbpeer) {
> +        if ((flags & VNC_CLIPBOARD_TEXT) &&
> +            vs->cbinfo->types[QEMU_CLIPBOARD_TYPE_TEXT].available) {
> +            if (vs->cbinfo->types[QEMU_CLIPBOARD_TYPE_TEXT].data) {
> +                vnc_clipboard_provide(vs, vs->cbinfo,
> QEMU_CLIPBOARD_TYPE_TEXT);
> +            } else {
> +                vs->cbpending |= (1 << QEMU_CLIPBOARD_TYPE_TEXT);
> +                qemu_clipboard_request(vs->cbinfo,
> QEMU_CLIPBOARD_TYPE_TEXT);
> +            }
> +        }
> +    }
> +}
> +
> +void vnc_client_cut_text(VncState *vs, size_t len, uint8_t *text)
> +{
> +    QemuClipboardInfo *info =
> +        qemu_clipboard_info_new(&vs->cbpeer,
> QEMU_CLIPBOARD_SELECTION_CLIPBOARD);
> +
> +    qemu_clipboard_set_data(&vs->cbpeer, info, QEMU_CLIPBOARD_TYPE_TEXT,
> +                            len, text, true);
> +    qemu_clipboard_info_put(info);
> +}
> +
> +void vnc_server_cut_text_caps(VncState *vs)
> +{
> +    uint32_t caps[2];
> +
> +    if (!vnc_has_feature(vs, VNC_FEATURE_CLIPBOARD_EXT)) {
> +        return;
> +    }
> +
> +    caps[0] = (VNC_CLIPBOARD_PROVIDE |
> +               VNC_CLIPBOARD_NOTIFY  |
> +               VNC_CLIPBOARD_REQUEST |
> +               VNC_CLIPBOARD_CAPS    |
> +               VNC_CLIPBOARD_TEXT);
> +    caps[1] = 0;
> +    vnc_clipboard_send(vs, 2, caps);
> +
> +    vs->cbpeer.name = "vnc";
> +    vs->cbpeer.update.notify = vnc_clipboard_notify;
> +    vs->cbpeer.request = vnc_clipboard_request;
> +    qemu_clipboard_peer_register(&vs->cbpeer);
> +}
> diff --git a/ui/vnc.c b/ui/vnc.c
> index 16bb3be770b2..91ec51c7c67d 100644
> --- a/ui/vnc.c
> +++ b/ui/vnc.c
> @@ -25,6 +25,7 @@
>   */
>
>  #include "qemu/osdep.h"
> +#include "qemu-common.h"
>  #include "vnc.h"
>  #include "vnc-jobs.h"
>  #include "trace.h"
> @@ -1309,6 +1310,9 @@ void vnc_disconnect_finish(VncState *vs)
>          /* last client gone */
>          vnc_update_server_surface(vs->vd);
>      }
> +    if (vs->cbpeer.update.notify) {
> +        qemu_clipboard_peer_unregister(&vs->cbpeer);
> +    }
>
>      vnc_unlock_output(vs);
>
> @@ -1734,10 +1738,6 @@ uint32_t read_u32(uint8_t *data, size_t offset)
>              (data[offset + 2] << 8) | data[offset + 3]);
>  }
>
> -static void client_cut_text(VncState *vs, size_t len, uint8_t *text)
> -{
> -}
> -
>  static void check_pointer_type_change(Notifier *notifier, void *data)
>  {
>      VncState *vs = container_of(notifier, VncState, mouse_mode_notifier);
> @@ -2179,6 +2179,10 @@ static void set_encodings(VncState *vs, int32_t
> *encodings, size_t n_encodings)
>                  send_xvp_message(vs, VNC_XVP_CODE_INIT);
>              }
>              break;
> +        case VNC_ENCODING_CLIPBOARD_EXT:
> +            vs->features |= VNC_FEATURE_CLIPBOARD_EXT_MASK;
> +            vnc_server_cut_text_caps(vs);
> +            break;
>          case VNC_ENCODING_COMPRESSLEVEL0 ... VNC_ENCODING_COMPRESSLEVEL0
> + 9:
>              vs->tight->compression = (enc & 0x0F);
>              break;
> @@ -2395,7 +2399,7 @@ static int protocol_client_msg(VncState *vs, uint8_t
> *data, size_t len)
>              return 8;
>          }
>          if (len == 8) {
> -            uint32_t dlen = read_u32(data, 4);
> +            uint32_t dlen = abs(read_s32(data, 4));
>              if (dlen > (1 << 20)) {
>                  error_report("vnc: client_cut_text msg payload has %u
> bytes"
>                               " which exceeds our limit of 1MB.", dlen);
> @@ -2407,7 +2411,11 @@ static int protocol_client_msg(VncState *vs,
> uint8_t *data, size_t len)
>              }
>          }
>
> -        client_cut_text(vs, read_u32(data, 4), data + 8);
> +        if (read_s32(data, 4) < 0) {
> +            vnc_client_cut_text_ext(vs, abs(read_s32(data, 4)),
> read_u32(data, 8), data + 12);
> +            break;
> +        }
> +        vnc_client_cut_text(vs, read_u32(data, 4), data + 8);
>          break;
>      case VNC_MSG_CLIENT_XVP:
>          if (!(vs->features & VNC_FEATURE_XVP)) {
> diff --git a/ui/meson.build b/ui/meson.build
> index 08447ac15c5e..a98f89b48978 100644
> --- a/ui/meson.build
> +++ b/ui/meson.build
> @@ -30,6 +30,7 @@ vnc_ss.add(files(
>    'vnc-auth-vencrypt.c',
>    'vnc-ws.c',
>    'vnc-jobs.c',
> +  'vnc-clipboard.c',
>  ))
>  vnc_ss.add(zlib, png, jpeg, gnutls)
>  vnc_ss.add(when: sasl, if_true: files('vnc-auth-sasl.c'))
> --
> 2.29.2
>
>
>

-- 
Marc-André Lureau

[-- Attachment #2: Type: text/html, Size: 22122 bytes --]

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH 7/7] ui/gtk: add clipboard support
  2021-02-19 13:13 ` [PATCH 7/7] ui/gtk: add clipboard support Gerd Hoffmann
@ 2021-02-25 19:45   ` Marc-André Lureau
  2021-03-03 12:20     ` Gerd Hoffmann
  0 siblings, 1 reply; 20+ messages in thread
From: Marc-André Lureau @ 2021-02-25 19:45 UTC (permalink / raw)
  To: Gerd Hoffmann; +Cc: Paolo Bonzini, QEMU, Markus Armbruster

[-- Attachment #1: Type: text/plain, Size: 8180 bytes --]

On Fri, Feb 19, 2021 at 5:29 PM Gerd Hoffmann <kraxel@redhat.com> wrote:

> This patch adds clipboard support to the qemu gtk ui.
>
> Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
> ---
>  include/ui/gtk.h   |   9 ++++
>  ui/gtk-clipboard.c | 124 +++++++++++++++++++++++++++++++++++++++++++++
>  ui/gtk.c           |   1 +
>  ui/meson.build     |   2 +-
>  4 files changed, 135 insertions(+), 1 deletion(-)
>  create mode 100644 ui/gtk-clipboard.c
>
> diff --git a/include/ui/gtk.h b/include/ui/gtk.h
> index 55319843758d..08999f8835e6 100644
> --- a/include/ui/gtk.h
> +++ b/include/ui/gtk.h
> @@ -18,6 +18,7 @@
>  #include <gdk/gdkwayland.h>
>  #endif
>
> +#include "ui/clipboard.h"
>  #include "ui/console.h"
>  #include "ui/kbd-state.h"
>  #if defined(CONFIG_OPENGL)
> @@ -137,6 +138,11 @@ struct GtkDisplayState {
>
>      bool external_pause_update;
>
> +    QemuClipboardPeer cbpeer;
> +    QemuClipboardInfo *cbinfo[QEMU_CLIPBOARD_SELECTION__COUNT];
> +    uint32_t cbpending[QEMU_CLIPBOARD_SELECTION__COUNT];
> +    GtkClipboard *gtkcb[QEMU_CLIPBOARD_SELECTION__COUNT];
> +
>      DisplayOptions *opts;
>  };
>
> @@ -208,4 +214,7 @@ QEMUGLContext
> gd_gl_area_get_current_context(DisplayChangeListener *dcl);
>  int gd_gl_area_make_current(DisplayChangeListener *dcl,
>                              QEMUGLContext ctx);
>
> +/* gtk-clipboard.c */
> +void gd_clipboard_init(GtkDisplayState *gd);
> +
>  #endif /* UI_GTK_H */
> diff --git a/ui/gtk-clipboard.c b/ui/gtk-clipboard.c
> new file mode 100644
> index 000000000000..4a7f44b25818
> --- /dev/null
> +++ b/ui/gtk-clipboard.c
> @@ -0,0 +1,124 @@
> +/*
> + * GTK UI -- clipboard support
> + *
> + * Copyright (C) 2021 Gerd Hoffmann <kraxel@redhat.com>
> + *
> + * This program is free software; you can redistribute it and/or modify
> + * it under the terms of the GNU General Public License as published by
> + * the Free Software Foundation; either version 2 of the License, or
> + * (at your option) any later version.
> + *
> + * This program is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
> + * General Public License for more details.
> + *
> + * You should have received a copy of the GNU General Public License
> + * along with this program; if not, see <http://www.gnu.org/licenses/>.
> + *
> + */
> +
> +#include "qemu/osdep.h"
> +#include "qemu-common.h"
> +
> +#include "ui/gtk.h"
> +
> +static void gd_clipboard_notify(Notifier *notifier, void *data)
> +{
> +    GtkDisplayState *gd = container_of(notifier, GtkDisplayState,
> cbpeer.update);
> +    QemuClipboardInfo *info = data;
> +    QemuClipboardSelection s = info->selection;
> +    bool self_update = info->owner == &gd->cbpeer;
> +
> +    if (info != gd->cbinfo[s]) {
> +        qemu_clipboard_info_put(gd->cbinfo[s]);
> +        gd->cbinfo[s] = qemu_clipboard_info_get(info);
> +        gd->cbpending[s] = 0;
> +        if (!self_update) {
> +            if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) {
> +                qemu_clipboard_request(info, QEMU_CLIPBOARD_TYPE_TEXT);
>

Always requesting the clipboard is a bit harsh, isn't it?

+            }
> +        }
> +        return;
> +    }
> +
> +    if (self_update) {
> +        return;
> +    }
> +
> +    if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available &&
> +        info->types[QEMU_CLIPBOARD_TYPE_TEXT].data) {
> +        gtk_clipboard_set_text(gd->gtkcb[s],
> +                               info->types[QEMU_CLIPBOARD_TYPE_TEXT].data,
> +
>  info->types[QEMU_CLIPBOARD_TYPE_TEXT].size);
> +    }
> +}
> +
> +static void gd_clipboard_request(QemuClipboardInfo *info,
> +                                 QemuClipboardType type)
> +{
> +    GtkDisplayState *gd = container_of(info->owner, GtkDisplayState,
> cbpeer);
> +    char *text;
> +
> +    switch (type) {
> +    case QEMU_CLIPBOARD_TYPE_TEXT:
> +        text = gtk_clipboard_wait_for_text(gd->gtkcb[info->selection]);
> +        qemu_clipboard_set_data(&gd->cbpeer, info, type,
> +                                strlen(text), text, true);
>

text might be NULL if it failed.

And you must free it.

+        break;
> +    default:
> +        break;
> +    }
> +}
> +
> +static QemuClipboardSelection gd_find_selection(GtkDisplayState *gd,
> +                                                GtkClipboard *clipboard)
> +{
> +    QemuClipboardSelection s;
> +
> +    for (s = 0; s < QEMU_CLIPBOARD_SELECTION__COUNT; s++) {
> +        if (gd->gtkcb[s] == clipboard) {
> +            return s;
> +        }
> +    }
> +    return QEMU_CLIPBOARD_SELECTION_CLIPBOARD;
> +}
> +
> +static void gd_owner_change(GtkClipboard *clipboard,
> +                            GdkEvent *event,
> +                            gpointer data)
> +{
> +    GtkDisplayState *gd = data;
> +    QemuClipboardSelection s = gd_find_selection(gd, clipboard);
> +    QemuClipboardInfo *info;
> +
> +    info = qemu_clipboard_info_new(&gd->cbpeer, s);
> +    if (gtk_clipboard_wait_is_text_available(clipboard)) {
> +        info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true;
> +    }
>

Hmm, so after gtk_clipboard_set_text() the client side is actually taking
the ownership away from the guest clipboard I presume. That might have some
weird interaction issues. Hopefully the other side isn't playing the same
game...

+
> +    qemu_clipboard_update(info);
> +    qemu_clipboard_info_put(info);
> +}
> +
> +void gd_clipboard_init(GtkDisplayState *gd)
> +{
> +    gd->cbpeer.name = "gtk";
> +    gd->cbpeer.update.notify = gd_clipboard_notify;
> +    gd->cbpeer.request = gd_clipboard_request;
> +    qemu_clipboard_peer_register(&gd->cbpeer);
> +
> +    gd->gtkcb[QEMU_CLIPBOARD_SELECTION_CLIPBOARD] =
> +        gtk_clipboard_get(gdk_atom_intern("CLIPBOARD", FALSE));
>

GDK_SELECTION_CLIPBOARD


> +    gd->gtkcb[QEMU_CLIPBOARD_SELECTION_PRIMARY] =
> +        gtk_clipboard_get(gdk_atom_intern("PRIMARY", FALSE));
>

 GDK_SELECTION_PRIMARY

+    gd->gtkcb[QEMU_CLIPBOARD_SELECTION_SECONDARY] =
> +        gtk_clipboard_get(gdk_atom_intern("SECONDARY", FALSE));
>

 GDK_SELECTION_SECONDARY


> +
> +    g_signal_connect(gd->gtkcb[QEMU_CLIPBOARD_SELECTION_CLIPBOARD],
> +                     "owner-change", G_CALLBACK(gd_owner_change), gd);
> +    g_signal_connect(gd->gtkcb[QEMU_CLIPBOARD_SELECTION_PRIMARY],
> +                     "owner-change", G_CALLBACK(gd_owner_change), gd);
> +    g_signal_connect(gd->gtkcb[QEMU_CLIPBOARD_SELECTION_SECONDARY],
> +                     "owner-change", G_CALLBACK(gd_owner_change), gd);
>


Might be worth having a deinit for signals, peer registration etc, even if
nothing is hooked yet in gtk.c..

Overall, calling wait & set variants of clipboard functions makes things
quite simpler to deal with. Hopefully it can stay that way...

+}
> diff --git a/ui/gtk.c b/ui/gtk.c
> index 7b412dd4fe0b..0ae3ec20f594 100644
> --- a/ui/gtk.c
> +++ b/ui/gtk.c
> @@ -2252,6 +2252,7 @@ static void gtk_display_init(DisplayState *ds,
> DisplayOptions *opts)
>          opts->u.gtk.grab_on_hover) {
>          gtk_menu_item_activate(GTK_MENU_ITEM(s->grab_on_hover_item));
>      }
> +    gd_clipboard_init(s);
>  }
>
>  static void early_gtk_display_init(DisplayOptions *opts)
> diff --git a/ui/meson.build b/ui/meson.build
> index a98f89b48978..3ea969a6210b 100644
> --- a/ui/meson.build
> +++ b/ui/meson.build
> @@ -64,7 +64,7 @@ if gtk.found()
>    softmmu_ss.add(when: 'CONFIG_WIN32', if_true: files('win32-kbd-hook.c'))
>
>    gtk_ss = ss.source_set()
> -  gtk_ss.add(gtk, vte, pixman, files('gtk.c'))
> +  gtk_ss.add(gtk, vte, pixman, files('gtk.c', 'gtk-clipboard.c'))
>    gtk_ss.add(when: x11, if_true: files('x_keymap.c'))
>    gtk_ss.add(when: [opengl, 'CONFIG_OPENGL'], if_true: files('gtk-egl.c',
> 'gtk-gl-area.c'))
>    ui_modules += {'gtk' : gtk_ss}
> --
> 2.29.2
>
>
>

-- 
Marc-André Lureau

[-- Attachment #2: Type: text/html, Size: 11291 bytes --]

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH 5/7] ui/vnc: clipboard support
  2021-02-25 19:09   ` Marc-André Lureau
@ 2021-03-03 12:13     ` Gerd Hoffmann
  2021-03-03 14:27       ` Marc-André Lureau
  0 siblings, 1 reply; 20+ messages in thread
From: Gerd Hoffmann @ 2021-03-03 12:13 UTC (permalink / raw)
  To: Marc-André Lureau; +Cc: Paolo Bonzini, QEMU, Markus Armbruster

  Hi,

> > +        case Z_BUF_ERROR:
> > +            out_len <<= 1;
> > +            if (out_len > (1 << 20)) {
> >
> 
> 1Mb isn't that much, is it? Well, since it handles only text for now it's
> probably enough. Would it make sense to make this a #define for clarity ?

Yep.  While talking about sizes:  How does vdagent handles large
clipboard chunks?  There is ...

#define VD_AGENT_MAX_DATA_SIZE 2048

... but I suspect clipboard content isn't limited to that ...

thanks,
  Gerd



^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH 7/7] ui/gtk: add clipboard support
  2021-02-25 19:45   ` Marc-André Lureau
@ 2021-03-03 12:20     ` Gerd Hoffmann
  2021-03-03 13:54       ` Marc-André Lureau
  0 siblings, 1 reply; 20+ messages in thread
From: Gerd Hoffmann @ 2021-03-03 12:20 UTC (permalink / raw)
  To: Marc-André Lureau; +Cc: Paolo Bonzini, QEMU, Markus Armbruster

  Hi,

> > +    if (info != gd->cbinfo[s]) {
> > +        qemu_clipboard_info_put(gd->cbinfo[s]);
> > +        gd->cbinfo[s] = qemu_clipboard_info_get(info);
> > +        gd->cbpending[s] = 0;
> > +        if (!self_update) {
> > +            if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) {
> > +                qemu_clipboard_request(info, QEMU_CLIPBOARD_TYPE_TEXT);
> 
> Always requesting the clipboard is a bit harsh, isn't it?
> 
> +            }
> > +        }
> > +        return;
> > +    }
> > +
> > +    if (self_update) {
> > +        return;
> > +    }
> > +
> > +    if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available &&
> > +        info->types[QEMU_CLIPBOARD_TYPE_TEXT].data) {
> > +        gtk_clipboard_set_text(gd->gtkcb[s],
> > +                               info->types[QEMU_CLIPBOARD_TYPE_TEXT].data,

Well, I want gtk deal with the compatibility stuff like all the
different target names we have for text ("STRING", "text/plain", ...),
and using gtk_clipboard_set_text() seems to be the only way to do that.

I'm open to better ideas.

take care,
  Gerd



^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH 7/7] ui/gtk: add clipboard support
  2021-03-03 12:20     ` Gerd Hoffmann
@ 2021-03-03 13:54       ` Marc-André Lureau
  2021-03-04  9:04         ` Gerd Hoffmann
  0 siblings, 1 reply; 20+ messages in thread
From: Marc-André Lureau @ 2021-03-03 13:54 UTC (permalink / raw)
  To: Gerd Hoffmann; +Cc: Paolo Bonzini, QEMU, Markus Armbruster

[-- Attachment #1: Type: text/plain, Size: 1675 bytes --]

Hi

On Wed, Mar 3, 2021 at 4:20 PM Gerd Hoffmann <kraxel@redhat.com> wrote:

>   Hi,
>
> > > +    if (info != gd->cbinfo[s]) {
> > > +        qemu_clipboard_info_put(gd->cbinfo[s]);
> > > +        gd->cbinfo[s] = qemu_clipboard_info_get(info);
> > > +        gd->cbpending[s] = 0;
> > > +        if (!self_update) {
> > > +            if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) {
> > > +                qemu_clipboard_request(info,
> QEMU_CLIPBOARD_TYPE_TEXT);
> >
> > Always requesting the clipboard is a bit harsh, isn't it?
> >
> > +            }
> > > +        }
> > > +        return;
> > > +    }
> > > +
> > > +    if (self_update) {
> > > +        return;
> > > +    }
> > > +
> > > +    if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available &&
> > > +        info->types[QEMU_CLIPBOARD_TYPE_TEXT].data) {
> > > +        gtk_clipboard_set_text(gd->gtkcb[s],
> > > +
>  info->types[QEMU_CLIPBOARD_TYPE_TEXT].data,
>
> Well, I want gtk deal with the compatibility stuff like all the
> different target names we have for text ("STRING", "text/plain", ...),
> and using gtk_clipboard_set_text() seems to be the only way to do that.
>
> I'm open to better ideas.
>
>
Basically implement an async version of the Gtk function:
https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/gtkclipboard.c#L850

Instead of "gtk_clipboard_set_with_data" taking a copy of the text,
implement the callbacks to request the data from the guest.

I am not sure about gtk_clipboard_set_can_store() interactions, it's
probably only when the application quits by default. I wouldn't worry about
it for now.


-- 
Marc-André Lureau

[-- Attachment #2: Type: text/html, Size: 2611 bytes --]

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH 5/7] ui/vnc: clipboard support
  2021-03-03 12:13     ` Gerd Hoffmann
@ 2021-03-03 14:27       ` Marc-André Lureau
  2021-03-04  8:58         ` Gerd Hoffmann
  0 siblings, 1 reply; 20+ messages in thread
From: Marc-André Lureau @ 2021-03-03 14:27 UTC (permalink / raw)
  To: Gerd Hoffmann; +Cc: Paolo Bonzini, QEMU, Markus Armbruster

[-- Attachment #1: Type: text/plain, Size: 1010 bytes --]

Hi

On Wed, Mar 3, 2021 at 4:13 PM Gerd Hoffmann <kraxel@redhat.com> wrote:

>   Hi,
>
> > > +        case Z_BUF_ERROR:
> > > +            out_len <<= 1;
> > > +            if (out_len > (1 << 20)) {
> > >
> >
> > 1Mb isn't that much, is it? Well, since it handles only text for now it's
> > probably enough. Would it make sense to make this a #define for clarity ?
>
> Yep.  While talking about sizes:  How does vdagent handles large
> clipboard chunks?  There is ...
>
> #define VD_AGENT_MAX_DATA_SIZE 2048
>
> ... but I suspect clipboard content isn't limited to that ...
>
>
The client splits large data to send in many messages.  I don't see the
agent doing the same (I might be missing something). I guess the rationale
behind is to be able to transmit other (agent or not) messages interleaved
on the main channel, and this is more important from the client side. The
amount of client outgoing agent messages is rate-controlled by a token
counter.

-- 
Marc-André Lureau

[-- Attachment #2: Type: text/html, Size: 1513 bytes --]

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH 5/7] ui/vnc: clipboard support
  2021-03-03 14:27       ` Marc-André Lureau
@ 2021-03-04  8:58         ` Gerd Hoffmann
  2021-03-04 10:07           ` Gerd Hoffmann
  0 siblings, 1 reply; 20+ messages in thread
From: Gerd Hoffmann @ 2021-03-04  8:58 UTC (permalink / raw)
  To: Marc-André Lureau; +Cc: Paolo Bonzini, QEMU, Markus Armbruster

On Wed, Mar 03, 2021 at 06:27:27PM +0400, Marc-André Lureau wrote:
> Hi
> 
> On Wed, Mar 3, 2021 at 4:13 PM Gerd Hoffmann <kraxel@redhat.com> wrote:
> 
> >   Hi,
> >
> > > > +        case Z_BUF_ERROR:
> > > > +            out_len <<= 1;
> > > > +            if (out_len > (1 << 20)) {
> > > >
> > >
> > > 1Mb isn't that much, is it? Well, since it handles only text for now it's
> > > probably enough. Would it make sense to make this a #define for clarity ?
> >
> > Yep.  While talking about sizes:  How does vdagent handles large
> > clipboard chunks?  There is ...
> >
> > #define VD_AGENT_MAX_DATA_SIZE 2048
> >
> > ... but I suspect clipboard content isn't limited to that ...
> >
> >
> The client splits large data to send in many messages.

Ok.  Is it documented anywhere how that split happens?  I suspect it'll
involve VDIChunkHeader somehow?

> I don't see the
> agent doing the same (I might be missing something).

Hmm, ok.  Guess I should better be prepared to receive messages larger
than VD_AGENT_MAX_DATA_SIZE ...

take care,
  Gerd



^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH 7/7] ui/gtk: add clipboard support
  2021-03-03 13:54       ` Marc-André Lureau
@ 2021-03-04  9:04         ` Gerd Hoffmann
  0 siblings, 0 replies; 20+ messages in thread
From: Gerd Hoffmann @ 2021-03-04  9:04 UTC (permalink / raw)
  To: Marc-André Lureau; +Cc: Paolo Bonzini, QEMU, Markus Armbruster

  Hi,

> > Well, I want gtk deal with the compatibility stuff like all the
> > different target names we have for text ("STRING", "text/plain", ...),
> > and using gtk_clipboard_set_text() seems to be the only way to do that.
> >
> > I'm open to better ideas.

> Basically implement an async version of the Gtk function:
> https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/gtkclipboard.c#L850

Ah, there is a nice gtk_target_list_add_text_targets() helper.
That helps.

thanks,
  Gerd



^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH 5/7] ui/vnc: clipboard support
  2021-03-04  8:58         ` Gerd Hoffmann
@ 2021-03-04 10:07           ` Gerd Hoffmann
  0 siblings, 0 replies; 20+ messages in thread
From: Gerd Hoffmann @ 2021-03-04 10:07 UTC (permalink / raw)
  To: Marc-André Lureau; +Cc: Paolo Bonzini, QEMU, Markus Armbruster

  Hi,

> > I don't see the
> > agent doing the same (I might be missing something).
> 
> Hmm, ok.  Guess I should better be prepared to receive messages larger
> than VD_AGENT_MAX_DATA_SIZE ...

Confirmed.  Cut+paste large text blocks in the guest -> hangs qemu
vdagent implementation, because the message doesn't fit into the
fixed-site (VD_AGENT_MAX_DATA_SIZE) message buffer.

So I need so switch to dynamic allocation when reworking the code for
proper size checks.

The other way around hangs too, seems the guest agent is not prepared
to receive large messages and expects them in chunks ...

take care,
  Gerd



^ permalink raw reply	[flat|nested] 20+ messages in thread

end of thread, other threads:[~2021-03-04 13:00 UTC | newest]

Thread overview: 20+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-02-19 13:13 [PATCH 0/7] ui: add vdagent implementation and clipboard support Gerd Hoffmann
2021-02-19 13:13 ` [PATCH 1/7] ui: add clipboard infrastructure Gerd Hoffmann
2021-02-19 13:13 ` [PATCH 2/7] ui/vdagent: core infrastructure Gerd Hoffmann
2021-02-25 18:24   ` Marc-André Lureau
2021-02-19 13:13 ` [PATCH 3/7] ui/vdagent: add mouse support Gerd Hoffmann
2021-02-19 13:13 ` [PATCH 4/7] ui/vdagent: add clipboard support Gerd Hoffmann
2021-02-25 18:37   ` Marc-André Lureau
2021-02-19 13:13 ` [PATCH 5/7] ui/vnc: " Gerd Hoffmann
2021-02-25 19:09   ` Marc-André Lureau
2021-03-03 12:13     ` Gerd Hoffmann
2021-03-03 14:27       ` Marc-André Lureau
2021-03-04  8:58         ` Gerd Hoffmann
2021-03-04 10:07           ` Gerd Hoffmann
2021-02-19 13:13 ` [PATCH 6/7] ui/gtk: move struct GtkDisplayState to ui/gtk.h Gerd Hoffmann
2021-02-19 13:13 ` [PATCH 7/7] ui/gtk: add clipboard support Gerd Hoffmann
2021-02-25 19:45   ` Marc-André Lureau
2021-03-03 12:20     ` Gerd Hoffmann
2021-03-03 13:54       ` Marc-André Lureau
2021-03-04  9:04         ` Gerd Hoffmann
2021-02-19 13:36 ` [PATCH 0/7] ui: add vdagent implementation and " no-reply

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.