Since commitf626116f("ui/vdagent: factor out clipboard peer registration"), the QEMU clipboard serial is reset whenever the vdagent chardev receives the guest caps. This triggers a CHR_EVENT_CLOSED which is handled by virtio_serial_close() to notify the guest. The "reconnection logic" is there to reset the agent when a client (dbus, spice etc) reconnects, or the agent is restarted. It is required to sync the clipboard serials and to prevent races or loops due to clipboard managers on both ends (but this is not implemented by windows vdagent). The Unix agent has been reconnecting without resending caps, thus working with this approach. However, the Windows agent does not seem to have a way to handle VIRTIO_CONSOLE_PORT_OPEN=0 event and do not receive further data... Let's not trigger this disconnection/reset logic if the agent does not support VD_AGENT_CAP_CLIPBOARD_GRAB_SERIAL. Fixes:f626116f("ui/vdagent: factor out clipboard peer registration") Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com> Reported-by: Lucas Kornicki <lucas.kornicki@nutanix.com> Tested-by: Fiona Ebner <f.ebner@proxmox.com> Reviewed-by: Fiona Ebner <f.ebner@proxmox.com> Tested-by: Lucas Kornicki <lucas.kornicki@nutanix.com>
1111 lines
33 KiB
C
1111 lines
33 KiB
C
#include "qemu/osdep.h"
|
|
#include "qapi/error.h"
|
|
#include "chardev/char.h"
|
|
#include "qemu/buffer.h"
|
|
#include "qemu/error-report.h"
|
|
#include "qemu/option.h"
|
|
#include "qemu/units.h"
|
|
#include "hw/qdev-core.h"
|
|
#include "ui/clipboard.h"
|
|
#include "ui/console.h"
|
|
#include "ui/input.h"
|
|
#include "migration/vmstate.h"
|
|
#include "trace.h"
|
|
|
|
#include "qapi/qapi-types-char.h"
|
|
#include "qapi/qapi-types-ui.h"
|
|
|
|
#include "spice/vd_agent.h"
|
|
|
|
#define CHECK_SPICE_PROTOCOL_VERSION(major, minor, micro) \
|
|
(CONFIG_SPICE_PROTOCOL_MAJOR > (major) || \
|
|
(CONFIG_SPICE_PROTOCOL_MAJOR == (major) && \
|
|
CONFIG_SPICE_PROTOCOL_MINOR > (minor)) || \
|
|
(CONFIG_SPICE_PROTOCOL_MAJOR == (major) && \
|
|
CONFIG_SPICE_PROTOCOL_MINOR == (minor) && \
|
|
CONFIG_SPICE_PROTOCOL_MICRO >= (micro)))
|
|
|
|
#define VDAGENT_BUFFER_LIMIT (1 * MiB)
|
|
#define VDAGENT_MOUSE_DEFAULT true
|
|
#define VDAGENT_CLIPBOARD_DEFAULT false
|
|
|
|
struct VDAgentChardev {
|
|
Chardev parent;
|
|
|
|
/* config */
|
|
bool mouse;
|
|
bool clipboard;
|
|
|
|
/* guest vdagent */
|
|
bool connected;
|
|
uint32_t caps;
|
|
VDIChunkHeader chunk;
|
|
uint32_t chunksize;
|
|
uint8_t *msgbuf;
|
|
uint32_t msgsize;
|
|
uint8_t *xbuf;
|
|
uint32_t xoff, xsize;
|
|
GByteArray *outbuf;
|
|
|
|
/* mouse */
|
|
DeviceState mouse_dev;
|
|
uint32_t mouse_x;
|
|
uint32_t mouse_y;
|
|
uint32_t mouse_btn;
|
|
uint32_t mouse_display;
|
|
QemuInputHandlerState *mouse_hs;
|
|
|
|
/* clipboard */
|
|
QemuClipboardPeer cbpeer;
|
|
uint32_t last_serial[QEMU_CLIPBOARD_SELECTION__COUNT];
|
|
uint32_t cbpending[QEMU_CLIPBOARD_SELECTION__COUNT];
|
|
};
|
|
typedef struct VDAgentChardev VDAgentChardev;
|
|
|
|
#define TYPE_CHARDEV_QEMU_VDAGENT "chardev-qemu-vdagent"
|
|
|
|
DECLARE_INSTANCE_CHECKER(VDAgentChardev, QEMU_VDAGENT_CHARDEV,
|
|
TYPE_CHARDEV_QEMU_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",
|
|
[VD_AGENT_CAP_GRAPHICS_DEVICE_INFO] = "graphics-device-info",
|
|
#if CHECK_SPICE_PROTOCOL_VERSION(0, 14, 1)
|
|
[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",
|
|
[VD_AGENT_GRAPHICS_DEVICE_INFO] = "graphics-device-info",
|
|
};
|
|
|
|
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 CHECK_SPICE_PROTOCOL_VERSION(0, 14, 3)
|
|
[VD_AGENT_CLIPBOARD_FILE_LIST] = "files",
|
|
#endif
|
|
};
|
|
|
|
#define GET_NAME(_m, _v) \
|
|
(((_v) < ARRAY_SIZE(_m) && (_m[_v])) ? (_m[_v]) : "???")
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* send messages */
|
|
|
|
static void vdagent_send_buf(VDAgentChardev *vd)
|
|
{
|
|
uint32_t len;
|
|
|
|
while (vd->outbuf->len) {
|
|
len = qemu_chr_be_can_write(CHARDEV(vd));
|
|
if (len == 0) {
|
|
return;
|
|
}
|
|
if (len > vd->outbuf->len) {
|
|
len = vd->outbuf->len;
|
|
}
|
|
qemu_chr_be_write(CHARDEV(vd), vd->outbuf->data, len);
|
|
g_byte_array_remove_range(vd->outbuf, 0, len);
|
|
}
|
|
}
|
|
|
|
static void vdagent_send_msg(VDAgentChardev *vd, VDAgentMessage *msg)
|
|
{
|
|
uint8_t *msgbuf = (void *)msg;
|
|
uint32_t msgsize = sizeof(VDAgentMessage) + msg->size;
|
|
uint32_t msgoff = 0;
|
|
VDIChunkHeader chunk;
|
|
|
|
trace_vdagent_send(GET_NAME(msg_name, msg->type));
|
|
|
|
msg->protocol = VD_AGENT_PROTOCOL;
|
|
|
|
if (vd->outbuf->len + msgsize > VDAGENT_BUFFER_LIMIT) {
|
|
error_report("buffer full, dropping message");
|
|
return;
|
|
}
|
|
|
|
while (msgoff < msgsize) {
|
|
chunk.port = VDP_CLIENT_PORT;
|
|
chunk.size = msgsize - msgoff;
|
|
if (chunk.size > 1024) {
|
|
chunk.size = 1024;
|
|
}
|
|
g_byte_array_append(vd->outbuf, (void *)&chunk, sizeof(chunk));
|
|
g_byte_array_append(vd->outbuf, msgbuf + msgoff, chunk.size);
|
|
msgoff += chunk.size;
|
|
}
|
|
vdagent_send_buf(vd);
|
|
}
|
|
|
|
static void vdagent_send_caps(VDAgentChardev *vd, bool request)
|
|
{
|
|
g_autofree 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);
|
|
}
|
|
if (vd->clipboard) {
|
|
caps->caps[0] |= (1 << VD_AGENT_CAP_CLIPBOARD_BY_DEMAND);
|
|
caps->caps[0] |= (1 << VD_AGENT_CAP_CLIPBOARD_SELECTION);
|
|
#if CHECK_SPICE_PROTOCOL_VERSION(0, 14, 1)
|
|
caps->caps[0] |= (1 << VD_AGENT_CAP_CLIPBOARD_GRAB_SERIAL);
|
|
#endif
|
|
}
|
|
|
|
caps->request = request;
|
|
vdagent_send_msg(vd, msg);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* mouse events */
|
|
|
|
static bool have_mouse(VDAgentChardev *vd)
|
|
{
|
|
return vd->mouse &&
|
|
(vd->caps & (1 << VD_AGENT_CAP_MOUSE_STATE));
|
|
}
|
|
|
|
static void vdagent_send_mouse(VDAgentChardev *vd)
|
|
{
|
|
g_autofree 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;
|
|
mouse->display_id = vd->mouse_display;
|
|
|
|
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,
|
|
#ifdef VD_AGENT_EBUTTON_MASK
|
|
[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);
|
|
}
|
|
vd->mouse_display = qemu_console_get_index(src);
|
|
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 const QemuInputHandler vdagent_mouse_handler = {
|
|
.name = "vdagent mouse",
|
|
.mask = INPUT_EVENT_MASK_BTN | INPUT_EVENT_MASK_ABS,
|
|
.event = vdagent_pointer_event,
|
|
.sync = vdagent_pointer_sync,
|
|
};
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* clipboard */
|
|
|
|
static bool have_clipboard(VDAgentChardev *vd)
|
|
{
|
|
return vd->clipboard &&
|
|
(vd->caps & (1 << VD_AGENT_CAP_CLIPBOARD_BY_DEMAND));
|
|
}
|
|
|
|
static bool have_selection(VDAgentChardev *vd)
|
|
{
|
|
return vd->caps & (1 << VD_AGENT_CAP_CLIPBOARD_SELECTION);
|
|
}
|
|
|
|
static bool have_clipboard_serial(VDAgentChardev *vd)
|
|
{
|
|
#if CHECK_SPICE_PROTOCOL_VERSION(0, 14, 1)
|
|
return vd->caps & (1 << VD_AGENT_CAP_CLIPBOARD_GRAB_SERIAL);
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
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)
|
|
{
|
|
g_autofree VDAgentMessage *msg =
|
|
g_malloc0(sizeof(VDAgentMessage) +
|
|
sizeof(uint32_t) * (QEMU_CLIPBOARD_TYPE__COUNT + 1) +
|
|
sizeof(uint32_t));
|
|
uint8_t *s = msg->data;
|
|
uint32_t *data = (uint32_t *)msg->data;
|
|
uint32_t q, type;
|
|
|
|
if (have_selection(vd)) {
|
|
*s = info->selection;
|
|
data++;
|
|
msg->size += sizeof(uint32_t);
|
|
} else if (info->selection != QEMU_CLIPBOARD_SELECTION_CLIPBOARD) {
|
|
return;
|
|
}
|
|
|
|
if (have_clipboard_serial(vd)) {
|
|
if (!info->has_serial) {
|
|
/* client should win */
|
|
info->serial = vd->last_serial[info->selection]++;
|
|
info->has_serial = true;
|
|
}
|
|
*data = info->serial;
|
|
data++;
|
|
msg->size += sizeof(uint32_t);
|
|
}
|
|
|
|
for (q = 0; q < QEMU_CLIPBOARD_TYPE__COUNT; q++) {
|
|
type = type_qemu_to_vdagent(q);
|
|
if (type != VD_AGENT_CLIPBOARD_NONE && info->types[q].available) {
|
|
*data = type;
|
|
data++;
|
|
msg->size += sizeof(uint32_t);
|
|
}
|
|
}
|
|
|
|
msg->type = VD_AGENT_CLIPBOARD_GRAB;
|
|
vdagent_send_msg(vd, msg);
|
|
}
|
|
|
|
static void vdagent_send_clipboard_release(VDAgentChardev *vd,
|
|
QemuClipboardInfo *info)
|
|
{
|
|
g_autofree VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) +
|
|
sizeof(uint32_t));
|
|
|
|
if (have_selection(vd)) {
|
|
uint8_t *s = msg->data;
|
|
*s = info->selection;
|
|
msg->size += sizeof(uint32_t);
|
|
} else if (info->selection != QEMU_CLIPBOARD_SELECTION_CLIPBOARD) {
|
|
return;
|
|
}
|
|
|
|
msg->type = VD_AGENT_CLIPBOARD_RELEASE;
|
|
vdagent_send_msg(vd, msg);
|
|
}
|
|
|
|
static void vdagent_send_clipboard_data(VDAgentChardev *vd,
|
|
QemuClipboardInfo *info,
|
|
QemuClipboardType type)
|
|
{
|
|
g_autofree VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) +
|
|
sizeof(uint32_t) * 2 +
|
|
info->types[type].size);
|
|
|
|
uint8_t *s = msg->data;
|
|
uint32_t *data = (uint32_t *)msg->data;
|
|
|
|
if (have_selection(vd)) {
|
|
*s = info->selection;
|
|
data++;
|
|
msg->size += sizeof(uint32_t);
|
|
} else if (info->selection != QEMU_CLIPBOARD_SELECTION_CLIPBOARD) {
|
|
return;
|
|
}
|
|
|
|
*data = type_qemu_to_vdagent(type);
|
|
data++;
|
|
msg->size += sizeof(uint32_t);
|
|
|
|
memcpy(data, info->types[type].data, info->types[type].size);
|
|
msg->size += info->types[type].size;
|
|
|
|
msg->type = VD_AGENT_CLIPBOARD;
|
|
vdagent_send_msg(vd, msg);
|
|
}
|
|
|
|
static void vdagent_send_empty_clipboard_data(VDAgentChardev *vd,
|
|
QemuClipboardSelection selection,
|
|
QemuClipboardType type)
|
|
{
|
|
g_autoptr(QemuClipboardInfo) info = qemu_clipboard_info_new(&vd->cbpeer, selection);
|
|
|
|
trace_vdagent_send_empty_clipboard();
|
|
vdagent_send_clipboard_data(vd, info, type);
|
|
}
|
|
|
|
static void vdagent_clipboard_update_info(VDAgentChardev *vd,
|
|
QemuClipboardInfo *info)
|
|
{
|
|
QemuClipboardSelection s = info->selection;
|
|
QemuClipboardType type;
|
|
bool self_update = info->owner == &vd->cbpeer;
|
|
|
|
if (info != qemu_clipboard_info(s)) {
|
|
vd->cbpending[s] = 0;
|
|
if (!self_update) {
|
|
if (info->owner) {
|
|
vdagent_send_clipboard_grab(vd, info);
|
|
} else {
|
|
vdagent_send_clipboard_release(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_reset_serial(VDAgentChardev *vd)
|
|
{
|
|
Chardev *chr = CHARDEV(vd);
|
|
|
|
if (!have_clipboard_serial(vd)) {
|
|
return;
|
|
}
|
|
/* reopen the agent connection to reset the serial state */
|
|
qemu_chr_be_event(chr, CHR_EVENT_CLOSED);
|
|
/* OPENED again after the guest disconnected, see set_fe_open */
|
|
}
|
|
|
|
static void vdagent_clipboard_notify(Notifier *notifier, void *data)
|
|
{
|
|
VDAgentChardev *vd =
|
|
container_of(notifier, VDAgentChardev, cbpeer.notifier);
|
|
QemuClipboardNotify *notify = data;
|
|
|
|
switch (notify->type) {
|
|
case QEMU_CLIPBOARD_UPDATE_INFO:
|
|
vdagent_clipboard_update_info(vd, notify->info);
|
|
return;
|
|
case QEMU_CLIPBOARD_RESET_SERIAL:
|
|
vdagent_clipboard_reset_serial(vd);
|
|
return;
|
|
}
|
|
}
|
|
|
|
static void vdagent_clipboard_request(QemuClipboardInfo *info,
|
|
QemuClipboardType qtype)
|
|
{
|
|
VDAgentChardev *vd = container_of(info->owner, VDAgentChardev, cbpeer);
|
|
g_autofree 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;
|
|
|
|
if (type == VD_AGENT_CLIPBOARD_NONE) {
|
|
return;
|
|
}
|
|
|
|
if (have_selection(vd)) {
|
|
*s = info->selection;
|
|
data++;
|
|
msg->size += sizeof(uint32_t);
|
|
}
|
|
|
|
*data = type;
|
|
msg->size += sizeof(uint32_t);
|
|
|
|
msg->type = VD_AGENT_CLIPBOARD_REQUEST;
|
|
vdagent_send_msg(vd, msg);
|
|
}
|
|
|
|
static void vdagent_clipboard_recv_grab(VDAgentChardev *vd, uint8_t s, uint32_t size, void *data)
|
|
{
|
|
g_autoptr(QemuClipboardInfo) info = NULL;
|
|
|
|
trace_vdagent_cb_grab_selection(GET_NAME(sel_name, s));
|
|
info = qemu_clipboard_info_new(&vd->cbpeer, s);
|
|
if (have_clipboard_serial(vd)) {
|
|
if (size < sizeof(uint32_t)) {
|
|
/* this shouldn't happen! */
|
|
return;
|
|
}
|
|
|
|
info->has_serial = true;
|
|
info->serial = *(uint32_t *)data;
|
|
if (info->serial < vd->last_serial[s]) {
|
|
trace_vdagent_cb_grab_discard(GET_NAME(sel_name, s),
|
|
vd->last_serial[s], info->serial);
|
|
/* discard lower-ordering guest grab */
|
|
return;
|
|
}
|
|
vd->last_serial[s] = info->serial;
|
|
data += sizeof(uint32_t);
|
|
size -= sizeof(uint32_t);
|
|
}
|
|
if (size > sizeof(uint32_t) * 10) {
|
|
/*
|
|
* spice has 6 types as of 2021. Limiting to 10 entries
|
|
* so we have some wiggle room.
|
|
*/
|
|
return;
|
|
}
|
|
while (size >= sizeof(uint32_t)) {
|
|
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 += sizeof(uint32_t);
|
|
size -= sizeof(uint32_t);
|
|
}
|
|
qemu_clipboard_update(info);
|
|
}
|
|
|
|
static void vdagent_clipboard_recv_request(VDAgentChardev *vd, uint8_t s, uint32_t size, void *data)
|
|
{
|
|
QemuClipboardType type;
|
|
QemuClipboardInfo *info;
|
|
|
|
if (size < sizeof(uint32_t)) {
|
|
return;
|
|
}
|
|
switch (*(uint32_t *)data) {
|
|
case VD_AGENT_CLIPBOARD_UTF8_TEXT:
|
|
type = QEMU_CLIPBOARD_TYPE_TEXT;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
info = qemu_clipboard_info(s);
|
|
if (info && info->types[type].available && info->owner != &vd->cbpeer) {
|
|
if (info->types[type].data) {
|
|
vdagent_send_clipboard_data(vd, info, type);
|
|
} else {
|
|
vd->cbpending[s] |= (1 << type);
|
|
qemu_clipboard_request(info, type);
|
|
}
|
|
} else {
|
|
vdagent_send_empty_clipboard_data(vd, s, type);
|
|
}
|
|
}
|
|
|
|
static void vdagent_clipboard_recv_data(VDAgentChardev *vd, uint8_t s, uint32_t size, void *data)
|
|
{
|
|
QemuClipboardType type;
|
|
|
|
if (size < sizeof(uint32_t)) {
|
|
return;
|
|
}
|
|
switch (*(uint32_t *)data) {
|
|
case VD_AGENT_CLIPBOARD_UTF8_TEXT:
|
|
type = QEMU_CLIPBOARD_TYPE_TEXT;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
data += 4;
|
|
size -= 4;
|
|
|
|
if (qemu_clipboard_peer_owns(&vd->cbpeer, s)) {
|
|
qemu_clipboard_set_data(&vd->cbpeer, qemu_clipboard_info(s),
|
|
type, size, data, true);
|
|
}
|
|
}
|
|
|
|
static void vdagent_clipboard_recv_release(VDAgentChardev *vd, uint8_t s)
|
|
{
|
|
qemu_clipboard_peer_release(&vd->cbpeer, s);
|
|
}
|
|
|
|
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;
|
|
|
|
if (have_selection(vd)) {
|
|
if (size < 4) {
|
|
return;
|
|
}
|
|
s = *(uint8_t *)data;
|
|
if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) {
|
|
return;
|
|
}
|
|
data += 4;
|
|
size -= 4;
|
|
}
|
|
|
|
switch (msg->type) {
|
|
case VD_AGENT_CLIPBOARD_GRAB:
|
|
return vdagent_clipboard_recv_grab(vd, s, size, data);
|
|
case VD_AGENT_CLIPBOARD_REQUEST:
|
|
return vdagent_clipboard_recv_request(vd, s, size, data);
|
|
case VD_AGENT_CLIPBOARD: /* data */
|
|
return vdagent_clipboard_recv_data(vd, s, size, data);
|
|
case VD_AGENT_CLIPBOARD_RELEASE:
|
|
return vdagent_clipboard_recv_release(vd, s);
|
|
default:
|
|
g_assert_not_reached();
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* chardev backend */
|
|
|
|
static void vdagent_chr_open(Chardev *chr,
|
|
ChardevBackend *backend,
|
|
bool *be_opened,
|
|
Error **errp)
|
|
{
|
|
VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(chr);
|
|
ChardevQemuVDAgent *cfg = backend->u.qemu_vdagent.data;
|
|
|
|
#if HOST_BIG_ENDIAN
|
|
/*
|
|
* TODO: vdagent protocol is defined to be LE,
|
|
* so we have to byteswap everything on BE hosts.
|
|
*/
|
|
error_setg(errp, "vdagent is not supported on bigendian hosts");
|
|
return;
|
|
#endif
|
|
|
|
vd->mouse = VDAGENT_MOUSE_DEFAULT;
|
|
if (cfg->has_mouse) {
|
|
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);
|
|
}
|
|
|
|
*be_opened = true;
|
|
}
|
|
|
|
static void vdagent_clipboard_peer_register(VDAgentChardev *vd)
|
|
{
|
|
if (vd->cbpeer.notifier.notify != NULL) {
|
|
return;
|
|
}
|
|
|
|
vd->cbpeer.name = "vdagent";
|
|
vd->cbpeer.notifier.notify = vdagent_clipboard_notify;
|
|
vd->cbpeer.request = vdagent_clipboard_request;
|
|
qemu_clipboard_peer_register(&vd->cbpeer);
|
|
}
|
|
|
|
static void vdagent_chr_recv_caps(VDAgentChardev *vd, VDAgentMessage *msg)
|
|
{
|
|
VDAgentAnnounceCapabilities *caps = (void *)msg->data;
|
|
int i;
|
|
|
|
if (msg->size < (sizeof(VDAgentAnnounceCapabilities) +
|
|
sizeof(uint32_t))) {
|
|
return;
|
|
}
|
|
|
|
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, false);
|
|
}
|
|
if (have_mouse(vd) && vd->mouse_hs) {
|
|
qemu_input_handler_activate(vd->mouse_hs);
|
|
}
|
|
|
|
memset(vd->last_serial, 0, sizeof(vd->last_serial));
|
|
|
|
if (have_clipboard(vd)) {
|
|
qemu_clipboard_reset_serial();
|
|
vdagent_clipboard_peer_register(vd);
|
|
}
|
|
}
|
|
|
|
static void vdagent_chr_recv_msg(VDAgentChardev *vd, VDAgentMessage *msg)
|
|
{
|
|
trace_vdagent_recv_msg(GET_NAME(msg_name, msg->type), msg->size);
|
|
|
|
switch (msg->type) {
|
|
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:
|
|
if (have_clipboard(vd)) {
|
|
vdagent_chr_recv_clipboard(vd, msg);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void vdagent_reset_xbuf(VDAgentChardev *vd)
|
|
{
|
|
g_clear_pointer(&vd->xbuf, g_free);
|
|
vd->xoff = 0;
|
|
vd->xsize = 0;
|
|
}
|
|
|
|
static void vdagent_chr_recv_chunk(VDAgentChardev *vd)
|
|
{
|
|
VDAgentMessage *msg = (void *)vd->msgbuf;
|
|
|
|
if (!vd->xsize) {
|
|
if (vd->msgsize < sizeof(*msg)) {
|
|
error_report("%s: message too small: %d < %zd", __func__,
|
|
vd->msgsize, sizeof(*msg));
|
|
return;
|
|
}
|
|
if (vd->msgsize == msg->size + sizeof(*msg)) {
|
|
vdagent_chr_recv_msg(vd, msg);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!vd->xsize) {
|
|
vd->xsize = msg->size + sizeof(*msg);
|
|
vd->xbuf = g_malloc0(vd->xsize);
|
|
}
|
|
|
|
if (vd->xoff + vd->msgsize > vd->xsize) {
|
|
error_report("%s: Oops: %d+%d > %d", __func__,
|
|
vd->xoff, vd->msgsize, vd->xsize);
|
|
vdagent_reset_xbuf(vd);
|
|
return;
|
|
}
|
|
|
|
memcpy(vd->xbuf + vd->xoff, vd->msgbuf, vd->msgsize);
|
|
vd->xoff += vd->msgsize;
|
|
if (vd->xoff < vd->xsize) {
|
|
return;
|
|
}
|
|
|
|
msg = (void *)vd->xbuf;
|
|
vdagent_chr_recv_msg(vd, msg);
|
|
vdagent_reset_xbuf(vd);
|
|
}
|
|
|
|
static void vdagent_reset_bufs(VDAgentChardev *vd)
|
|
{
|
|
memset(&vd->chunk, 0, sizeof(vd->chunk));
|
|
vd->chunksize = 0;
|
|
g_free(vd->msgbuf);
|
|
vd->msgbuf = NULL;
|
|
vd->msgsize = 0;
|
|
}
|
|
|
|
static int vdagent_chr_write(Chardev *chr, const uint8_t *buf, int len)
|
|
{
|
|
VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(chr);
|
|
uint32_t copy, ret = len;
|
|
|
|
while (len) {
|
|
if (vd->chunksize < sizeof(vd->chunk)) {
|
|
copy = sizeof(vd->chunk) - vd->chunksize;
|
|
if (copy > len) {
|
|
copy = len;
|
|
}
|
|
memcpy((void *)(&vd->chunk) + vd->chunksize, buf, copy);
|
|
vd->chunksize += copy;
|
|
buf += copy;
|
|
len -= copy;
|
|
if (vd->chunksize < sizeof(vd->chunk)) {
|
|
break;
|
|
}
|
|
|
|
assert(vd->msgbuf == NULL);
|
|
vd->msgbuf = g_malloc0(vd->chunk.size);
|
|
}
|
|
|
|
copy = vd->chunk.size - vd->msgsize;
|
|
if (copy > len) {
|
|
copy = len;
|
|
}
|
|
memcpy(vd->msgbuf + vd->msgsize, buf, copy);
|
|
vd->msgsize += copy;
|
|
buf += copy;
|
|
len -= copy;
|
|
|
|
if (vd->msgsize == vd->chunk.size) {
|
|
trace_vdagent_recv_chunk(vd->chunk.size);
|
|
vdagent_chr_recv_chunk(vd);
|
|
vdagent_reset_bufs(vd);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void vdagent_chr_accept_input(Chardev *chr)
|
|
{
|
|
VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(chr);
|
|
|
|
vdagent_send_buf(vd);
|
|
}
|
|
|
|
static void vdagent_disconnect(VDAgentChardev *vd)
|
|
{
|
|
trace_vdagent_disconnect();
|
|
|
|
vd->connected = false;
|
|
g_byte_array_set_size(vd->outbuf, 0);
|
|
vdagent_reset_bufs(vd);
|
|
vd->caps = 0;
|
|
if (vd->mouse_hs) {
|
|
qemu_input_handler_deactivate(vd->mouse_hs);
|
|
}
|
|
if (vd->cbpeer.notifier.notify) {
|
|
qemu_clipboard_peer_unregister(&vd->cbpeer);
|
|
memset(&vd->cbpeer, 0, sizeof(vd->cbpeer));
|
|
}
|
|
}
|
|
|
|
static void vdagent_chr_set_fe_open(struct Chardev *chr, int fe_open)
|
|
{
|
|
VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(chr);
|
|
|
|
trace_vdagent_fe_open(fe_open);
|
|
|
|
if (vd->connected == fe_open) {
|
|
return;
|
|
}
|
|
|
|
if (!fe_open) {
|
|
trace_vdagent_close();
|
|
vdagent_disconnect(vd);
|
|
/* To reset_serial, we CLOSED our side. Make sure the other end knows we
|
|
* are ready again. */
|
|
qemu_chr_be_event(chr, CHR_EVENT_OPENED);
|
|
return;
|
|
}
|
|
|
|
vd->connected = true;
|
|
vdagent_send_caps(vd, true);
|
|
}
|
|
|
|
static void vdagent_chr_parse(QemuOpts *opts, ChardevBackend *backend,
|
|
Error **errp)
|
|
{
|
|
ChardevQemuVDAgent *cfg;
|
|
|
|
backend->type = CHARDEV_BACKEND_KIND_QEMU_VDAGENT;
|
|
cfg = backend->u.qemu_vdagent.data = g_new0(ChardevQemuVDAgent, 1);
|
|
qemu_chr_parse_common(opts, qapi_ChardevQemuVDAgent_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);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void vdagent_chr_class_init(ObjectClass *oc, const 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;
|
|
cc->chr_accept_input = vdagent_chr_accept_input;
|
|
}
|
|
|
|
static int post_load(void *opaque, int version_id)
|
|
{
|
|
VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(opaque);
|
|
|
|
if (have_mouse(vd) && vd->mouse_hs) {
|
|
qemu_input_handler_activate(vd->mouse_hs);
|
|
}
|
|
|
|
if (have_clipboard(vd)) {
|
|
vdagent_clipboard_peer_register(vd);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const VMStateDescription vmstate_chunk = {
|
|
.name = "vdagent/chunk",
|
|
.version_id = 0,
|
|
.minimum_version_id = 0,
|
|
.fields = (const VMStateField[]) {
|
|
VMSTATE_UINT32(port, VDIChunkHeader),
|
|
VMSTATE_UINT32(size, VDIChunkHeader),
|
|
VMSTATE_END_OF_LIST()
|
|
}
|
|
};
|
|
|
|
static const VMStateDescription vmstate_vdba = {
|
|
.name = "vdagent/bytearray",
|
|
.version_id = 0,
|
|
.minimum_version_id = 0,
|
|
.fields = (const VMStateField[]) {
|
|
VMSTATE_UINT32(len, GByteArray),
|
|
VMSTATE_VBUFFER_ALLOC_UINT32(data, GByteArray, 0, 0, len),
|
|
VMSTATE_END_OF_LIST()
|
|
}
|
|
};
|
|
|
|
struct CBInfoArray {
|
|
uint32_t n;
|
|
QemuClipboardInfo cbinfo[QEMU_CLIPBOARD_SELECTION__COUNT];
|
|
};
|
|
|
|
static const VMStateDescription vmstate_cbinfo_array = {
|
|
.name = "cbinfoarray",
|
|
.fields = (const VMStateField[]) {
|
|
VMSTATE_UINT32(n, struct CBInfoArray),
|
|
VMSTATE_STRUCT_VARRAY_UINT32(cbinfo, struct CBInfoArray, n,
|
|
0, vmstate_cbinfo, QemuClipboardInfo),
|
|
VMSTATE_END_OF_LIST()
|
|
}
|
|
};
|
|
|
|
static int put_cbinfo(QEMUFile *f, void *pv, size_t size,
|
|
const VMStateField *field, JSONWriter *vmdesc)
|
|
{
|
|
VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(pv);
|
|
struct CBInfoArray cbinfo = {};
|
|
int i;
|
|
|
|
if (!have_clipboard(vd)) {
|
|
return 0;
|
|
}
|
|
|
|
for (i = 0; i < QEMU_CLIPBOARD_SELECTION__COUNT; i++) {
|
|
if (qemu_clipboard_peer_owns(&vd->cbpeer, i)) {
|
|
cbinfo.cbinfo[cbinfo.n++] = *qemu_clipboard_info(i);
|
|
}
|
|
}
|
|
|
|
return vmstate_save_state(f, &vmstate_cbinfo_array, &cbinfo, vmdesc,
|
|
&error_fatal);
|
|
}
|
|
|
|
static int get_cbinfo(QEMUFile *f, void *pv, size_t size,
|
|
const VMStateField *field)
|
|
{
|
|
VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(pv);
|
|
struct CBInfoArray cbinfo = {};
|
|
int i, ret;
|
|
Error *local_err = NULL;
|
|
|
|
if (!have_clipboard(vd)) {
|
|
return 0;
|
|
}
|
|
|
|
vdagent_clipboard_peer_register(vd);
|
|
|
|
ret = vmstate_load_state(f, &vmstate_cbinfo_array, &cbinfo, 0,
|
|
&local_err);
|
|
if (ret) {
|
|
error_report_err(local_err);
|
|
return ret;
|
|
}
|
|
|
|
for (i = 0; i < cbinfo.n; i++) {
|
|
g_autoptr(QemuClipboardInfo) info =
|
|
qemu_clipboard_info_new(&vd->cbpeer, cbinfo.cbinfo[i].selection);
|
|
/* this will steal clipboard data pointer from cbinfo.types */
|
|
memcpy(info->types, cbinfo.cbinfo[i].types, sizeof(cbinfo.cbinfo[i].types));
|
|
qemu_clipboard_update(info);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const VMStateInfo vmstate_cbinfos = {
|
|
.name = "vdagent/cbinfos",
|
|
.get = get_cbinfo,
|
|
.put = put_cbinfo,
|
|
};
|
|
|
|
static const VMStateDescription vmstate_vdagent = {
|
|
.name = "vdagent",
|
|
.version_id = 0,
|
|
.minimum_version_id = 0,
|
|
.post_load = post_load,
|
|
.fields = (const VMStateField[]) {
|
|
VMSTATE_BOOL(connected, VDAgentChardev),
|
|
VMSTATE_UINT32(caps, VDAgentChardev),
|
|
VMSTATE_STRUCT(chunk, VDAgentChardev, 0, vmstate_chunk, VDIChunkHeader),
|
|
VMSTATE_UINT32(chunksize, VDAgentChardev),
|
|
VMSTATE_UINT32(msgsize, VDAgentChardev),
|
|
VMSTATE_VBUFFER_ALLOC_UINT32(msgbuf, VDAgentChardev, 0, 0, msgsize),
|
|
VMSTATE_UINT32(xsize, VDAgentChardev),
|
|
VMSTATE_UINT32(xoff, VDAgentChardev),
|
|
VMSTATE_VBUFFER_ALLOC_UINT32(xbuf, VDAgentChardev, 0, 0, xsize),
|
|
VMSTATE_STRUCT_POINTER(outbuf, VDAgentChardev, vmstate_vdba, GByteArray),
|
|
VMSTATE_UINT32(mouse_x, VDAgentChardev),
|
|
VMSTATE_UINT32(mouse_y, VDAgentChardev),
|
|
VMSTATE_UINT32(mouse_btn, VDAgentChardev),
|
|
VMSTATE_UINT32(mouse_display, VDAgentChardev),
|
|
VMSTATE_UINT32_ARRAY(last_serial, VDAgentChardev,
|
|
QEMU_CLIPBOARD_SELECTION__COUNT),
|
|
VMSTATE_UINT32_ARRAY(cbpending, VDAgentChardev,
|
|
QEMU_CLIPBOARD_SELECTION__COUNT),
|
|
{
|
|
.name = "cbinfos",
|
|
.info = &vmstate_cbinfos,
|
|
.flags = VMS_SINGLE,
|
|
},
|
|
VMSTATE_END_OF_LIST()
|
|
}
|
|
};
|
|
|
|
static void vdagent_chr_init(Object *obj)
|
|
{
|
|
VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(obj);
|
|
|
|
vd->outbuf = g_byte_array_new();
|
|
vmstate_register_any(NULL, &vmstate_vdagent, vd);
|
|
}
|
|
|
|
static void vdagent_chr_fini(Object *obj)
|
|
{
|
|
VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(obj);
|
|
|
|
vdagent_disconnect(vd);
|
|
if (vd->mouse_hs) {
|
|
qemu_input_handler_unregister(vd->mouse_hs);
|
|
}
|
|
g_clear_pointer(&vd->outbuf, g_byte_array_unref);
|
|
}
|
|
|
|
static const TypeInfo vdagent_chr_type_info = {
|
|
.name = TYPE_CHARDEV_QEMU_VDAGENT,
|
|
.parent = TYPE_CHARDEV,
|
|
.instance_size = sizeof(VDAgentChardev),
|
|
.instance_init = vdagent_chr_init,
|
|
.instance_finalize = vdagent_chr_fini,
|
|
.class_init = vdagent_chr_class_init,
|
|
};
|
|
|
|
static void register_types(void)
|
|
{
|
|
type_register_static(&vdagent_chr_type_info);
|
|
}
|
|
|
|
type_init(register_types);
|