master
c 376 lines 10.9 KB
Raw
1 /*
2 * Standalone VNC server connecting to QEMU via D-Bus display interface.
3 *
4 * Copyright (C) 2026 Red Hat, Inc.
5 *
6 * SPDX-License-Identifier: GPL-2.0-or-later
7 */
8
9 #include "qemu/osdep.h"
10
11 #include "qemu/error-report.h"
12 #include "ui/clipboard.h"
13 #include "ui/dbus-display1.h"
14 #include "trace.h"
15 #include "qemu-vnc.h"
16
17 #define MIME_TEXT_PLAIN_UTF8 "text/plain;charset=utf-8"
18
19 typedef struct {
20 GDBusMethodInvocation *invocation;
21 QemuClipboardType type;
22 guint timeout_id;
23 } VncDBusClipboardRequest;
24
25 static QemuDBusDisplay1Clipboard *clipboard_proxy;
26 static QemuDBusDisplay1Clipboard *clipboard_skel;
27 static QemuClipboardPeer clipboard_peer;
28 static uint32_t clipboard_serial;
29 static VncDBusClipboardRequest
30 clipboard_request[QEMU_CLIPBOARD_SELECTION__COUNT];
31
32 static void
33 vnc_dbus_clipboard_complete_request(
34 GDBusMethodInvocation *invocation,
35 QemuClipboardInfo *info,
36 QemuClipboardType type)
37 {
38 GVariant *v_data = g_variant_new_from_data(
39 G_VARIANT_TYPE("ay"),
40 info->types[type].data,
41 info->types[type].size,
42 TRUE,
43 (GDestroyNotify)qemu_clipboard_info_unref,
44 qemu_clipboard_info_ref(info));
45
46 qemu_dbus_display1_clipboard_complete_request(
47 clipboard_skel, invocation,
48 MIME_TEXT_PLAIN_UTF8, v_data);
49 }
50
51 static void
52 vnc_dbus_clipboard_request_cancelled(VncDBusClipboardRequest *req)
53 {
54 if (!req->invocation) {
55 return;
56 }
57
58 g_dbus_method_invocation_return_error(
59 req->invocation,
60 G_DBUS_ERROR,
61 G_DBUS_ERROR_FAILED,
62 "Cancelled clipboard request");
63
64 g_clear_object(&req->invocation);
65 g_clear_handle_id(&req->timeout_id, g_source_remove);
66 }
67
68 static gboolean
69 vnc_dbus_clipboard_request_timeout(gpointer user_data)
70 {
71 vnc_dbus_clipboard_request_cancelled(user_data);
72 return G_SOURCE_REMOVE;
73 }
74
75 static void
76 vnc_dbus_clipboard_request(QemuClipboardInfo *info,
77 QemuClipboardType type)
78 {
79 g_autofree char *mime = NULL;
80 g_autoptr(GVariant) v_data = NULL;
81 g_autoptr(GError) err = NULL;
82 const char *data = NULL;
83 const char *mimes[] = { MIME_TEXT_PLAIN_UTF8, NULL };
84 size_t n;
85
86 if (type != QEMU_CLIPBOARD_TYPE_TEXT) {
87 return;
88 }
89
90 if (!clipboard_proxy) {
91 return;
92 }
93
94 if (!qemu_dbus_display1_clipboard_call_request_sync(
95 clipboard_proxy,
96 info->selection,
97 mimes,
98 G_DBUS_CALL_FLAGS_NONE, -1, &mime, &v_data, NULL, &err)) {
99 error_report("Failed to request clipboard: %s", err->message);
100 return;
101 }
102
103 if (!g_str_equal(mime, MIME_TEXT_PLAIN_UTF8)) {
104 error_report("Unsupported returned MIME: %s", mime);
105 return;
106 }
107
108 data = g_variant_get_fixed_array(v_data, &n, 1);
109 qemu_clipboard_set_data(&clipboard_peer, info, type,
110 n, data, true);
111 }
112
113 static void
114 vnc_dbus_clipboard_update_info(QemuClipboardInfo *info)
115 {
116 bool self_update = info->owner == &clipboard_peer;
117 const char *mime[QEMU_CLIPBOARD_TYPE__COUNT + 1] = { 0, };
118 VncDBusClipboardRequest *req;
119 int i = 0;
120
121 if (info->owner == NULL) {
122 if (clipboard_proxy) {
123 qemu_dbus_display1_clipboard_call_release(
124 clipboard_proxy,
125 info->selection,
126 G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
127 }
128 return;
129 }
130
131 if (self_update) {
132 return;
133 }
134
135 req = &clipboard_request[info->selection];
136 if (req->invocation && info->types[req->type].data) {
137 vnc_dbus_clipboard_complete_request(
138 req->invocation, info, req->type);
139 g_clear_object(&req->invocation);
140 g_clear_handle_id(&req->timeout_id, g_source_remove);
141 return;
142 }
143
144 if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) {
145 mime[i++] = MIME_TEXT_PLAIN_UTF8;
146 }
147
148 if (i > 0 && clipboard_proxy) {
149 uint32_t serial = info->has_serial ?
150 info->serial : ++clipboard_serial;
151 qemu_dbus_display1_clipboard_call_grab(
152 clipboard_proxy,
153 info->selection,
154 serial,
155 mime,
156 G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
157 }
158 }
159
160 static void
161 vnc_dbus_clipboard_notify(Notifier *notifier, void *data)
162 {
163 QemuClipboardNotify *notify = data;
164
165 switch (notify->type) {
166 case QEMU_CLIPBOARD_UPDATE_INFO:
167 vnc_dbus_clipboard_update_info(notify->info);
168 return;
169 case QEMU_CLIPBOARD_RESET_SERIAL:
170 if (clipboard_proxy) {
171 qemu_dbus_display1_clipboard_call_register(
172 clipboard_proxy,
173 G_DBUS_CALL_FLAGS_NONE,
174 -1, NULL, NULL, NULL);
175 }
176 return;
177 }
178 }
179
180 static gboolean
181 on_clipboard_register(QemuDBusDisplay1Clipboard *clipboard,
182 GDBusMethodInvocation *invocation,
183 gpointer user_data)
184 {
185 clipboard_serial = 0;
186 qemu_clipboard_reset_serial();
187
188 qemu_dbus_display1_clipboard_complete_register(
189 clipboard, invocation);
190 return DBUS_METHOD_INVOCATION_HANDLED;
191 }
192
193 static gboolean
194 on_clipboard_unregister(QemuDBusDisplay1Clipboard *clipboard,
195 GDBusMethodInvocation *invocation,
196 gpointer user_data)
197 {
198 int i;
199
200 for (i = 0; i < G_N_ELEMENTS(clipboard_request); ++i) {
201 vnc_dbus_clipboard_request_cancelled(&clipboard_request[i]);
202 }
203
204 qemu_dbus_display1_clipboard_complete_unregister(
205 clipboard, invocation);
206 return DBUS_METHOD_INVOCATION_HANDLED;
207 }
208
209 static gboolean
210 on_clipboard_grab(QemuDBusDisplay1Clipboard *clipboard,
211 GDBusMethodInvocation *invocation,
212 gint arg_selection,
213 guint arg_serial,
214 const gchar *const *arg_mimes,
215 gpointer user_data)
216 {
217 QemuClipboardSelection s = arg_selection;
218 g_autoptr(QemuClipboardInfo) info = NULL;
219
220 if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) {
221 g_dbus_method_invocation_return_error(
222 invocation,
223 G_DBUS_ERROR,
224 G_DBUS_ERROR_FAILED,
225 "Invalid clipboard selection: %d", arg_selection);
226 return DBUS_METHOD_INVOCATION_HANDLED;
227 }
228
229 trace_qemu_vnc_clipboard_grab(arg_selection, arg_serial);
230
231 info = qemu_clipboard_info_new(&clipboard_peer, s);
232 if (g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8)) {
233 info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true;
234 }
235 info->serial = arg_serial;
236 info->has_serial = true;
237 if (qemu_clipboard_check_serial(info, true)) {
238 qemu_clipboard_update(info);
239 }
240
241 qemu_dbus_display1_clipboard_complete_grab(
242 clipboard, invocation);
243 return DBUS_METHOD_INVOCATION_HANDLED;
244 }
245
246 static gboolean
247 on_clipboard_release(QemuDBusDisplay1Clipboard *clipboard,
248 GDBusMethodInvocation *invocation,
249 gint arg_selection,
250 gpointer user_data)
251 {
252 trace_qemu_vnc_clipboard_release(arg_selection);
253
254 qemu_clipboard_peer_release(&clipboard_peer, arg_selection);
255
256 qemu_dbus_display1_clipboard_complete_release(
257 clipboard, invocation);
258 return DBUS_METHOD_INVOCATION_HANDLED;
259 }
260
261 static gboolean
262 on_clipboard_request(QemuDBusDisplay1Clipboard *clipboard,
263 GDBusMethodInvocation *invocation,
264 gint arg_selection,
265 const gchar *const *arg_mimes,
266 gpointer user_data)
267 {
268 QemuClipboardSelection s = arg_selection;
269 QemuClipboardType type = QEMU_CLIPBOARD_TYPE_TEXT;
270 QemuClipboardInfo *info = NULL;
271
272 trace_qemu_vnc_clipboard_request(arg_selection);
273
274 if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) {
275 g_dbus_method_invocation_return_error(
276 invocation,
277 G_DBUS_ERROR,
278 G_DBUS_ERROR_FAILED,
279 "Invalid clipboard selection: %d", arg_selection);
280 return DBUS_METHOD_INVOCATION_HANDLED;
281 }
282
283 if (clipboard_request[s].invocation) {
284 g_dbus_method_invocation_return_error(
285 invocation,
286 G_DBUS_ERROR,
287 G_DBUS_ERROR_FAILED,
288 "Pending request");
289 return DBUS_METHOD_INVOCATION_HANDLED;
290 }
291
292 info = qemu_clipboard_info(s);
293 if (!info || !info->owner || info->owner == &clipboard_peer) {
294 g_dbus_method_invocation_return_error(
295 invocation,
296 G_DBUS_ERROR,
297 G_DBUS_ERROR_FAILED,
298 "Empty clipboard");
299 return DBUS_METHOD_INVOCATION_HANDLED;
300 }
301
302 if (!g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8) ||
303 !info->types[type].available) {
304 g_dbus_method_invocation_return_error(
305 invocation,
306 G_DBUS_ERROR,
307 G_DBUS_ERROR_FAILED,
308 "Unhandled MIME types requested");
309 return DBUS_METHOD_INVOCATION_HANDLED;
310 }
311
312 if (info->types[type].data) {
313 vnc_dbus_clipboard_complete_request(invocation, info, type);
314 } else {
315 qemu_clipboard_request(info, type);
316
317 clipboard_request[s].invocation = g_object_ref(invocation);
318 clipboard_request[s].type = type;
319 clipboard_request[s].timeout_id =
320 g_timeout_add_seconds(5,
321 vnc_dbus_clipboard_request_timeout,
322 &clipboard_request[s]);
323 }
324
325 return DBUS_METHOD_INVOCATION_HANDLED;
326 }
327
328 void clipboard_setup(GDBusObjectManager *manager, GDBusConnection *bus)
329 {
330 g_autoptr(GError) err = NULL;
331 g_autoptr(GDBusInterface) iface = NULL;
332
333 iface = g_dbus_object_manager_get_interface(
334 manager, DBUS_DISPLAY1_ROOT "/Clipboard",
335 "org.qemu.Display1.Clipboard");
336 if (!iface) {
337 return;
338 }
339
340 clipboard_proxy = g_object_ref(QEMU_DBUS_DISPLAY1_CLIPBOARD(iface));
341
342 clipboard_skel = qemu_dbus_display1_clipboard_skeleton_new();
343 g_object_connect(clipboard_skel,
344 "signal::handle-register",
345 on_clipboard_register, NULL,
346 "signal::handle-unregister",
347 on_clipboard_unregister, NULL,
348 "signal::handle-grab",
349 on_clipboard_grab, NULL,
350 "signal::handle-release",
351 on_clipboard_release, NULL,
352 "signal::handle-request",
353 on_clipboard_request, NULL,
354 NULL);
355
356 if (!g_dbus_interface_skeleton_export(
357 G_DBUS_INTERFACE_SKELETON(clipboard_skel),
358 bus,
359 DBUS_DISPLAY1_ROOT "/Clipboard",
360 &err)) {
361 error_report("Failed to export clipboard: %s", err->message);
362 g_clear_object(&clipboard_skel);
363 g_clear_object(&clipboard_proxy);
364 return;
365 }
366
367 clipboard_peer.name = "dbus";
368 clipboard_peer.notifier.notify = vnc_dbus_clipboard_notify;
369 clipboard_peer.request = vnc_dbus_clipboard_request;
370 qemu_clipboard_peer_register(&clipboard_peer);
371
372 qemu_dbus_display1_clipboard_call_register(
373 clipboard_proxy,
374 G_DBUS_CALL_FLAGS_NONE,
375 -1, NULL, NULL, NULL);
376 }