diff options
author | Richard Henderson <richard.henderson@linaro.org> | 2021-12-21 08:00:26 -0800 |
---|---|---|
committer | Richard Henderson <richard.henderson@linaro.org> | 2021-12-21 08:00:26 -0800 |
commit | 5316e12bb2b4408a1597b283ef4bb4794dd7b4f7 (patch) | |
tree | 7433951bf002780d937f18539156d97af13d5bc7 | |
parent | 2bf40d0841b942e7ba12953d515e62a436f0af84 (diff) | |
parent | 89f4df9595e162ce4cc65f31a994a31e3e45ff3a (diff) |
Merge tag 'dbus-pull-request' of https://gitlab.com/marcandre.lureau/qemu into staging
Add D-Bus display backend
# gpg: Signature made Mon 20 Dec 2021 10:57:18 PM PST
# gpg: using RSA key 87A9BD933F87C606D276F62DDAE8E10975969CE5
# gpg: issuer "marcandre.lureau@redhat.com"
# gpg: Good signature from "Marc-André Lureau <marcandre.lureau@redhat.com>" [unknown]
# gpg: aka "Marc-André Lureau <marcandre.lureau@gmail.com>" [unknown]
# gpg: WARNING: This key is not certified with a trusted signature!
# gpg: There is no indication that the signature belongs to the owner.
# Primary key fingerprint: 87A9 BD93 3F87 C606 D276 F62D DAE8 E109 7596 9CE5
* tag 'dbus-pull-request' of https://gitlab.com/marcandre.lureau/qemu: (36 commits)
MAINTAINERS: update D-Bus section
ui/dbus: register D-Bus VC handler
ui/dbus: add chardev backend & interface
option: add g_auto for QemuOpts
chardev: make socket derivable
chardev: teach socket to accept no addresses
ui/dbus: add clipboard interface
audio: add "dbus" audio backend
tests: start dbus-display-test
tests/qtests: add qtest_qmp_add_client()
ui/dbus: add p2p=on/off option
ui: add a D-Bus display backend
build-sys: set glib dependency version
docs: add dbus-display documentation
docs: move D-Bus VMState documentation to source XML
backends: move dbus-vmstate1.xml to backends/
docs/sphinx: add sphinx modules to include D-Bus documentation
scripts: teach modinfo to skip non-C sources
console: save current scanout details
ui: move qemu_spice_fill_device_address to ui/util.c
...
Signed-off-by: Richard Henderson <richard.henderson@linaro.org>
79 files changed, 6249 insertions, 401 deletions
diff --git a/MAINTAINERS b/MAINTAINERS index 1de6ce6e44..dc4b6f7c1e 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2873,11 +2873,15 @@ D-Bus M: Marc-André Lureau <marcandre.lureau@redhat.com> S: Maintained F: backends/dbus-vmstate.c -F: tests/dbus-vmstate* +F: ui/dbus* +F: audio/dbus* F: util/dbus.c +F: include/ui/dbus* F: include/qemu/dbus.h -F: docs/interop/dbus.rst -F: docs/interop/dbus-vmstate.rst +F: docs/interop/dbus* +F: docs/sphinx/dbus* +F: docs/sphinx/fakedbusdoc.py +F: tests/qtest/dbus* Seccomp M: Eduardo Otubo <otubo@redhat.com> diff --git a/audio/audio.c b/audio/audio.c index 54a153c0ef..dc28685d22 100644 --- a/audio/audio.c +++ b/audio/audio.c @@ -2000,6 +2000,7 @@ void audio_create_pdos(Audiodev *dev) CASE(NONE, none, ); CASE(ALSA, alsa, Alsa); CASE(COREAUDIO, coreaudio, Coreaudio); + CASE(DBUS, dbus, ); CASE(DSOUND, dsound, ); CASE(JACK, jack, Jack); CASE(OSS, oss, Oss); diff --git a/audio/audio_int.h b/audio/audio_int.h index 6d685e24a3..428a091d05 100644 --- a/audio/audio_int.h +++ b/audio/audio_int.h @@ -31,6 +31,10 @@ #endif #include "mixeng.h" +#ifdef CONFIG_GIO +#include <gio/gio.h> +#endif + struct audio_pcm_ops; struct audio_callback { @@ -140,6 +144,9 @@ struct audio_driver { const char *descr; void *(*init) (Audiodev *); void (*fini) (void *); +#ifdef CONFIG_GIO + void (*set_dbus_server) (AudioState *s, GDBusObjectManagerServer *manager); +#endif struct audio_pcm_ops *pcm_ops; int can_be_default; int max_voices_out; diff --git a/audio/audio_template.h b/audio/audio_template.h index c6714946aa..d2d348638b 100644 --- a/audio/audio_template.h +++ b/audio/audio_template.h @@ -327,6 +327,8 @@ AudiodevPerDirectionOptions *glue(audio_get_pdo_, TYPE)(Audiodev *dev) case AUDIODEV_DRIVER_COREAUDIO: return qapi_AudiodevCoreaudioPerDirectionOptions_base( dev->u.coreaudio.TYPE); + case AUDIODEV_DRIVER_DBUS: + return dev->u.dbus.TYPE; case AUDIODEV_DRIVER_DSOUND: return dev->u.dsound.TYPE; case AUDIODEV_DRIVER_JACK: diff --git a/audio/dbusaudio.c b/audio/dbusaudio.c new file mode 100644 index 0000000000..f178b47dee --- /dev/null +++ b/audio/dbusaudio.c @@ -0,0 +1,654 @@ +/* + * QEMU DBus audio + * + * Copyright (c) 2021 Red Hat, Inc. + * + * 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/error-report.h" +#include "qemu/host-utils.h" +#include "qemu/module.h" +#include "qemu/timer.h" +#include "qemu/dbus.h" + +#include <gio/gunixfdlist.h> +#include "ui/dbus-display1.h" + +#define AUDIO_CAP "dbus" +#include "audio.h" +#include "audio_int.h" +#include "trace.h" + +#define DBUS_DISPLAY1_AUDIO_PATH DBUS_DISPLAY1_ROOT "/Audio" + +#define DBUS_AUDIO_NSAMPLES 1024 /* could be configured? */ + +typedef struct DBusAudio { + GDBusObjectManagerServer *server; + GDBusObjectSkeleton *audio; + QemuDBusDisplay1Audio *iface; + GHashTable *out_listeners; + GHashTable *in_listeners; +} DBusAudio; + +typedef struct DBusVoiceOut { + HWVoiceOut hw; + bool enabled; + RateCtl rate; + + void *buf; + size_t buf_pos; + size_t buf_size; + + bool has_volume; + Volume volume; +} DBusVoiceOut; + +typedef struct DBusVoiceIn { + HWVoiceIn hw; + bool enabled; + RateCtl rate; + + bool has_volume; + Volume volume; +} DBusVoiceIn; + +static void *dbus_get_buffer_out(HWVoiceOut *hw, size_t *size) +{ + DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw); + + if (!vo->buf) { + vo->buf_size = hw->samples * hw->info.bytes_per_frame; + vo->buf = g_malloc(vo->buf_size); + vo->buf_pos = 0; + } + + *size = MIN(vo->buf_size - vo->buf_pos, *size); + *size = audio_rate_get_bytes(&hw->info, &vo->rate, *size); + + return vo->buf + vo->buf_pos; + +} + +static size_t dbus_put_buffer_out(HWVoiceOut *hw, void *buf, size_t size) +{ + DBusAudio *da = (DBusAudio *)hw->s->drv_opaque; + DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw); + GHashTableIter iter; + QemuDBusDisplay1AudioOutListener *listener = NULL; + g_autoptr(GBytes) bytes = NULL; + g_autoptr(GVariant) v_data = NULL; + + assert(buf == vo->buf + vo->buf_pos && vo->buf_pos + size <= vo->buf_size); + vo->buf_pos += size; + + trace_dbus_audio_put_buffer_out(size); + + if (vo->buf_pos < vo->buf_size) { + return size; + } + + bytes = g_bytes_new_take(g_steal_pointer(&vo->buf), vo->buf_size); + v_data = g_variant_new_from_bytes(G_VARIANT_TYPE("ay"), bytes, TRUE); + g_variant_ref_sink(v_data); + + g_hash_table_iter_init(&iter, da->out_listeners); + while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) { + qemu_dbus_display1_audio_out_listener_call_write( + listener, + (uintptr_t)hw, + v_data, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + } + + return size; +} + +#ifdef HOST_WORDS_BIGENDIAN +#define AUDIO_HOST_BE TRUE +#else +#define AUDIO_HOST_BE FALSE +#endif + +static void +dbus_init_out_listener(QemuDBusDisplay1AudioOutListener *listener, + HWVoiceOut *hw) +{ + qemu_dbus_display1_audio_out_listener_call_init( + listener, + (uintptr_t)hw, + hw->info.bits, + hw->info.is_signed, + hw->info.is_float, + hw->info.freq, + hw->info.nchannels, + hw->info.bytes_per_frame, + hw->info.bytes_per_second, + hw->info.swap_endianness ? !AUDIO_HOST_BE : AUDIO_HOST_BE, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); +} + +static int +dbus_init_out(HWVoiceOut *hw, struct audsettings *as, void *drv_opaque) +{ + DBusAudio *da = (DBusAudio *)hw->s->drv_opaque; + DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw); + GHashTableIter iter; + QemuDBusDisplay1AudioOutListener *listener = NULL; + + audio_pcm_init_info(&hw->info, as); + hw->samples = DBUS_AUDIO_NSAMPLES; + audio_rate_start(&vo->rate); + + g_hash_table_iter_init(&iter, da->out_listeners); + while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) { + dbus_init_out_listener(listener, hw); + } + return 0; +} + +static void +dbus_fini_out(HWVoiceOut *hw) +{ + DBusAudio *da = (DBusAudio *)hw->s->drv_opaque; + DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw); + GHashTableIter iter; + QemuDBusDisplay1AudioOutListener *listener = NULL; + + g_hash_table_iter_init(&iter, da->out_listeners); + while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) { + qemu_dbus_display1_audio_out_listener_call_fini( + listener, + (uintptr_t)hw, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + } + + g_clear_pointer(&vo->buf, g_free); +} + +static void +dbus_enable_out(HWVoiceOut *hw, bool enable) +{ + DBusAudio *da = (DBusAudio *)hw->s->drv_opaque; + DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw); + GHashTableIter iter; + QemuDBusDisplay1AudioOutListener *listener = NULL; + + vo->enabled = enable; + if (enable) { + audio_rate_start(&vo->rate); + } + + g_hash_table_iter_init(&iter, da->out_listeners); + while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) { + qemu_dbus_display1_audio_out_listener_call_set_enabled( + listener, (uintptr_t)hw, enable, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + } +} + +static void +dbus_volume_out_listener(HWVoiceOut *hw, + QemuDBusDisplay1AudioOutListener *listener) +{ + DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw); + Volume *vol = &vo->volume; + g_autoptr(GBytes) bytes = NULL; + GVariant *v_vol = NULL; + + if (!vo->has_volume) { + return; + } + + assert(vol->channels < sizeof(vol->vol)); + bytes = g_bytes_new(vol->vol, vol->channels); + v_vol = g_variant_new_from_bytes(G_VARIANT_TYPE("ay"), bytes, TRUE); + qemu_dbus_display1_audio_out_listener_call_set_volume( + listener, (uintptr_t)hw, vol->mute, v_vol, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); +} + +static void +dbus_volume_out(HWVoiceOut *hw, Volume *vol) +{ + DBusAudio *da = (DBusAudio *)hw->s->drv_opaque; + DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw); + GHashTableIter iter; + QemuDBusDisplay1AudioOutListener *listener = NULL; + + vo->has_volume = true; + vo->volume = *vol; + + g_hash_table_iter_init(&iter, da->out_listeners); + while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) { + dbus_volume_out_listener(hw, listener); + } +} + +static void +dbus_init_in_listener(QemuDBusDisplay1AudioInListener *listener, HWVoiceIn *hw) +{ + qemu_dbus_display1_audio_in_listener_call_init( + listener, + (uintptr_t)hw, + hw->info.bits, + hw->info.is_signed, + hw->info.is_float, + hw->info.freq, + hw->info.nchannels, + hw->info.bytes_per_frame, + hw->info.bytes_per_second, + hw->info.swap_endianness ? !AUDIO_HOST_BE : AUDIO_HOST_BE, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); +} + +static int +dbus_init_in(HWVoiceIn *hw, struct audsettings *as, void *drv_opaque) +{ + DBusAudio *da = (DBusAudio *)hw->s->drv_opaque; + DBusVoiceIn *vo = container_of(hw, DBusVoiceIn, hw); + GHashTableIter iter; + QemuDBusDisplay1AudioInListener *listener = NULL; + + audio_pcm_init_info(&hw->info, as); + hw->samples = DBUS_AUDIO_NSAMPLES; + audio_rate_start(&vo->rate); + + g_hash_table_iter_init(&iter, da->in_listeners); + while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) { + dbus_init_in_listener(listener, hw); + } + return 0; +} + +static void +dbus_fini_in(HWVoiceIn *hw) +{ + DBusAudio *da = (DBusAudio *)hw->s->drv_opaque; + GHashTableIter iter; + QemuDBusDisplay1AudioInListener *listener = NULL; + + g_hash_table_iter_init(&iter, da->in_listeners); + while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) { + qemu_dbus_display1_audio_in_listener_call_fini( + listener, + (uintptr_t)hw, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + } +} + +static void +dbus_volume_in_listener(HWVoiceIn *hw, + QemuDBusDisplay1AudioInListener *listener) +{ + DBusVoiceIn *vo = container_of(hw, DBusVoiceIn, hw); + Volume *vol = &vo->volume; + g_autoptr(GBytes) bytes = NULL; + GVariant *v_vol = NULL; + + if (!vo->has_volume) { + return; + } + + assert(vol->channels < sizeof(vol->vol)); + bytes = g_bytes_new(vol->vol, vol->channels); + v_vol = g_variant_new_from_bytes(G_VARIANT_TYPE("ay"), bytes, TRUE); + qemu_dbus_display1_audio_in_listener_call_set_volume( + listener, (uintptr_t)hw, vol->mute, v_vol, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); +} + +static void +dbus_volume_in(HWVoiceIn *hw, Volume *vol) +{ + DBusAudio *da = (DBusAudio *)hw->s->drv_opaque; + DBusVoiceIn *vo = container_of(hw, DBusVoiceIn, hw); + GHashTableIter iter; + QemuDBusDisplay1AudioInListener *listener = NULL; + + vo->has_volume = true; + vo->volume = *vol; + + g_hash_table_iter_init(&iter, da->in_listeners); + while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) { + dbus_volume_in_listener(hw, listener); + } +} + +static size_t +dbus_read(HWVoiceIn *hw, void *buf, size_t size) +{ + DBusAudio *da = (DBusAudio *)hw->s->drv_opaque; + /* DBusVoiceIn *vo = container_of(hw, DBusVoiceIn, hw); */ + GHashTableIter iter; + QemuDBusDisplay1AudioInListener *listener = NULL; + + trace_dbus_audio_read(size); + + /* size = audio_rate_get_bytes(&hw->info, &vo->rate, size); */ + + g_hash_table_iter_init(&iter, da->in_listeners); + while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) { + g_autoptr(GVariant) v_data = NULL; + const char *data; + gsize n = 0; + + if (qemu_dbus_display1_audio_in_listener_call_read_sync( + listener, + (uintptr_t)hw, + size, + G_DBUS_CALL_FLAGS_NONE, -1, + &v_data, NULL, NULL)) { + data = g_variant_get_fixed_array(v_data, &n, 1); + g_warn_if_fail(n <= size); + size = MIN(n, size); + memcpy(buf, data, size); + break; + } + } + + return size; +} + +static void +dbus_enable_in(HWVoiceIn *hw, bool enable) +{ + DBusAudio *da = (DBusAudio *)hw->s->drv_opaque; + DBusVoiceIn *vo = container_of(hw, DBusVoiceIn, hw); + GHashTableIter iter; + QemuDBusDisplay1AudioInListener *listener = NULL; + + vo->enabled = enable; + if (enable) { + audio_rate_start(&vo->rate); + } + + g_hash_table_iter_init(&iter, da->in_listeners); + while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) { + qemu_dbus_display1_audio_in_listener_call_set_enabled( + listener, (uintptr_t)hw, enable, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + } +} + +static void * +dbus_audio_init(Audiodev *dev) +{ + DBusAudio *da = g_new0(DBusAudio, 1); + + da->out_listeners = g_hash_table_new_full(g_str_hash, g_str_equal, + g_free, g_object_unref); + da->in_listeners = g_hash_table_new_full(g_str_hash, g_str_equal, + g_free, g_object_unref); + return da; +} + +static void +dbus_audio_fini(void *opaque) +{ + DBusAudio *da = opaque; + + if (da->server) { + g_dbus_object_manager_server_unexport(da->server, + DBUS_DISPLAY1_AUDIO_PATH); + } + g_clear_object(&da->audio); + g_clear_object(&da->iface); + g_clear_pointer(&da->in_listeners, g_hash_table_unref); + g_clear_pointer(&da->out_listeners, g_hash_table_unref); + g_clear_object(&da->server); + g_free(da); +} + +static void +listener_out_vanished_cb(GDBusConnection *connection, + gboolean remote_peer_vanished, + GError *error, + DBusAudio *da) +{ + char *name = g_object_get_data(G_OBJECT(connection), "name"); + + g_hash_table_remove(da->out_listeners, name); +} + +static void +listener_in_vanished_cb(GDBusConnection *connection, + gboolean remote_peer_vanished, + GError *error, + DBusAudio *da) +{ + char *name = g_object_get_data(G_OBJECT(connection), "name"); + + g_hash_table_remove(da->in_listeners, name); +} + +static gboolean +dbus_audio_register_listener(AudioState *s, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + GVariant *arg_listener, + bool out) +{ + DBusAudio *da = s->drv_opaque; + const char *sender = g_dbus_method_invocation_get_sender(invocation); + g_autoptr(GDBusConnection) listener_conn = NULL; + g_autoptr(GError) err = NULL; + g_autoptr(GSocket) socket = NULL; + g_autoptr(GSocketConnection) socket_conn = NULL; + g_autofree char *guid = g_dbus_generate_guid(); + GHashTable *listeners = out ? da->out_listeners : da->in_listeners; + GObject *listener; + int fd; + + trace_dbus_audio_register(sender, out ? "out" : "in"); + + if (g_hash_table_contains(listeners, sender)) { + g_dbus_method_invocation_return_error(invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_INVALID, + "`%s` is already registered!", + sender); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + fd = g_unix_fd_list_get(fd_list, g_variant_get_handle(arg_listener), &err); + if (err) { + g_dbus_method_invocation_return_error(invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Couldn't get peer fd: %s", + err->message); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + socket = g_socket_new_from_fd(fd, &err); + if (err) { + g_dbus_method_invocation_return_error(invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Couldn't make a socket: %s", + err->message); + return DBUS_METHOD_INVOCATION_HANDLED; + } + socket_conn = g_socket_connection_factory_create_connection(socket); + if (out) { + qemu_dbus_display1_audio_complete_register_out_listener( + da->iface, invocation, NULL); + } else { + qemu_dbus_display1_audio_complete_register_in_listener( + da->iface, invocation, NULL); + } + + listener_conn = + g_dbus_connection_new_sync( + G_IO_STREAM(socket_conn), + guid, + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_SERVER, + NULL, NULL, &err); + if (err) { + error_report("Failed to setup peer connection: %s", err->message); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + listener = out ? + G_OBJECT(qemu_dbus_display1_audio_out_listener_proxy_new_sync( + listener_conn, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + NULL, + "/org/qemu/Display1/AudioOutListener", + NULL, + &err)) : + G_OBJECT(qemu_dbus_display1_audio_in_listener_proxy_new_sync( + listener_conn, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + NULL, + "/org/qemu/Display1/AudioInListener", + NULL, + &err)); + if (!listener) { + error_report("Failed to setup proxy: %s", err->message); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + if (out) { + HWVoiceOut *hw; + + QLIST_FOREACH(hw, &s->hw_head_out, entries) { + DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw); + QemuDBusDisplay1AudioOutListener *l = + QEMU_DBUS_DISPLAY1_AUDIO_OUT_LISTENER(listener); + + dbus_init_out_listener(l, hw); + qemu_dbus_display1_audio_out_listener_call_set_enabled( + l, (uintptr_t)hw, vo->enabled, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + } + } else { + HWVoiceIn *hw; + + QLIST_FOREACH(hw, &s->hw_head_in, entries) { + DBusVoiceIn *vo = container_of(hw, DBusVoiceIn, hw); + QemuDBusDisplay1AudioInListener *l = + QEMU_DBUS_DISPLAY1_AUDIO_IN_LISTENER(listener); + + dbus_init_in_listener( + QEMU_DBUS_DISPLAY1_AUDIO_IN_LISTENER(listener), hw); + qemu_dbus_display1_audio_in_listener_call_set_enabled( + l, (uintptr_t)hw, vo->enabled, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + } + } + + g_object_set_data_full(G_OBJECT(listener_conn), "name", + g_strdup(sender), g_free); + g_hash_table_insert(listeners, g_strdup(sender), listener); + g_object_connect(listener_conn, + "signal::closed", + out ? listener_out_vanished_cb : listener_in_vanished_cb, + da, + NULL); + + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_audio_register_out_listener(AudioState *s, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + GVariant *arg_listener) +{ + return dbus_audio_register_listener(s, invocation, + fd_list, arg_listener, true); + +} + +static gboolean +dbus_audio_register_in_listener(AudioState *s, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + GVariant *arg_listener) +{ + return dbus_audio_register_listener(s, invocation, + fd_list, arg_listener, false); +} + +static void +dbus_audio_set_server(AudioState *s, GDBusObjectManagerServer *server) +{ + DBusAudio *da = s->drv_opaque; + + g_assert(da); + g_assert(!da->server); + + da->server = g_object_ref(server); + + da->audio = g_dbus_object_skeleton_new(DBUS_DISPLAY1_AUDIO_PATH); + da->iface = qemu_dbus_display1_audio_skeleton_new(); + g_object_connect(da->iface, + "swapped-signal::handle-register-in-listener", + dbus_audio_register_in_listener, s, + "swapped-signal::handle-register-out-listener", + dbus_audio_register_out_listener, s, + NULL); + + g_dbus_object_skeleton_add_interface(G_DBUS_OBJECT_SKELETON(da->audio), + G_DBUS_INTERFACE_SKELETON(da->iface)); + g_dbus_object_manager_server_export(da->server, da->audio); +} + +static struct audio_pcm_ops dbus_pcm_ops = { + .init_out = dbus_init_out, + .fini_out = dbus_fini_out, + .write = audio_generic_write, + .get_buffer_out = dbus_get_buffer_out, + .put_buffer_out = dbus_put_buffer_out, + .enable_out = dbus_enable_out, + .volume_out = dbus_volume_out, + + .init_in = dbus_init_in, + .fini_in = dbus_fini_in, + .read = dbus_read, + .run_buffer_in = audio_generic_run_buffer_in, + .enable_in = dbus_enable_in, + .volume_in = dbus_volume_in, +}; + +static struct audio_driver dbus_audio_driver = { + .name = "dbus", + .descr = "Timer based audio exposed with DBus interface", + .init = dbus_audio_init, + .fini = dbus_audio_fini, + .set_dbus_server = dbus_audio_set_server, + .pcm_ops = &dbus_pcm_ops, + .can_be_default = 1, + .max_voices_out = INT_MAX, + .max_voices_in = INT_MAX, + .voice_size_out = sizeof(DBusVoiceOut), + .voice_size_in = sizeof(DBusVoiceIn) +}; + +static void register_audio_dbus(void) +{ + audio_driver_register(&dbus_audio_driver); +} +type_init(register_audio_dbus); + +module_dep("ui-dbus") diff --git a/audio/meson.build b/audio/meson.build index 462533bb8c..0ac3791d0b 100644 --- a/audio/meson.build +++ b/audio/meson.build @@ -26,4 +26,10 @@ foreach m : [ endif endforeach +if dbus_display + module_ss = ss.source_set() + module_ss.add(when: gio, if_true: files('dbusaudio.c')) + audio_modules += {'dbus': module_ss} +endif + modules += {'audio': audio_modules} diff --git a/audio/trace-events b/audio/trace-events index 957c92337b..e1ab643add 100644 --- a/audio/trace-events +++ b/audio/trace-events @@ -13,6 +13,11 @@ alsa_resume_out(void) "Resuming suspended output stream" # ossaudio.c oss_version(int version) "OSS version = 0x%x" +# dbusaudio.c +dbus_audio_register(const char *s, const char *dir) "sender = %s, dir = %s" +dbus_audio_put_buffer_out(size_t len) "len = %zu" +dbus_audio_read(size_t len) "len = %zu" + # audio.c audio_timer_start(int interval) "interval %d ms" audio_timer_stop(void) "" diff --git a/backends/dbus-vmstate1.xml b/backends/dbus-vmstate1.xml new file mode 100644 index 0000000000..601ee8dc7e --- /dev/null +++ b/backends/dbus-vmstate1.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="utf-8"?> +<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd"> + <!-- + org.qemu.VMState1: + + This interface must be implemented at the object path + ``/org/qemu/VMState1`` to support helper migration. + --> + <interface name="org.qemu.VMState1"> + + <!-- + Id: + + A string that identifies the helper uniquely. (maximum 256 bytes + including terminating NUL byte) + + .. note:: + + The VMState helper ID namespace is its own namespace. In particular, + it is not related to QEMU "id" used in -object/-device objects. + --> + <property name="Id" type="s" access="read"/> + + <!-- + Load: + @data: data to restore the state. + + The method called on destination with the state to restore. + + The helper may be initially started in a waiting state (with an + ``-incoming`` argument for example), and it may resume on success. + + An error may be returned to the caller. + --> + <method name="Load"> + <arg type="ay" name="data" direction="in"/> + </method> + + <!-- + Save: + @data: state data to save for later resume. + + The method called on the source to get the current state to be + migrated. The helper should continue to run normally. + + An error may be returned to the caller. + --> + <method name="Save"> + <arg type="ay" name="data" direction="out"/> + </method> + </interface> +</node> diff --git a/chardev/char-socket.c b/chardev/char-socket.c index 836cfa0bc2..d619088232 100644 --- a/chardev/char-socket.c +++ b/chardev/char-socket.c @@ -25,9 +25,7 @@ #include "qemu/osdep.h" #include "chardev/char.h" #include "io/channel-socket.h" -#include "io/channel-tls.h" #include "io/channel-websock.h" -#include "io/net-listener.h" #include "qemu/error-report.h" #include "qemu/module.h" #include "qemu/option.h" @@ -37,61 +35,7 @@ #include "qemu/yank.h" #include "chardev/char-io.h" -#include "qom/object.h" - -/***********************************************************/ -/* TCP Net console */ - -#define TCP_MAX_FDS 16 - -typedef struct { - char buf[21]; - size_t buflen; -} TCPChardevTelnetInit; - -typedef enum { - TCP_CHARDEV_STATE_DISCONNECTED, - TCP_CHARDEV_STATE_CONNECTING, - TCP_CHARDEV_STATE_CONNECTED, -} TCPChardevState; - -struct SocketChardev { - Chardev parent; - QIOChannel *ioc; /* Client I/O channel */ - QIOChannelSocket *sioc; /* Client master channel */ - QIONetListener *listener; - GSource *hup_source; - QCryptoTLSCreds *tls_creds; - char *tls_authz; - TCPChardevState state; - int max_size; - int do_telnetopt; - int do_nodelay; - int *read_msgfds; - size_t read_msgfds_num; - int *write_msgfds; - size_t write_msgfds_num; - bool registered_yank; - - SocketAddress *addr; - bool is_listen; - bool is_telnet; - bool is_tn3270; - GSource *telnet_source; - TCPChardevTelnetInit *telnet_init; - - bool is_websock; - - GSource *reconnect_timer; - int64_t reconnect_time; - bool connect_err_reported; - - QIOTask *connect_task; -}; -typedef struct SocketChardev SocketChardev; - -DECLARE_INSTANCE_CHECKER(SocketChardev, SOCKET_CHARDEV, - TYPE_CHARDEV_SOCKET) +#include "chardev/char-socket.h" static gboolean socket_reconnect_timeout(gpointer opaque); static void tcp_chr_telnet_init(Chardev *chr); @@ -1248,6 +1192,10 @@ static int qmp_chardev_open_socket_server(Chardev *chr, qio_net_listener_set_name(s->listener, name); g_free(name); + if (s->addr->type == SOCKET_ADDRESS_TYPE_FD && !*s->addr->u.fd.str) { + goto skip_listen; + } + if (qio_net_listener_open_sync(s->listener, s->addr, 1, errp) < 0) { object_unref(OBJECT(s->listener)); s->listener = NULL; @@ -1256,6 +1204,8 @@ static int qmp_chardev_open_socket_server(Chardev *chr, qapi_free_SocketAddress(s->addr); s->addr = socket_local_address(s->listener->sioc[0]->fd, errp); + +skip_listen: update_disconnected_filename(s); if (is_waitconnect) { @@ -1466,9 +1416,9 @@ static void qemu_chr_parse_socket(QemuOpts *opts, ChardevBackend *backend, SocketAddressLegacy *addr; ChardevSocket *sock; - if ((!!path + !!fd + !!host) != 1) { + if ((!!path + !!fd + !!host) > 1) { error_setg(errp, - "Exactly one of 'path', 'fd' or 'host' required"); + "None or one of 'path', 'fd' or 'host' option required."); return; } @@ -1542,12 +1492,10 @@ static void qemu_chr_parse_socket(QemuOpts *opts, ChardevBackend *backend, .has_ipv6 = qemu_opt_get(opts, "ipv6"), .ipv6 = qemu_opt_get_bool(opts, "ipv6", 0), }; - } else if (fd) { + } else { addr->type = SOCKET_ADDRESS_TYPE_FD; addr->u.fd.data = g_new(String, 1); addr->u.fd.data->str = g_strdup(fd); - } else { - g_assert_not_reached(); } sock->addr = addr; } @@ -3694,6 +3694,7 @@ echo "QEMU_CFLAGS=$QEMU_CFLAGS" >> $config_host_mak echo "QEMU_CXXFLAGS=$QEMU_CXXFLAGS" >> $config_host_mak echo "GLIB_CFLAGS=$glib_cflags" >> $config_host_mak echo "GLIB_LIBS=$glib_libs" >> $config_host_mak +echo "GLIB_VERSION=$(pkg-config --modversion glib-2.0)" >> $config_host_mak echo "QEMU_LDFLAGS=$QEMU_LDFLAGS" >> $config_host_mak echo "LD_I386_EMULATION=$ld_i386_emulation" >> $config_host_mak echo "EXESUF=$EXESUF" >> $config_host_mak diff --git a/docs/conf.py b/docs/conf.py index 763e7d2434..e79015975e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -73,6 +73,12 @@ needs_sphinx = '1.6' # ones. extensions = ['kerneldoc', 'qmp_lexer', 'hxtool', 'depfile', 'qapidoc'] +if sphinx.version_info[:3] > (4, 0, 0): + tags.add('sphinx4') + extensions += ['dbusdoc'] +else: + extensions += ['fakedbusdoc'] + # Add any paths that contain templates here, relative to this directory. templates_path = [os.path.join(qemu_docdir, '_templates')] @@ -311,3 +317,5 @@ kerneldoc_bin = ['perl', os.path.join(qemu_docdir, '../scripts/kernel-doc')] kerneldoc_srctree = os.path.join(qemu_docdir, '..') hxtool_srctree = os.path.join(qemu_docdir, '..') qapidoc_srctree = os.path.join(qemu_docdir, '..') +dbusdoc_srctree = os.path.join(qemu_docdir, '..') +dbus_index_common_prefix = ["org.qemu."] diff --git a/docs/interop/dbus-display.rst b/docs/interop/dbus-display.rst new file mode 100644 index 0000000000..8c6e8e0f5a --- /dev/null +++ b/docs/interop/dbus-display.rst @@ -0,0 +1,31 @@ +D-Bus display +============= + +QEMU can export the VM display through D-Bus (when started with ``-display +dbus``), to allow out-of-process UIs, remote protocol servers or other +interactive display usages. + +Various specialized D-Bus interfaces are available on different object paths +under ``/org/qemu/Display1/``, depending on the VM configuration. + +QEMU also implements the standard interfaces, such as +`org.freedesktop.DBus.Introspectable +<https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces>`_. + +.. contents:: + :local: + :depth: 1 + +.. only:: sphinx4 + + .. dbus-doc:: ui/dbus-display1.xml + +.. only:: not sphinx4 + + .. warning:: + Sphinx 4 is required to build D-Bus documentation. + + This is the content of ``ui/dbus-display1.xml``: + + .. literalinclude:: ../../ui/dbus-display1.xml + :language: xml diff --git a/docs/interop/dbus-vmstate.rst b/docs/interop/dbus-vmstate.rst index 1d719c1c60..5fb3f279e2 100644 --- a/docs/interop/dbus-vmstate.rst +++ b/docs/interop/dbus-vmstate.rst @@ -2,9 +2,6 @@ D-Bus VMState ============= -Introduction -============ - The QEMU dbus-vmstate object's aim is to migrate helpers' data running on a QEMU D-Bus bus. (refer to the :doc:`dbus` document for some recommendations on D-Bus usage) @@ -26,49 +23,16 @@ dbus-vmstate object can be configured with the expected list of helpers by setting its ``id-list`` property, with a comma-separated ``Id`` list. -Interface -========= - -On object path ``/org/qemu/VMState1``, the following -``org.qemu.VMState1`` interface should be implemented: - -.. code:: xml - - <interface name="org.qemu.VMState1"> - <property name="Id" type="s" access="read"/> - <method name="Load"> - <arg type="ay" name="data" direction="in"/> - </method> - <method name="Save"> - <arg type="ay" name="data" direction="out"/> - </method> - </interface> - -"Id" property -------------- - -A string that identifies the helper uniquely. (maximum 256 bytes -including terminating NUL byte) - -.. note:: - - The helper ID namespace is a separate namespace. In particular, it is not - related to QEMU "id" used in -object/-device objects. - -Load(in u8[] bytes) method --------------------------- - -The method called on destination with the state to restore. +.. only:: sphinx4 -The helper may be initially started in a waiting state (with -an --incoming argument for example), and it may resume on success. + .. dbus-doc:: backends/dbus-vmstate1.xml -An error may be returned to the caller. +.. only:: not sphinx4 -Save(out u8[] bytes) method ---------------------------- + .. warning:: + Sphinx 4 is required to build D-Bus documentation. -The method called on the source to get the current state to be -migrated. The helper should continue to run normally. + This is the content of ``backends/dbus-vmstate1.xml``: -An error may be returned to the caller. + .. literalinclude:: ../../backends/dbus-vmstate1.xml + :language: xml diff --git a/docs/interop/dbus.rst b/docs/interop/dbus.rst index be596d3f41..427debc9c5 100644 --- a/docs/interop/dbus.rst +++ b/docs/interop/dbus.rst @@ -108,3 +108,5 @@ QEMU Interfaces =============== :doc:`dbus-vmstate` + +:doc:`dbus-display` diff --git a/docs/interop/index.rst b/docs/interop/index.rst index 47b9ed82bb..c59bac9834 100644 --- a/docs/interop/index.rst +++ b/docs/interop/index.rst @@ -12,6 +12,7 @@ are useful for making QEMU interoperate with other software. bitmaps dbus dbus-vmstate + dbus-display live-block-operations pr-helper qemu-ga diff --git a/docs/sphinx/dbusdoc.py b/docs/sphinx/dbusdoc.py new file mode 100644 index 0000000000..be284ed08f --- /dev/null +++ b/docs/sphinx/dbusdoc.py @@ -0,0 +1,166 @@ +# D-Bus XML documentation extension +# +# Copyright (C) 2021, Red Hat Inc. +# +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Author: Marc-André Lureau <marcandre.lureau@redhat.com> +"""dbus-doc is a Sphinx extension that provides documentation from D-Bus XML.""" + +import os +import re +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Iterator, + List, + Optional, + Sequence, + Set, + Tuple, + Type, + TypeVar, + Union, +) + +import sphinx +from docutils import nodes +from docutils.nodes import Element, Node +from docutils.parsers.rst import Directive, directives +from docutils.parsers.rst.states import RSTState +from docutils.statemachine import StringList, ViewList +from sphinx.application import Sphinx +from sphinx.errors import ExtensionError +from sphinx.util import logging +from sphinx.util.docstrings import prepare_docstring +from sphinx.util.docutils import SphinxDirective, switch_source_input +from sphinx.util.nodes import nested_parse_with_titles + +import dbusdomain +from dbusparser import parse_dbus_xml + +logger = logging.getLogger(__name__) + +__version__ = "1.0" + + +class DBusDoc: + def __init__(self, sphinx_directive, dbusfile): + self._cur_doc = None + self._sphinx_directive = sphinx_directive + self._dbusfile = dbusfile + self._top_node = nodes.section() + self.result = StringList() + self.indent = "" + + def add_line(self, line: str, *lineno: int) -> None: + """Append one line of generated reST to the output.""" + if line.strip(): # not a blank line + self.result.append(self.indent + line, self._dbusfile, *lineno) + else: + self.result.append("", self._dbusfile, *lineno) + + def add_method(self, method): + self.add_line(f".. dbus:method:: {method.name}") + self.add_line("") + self.indent += " " + for arg in method.in_args: + self.add_line(f":arg {arg.signature} {arg.name}: {arg.doc_string}") + for arg in method.out_args: + self.add_line(f":ret {arg.signature} {arg.name}: {arg.doc_string}") + self.add_line("") + for line in prepare_docstring("\n" + method.doc_string): + self.add_line(line) + self.indent = self.indent[:-3] + + def add_signal(self, signal): + self.add_line(f".. dbus:signal:: {signal.name}") + self.add_line("") + self.indent += " " + for arg in signal.args: + self.add_line(f":arg {arg.signature} {arg.name}: {arg.doc_string}") + self.add_line("") + for line in prepare_docstring("\n" + signal.doc_string): + self.add_line(line) + self.indent = self.indent[:-3] + + def add_property(self, prop): + self.add_line(f".. dbus:property:: {prop.name}") + self.indent += " " + self.add_line(f":type: {prop.signature}") + access = {"read": "readonly", "write": "writeonly", "readwrite": "readwrite"}[ + prop.access + ] + self.add_line(f":{access}:") + if prop.emits_changed_signal: + self.add_line(f":emits-changed: yes") + self.add_line("") + for line in prepare_docstring("\n" + prop.doc_string): + self.add_line(line) + self.indent = self.indent[:-3] + + def add_interface(self, iface): + self.add_line(f".. dbus:interface:: {iface.name}") + self.add_line("") + self.indent += " " + for line in prepare_docstring("\n" + iface.doc_string): + self.add_line(line) + for method in iface.methods: + self.add_method(method) + for sig in iface.signals: + self.add_signal(sig) + for prop in iface.properties: + self.add_property(prop) + self.indent = self.indent[:-3] + + +def parse_generated_content(state: RSTState, content: StringList) -> List[Node]: + """Parse a generated content by Documenter.""" + with switch_source_input(state, content): + node = nodes.paragraph() + node.document = state.document + state.nested_parse(content, 0, node) + + return node.children + + +class DBusDocDirective(SphinxDirective): + """Extract documentation from the specified D-Bus XML file""" + + has_content = True + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + + def run(self): + reporter = self.state.document.reporter + + try: + source, lineno = reporter.get_source_and_line(self.lineno) # type: ignore + except AttributeError: + source, lineno = (None, None) + + logger.debug("[dbusdoc] %s:%s: input:\n%s", source, lineno, self.block_text) + + env = self.state.document.settings.env + dbusfile = env.config.qapidoc_srctree + "/" + self.arguments[0] + with open(dbusfile, "rb") as f: + xml_data = f.read() + xml = parse_dbus_xml(xml_data) + doc = DBusDoc(self, dbusfile) + for iface in xml: + doc.add_interface(iface) + + result = parse_generated_content(self.state, doc.result) + return result + + +def setup(app: Sphinx) -> Dict[str, Any]: + """Register dbus-doc directive with Sphinx""" + app.add_config_value("dbusdoc_srctree", None, "env") + app.add_directive("dbus-doc", DBusDocDirective) + dbusdomain.setup(app) + + return dict(version=__version__, parallel_read_safe=True, parallel_write_safe=True) diff --git a/docs/sphinx/dbusdomain.py b/docs/sphinx/dbusdomain.py new file mode 100644 index 0000000000..2ea95af623 --- /dev/null +++ b/docs/sphinx/dbusdomain.py @@ -0,0 +1,406 @@ +# D-Bus sphinx domain extension +# +# Copyright (C) 2021, Red Hat Inc. +# +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Author: Marc-André Lureau <marcandre.lureau@redhat.com> + +from typing import ( + Any, + Dict, + Iterable, + Iterator, + List, + NamedTuple, + Optional, + Tuple, + cast, +) + +from docutils import nodes +from docutils.nodes import Element, Node +from docutils.parsers.rst import directives +from sphinx import addnodes +from sphinx.addnodes import desc_signature, pending_xref +from sphinx.directives import ObjectDescription +from sphinx.domains import Domain, Index, IndexEntry, ObjType +from sphinx.locale import _ +from sphinx.roles import XRefRole +from sphinx.util import nodes as node_utils +from sphinx.util.docfields import Field, TypedField +from sphinx.util.typing import OptionSpec + + +class DBusDescription(ObjectDescription[str]): + """Base class for DBus objects""" + + option_spec: OptionSpec = ObjectDescription.option_spec.copy() + option_spec.update( + { + "deprecated": directives.flag, + } + ) + + def get_index_text(self, modname: str, name: str) -> str: + """Return the text for the index entry of the object.""" + raise NotImplementedError("must be implemented in subclasses") + + def add_target_and_index( + self, name: str, sig: str, signode: desc_signature + ) -> None: + ifacename = self.env.ref_context.get("dbus:interface") + node_id = name + if ifacename: + node_id = f"{ifacename}.{node_id}" + + signode["names"].append(name) + signode["ids"].append(node_id) + + if "noindexentry" not in self.options: + indextext = self.get_index_text(ifacename, name) + if indextext: + self.indexnode["entries"].append( + ("single", indextext, node_id, "", None) + ) + + domain = cast(DBusDomain, self.env.get_domain("dbus")) + domain.note_object(name, self.objtype, node_id, location=signode) + + +class DBusInterface(DBusDescription): + """ + Implementation of ``dbus:interface``. + """ + + def get_index_text(self, ifacename: str, name: str) -> str: + return ifacename + + def before_content(self) -> None: + self.env.ref_context["dbus:interface"] = self.arguments[0] + + def after_content(self) -> None: + self.env.ref_context.pop("dbus:interface") + + def handle_signature(self, sig: str, signode: desc_signature) -> str: + signode += addnodes.desc_annotation("interface ", "interface ") + signode += addnodes.desc_name(sig, sig) + return sig + + def run(self) -> List[Node]: + _, node = super().run() + name = self.arguments[0] + section = nodes.section(ids=[name + "-section"]) + section += nodes.title(name, "%s interface" % name) + section += node + return [self.indexnode, section] + + +class DBusMember(DBusDescription): + + signal = False + + +class DBusMethod(DBusMember): + """ + Implementation of ``dbus:method``. + """ + + option_spec: OptionSpec = DBusMember.option_spec.copy() + option_spec.update( + { + "noreply": directives.flag, + } + ) + + doc_field_types: List[Field] = [ + TypedField( + "arg", + label=_("Arguments"), + names=("arg",), + rolename="arg", + typerolename=None, + typenames=("argtype", "type"), + ), + TypedField( + "ret", + label=_("Returns"), + names=("ret",), + rolename="ret", + typerolename=None, + typenames=("rettype", "type"), + ), + ] + + def get_index_text(self, ifacename: str, name: str) -> str: + return _("%s() (%s method)") % (name, ifacename) + + def handle_signature(self, sig: str, signode: desc_signature) -> str: + params = addnodes.desc_parameterlist() + returns = addnodes.desc_parameterlist() + + contentnode = addnodes.desc_content() + self.state.nested_parse(self.content, self.content_offset, contentnode) + for child in contentnode: + if isinstance(child, nodes.field_list): + for field in child: + ty, sg, name = field[0].astext().split(None, 2) + param = addnodes.desc_parameter() + param += addnodes.desc_sig_keyword_type(sg, sg) + param += addnodes.desc_sig_space() + param += addnodes.desc_sig_name(name, name) + if ty == "arg": + params += param + elif ty == "ret": + returns += param + + anno = "signal " if self.signal else "method " + signode += addnodes.desc_annotation(anno, anno) + signode += addnodes.desc_name(sig, sig) + signode += params + if not self.signal and "noreply" not in self.options: + ret = addnodes.desc_returns() + ret += returns + signode += ret + + return sig + + +class DBusSignal(DBusMethod): + """ + Implementation of ``dbus:signal``. + """ + + doc_field_types: List[Field] = [ + TypedField( + "arg", + label=_("Arguments"), + names=("arg",), + rolename="arg", + typerolename=None, + typenames=("argtype", "type"), + ), + ] + signal = True + + def get_index_text(self, ifacename: str, name: str) -> str: + return _("%s() (%s signal)") % (name, ifacename) + + +class DBusProperty(DBusMember): + """ + Implementation of ``dbus:property``. + """ + + option_spec: OptionSpec = DBusMember.option_spec.copy() + option_spec.update( + { + "type": directives.unchanged, + "readonly": directives.flag, + "writeonly": directives.flag, + "readwrite": directives.flag, + "emits-changed": directives.unchanged, + } + ) + + doc_field_types: List[Field] = [] + + def get_index_text(self, ifacename: str, name: str) -> str: + return _("%s (%s property)") % (name, ifacename) + + def transform_content(self, contentnode: addnodes.desc_content) -> None: + fieldlist = nodes.field_list() + access = None + if "readonly" in self.options: + access = _("read-only") + if "writeonly" in self.options: + access = _("write-only") + if "readwrite" in self.options: + access = _("read & write") + if access: + content = nodes.Text(access) + fieldname = nodes.field_name("", _("Access")) + fieldbody = nodes.field_body("", nodes.paragraph("", "", content)) + field = nodes.field("", fieldname, fieldbody) + fieldlist += field + emits = self.options.get("emits-changed", None) + if emits: + content = nodes.Text(emits) + fieldname = nodes.field_name("", _("Emits Changed")) + fieldbody = nodes.field_body("", nodes.paragraph("", "", content)) + field = nodes.field("", fieldname, fieldbody) + fieldlist += field + if len(fieldlist) > 0: + contentnode.insert(0, fieldlist) + + def handle_signature(self, sig: str, signode: desc_signature) -> str: + contentnode = addnodes.desc_content() + self.state.nested_parse(self.content, self.content_offset, contentnode) + ty = self.options.get("type") + + signode += addnodes.desc_annotation("property ", "property ") + signode += addnodes.desc_name(sig, sig) + signode += addnodes.desc_sig_punctuation("", ":") + signode += addnodes.desc_sig_keyword_type(ty, ty) + return sig + + def run(self) -> List[Node]: + self.name = "dbus:member" + return super().run() + + +class DBusXRef(XRefRole): + def process_link(self, env, refnode, has_explicit_title, title, target): + refnode["dbus:interface"] = env.ref_context.get("dbus:interface") + if not has_explicit_title: + title = title.lstrip(".") # only has a meaning for the target + target = target.lstrip("~") # only has a meaning for the title + # if the first character is a tilde, don't display the module/class + # parts of the contents + if title[0:1] == "~": + title = title[1:] + dot = title.rfind(".") + if dot != -1: + title = title[dot + 1 :] + # if the first character is a dot, search more specific namespaces first + # else search builtins first + if target[0:1] == ".": + target = target[1:] + refnode["refspecific"] = True + return title, target + + +class DBusIndex(Index): + """ + Index subclass to provide a D-Bus interfaces index. + """ + + name = "dbusindex" + localname = _("D-Bus Interfaces Index") + shortname = _("dbus") + + def generate( + self, docnames: Iterable[str] = None + ) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]: + content: Dict[str, List[IndexEntry]] = {} + # list of prefixes to ignore + ignores: List[str] = self.domain.env.config["dbus_index_common_prefix"] + ignores = sorted(ignores, key=len, reverse=True) + + ifaces = sorted( + [ + x + for x in self.domain.data["objects"].items() + if x[1].objtype == "interface" + ], + key=lambda x: x[0].lower(), + ) + for name, (docname, node_id, _) in ifaces: + if docnames and docname not in docnames: + continue + + for ignore in ignores: + if name.startswith(ignore): + name = name[len(ignore) :] + stripped = ignore + break + else: + stripped = "" + + entries = content.setdefault(name[0].lower(), []) + entries.append(IndexEntry(stripped + name, 0, docname, node_id, "", "", "")) + + # sort by first letter + sorted_content = sorted(content.items()) + + return sorted_content, False + + +class ObjectEntry(NamedTuple): + docname: str + node_id: str + objtype: str + + +class DBusDomain(Domain): + """ + Implementation of the D-Bus domain. + """ + + name = "dbus" + label = "D-Bus" + object_types: Dict[str, ObjType] = { + "interface": ObjType(_("interface"), "iface", "obj"), + "method": ObjType(_("method"), "meth", "obj"), + "signal": ObjType(_("signal"), "sig", "obj"), + "property": ObjType(_("property"), "attr", "_prop", "obj"), + } + directives = { + "interface": DBusInterface, + "method": DBusMethod, + "signal": DBusSignal, + "property": DBusProperty, + } + roles = { + "iface": DBusXRef(), + "meth": DBusXRef(), + "sig": DBusXRef(), + "prop": DBusXRef(), + } + initial_data: Dict[str, Dict[str, Tuple[Any]]] = { + "objects": {}, # fullname -> ObjectEntry + } + indices = [ + DBusIndex, + ] + + @property + def objects(self) -> Dict[str, ObjectEntry]: + return self.data.setdefault("objects", {}) # fullname -> ObjectEntry + + def note_object( + self, name: str, objtype: str, node_id: str, location: Any = None + ) -> None: + self.objects[name] = ObjectEntry(self.env.docname, node_id, objtype) + + def clear_doc(self, docname: str) -> None: + for fullname, obj in list(self.objects.items()): + if obj.docname == docname: + del self.objects[fullname] + + def find_obj(self, typ: str, name: str) -> Optional[Tuple[str, ObjectEntry]]: + # skip parens + if name[-2:] == "()": + name = name[:-2] + if typ in ("meth", "sig", "prop"): + try: + ifacename, name = name.rsplit(".", 1) + except ValueError: + pass + return self.objects.get(name) + + def resolve_xref( + self, + env: "BuildEnvironment", + fromdocname: str, + builder: "Builder", + typ: str, + target: str, + node: pending_xref, + contnode: Element, + ) -> Optional[Element]: + """Resolve the pending_xref *node* with the given *typ* and *target*.""" + objdef = self.find_obj(typ, target) + if objdef: + return node_utils.make_refnode( + builder, fromdocname, objdef.docname, objdef.node_id, contnode + ) + + def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]: + for refname, obj in self.objects.items(): + yield (refname, refname, obj.objtype, obj.docname, obj.node_id, 1) + + +def setup(app): + app.add_domain(DBusDomain) + app.add_config_value("dbus_index_common_prefix", [], "env") diff --git a/docs/sphinx/dbusparser.py b/docs/sphinx/dbusparser.py new file mode 100644 index 0000000000..024553eae7 --- /dev/null +++ b/docs/sphinx/dbusparser.py @@ -0,0 +1,373 @@ +# Based from "GDBus - GLib D-Bus Library": +# +# Copyright (C) 2008-2011 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General +# Public License along with this library; if not, see <http://www.gnu.org/licenses/>. +# +# Author: David Zeuthen <davidz@redhat.com> + +import xml.parsers.expat + + +class Annotation: + def __init__(self, key, value): + self.key = key + self.value = value + self.annotations = [] + self.since = "" + + +class Arg: + def __init__(self, name, signature): + self.name = name + self.signature = signature + self.annotations = [] + self.doc_string = "" + self.since = "" + + +class Method: + def __init__(self, name, h_type_implies_unix_fd=True): + self.name = name + self.h_type_implies_unix_fd = h_type_implies_unix_fd + self.in_args = [] + self.out_args = [] + self.annotations = [] + self.doc_string = "" + self.since = "" + self.deprecated = False + self.unix_fd = False + + +class Signal: + def __init__(self, name): + self.name = name + self.args = [] + self.annotations = [] + self.doc_string = "" + self.since = "" + self.deprecated = False + + +class Property: + def __init__(self, name, signature, access): + self.name = name + self.signature = signature + self.access = access + self.annotations = [] + self.arg = Arg("value", self.signature) + self.arg.annotations = self.annotations + self.readable = False + self.writable = False + if self.access == "readwrite": + self.readable = True + self.writable = True + elif self.access == "read": + self.readable = True + elif self.access == "write": + self.writable = True + else: + raise ValueError('Invalid access type "{}"'.format(self.access)) + self.doc_string = "" + self.since = "" + self.deprecated = False + self.emits_changed_signal = True + + +class Interface: + def __init__(self, name): + self.name = name + self.methods = [] + self.signals = [] + self.properties = [] + self.annotations = [] + self.doc_string = "" + self.doc_string_brief = "" + self.since = "" + self.deprecated = False + + +class DBusXMLParser: + STATE_TOP = "top" + STATE_NODE = "node" + STATE_INTERFACE = "interface" + STATE_METHOD = "method" + STATE_SIGNAL = "signal" + STATE_PROPERTY = "property" + STATE_ARG = "arg" + STATE_ANNOTATION = "annotation" + STATE_IGNORED = "ignored" + + def __init__(self, xml_data, h_type_implies_unix_fd=True): + self._parser = xml.parsers.expat.ParserCreate() + self._parser.CommentHandler = self.handle_comment + self._parser.CharacterDataHandler = self.handle_char_data + self._parser.StartElementHandler = self.handle_start_element + self._parser.EndElementHandler = self.handle_end_element + + self.parsed_interfaces = [] + self._cur_object = None + + self.state = DBusXMLParser.STATE_TOP + self.state_stack = [] + self._cur_object = None + self._cur_object_stack = [] + + self.doc_comment_last_symbol = "" + + self._h_type_implies_unix_fd = h_type_implies_unix_fd + + self._parser.Parse(xml_data) + + COMMENT_STATE_BEGIN = "begin" + COMMENT_STATE_PARAMS = "params" + COMMENT_STATE_BODY = "body" + COMMENT_STATE_SKIP = "skip" + + def handle_comment(self, data): + comment_state = DBusXMLParser.COMMENT_STATE_BEGIN + lines = data.split("\n") + symbol = "" + body = "" + in_para = False + params = {} + for line in lines: + orig_line = line + line = line.lstrip() + if comment_state == DBusXMLParser.COMMENT_STATE_BEGIN: + if len(line) > 0: + colon_index = line.find(": ") + if colon_index == -1: + if line.endswith(":"): + symbol = line[0 : len(line) - 1] + comment_state = DBusXMLParser.COMMENT_STATE_PARAMS + else: + comment_state = DBusXMLParser.COMMENT_STATE_SKIP + else: + symbol = line[0:colon_index] + rest_of_line = line[colon_index + 2 :].strip() + if len(rest_of_line) > 0: + body += rest_of_line + "\n" + comment_state = DBusXMLParser.COMMENT_STATE_PARAMS + elif comment_state == DBusXMLParser.COMMENT_STATE_PARAMS: + if line.startswith("@"): + colon_index = line.find(": ") + if colon_index == -1: + comment_state = DBusXMLParser.COMMENT_STATE_BODY + if not in_para: + in_para = True + body += orig_line + "\n" + else: + param = line[1:colon_index] + docs = line[colon_index + 2 :] + params[param] = docs + else: + comment_state = DBusXMLParser.COMMENT_STATE_BODY + if len(line) > 0: + if not in_para: + in_para = True + body += orig_line + "\n" + elif comment_state == DBusXMLParser.COMMENT_STATE_BODY: + if len(line) > 0: + if not in_para: + in_para = True + body += orig_line + "\n" + else: + if in_para: + body += "\n" + in_para = False + if in_para: + body += "\n" + + if symbol != "": + self.doc_comment_last_symbol = symbol + self.doc_comment_params = params + self.doc_comment_body = body + + def handle_char_data(self, data): + # print 'char_data=%s'%data + pass + + def handle_start_element(self, name, attrs): + old_state = self.state + old_cur_object = self._cur_object + if self.state == DBusXMLParser.STATE_IGNORED: + self.state = DBusXMLParser.STATE_IGNORED + elif self.state == DBusXMLParser.STATE_TOP: + if name == DBusXMLParser.STATE_NODE: + self.state = DBusXMLParser.STATE_NODE + else: + self.state = DBusXMLParser.STATE_IGNORED + elif self.state == DBusXMLParser.STATE_NODE: + if name == DBusXMLParser.STATE_INTERFACE: + self.state = DBusXMLParser.STATE_INTERFACE + iface = Interface(attrs["name"]) + self._cur_object = iface + self.parsed_interfaces.append(iface) + elif name == DBusXMLParser.STATE_ANNOTATION: + self.state = DBusXMLParser.STATE_ANNOTATION + anno = Annotation(attrs["name"], attrs["value"]) + self._cur_object.annotations.append(anno) + self._cur_object = anno + else: + self.state = DBusXMLParser.STATE_IGNORED + + # assign docs, if any + if "name" in attrs and self.doc_comment_last_symbol == attrs["name"]: + self._cur_object.doc_string = self.doc_comment_body + if "short_description" in self.doc_comment_params: + short_description = self.doc_comment_params["short_description"] + self._cur_object.doc_string_brief = short_description + if "since" in self.doc_comment_params: + self._cur_object.since = self.doc_comment_params["since"].strip() + + elif self.state == DBusXMLParser.STATE_INTERFACE: + if name == DBusXMLParser.STATE_METHOD: + self.state = DBusXMLParser.STATE_METHOD + method = Method( + attrs["name"], h_type_implies_unix_fd=self._h_type_implies_unix_fd + ) + self._cur_object.methods.append(method) + self._cur_object = method + elif name == DBusXMLParser.STATE_SIGNAL: + self.state = DBusXMLParser.STATE_SIGNAL + signal = Signal(attrs["name"]) + self._cur_object.signals.append(signal) + self._cur_object = signal + elif name == DBusXMLParser.STATE_PROPERTY: + self.state = DBusXMLParser.STATE_PROPERTY + prop = Property(attrs["name"], attrs["type"], attrs["access"]) + self._cur_object.properties.append(prop) + self._cur_object = prop + elif name == DBusXMLParser.STATE_ANNOTATION: + self.state = DBusXMLParser.STATE_ANNOTATION + anno = Annotation(attrs["name"], attrs["value"]) + self._cur_object.annotations.append(anno) + self._cur_object = anno + else: + self.state = DBusXMLParser.STATE_IGNORED + + # assign docs, if any + if "name" in attrs and self.doc_comment_last_symbol == attrs["name"]: + self._cur_object.doc_string = self.doc_comment_body + if "since" in self.doc_comment_params: + self._cur_object.since = self.doc_comment_params["since"].strip() + + elif self.state == DBusXMLParser.STATE_METHOD: + if name == DBusXMLParser.STATE_ARG: + self.state = DBusXMLParser.STATE_ARG + arg_name = None + if "name" in attrs: + arg_name = attrs["name"] + arg = Arg(arg_name, attrs["type"]) + direction = attrs.get("direction", "in") + if direction == "in": + self._cur_object.in_args.append(arg) + elif direction == "out": + self._cur_object.out_args.append(arg) + else: + raise ValueError('Invalid direction "{}"'.format(direction)) + self._cur_object = arg + elif name == DBusXMLParser.STATE_ANNOTATION: + self.state = DBusXMLParser.STATE_ANNOTATION + anno = Annotation(attrs["name"], attrs["value"]) + self._cur_object.annotations.append(anno) + self._cur_object = anno + else: + self.state = DBusXMLParser.STATE_IGNORED + + # assign docs, if any + if self.doc_comment_last_symbol == old_cur_object.name: + if "name" in attrs and attrs["name"] in self.doc_comment_params: + doc_string = self.doc_comment_params[attrs["name"]] + if doc_string is not None: + self._cur_object.doc_string = doc_string + if "since" in self.doc_comment_params: + self._cur_object.since = self.doc_comment_params[ + "since" + ].strip() + + elif self.state == DBusXMLParser.STATE_SIGNAL: + if name == DBusXMLParser.STATE_ARG: + self.state = DBusXMLParser.STATE_ARG + arg_name = None + if "name" in attrs: + arg_name = attrs["name"] + arg = Arg(arg_name, attrs["type"]) + self._cur_object.args.append(arg) + self._cur_object = arg + elif name == DBusXMLParser.STATE_ANNOTATION: + self.state = DBusXMLParser.STATE_ANNOTATION + anno = Annotation(attrs["name"], attrs["value"]) + self._cur_object.annotations.append(anno) + self._cur_object = anno + else: + self.state = DBusXMLParser.STATE_IGNORED + + # assign docs, if any + if self.doc_comment_last_symbol == old_cur_object.name: + if "name" in attrs and attrs["name"] in self.doc_comment_params: + doc_string = self.doc_comment_params[attrs["name"]] + if doc_string is not None: + self._cur_object.doc_string = doc_string + if "since" in self.doc_comment_params: + self._cur_object.since = self.doc_comment_params[ + "since" + ].strip() + + elif self.state == DBusXMLParser.STATE_PROPERTY: + if name == DBusXMLParser.STATE_ANNOTATION: + self.state = DBusXMLParser.STATE_ANNOTATION + anno = Annotation(attrs["name"], attrs["value"]) + self._cur_object.annotations.append(anno) + self._cur_object = anno + else: + self.state = DBusXMLParser.STATE_IGNORED + + elif self.state == DBusXMLParser.STATE_ARG: + if name == DBusXMLParser.STATE_ANNOTATION: + self.state = DBusXMLParser.STATE_ANNOTATION + anno = Annotation(attrs["name"], attrs["value"]) + self._cur_object.annotations.append(anno) + self._cur_object = anno + else: + self.state = DBusXMLParser.STATE_IGNORED + + elif self.state == DBusXMLParser.STATE_ANNOTATION: + if name == DBusXMLParser.STATE_ANNOTATION: + self.state = DBusXMLParser.STATE_ANNOTATION + anno = Annotation(attrs["name"], attrs["value"]) + self._cur_object.annotations.append(anno) + self._cur_object = anno + else: + self.state = DBusXMLParser.STATE_IGNORED + + else: + raise ValueError( + 'Unhandled state "{}" while entering element with name "{}"'.format( + self.state, name + ) + ) + + self.state_stack.append(old_state) + self._cur_object_stack.append(old_cur_object) + + def handle_end_element(self, name): + self.state = self.state_stack.pop() + self._cur_object = self._cur_object_stack.pop() + + +def parse_dbus_xml(xml_data): + parser = DBusXMLParser(xml_data, True) + return parser.parsed_interfaces diff --git a/docs/sphinx/fakedbusdoc.py b/docs/sphinx/fakedbusdoc.py new file mode 100644 index 0000000000..a680b25754 --- /dev/null +++ b/docs/sphinx/fakedbusdoc.py @@ -0,0 +1,25 @@ +# D-Bus XML documentation extension, compatibility gunk for <sphinx4 +# +# Copyright (C) 2021, Red Hat Inc. +# +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Author: Marc-André Lureau <marcandre.lureau@redhat.com> +"""dbus-doc is a Sphinx extension that provides documentation from D-Bus XML.""" + +from sphinx.application import Sphinx +from sphinx.util.docutils import SphinxDirective +from typing import Any, Dict + + +class FakeDBusDocDirective(SphinxDirective): + has_content = True + required_arguments = 1 + + def run(self): + return [] + + +def setup(app: Sphinx) -> Dict[str, Any]: + """Register a fake dbus-doc directive with Sphinx""" + app.add_directive("dbus-doc", FakeDBusDocDirective) diff --git a/hw/display/qxl.c b/hw/display/qxl.c index 29c80b4289..e2d6e317da 100644 --- a/hw/display/qxl.c +++ b/hw/display/qxl.c @@ -2171,12 +2171,17 @@ static void qxl_realize_common(PCIQXLDevice *qxl, Error **errp) } #if SPICE_SERVER_VERSION >= 0x000e02 /* release 0.14.2 */ + Error *err = NULL; char device_address[256] = ""; - if (qemu_spice_fill_device_address(qxl->vga.con, device_address, 256)) { + if (qemu_console_fill_device_address(qxl->vga.con, + device_address, sizeof(device_address), + &err)) { spice_qxl_set_device_info(&qxl->ssd.qxl, device_address, 0, qxl->max_outputs); + } else { + error_report_err(err); } #endif diff --git a/hw/display/vhost-user-gpu.c b/hw/display/vhost-user-gpu.c index 49df56cd14..09818231bd 100644 --- a/hw/display/vhost-user-gpu.c +++ b/hw/display/vhost-user-gpu.c @@ -254,8 +254,8 @@ vhost_user_gpu_handle_display(VhostUserGPU *g, VhostUserGpuMsg *msg) vhost_user_gpu_unblock(g); break; } - dpy_gl_update(con, m->x, m->y, m->width, m->height); g->backend_blocked = true; + dpy_gl_update(con, m->x, m->y, m->width, m->height); break; } case VHOST_USER_GPU_UPDATE: { diff --git a/hw/display/virtio-gpu-base.c b/hw/display/virtio-gpu-base.c index c8da4806e0..fff0fb4a82 100644 --- a/hw/display/virtio-gpu-base.c +++ b/hw/display/virtio-gpu-base.c @@ -117,6 +117,10 @@ virtio_gpu_gl_block(void *opaque, bool block) g->renderer_blocked--; } assert(g->renderer_blocked >= 0); + + if (!block && g->renderer_blocked == 0) { + virtio_gpu_gl_flushed(g); + } } static int @@ -143,7 +147,6 @@ static const GraphicHwOps virtio_gpu_ops = { .text_update = virtio_gpu_text_update, .ui_info = virtio_gpu_ui_info, .gl_block = virtio_gpu_gl_block, - .gl_flushed = virtio_gpu_gl_flushed, }; bool diff --git a/hw/display/virtio-gpu-virgl.c b/hw/display/virtio-gpu-virgl.c index 18d054922f..73cb92c8d5 100644 --- a/hw/display/virtio-gpu-virgl.c +++ b/hw/display/virtio-gpu-virgl.c @@ -175,7 +175,7 @@ static void virgl_cmd_set_scanout(VirtIOGPU *g, virgl_renderer_force_ctx_0(); dpy_gl_scanout_texture( g->parent_obj.scanout[ss.scanout_id].con, info.tex_id, - info.flags & 1 /* FIXME: Y_0_TOP */, + info.flags & VIRTIO_GPU_RESOURCE_FLAG_Y_0_TOP, info.width, info.height, ss.r.x, ss.r.y, ss.r.width, ss.r.height); } else { @@ -609,6 +609,7 @@ int virtio_gpu_virgl_init(VirtIOGPU *g) ret = virgl_renderer_init(g, 0, &virtio_gpu_3d_cbs); if (ret != 0) { + error_report("virgl could not be initialized: %d", ret); return ret; } diff --git a/hw/display/virtio-vga.c b/hw/display/virtio-vga.c index 9e57f61e9e..b23a75a04b 100644 --- a/hw/display/virtio-vga.c +++ b/hw/display/virtio-vga.c @@ -68,16 +68,6 @@ static void virtio_vga_base_gl_block(void *opaque, bool block) } } -static void virtio_vga_base_gl_flushed(void *opaque) -{ - VirtIOVGABase *vvga = opaque; - VirtIOGPUBase *g = vvga->vgpu; - - if (g->hw_ops->gl_flushed) { - g->hw_ops->gl_flushed(g); - } -} - static int virtio_vga_base_get_flags(void *opaque) { VirtIOVGABase *vvga = opaque; @@ -93,7 +83,6 @@ static const GraphicHwOps virtio_vga_base_ops = { .text_update = virtio_vga_base_text_update, .ui_info = virtio_vga_base_ui_info, .gl_block = virtio_vga_base_gl_block, - .gl_flushed = virtio_vga_base_gl_flushed, }; static const VMStateDescription vmstate_virtio_vga_base = { diff --git a/include/chardev/char-socket.h b/include/chardev/char-socket.h new file mode 100644 index 0000000000..6b6e2ceba1 --- /dev/null +++ b/include/chardev/char-socket.h @@ -0,0 +1,86 @@ +/* + * QEMU System Emulator + * + * Copyright (c) 2003-2008 Fabrice Bellard + * + * 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. + */ +#ifndef CHAR_SOCKET_H_ +#define CHAR_SOCKET_H_ + +#include "io/channel-socket.h" +#include "io/channel-tls.h" +#include "io/net-listener.h" +#include "chardev/char.h" +#include "qom/object.h" + +#define TCP_MAX_FDS 16 + +typedef struct { + char buf[21]; + size_t buflen; +} TCPChardevTelnetInit; + +typedef enum { + TCP_CHARDEV_STATE_DISCONNECTED, + TCP_CHARDEV_STATE_CONNECTING, + TCP_CHARDEV_STATE_CONNECTED, +} TCPChardevState; + +typedef ChardevClass SocketChardevClass; + +struct SocketChardev { + Chardev parent; + QIOChannel *ioc; /* Client I/O channel */ + QIOChannelSocket *sioc; /* Client master channel */ + QIONetListener *listener; + GSource *hup_source; + QCryptoTLSCreds *tls_creds; + char *tls_authz; + TCPChardevState state; + int max_size; + int do_telnetopt; + int do_nodelay; + int *read_msgfds; + size_t read_msgfds_num; + int *write_msgfds; + size_t write_msgfds_num; + bool registered_yank; + + SocketAddress *addr; + bool is_listen; + bool is_telnet; + bool is_tn3270; + GSource *telnet_source; + TCPChardevTelnetInit *telnet_init; + + bool is_websock; + + GSource *reconnect_timer; + int64_t reconnect_time; + bool connect_err_reported; + + QIOTask *connect_task; +}; +typedef struct SocketChardev SocketChardev; + +DECLARE_INSTANCE_CHECKER(SocketChardev, SOCKET_CHARDEV, + TYPE_CHARDEV_SOCKET) + +#endif /* CHAR_SOCKET_H_ */ diff --git a/include/qemu/cutils.h b/include/qemu/cutils.h index 986ed8e15f..320543950c 100644 --- a/include/qemu/cutils.h +++ b/include/qemu/cutils.h @@ -209,4 +209,9 @@ int qemu_pstrcmp0(const char **str1, const char **str2); */ char *get_relocated_path(const char *dir); +static inline const char *yes_no(bool b) +{ + return b ? "yes" : "no"; +} + #endif diff --git a/include/qemu/dbus.h b/include/qemu/dbus.h index 9d591f9ee4..08f00dfd53 100644 --- a/include/qemu/dbus.h +++ b/include/qemu/dbus.h @@ -12,6 +12,30 @@ #include <gio/gio.h> +#include "qom/object.h" +#include "chardev/char.h" +#include "qemu/notify.h" +#include "qemu/typedefs.h" + +/* glib/gio 2.68 */ +#define DBUS_METHOD_INVOCATION_HANDLED TRUE +#define DBUS_METHOD_INVOCATION_UNHANDLED FALSE + +/* in msec */ +#define DBUS_DEFAULT_TIMEOUT 1000 + +#define DBUS_DISPLAY1_ROOT "/org/qemu/Display1" + +#define DBUS_DISPLAY_ERROR (dbus_display_error_quark()) +GQuark dbus_display_error_quark(void); + +typedef enum { + DBUS_DISPLAY_ERROR_FAILED, + DBUS_DISPLAY_ERROR_INVALID, + DBUS_DISPLAY_ERROR_UNSUPPORTED, + DBUS_DISPLAY_N_ERRORS, +} DBusDisplayError; + GStrv qemu_dbus_get_queued_owners(GDBusConnection *connection, const char *name, Error **errp); diff --git a/include/qemu/option.h b/include/qemu/option.h index 306bf07575..bbd86e1c4e 100644 --- a/include/qemu/option.h +++ b/include/qemu/option.h @@ -150,4 +150,6 @@ QDict *keyval_parse(const char *params, const char *implied_key, bool *help, Error **errp); void keyval_merge(QDict *old, const QDict *new, Error **errp); +G_DEFINE_AUTOPTR_CLEANUP_FUNC(QemuOpts, qemu_opts_del) + #endif diff --git a/include/ui/clipboard.h b/include/ui/clipboard.h index 6298986b15..ce76aa451f 100644 --- a/include/ui/clipboard.h +++ b/include/ui/clipboard.h @@ -20,8 +20,10 @@ */ typedef enum QemuClipboardType QemuClipboardType; +typedef enum QemuClipboardNotifyType QemuClipboardNotifyType; typedef enum QemuClipboardSelection QemuClipboardSelection; typedef struct QemuClipboardPeer QemuClipboardPeer; +typedef struct QemuClipboardNotify QemuClipboardNotify; typedef struct QemuClipboardInfo QemuClipboardInfo; /** @@ -55,25 +57,55 @@ enum QemuClipboardSelection { * struct QemuClipboardPeer * * @name: peer name. - * @update: notifier for clipboard updates. + * @notifier: notifier for clipboard updates. * @request: callback for clipboard data requests. * * Clipboard peer description. */ struct QemuClipboardPeer { const char *name; - Notifier update; + Notifier notifier; void (*request)(QemuClipboardInfo *info, QemuClipboardType type); }; /** + * enum QemuClipboardNotifyType + * + * @QEMU_CLIPBOARD_UPDATE_INFO: clipboard info update + * @QEMU_CLIPBOARD_RESET_SERIAL: reset clipboard serial + * + * Clipboard notify type. + */ +enum QemuClipboardNotifyType { + QEMU_CLIPBOARD_UPDATE_INFO, + QEMU_CLIPBOARD_RESET_SERIAL, +}; + +/** + * struct QemuClipboardNotify + * + * @type: the type of event. + * @info: a QemuClipboardInfo event. + * + * Clipboard notify data. + */ +struct QemuClipboardNotify { + QemuClipboardNotifyType type; + union { + QemuClipboardInfo *info; + }; +}; + +/** * struct QemuClipboardInfo * * @refcount: reference counter. * @owner: clipboard owner. * @selection: clipboard selection. * @types: clipboard data array (one entry per type). + * @has_serial: whether @serial is available. + * @serial: the grab serial counter. * * Clipboard content data and metadata. */ @@ -81,6 +113,8 @@ struct QemuClipboardInfo { uint32_t refcount; QemuClipboardPeer *owner; QemuClipboardSelection selection; + bool has_serial; + uint32_t serial; struct { bool available; bool requested; @@ -141,6 +175,16 @@ void qemu_clipboard_peer_release(QemuClipboardPeer *peer, QemuClipboardInfo *qemu_clipboard_info(QemuClipboardSelection selection); /** + * qemu_clipboard_check_serial + * + * @info: clipboard info. + * @client: whether to check from the client context and priority. + * + * Return TRUE if the @info has a higher serial than the current clipboard. + */ +bool qemu_clipboard_check_serial(QemuClipboardInfo *info, bool client); + +/** * qemu_clipboard_info_new * * @owner: clipboard owner. @@ -189,6 +233,13 @@ void qemu_clipboard_info_unref(QemuClipboardInfo *info); void qemu_clipboard_update(QemuClipboardInfo *info); /** + * qemu_clipboard_reset_serial + * + * Reset the clipboard serial. + */ +void qemu_clipboard_reset_serial(void); + +/** * qemu_clipboard_request * * @info: clipboard info. diff --git a/include/ui/console.h b/include/ui/console.h index 6d678924f6..f590819880 100644 --- a/include/ui/console.h +++ b/include/ui/console.h @@ -108,6 +108,17 @@ struct QemuConsoleClass { #define QEMU_ALLOCATED_FLAG 0x01 #define QEMU_PLACEHOLDER_FLAG 0x02 +typedef struct ScanoutTexture { + uint32_t backing_id; + bool backing_y_0_top; + uint32_t backing_width; + uint32_t backing_height; + uint32_t x; + uint32_t y; + uint32_t width; + uint32_t height; +} ScanoutTexture; + typedef struct DisplaySurface { pixman_format_code_t format; pixman_image_t *image; @@ -178,7 +189,24 @@ typedef struct QemuDmaBuf { bool draw_submitted; } QemuDmaBuf; +enum display_scanout { + SCANOUT_NONE, + SCANOUT_SURFACE, + SCANOUT_TEXTURE, + SCANOUT_DMABUF, +}; + +typedef struct DisplayScanout { + enum display_scanout kind; + union { + /* DisplaySurface *surface; is kept in QemuConsole */ + ScanoutTexture texture; + QemuDmaBuf *dmabuf; + }; +} DisplayScanout; + typedef struct DisplayState DisplayState; +typedef struct DisplayGLCtx DisplayGLCtx; typedef struct DisplayChangeListenerOps { const char *dpy_name; @@ -214,16 +242,6 @@ typedef struct DisplayChangeListenerOps { QEMUCursor *cursor); /* required if GL */ - QEMUGLContext (*dpy_gl_ctx_create)(DisplayChangeListener *dcl, - QEMUGLParams *params); - /* required if GL */ - void (*dpy_gl_ctx_destroy)(DisplayChangeListener *dcl, - QEMUGLContext ctx); - /* required if GL */ - int (*dpy_gl_ctx_make_current)(DisplayChangeListener *dcl, - QEMUGLContext ctx); - - /* required if GL */ void (*dpy_gl_scanout_disable)(DisplayChangeListener *dcl); /* required if GL */ void (*dpy_gl_scanout_texture)(DisplayChangeListener *dcl, @@ -263,6 +281,26 @@ struct DisplayChangeListener { QLIST_ENTRY(DisplayChangeListener) next; }; +typedef struct DisplayGLCtxOps { + /* + * We only check if the GLCtx is compatible with a DCL via ops. A natural + * evolution of this would be a callback to check some runtime requirements + * and allow various DCL kinds. + */ + const DisplayChangeListenerOps *compatible_dcl; + + QEMUGLContext (*dpy_gl_ctx_create)(DisplayGLCtx *dgc, + QEMUGLParams *params); + void (*dpy_gl_ctx_destroy)(DisplayGLCtx *dgc, + QEMUGLContext ctx); + int (*dpy_gl_ctx_make_current)(DisplayGLCtx *dgc, + QEMUGLContext ctx); +} DisplayGLCtxOps; + +struct DisplayGLCtx { + const DisplayGLCtxOps *ops; +}; + DisplayState *init_displaystate(void); DisplaySurface *qemu_create_displaysurface_from(int width, int height, pixman_format_code_t format, @@ -292,7 +330,7 @@ void unregister_displaychangelistener(DisplayChangeListener *dcl); bool dpy_ui_info_supported(QemuConsole *con); const QemuUIInfo *dpy_get_ui_info(const QemuConsole *con); -int dpy_set_ui_info(QemuConsole *con, QemuUIInfo *info); +int dpy_set_ui_info(QemuConsole *con, QemuUIInfo *info, bool delay); void dpy_gfx_update(QemuConsole *con, int x, int y, int w, int h); void dpy_gfx_update_full(QemuConsole *con); @@ -391,7 +429,6 @@ typedef struct GraphicHwOps { void (*update_interval)(void *opaque, uint64_t interval); int (*ui_info)(void *opaque, uint32_t head, QemuUIInfo *info); void (*gl_block)(void *opaque, bool block); - void (*gl_flushed)(void *opaque); } GraphicHwOps; QemuConsole *graphic_console_init(DeviceState *dev, uint32_t head, @@ -407,10 +444,11 @@ void graphic_hw_update_done(QemuConsole *con); void graphic_hw_invalidate(QemuConsole *con); void graphic_hw_text_update(QemuConsole *con, console_ch_t *chardata); void graphic_hw_gl_block(QemuConsole *con, bool block); -void graphic_hw_gl_flushed(QemuConsole *con); void qemu_console_early_init(void); +void qemu_console_set_display_gl_ctx(QemuConsole *con, DisplayGLCtx *ctx); + QemuConsole *qemu_console_lookup_by_index(unsigned int index); QemuConsole *qemu_console_lookup_by_device(DeviceState *dev, uint32_t head); QemuConsole *qemu_console_lookup_by_device_name(const char *device_id, @@ -484,4 +522,10 @@ int index_from_key(const char *key, size_t key_length); int udmabuf_fd(void); #endif +/* util.c */ +bool qemu_console_fill_device_address(QemuConsole *con, + char *device_address, + size_t size, + Error **errp); + #endif diff --git a/include/ui/dbus-display.h b/include/ui/dbus-display.h new file mode 100644 index 0000000000..88f153c237 --- /dev/null +++ b/include/ui/dbus-display.h @@ -0,0 +1,17 @@ +#ifndef DBUS_DISPLAY_H_ +#define DBUS_DISPLAY_H_ + +#include "qapi/error.h" +#include "ui/dbus-module.h" + +static inline bool qemu_using_dbus_display(Error **errp) +{ + if (!using_dbus_display) { + error_set(errp, ERROR_CLASS_DEVICE_NOT_ACTIVE, + "D-Bus display is not in use"); + return false; + } + return true; +} + +#endif /* DBUS_DISPLAY_H_ */ diff --git a/include/ui/dbus-module.h b/include/ui/dbus-module.h new file mode 100644 index 0000000000..ace4a17a5c --- /dev/null +++ b/include/ui/dbus-module.h @@ -0,0 +1,11 @@ +#ifndef DBUS_MODULE_H_ +#define DBUS_MODULE_H_ + +struct QemuDBusDisplayOps { + bool (*add_client)(int csock, Error **errp); +}; + +extern int using_dbus_display; +extern struct QemuDBusDisplayOps qemu_dbus_display; + +#endif /* DBUS_MODULE_H_*/ diff --git a/include/ui/egl-context.h b/include/ui/egl-context.h index 9374fe41e3..c2761d747a 100644 --- a/include/ui/egl-context.h +++ b/include/ui/egl-context.h @@ -4,10 +4,10 @@ #include "ui/console.h" #include "ui/egl-helpers.h" -QEMUGLContext qemu_egl_create_context(DisplayChangeListener *dcl, +QEMUGLContext qemu_egl_create_context(DisplayGLCtx *dgc, QEMUGLParams *params); -void qemu_egl_destroy_context(DisplayChangeListener *dcl, QEMUGLContext ctx); -int qemu_egl_make_context_current(DisplayChangeListener *dcl, +void qemu_egl_destroy_context(DisplayGLCtx *dgc, QEMUGLContext ctx); +int qemu_egl_make_context_current(DisplayGLCtx *dgc, QEMUGLContext ctx); #endif /* EGL_CONTEXT_H */ diff --git a/include/ui/gtk.h b/include/ui/gtk.h index 7d22affd38..101b147d1b 100644 --- a/include/ui/gtk.h +++ b/include/ui/gtk.h @@ -35,6 +35,7 @@ typedef struct GtkDisplayState GtkDisplayState; typedef struct VirtualGfxConsole { GtkWidget *drawing_area; + DisplayGLCtx dgc; DisplayChangeListener dcl; QKbdState *kbd; DisplaySurface *ds; @@ -165,7 +166,7 @@ void gd_egl_update(DisplayChangeListener *dcl, void gd_egl_refresh(DisplayChangeListener *dcl); void gd_egl_switch(DisplayChangeListener *dcl, DisplaySurface *surface); -QEMUGLContext gd_egl_create_context(DisplayChangeListener *dcl, +QEMUGLContext gd_egl_create_context(DisplayGLCtx *dgc, QEMUGLParams *params); void gd_egl_scanout_disable(DisplayChangeListener *dcl); void gd_egl_scanout_texture(DisplayChangeListener *dcl, @@ -187,7 +188,7 @@ void gd_egl_flush(DisplayChangeListener *dcl, void gd_egl_scanout_flush(DisplayChangeListener *dcl, uint32_t x, uint32_t y, uint32_t w, uint32_t h); void gtk_egl_init(DisplayGLMode mode); -int gd_egl_make_current(DisplayChangeListener *dcl, +int gd_egl_make_current(DisplayGLCtx *dgc, QEMUGLContext ctx); /* ui/gtk-gl-area.c */ @@ -198,9 +199,9 @@ void gd_gl_area_update(DisplayChangeListener *dcl, void gd_gl_area_refresh(DisplayChangeListener *dcl); void gd_gl_area_switch(DisplayChangeListener *dcl, DisplaySurface *surface); -QEMUGLContext gd_gl_area_create_context(DisplayChangeListener *dcl, +QEMUGLContext gd_gl_area_create_context(DisplayGLCtx *dgc, QEMUGLParams *params); -void gd_gl_area_destroy_context(DisplayChangeListener *dcl, +void gd_gl_area_destroy_context(DisplayGLCtx *dgc, QEMUGLContext ctx); void gd_gl_area_scanout_dmabuf(DisplayChangeListener *dcl, QemuDmaBuf *dmabuf); @@ -215,7 +216,7 @@ void gd_gl_area_scanout_disable(DisplayChangeListener *dcl); void gd_gl_area_scanout_flush(DisplayChangeListener *dcl, uint32_t x, uint32_t y, uint32_t w, uint32_t h); void gtk_gl_area_init(void); -int gd_gl_area_make_current(DisplayChangeListener *dcl, +int gd_gl_area_make_current(DisplayGLCtx *dgc, QEMUGLContext ctx); /* gtk-clipboard.c */ diff --git a/include/ui/sdl2.h b/include/ui/sdl2.h index f85c117a78..71bcf7ebda 100644 --- a/include/ui/sdl2.h +++ b/include/ui/sdl2.h @@ -16,6 +16,7 @@ #endif struct sdl2_console { + DisplayGLCtx dgc; DisplayChangeListener dcl; DisplaySurface *surface; DisplayOptions *opts; @@ -65,10 +66,10 @@ void sdl2_gl_switch(DisplayChangeListener *dcl, void sdl2_gl_refresh(DisplayChangeListener *dcl); void sdl2_gl_redraw(struct sdl2_console *scon); -QEMUGLContext sdl2_gl_create_context(DisplayChangeListener *dcl, +QEMUGLContext sdl2_gl_create_context(DisplayGLCtx *dgc, QEMUGLParams *params); -void sdl2_gl_destroy_context(DisplayChangeListener *dcl, QEMUGLContext ctx); -int sdl2_gl_make_context_current(DisplayChangeListener *dcl, +void sdl2_gl_destroy_context(DisplayGLCtx *dgc, QEMUGLContext ctx); +int sdl2_gl_make_context_current(DisplayGLCtx *dgc, QEMUGLContext ctx); void sdl2_gl_scanout_disable(DisplayChangeListener *dcl); diff --git a/include/ui/spice-display.h b/include/ui/spice-display.h index ed298d58f0..e271e011da 100644 --- a/include/ui/spice-display.h +++ b/include/ui/spice-display.h @@ -86,6 +86,7 @@ typedef struct SimpleSpiceCursor SimpleSpiceCursor; struct SimpleSpiceDisplay { DisplaySurface *ds; + DisplayGLCtx dgc; DisplayChangeListener dcl; void *buf; int bufsize; @@ -183,8 +184,4 @@ void qemu_spice_display_start(void); void qemu_spice_display_stop(void); int qemu_spice_display_is_running(SimpleSpiceDisplay *ssd); -bool qemu_spice_fill_device_address(QemuConsole *con, - char *device_address, - size_t size); - #endif diff --git a/meson.build b/meson.build index f45ecf31bd..73d4b241df 100644 --- a/meson.build +++ b/meson.build @@ -404,14 +404,16 @@ endif add_project_arguments(config_host['GLIB_CFLAGS'].split(), native: false, language: ['c', 'cpp', 'objc']) glib = declare_dependency(compile_args: config_host['GLIB_CFLAGS'].split(), - link_args: config_host['GLIB_LIBS'].split()) + link_args: config_host['GLIB_LIBS'].split(), + version: config_host['GLIB_VERSION']) # override glib dep with the configure results (for subprojects) meson.override_dependency('glib-2.0', glib) gio = not_found if 'CONFIG_GIO' in config_host gio = declare_dependency(compile_args: config_host['GIO_CFLAGS'].split(), - link_args: config_host['GIO_LIBS'].split()) + link_args: config_host['GIO_LIBS'].split(), + version: config_host['GLIB_VERSION']) endif lttng = not_found if 'ust' in get_option('trace_backends') @@ -1395,6 +1397,15 @@ endif have_host_block_device = (targetos != 'darwin' or cc.has_header('IOKit/storage/IOMedia.h')) +dbus_display = false +if not get_option('dbus_display').disabled() + # FIXME enable_modules shouldn't be necessary, but: https://github.com/mesonbuild/meson/issues/8333 + dbus_display = gio.version().version_compare('>=2.64') and config_host.has_key('GDBUS_CODEGEN') and enable_modules + if get_option('dbus_display').enabled() and not dbus_display + error('Requirements missing to enable -display dbus (glib>=2.64 && --enable-modules)') + endif +endif + have_virtfs = (targetos == 'linux' and have_system and libattr.found() and @@ -1497,8 +1508,14 @@ config_host_data.set('CONFIG_ZSTD', zstd.found()) config_host_data.set('CONFIG_FUSE', fuse.found()) config_host_data.set('CONFIG_FUSE_LSEEK', fuse_lseek.found()) config_host_data.set('CONFIG_SPICE_PROTOCOL', spice_protocol.found()) +if spice_protocol.found() +config_host_data.set('CONFIG_SPICE_PROTOCOL_MAJOR', spice_protocol.version().split('.')[0]) +config_host_data.set('CONFIG_SPICE_PROTOCOL_MINOR', spice_protocol.version().split('.')[1]) +config_host_data.set('CONFIG_SPICE_PROTOCOL_MICRO', spice_protocol.version().split('.')[2]) +endif config_host_data.set('CONFIG_SPICE', spice.found()) config_host_data.set('CONFIG_X11', x11.found()) +config_host_data.set('CONFIG_DBUS_DISPLAY', dbus_display) config_host_data.set('CONFIG_CFI', get_option('cfi')) config_host_data.set('CONFIG_SELINUX', selinux.found()) config_host_data.set('QEMU_VERSION', '"@0@"'.format(meson.project_version())) @@ -3222,6 +3239,7 @@ summary_info += {'Trace backends': ','.join(get_option('trace_backends'))} if 'simple' in get_option('trace_backends') summary_info += {'Trace output file': get_option('trace_file') + '-<pid>'} endif +summary_info += {'D-Bus display': dbus_display} summary_info += {'QOM debugging': config_host.has_key('CONFIG_QOM_CAST_DEBUG')} summary_info += {'vhost-kernel support': config_host.has_key('CONFIG_VHOST_KERNEL')} summary_info += {'vhost-net support': config_host.has_key('CONFIG_VHOST_NET')} diff --git a/meson_options.txt b/meson_options.txt index 4114bfcaa4..921967eddb 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -66,6 +66,8 @@ option('cfi_debug', type: 'boolean', value: 'false', description: 'Verbose errors in case of CFI violation') option('multiprocess', type: 'feature', value: 'auto', description: 'Out of process device emulation support') +option('dbus_display', type: 'feature', value: 'auto', + description: '-display dbus support') option('attr', type : 'feature', value : 'auto', description: 'attr/xattr support') diff --git a/monitor/qmp-cmds.c b/monitor/qmp-cmds.c index 343353e27a..14e3beeaaf 100644 --- a/monitor/qmp-cmds.c +++ b/monitor/qmp-cmds.c @@ -24,6 +24,7 @@ #include "chardev/char.h" #include "ui/qemu-spice.h" #include "ui/console.h" +#include "ui/dbus-display.h" #include "sysemu/kvm.h" #include "sysemu/runstate.h" #include "sysemu/runstate-action.h" @@ -286,6 +287,18 @@ void qmp_add_client(const char *protocol, const char *fdname, vnc_display_add_client(NULL, fd, skipauth); return; #endif +#ifdef CONFIG_DBUS_DISPLAY + } else if (strcmp(protocol, "@dbus-display") == 0) { + if (!qemu_using_dbus_display(errp)) { + close(fd); + return; + } + if (!qemu_dbus_display.add_client(fd, errp)) { + close(fd); + return; + } + return; +#endif } else if ((s = qemu_chr_find(protocol)) != NULL) { if (qemu_chr_add_client(s, fd) < 0) { error_setg(errp, "failed to add client"); diff --git a/qapi/audio.json b/qapi/audio.json index 9cba0df8a4..693e327c6b 100644 --- a/qapi/audio.json +++ b/qapi/audio.json @@ -386,7 +386,7 @@ # Since: 4.0 ## { 'enum': 'AudiodevDriver', - 'data': [ 'none', 'alsa', 'coreaudio', 'dsound', 'jack', 'oss', 'pa', + 'data': [ 'none', 'alsa', 'coreaudio', 'dbus', 'dsound', 'jack', 'oss', 'pa', 'sdl', 'spice', 'wav' ] } ## @@ -412,6 +412,7 @@ 'none': 'AudiodevGenericOptions', 'alsa': 'AudiodevAlsaOptions', 'coreaudio': 'AudiodevCoreaudioOptions', + 'dbus': 'AudiodevGenericOptions', 'dsound': 'AudiodevDsoundOptions', 'jack': 'AudiodevJackOptions', 'oss': 'AudiodevOssOptions', diff --git a/qapi/char.json b/qapi/char.json index f5133a5eeb..7b42151575 100644 --- a/qapi/char.json +++ b/qapi/char.json @@ -359,6 +359,20 @@ 'if': 'CONFIG_SPICE' } ## +# @ChardevDBus: +# +# Configuration info for DBus chardevs. +# +# @name: name of the channel (following docs/spice-port-fqdn.txt) +# +# Since: 7.0 +## +{ 'struct': 'ChardevDBus', + 'data': { 'name': 'str' }, + 'base': 'ChardevCommon', + 'if': 'CONFIG_DBUS_DISPLAY' } + +## # @ChardevVC: # # Configuration info for virtual console chardevs. @@ -422,6 +436,7 @@ # @spicevmc: Since 1.5 # @spiceport: Since 1.5 # @qemu-vdagent: Since 6.1 +# @dbus: Since 7.0 # @vc: v1.5 # @ringbuf: Since 1.6 # @memory: Since 1.5 @@ -447,6 +462,7 @@ { 'name': 'spicevmc', 'if': 'CONFIG_SPICE' }, { 'name': 'spiceport', 'if': 'CONFIG_SPICE' }, { 'name': 'qemu-vdagent', 'if': 'CONFIG_SPICE_PROTOCOL' }, + { 'name': 'dbus', 'if': 'CONFIG_DBUS_DISPLAY' }, 'vc', 'ringbuf', # next one is just for compatibility @@ -536,6 +552,15 @@ 'if': 'CONFIG_SPICE_PROTOCOL' } ## +# @ChardevDBusWrapper: +# +# Since: 7.0 +## +{ 'struct': 'ChardevDBusWrapper', + 'data': { 'data': 'ChardevDBus' }, + 'if': 'CONFIG_DBUS_DISPLAY' } + +## # @ChardevVCWrapper: # # Since: 1.5 @@ -582,6 +607,8 @@ 'if': 'CONFIG_SPICE' }, 'qemu-vdagent': { 'type': 'ChardevQemuVDAgentWrapper', 'if': 'CONFIG_SPICE_PROTOCOL' }, + 'dbus': { 'type': 'ChardevDBusWrapper', + 'if': 'CONFIG_DBUS_DISPLAY' }, 'vc': 'ChardevVCWrapper', 'ringbuf': 'ChardevRingbufWrapper', # next one is just for compatibility diff --git a/qapi/misc.json b/qapi/misc.json index 358548abe1..e8054f415b 100644 --- a/qapi/misc.json +++ b/qapi/misc.json @@ -14,8 +14,8 @@ # Allow client connections for VNC, Spice and socket based # character devices to be passed in to QEMU via SCM_RIGHTS. # -# @protocol: protocol name. Valid names are "vnc", "spice" or the -# name of a character device (eg. from -chardev id=XXXX) +# @protocol: protocol name. Valid names are "vnc", "spice", "@dbus-display" or +# the name of a character device (eg. from -chardev id=XXXX) # # @fdname: file descriptor name previously passed via 'getfd' command # diff --git a/qapi/ui.json b/qapi/ui.json index d7567ac866..2b4371da37 100644 --- a/qapi/ui.json +++ b/qapi/ui.json @@ -1121,6 +1121,30 @@ { 'struct' : 'DisplayEGLHeadless', 'data' : { '*rendernode' : 'str' } } +## +# @DisplayDBus: +# +# DBus display options. +# +# @addr: The D-Bus bus address (default to the session bus). +# +# @rendernode: Which DRM render node should be used. Default is the first +# available node on the host. +# +# @p2p: Whether to use peer-to-peer connections (accepted through +# ``add_client``). +# +# @audiodev: Use the specified DBus audiodev to export audio. +# +# Since: 7.0 +# +## +{ 'struct' : 'DisplayDBus', + 'data' : { '*rendernode' : 'str', + '*addr': 'str', + '*p2p': 'bool', + '*audiodev': 'str' } } + ## # @DisplayGLMode: # @@ -1186,6 +1210,8 @@ # application to connect to it. The server will redirect # the serial console and QEMU monitors. (Since 4.0) # +# @dbus: Start a D-Bus service for the display. (Since 7.0) +# # Since: 2.12 # ## @@ -1199,7 +1225,10 @@ 'if': { 'all': ['CONFIG_OPENGL', 'CONFIG_GBM'] } }, { 'name': 'curses', 'if': 'CONFIG_CURSES' }, { 'name': 'cocoa', 'if': 'CONFIG_COCOA' }, - { 'name': 'spice-app', 'if': 'CONFIG_SPICE'} ] } + { 'name': 'spice-app', 'if': 'CONFIG_SPICE' }, + { 'name': 'dbus', 'if': 'CONFIG_DBUS_DISPLAY' } + ] +} ## # @DisplayOptions: @@ -1227,7 +1256,8 @@ 'gtk': { 'type': 'DisplayGTK', 'if': 'CONFIG_GTK' }, 'curses': { 'type': 'DisplayCurses', 'if': 'CONFIG_CURSES' }, 'egl-headless': { 'type': 'DisplayEGLHeadless', - 'if': { 'all': ['CONFIG_OPENGL', 'CONFIG_GBM'] } } + 'if': { 'all': ['CONFIG_OPENGL', 'CONFIG_GBM'] } }, + 'dbus': { 'type': 'DisplayDBus', 'if': 'CONFIG_DBUS_DISPLAY' } } } diff --git a/qemu-options.hx b/qemu-options.hx index 489b58e151..7d47510947 100644 --- a/qemu-options.hx +++ b/qemu-options.hx @@ -660,6 +660,9 @@ DEF("audiodev", HAS_ARG, QEMU_OPTION_audiodev, #ifdef CONFIG_SPICE "-audiodev spice,id=id[,prop[=value][,...]]\n" #endif +#ifdef CONFIG_DBUS_DISPLAY + "-audiodev dbus,id=id[,prop[=value][,...]]\n" +#endif "-audiodev wav,id=id[,prop[=value][,...]]\n" " path= path of wav file to record\n", QEMU_ARCH_ALL) @@ -1863,6 +1866,10 @@ DEF("display", HAS_ARG, QEMU_OPTION_display, #if defined(CONFIG_OPENGL) "-display egl-headless[,rendernode=<file>]\n" #endif +#if defined(CONFIG_DBUS_DISPLAY) + "-display dbus[,addr=<dbusaddr>]\n" + " [,gl=on|core|es|off][,rendernode=<file>]\n" +#endif "-display none\n" " select display backend type\n" " The default display is equivalent to\n " @@ -1889,6 +1896,19 @@ SRST application. The Spice server will redirect the serial consoles and QEMU monitors. (Since 4.0) + ``dbus`` + Export the display over D-Bus interfaces. (Since 7.0) + + The connection is registered with the "org.qemu" name (and queued when + already owned). + + ``addr=<dbusaddr>`` : D-Bus bus address to connect to. + + ``p2p=yes|no`` : Use peer-to-peer connection, accepted via QMP ``add_client``. + + ``gl=on|off|core|es`` : Use OpenGL for rendering (the D-Bus interface + will share framebuffers with DMABUF file descriptors). + ``sdl`` Display video output via SDL (usually in a separate graphics window; see the SDL documentation for other possibilities). diff --git a/scripts/meson-buildoptions.sh b/scripts/meson-buildoptions.sh index ae8f18edc2..50bd7bed4d 100644 --- a/scripts/meson-buildoptions.sh +++ b/scripts/meson-buildoptions.sh @@ -33,6 +33,7 @@ meson_options_help() { printf "%s\n" ' coreaudio CoreAudio sound support' printf "%s\n" ' curl CURL block device driver' printf "%s\n" ' curses curses UI' + printf "%s\n" ' dbus-display -display dbus support' printf "%s\n" ' docs Documentations build support' printf "%s\n" ' dsound DirectSound sound support' printf "%s\n" ' fuse FUSE block device export' @@ -131,6 +132,8 @@ _meson_option_parse() { --disable-curl) printf "%s" -Dcurl=disabled ;; --enable-curses) printf "%s" -Dcurses=enabled ;; --disable-curses) printf "%s" -Dcurses=disabled ;; + --enable-dbus-display) printf "%s" -Ddbus_display=enabled ;; + --disable-dbus-display) printf "%s" -Ddbus_display=disabled ;; --enable-docs) printf "%s" -Ddocs=enabled ;; --disable-docs) printf "%s" -Ddocs=disabled ;; --enable-dsound) printf "%s" -Ddsound=enabled ;; diff --git a/scripts/modinfo-collect.py b/scripts/modinfo-collect.py index 4acb188c3e..61b90688c6 100755 --- a/scripts/modinfo-collect.py +++ b/scripts/modinfo-collect.py @@ -51,6 +51,9 @@ def main(args): with open('compile_commands.json') as f: compile_commands = json.load(f) for src in args: + if not src.endswith('.c'): + print("MODINFO_DEBUG skip %s" % src) + continue print("MODINFO_DEBUG src %s" % src) command = find_command(src, target, compile_commands) cmdline = process_command(src, command) diff --git a/tests/qtest/dbus-display-test.c b/tests/qtest/dbus-display-test.c new file mode 100644 index 0000000000..43c77aff04 --- /dev/null +++ b/tests/qtest/dbus-display-test.c @@ -0,0 +1,257 @@ +#include "qemu/osdep.h" +#include "qemu/dbus.h" +#include <gio/gio.h> +#include <gio/gunixfdlist.h> +#include "libqos/libqtest.h" +#include "qemu-common.h" +#include "dbus-display1.h" + +static GDBusConnection* +test_dbus_p2p_from_fd(int fd) +{ + g_autoptr(GError) err = NULL; + g_autoptr(GSocket) socket = NULL; + g_autoptr(GSocketConnection) socketc = NULL; + GDBusConnection *conn; + + socket = g_socket_new_from_fd(fd, &err); + g_assert_no_error(err); + + socketc = g_socket_connection_factory_create_connection(socket); + g_assert(socketc != NULL); + + conn = g_dbus_connection_new_sync( + G_IO_STREAM(socketc), NULL, + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | + G_DBUS_CONNECTION_FLAGS_DELAY_MESSAGE_PROCESSING, + NULL, NULL, &err); + g_assert_no_error(err); + + return conn; +} + +static void +test_setup(QTestState **qts, GDBusConnection **conn) +{ + int pair[2]; + + *qts = qtest_init("-display dbus,p2p=yes -name dbus-test"); + + g_assert_cmpint(socketpair(AF_UNIX, SOCK_STREAM, 0, pair), ==, 0); + + qtest_qmp_add_client(*qts, "@dbus-display", pair[1]); + + *conn = test_dbus_p2p_from_fd(pair[0]); + g_dbus_connection_start_message_processing(*conn); +} + +static void +test_dbus_display_vm(void) +{ + g_autoptr(GError) err = NULL; + g_autoptr(GDBusConnection) conn = NULL; + g_autoptr(QemuDBusDisplay1VMProxy) vm = NULL; + QTestState *qts = NULL; + + test_setup(&qts, &conn); + + vm = QEMU_DBUS_DISPLAY1_VM_PROXY( + qemu_dbus_display1_vm_proxy_new_sync( + conn, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + DBUS_DISPLAY1_ROOT "/VM", + NULL, + &err)); + g_assert_no_error(err); + + g_assert_cmpstr( + qemu_dbus_display1_vm_get_name(QEMU_DBUS_DISPLAY1_VM(vm)), + ==, + "dbus-test"); + qtest_quit(qts); +} + +typedef struct TestDBusConsoleRegister { + GMainLoop *loop; + GThread *thread; + GDBusConnection *listener_conn; + GDBusObjectManagerServer *server; +} TestDBusConsoleRegister; + +static gboolean listener_handle_scanout( + QemuDBusDisplay1Listener *object, + GDBusMethodInvocation *invocation, + guint arg_width, + guint arg_height, + guint arg_stride, + guint arg_pixman_format, + GVariant *arg_data, + TestDBusConsoleRegister *test) +{ + g_main_loop_quit(test->loop); + + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +test_dbus_console_setup_listener(TestDBusConsoleRegister *test) +{ + g_autoptr(GDBusObjectSkeleton) listener = NULL; + g_autoptr(QemuDBusDisplay1ListenerSkeleton) iface = NULL; + + test->server = g_dbus_object_manager_server_new(DBUS_DISPLAY1_ROOT); + listener = g_dbus_object_skeleton_new(DBUS_DISPLAY1_ROOT "/Listener"); + iface = QEMU_DBUS_DISPLAY1_LISTENER_SKELETON( + qemu_dbus_display1_listener_skeleton_new()); + g_object_connect(iface, + "signal::handle-scanout", listener_handle_scanout, test, + NULL); + g_dbus_object_skeleton_add_interface(listener, + G_DBUS_INTERFACE_SKELETON(iface)); + g_dbus_object_manager_server_export(test->server, listener); + g_dbus_object_manager_server_set_connection(test->server, + test->listener_conn); + + g_dbus_connection_start_message_processing(test->listener_conn); +} + +static void +test_dbus_console_registered(GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + TestDBusConsoleRegister *test = user_data; + g_autoptr(GError) err = NULL; + + qemu_dbus_display1_console_call_register_listener_finish( + QEMU_DBUS_DISPLAY1_CONSOLE(source_object), + NULL, res, &err); + g_assert_no_error(err); + + test->listener_conn = g_thread_join(test->thread); + test_dbus_console_setup_listener(test); +} + +static gpointer +test_dbus_p2p_server_setup_thread(gpointer data) +{ + return test_dbus_p2p_from_fd(GPOINTER_TO_INT(data)); +} + +static void +test_dbus_display_console(void) +{ + g_autoptr(GError) err = NULL; + g_autoptr(GDBusConnection) conn = NULL; + g_autoptr(QemuDBusDisplay1ConsoleProxy) console = NULL; + g_autoptr(GUnixFDList) fd_list = NULL; + g_autoptr(GMainLoop) loop = NULL; + QTestState *qts = NULL; + int pair[2], idx; + TestDBusConsoleRegister test; + + test_setup(&qts, &conn); + + g_assert_cmpint(socketpair(AF_UNIX, SOCK_STREAM, 0, pair), ==, 0); + fd_list = g_unix_fd_list_new(); + idx = g_unix_fd_list_append(fd_list, pair[1], NULL); + + console = QEMU_DBUS_DISPLAY1_CONSOLE_PROXY( + qemu_dbus_display1_console_proxy_new_sync( + conn, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "/org/qemu/Display1/Console_0", + NULL, + &err)); + g_assert_no_error(err); + + test.loop = loop = g_main_loop_new(NULL, FALSE); + test.thread = g_thread_new(NULL, test_dbus_p2p_server_setup_thread, + GINT_TO_POINTER(pair[0])); + + qemu_dbus_display1_console_call_register_listener( + QEMU_DBUS_DISPLAY1_CONSOLE(console), + g_variant_new_handle(idx), + G_DBUS_CALL_FLAGS_NONE, + -1, + fd_list, + NULL, + test_dbus_console_registered, + &test); + + g_main_loop_run(loop); + + g_clear_object(&test.server); + g_clear_object(&test.listener_conn); + qtest_quit(qts); +} + +static void +test_dbus_display_keyboard(void) +{ + g_autoptr(GError) err = NULL; + g_autoptr(GDBusConnection) conn = NULL; + g_autoptr(QemuDBusDisplay1KeyboardProxy) keyboard = NULL; + QTestState *qts = NULL; + + test_setup(&qts, &conn); + + keyboard = QEMU_DBUS_DISPLAY1_KEYBOARD_PROXY( + qemu_dbus_display1_keyboard_proxy_new_sync( + conn, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "/org/qemu/Display1/Console_0", + NULL, + &err)); + g_assert_no_error(err); + + + g_assert_cmpint(qtest_inb(qts, 0x64) & 0x1, ==, 0); + g_assert_cmpint(qtest_inb(qts, 0x60), ==, 0); + + qemu_dbus_display1_keyboard_call_press_sync( + QEMU_DBUS_DISPLAY1_KEYBOARD(keyboard), + 0x1C, /* qnum enter */ + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + &err); + g_assert_no_error(err); + + /* may be should wait for interrupt? */ + g_assert_cmpint(qtest_inb(qts, 0x64) & 0x1, ==, 1); + g_assert_cmpint(qtest_inb(qts, 0x60), ==, 0x5A); /* scan code 2 enter */ + + qemu_dbus_display1_keyboard_call_release_sync( + QEMU_DBUS_DISPLAY1_KEYBOARD(keyboard), + 0x1C, /* qnum enter */ + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + &err); + g_assert_no_error(err); + + g_assert_cmpint(qtest_inb(qts, 0x64) & 0x1, ==, 1); + g_assert_cmpint(qtest_inb(qts, 0x60), ==, 0xF0); /* scan code 2 release */ + g_assert_cmpint(qtest_inb(qts, 0x60), ==, 0x5A); /* scan code 2 enter */ + + g_assert_cmpint(qemu_dbus_display1_keyboard_get_modifiers( + QEMU_DBUS_DISPLAY1_KEYBOARD(keyboard)), ==, 0); + + qtest_quit(qts); +} + +int +main(int argc, char **argv) +{ + g_test_init(&argc, &argv, NULL); + + qtest_add_func("/dbus-display/vm", test_dbus_display_vm); + qtest_add_func("/dbus-display/console", test_dbus_display_console); + qtest_add_func("/dbus-display/keyboard", test_dbus_display_keyboard); + + return g_test_run(); +} diff --git a/tests/qtest/dbus-vmstate1.xml b/tests/qtest/dbus-vmstate1.xml deleted file mode 100644 index cc8563be4c..0000000000 --- a/tests/qtest/dbus-vmstate1.xml +++ /dev/null @@ -1,12 +0,0 @@ -<?xml version="1.0"?> -<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd"> - <interface name="org.qemu.VMState1"> - <property name="Id" type="s" access="read"/> - <method name="Load"> - <arg type="ay" name="data" direction="in"/> - </method> - <method name="Save"> - <arg type="ay" name="data" direction="out"/> - </method> - </interface> -</node> diff --git a/tests/qtest/libqos/libqtest.h b/tests/qtest/libqos/libqtest.h index dff6b31cf0..a6d38d7ef7 100644 --- a/tests/qtest/libqos/libqtest.h +++ b/tests/qtest/libqos/libqtest.h @@ -745,6 +745,16 @@ void qtest_qmp_device_add(QTestState *qts, const char *driver, const char *id, const char *fmt, ...) GCC_FMT_ATTR(4, 5); /** + * qtest_qmp_add_client: + * @qts: QTestState instance to operate on + * @protocol: the protocol to add to + * @fd: the client file-descriptor + * + * Call QMP ``getfd`` followed by ``add_client`` with the given @fd. + */ +void qtest_qmp_add_client(QTestState *qts, const char *protocol, int fd); + +/** * qtest_qmp_device_del: * @qts: QTestState instance to operate on * @id: Identification string diff --git a/tests/qtest/libqtest.c b/tests/qtest/libqtest.c index 65ed949685..a68326caae 100644 --- a/tests/qtest/libqtest.c +++ b/tests/qtest/libqtest.c @@ -1453,6 +1453,25 @@ void qtest_qmp_device_add(QTestState *qts, const char *driver, const char *id, qobject_unref(args); } +void qtest_qmp_add_client(QTestState *qts, const char *protocol, int fd) +{ + QDict *resp; + + resp = qtest_qmp_fds(qts, &fd, 1, "{'execute': 'getfd'," + "'arguments': {'fdname': 'fdname'}}"); + g_assert(resp); + g_assert(!qdict_haskey(resp, "event")); /* We don't expect any events */ + g_assert(!qdict_haskey(resp, "error")); + qobject_unref(resp); + + resp = qtest_qmp( + qts, "{'execute': 'add_client'," + "'arguments': {'protocol': %s, 'fdname': 'fdname'}}", protocol); + g_assert(resp); + g_assert(!qdict_haskey(resp, "event")); /* We don't expect any events */ + g_assert(!qdict_haskey(resp, "error")); + qobject_unref(resp); +} /* * Generic hot-unplugging test via the device_del QMP command. diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build index ebeac59b3f..1b2bde6660 100644 --- a/tests/qtest/meson.build +++ b/tests/qtest/meson.build @@ -92,13 +92,17 @@ qtests_i386 = \ 'test-x86-cpuid-compat', 'numa-test'] +if dbus_display + qtests_i386 += ['dbus-display-test'] +endif + dbus_daemon = find_program('dbus-daemon', required: false) if dbus_daemon.found() and config_host.has_key('GDBUS_CODEGEN') # Temporarily disabled due to Patchew failures: #qtests_i386 += ['dbus-vmstate-test'] dbus_vmstate1 = custom_target('dbus-vmstate description', output: ['dbus-vmstate1.h', 'dbus-vmstate1.c'], - input: files('dbus-vmstate1.xml'), + input: meson.source_root() / 'backends/dbus-vmstate1.xml', command: [config_host['GDBUS_CODEGEN'], '@INPUT@', '--interface-prefix', 'org.qemu', @@ -265,6 +269,10 @@ qtests = { 'vmgenid-test': files('boot-sector.c', 'acpi-utils.c'), } +if dbus_display +qtests += {'dbus-display-test': [dbus_display1, gio]} +endif + qtest_executables = {} foreach dir : target_dirs if not dir.endswith('-softmmu') diff --git a/ui/clipboard.c b/ui/clipboard.c index d7b008d62a..82572ea116 100644 --- a/ui/clipboard.c +++ b/ui/clipboard.c @@ -8,7 +8,7 @@ static QemuClipboardInfo *cbinfo[QEMU_CLIPBOARD_SELECTION__COUNT]; void qemu_clipboard_peer_register(QemuClipboardPeer *peer) { - notifier_list_add(&clipboard_notifiers, &peer->update); + notifier_list_add(&clipboard_notifiers, &peer->notifier); } void qemu_clipboard_peer_unregister(QemuClipboardPeer *peer) @@ -18,8 +18,7 @@ void qemu_clipboard_peer_unregister(QemuClipboardPeer *peer) for (i = 0; i < QEMU_CLIPBOARD_SELECTION__COUNT; i++) { qemu_clipboard_peer_release(peer, i); } - - notifier_remove(&peer->update); + notifier_remove(&peer->notifier); } bool qemu_clipboard_peer_owns(QemuClipboardPeer *peer, @@ -42,12 +41,32 @@ void qemu_clipboard_peer_release(QemuClipboardPeer *peer, } } +bool qemu_clipboard_check_serial(QemuClipboardInfo *info, bool client) +{ + if (!info->has_serial || + !cbinfo[info->selection] || + !cbinfo[info->selection]->has_serial) { + return true; + } + + if (client) { + return cbinfo[info->selection]->serial >= info->serial; + } else { + return cbinfo[info->selection]->serial > info->serial; + } +} + void qemu_clipboard_update(QemuClipboardInfo *info) { + QemuClipboardNotify notify = { + .type = QEMU_CLIPBOARD_UPDATE_INFO, + .info = info, + }; g_autoptr(QemuClipboardInfo) old = NULL; + assert(info->selection < QEMU_CLIPBOARD_SELECTION__COUNT); - notifier_list_notify(&clipboard_notifiers, info); + notifier_list_notify(&clipboard_notifiers, ¬ify); old = cbinfo[info->selection]; cbinfo[info->selection] = qemu_clipboard_info_ref(info); @@ -110,6 +129,13 @@ void qemu_clipboard_request(QemuClipboardInfo *info, info->owner->request(info, type); } +void qemu_clipboard_reset_serial(void) +{ + QemuClipboardNotify notify = { .type = QEMU_CLIPBOARD_RESET_SERIAL }; + + notifier_list_notify(&clipboard_notifiers, ¬ify); +} + void qemu_clipboard_set_data(QemuClipboardPeer *peer, QemuClipboardInfo *info, QemuClipboardType type, diff --git a/ui/cocoa.m b/ui/cocoa.m index 68a6302184..69745c483b 100644 --- a/ui/cocoa.m +++ b/ui/cocoa.m @@ -552,7 +552,7 @@ QemuCocoaView *cocoaView; info.width = frameSize.width; info.height = frameSize.height; - dpy_set_ui_info(dcl.con, &info); + dpy_set_ui_info(dcl.con, &info, TRUE); } - (void)viewDidMoveToWindow @@ -1808,14 +1808,12 @@ static void cocoa_clipboard_request(QemuClipboardInfo *info, static QemuClipboardPeer cbpeer = { .name = "cocoa", - .update = { .notify = cocoa_clipboard_notify }, + .notifier = { .notify = cocoa_clipboard_notify }, .request = cocoa_clipboard_request }; -static void cocoa_clipboard_notify(Notifier *notifier, void *data) +static void cocoa_clipboard_update_info(QemuClipboardInfo *info) { - QemuClipboardInfo *info = data; - if (info->owner == &cbpeer || info->selection != QEMU_CLIPBOARD_SELECTION_CLIPBOARD) { return; } @@ -1831,6 +1829,20 @@ static void cocoa_clipboard_notify(Notifier *notifier, void *data) qemu_event_set(&cbevent); } +static void cocoa_clipboard_notify(Notifier *notifier, void *data) +{ + QemuClipboardNotify *notify = data; + + switch (notify->type) { + case QEMU_CLIPBOARD_UPDATE_INFO: + cocoa_clipboard_update_info(notify->info); + return; + case QEMU_CLIPBOARD_RESET_SERIAL: + /* ignore */ + return; + } +} + static void cocoa_clipboard_request(QemuClipboardInfo *info, QemuClipboardType type) { diff --git a/ui/console.c b/ui/console.c index 29a3e3f0f5..40eebb6d2c 100644 --- a/ui/console.c +++ b/ui/console.c @@ -77,9 +77,11 @@ struct QemuConsole { console_type_t console_type; DisplayState *ds; DisplaySurface *surface; + DisplayScanout scanout; int dcls; - DisplayChangeListener *gl; - bool gl_block; + DisplayGLCtx *gl; + int gl_block; + QEMUTimer *gl_unblock_timer; int window_id; /* Graphic console state. */ @@ -145,6 +147,7 @@ static void dpy_refresh(DisplayState *s); static DisplayState *get_alloc_displaystate(void); static void text_console_update_cursor_timer(void); static void text_console_update_cursor(void *opaque); +static bool displaychangelistener_has_dmabuf(DisplayChangeListener *dcl); static void gui_update(void *opaque) { @@ -233,22 +236,36 @@ void graphic_hw_update(QemuConsole *con) } } -void graphic_hw_gl_block(QemuConsole *con, bool block) +static void graphic_hw_gl_unblock_timer(void *opaque) { - assert(con != NULL); - - con->gl_block = block; - if (con->hw_ops->gl_block) { - con->hw_ops->gl_block(con->hw, block); - } + warn_report("console: no gl-unblock within one second"); } -void graphic_hw_gl_flushed(QemuConsole *con) +void graphic_hw_gl_block(QemuConsole *con, bool block) { + uint64_t timeout; assert(con != NULL); - if (con->hw_ops->gl_flushed) { - con->hw_ops->gl_flushed(con->hw); + if (block) { + con->gl_block++; + } else { + con->gl_block--; + } + assert(con->gl_block >= 0); + if (!con->hw_ops->gl_block) { + return; + } + if ((block && con->gl_block != 1) || (!block && con->gl_block != 0)) { + return; + } + con->hw_ops->gl_block(con->hw, block); + + if (block) { + timeout = qemu_clock_get_ms(QEMU_CLOCK_REALTIME); + timeout += 1000; /* one sec */ + timer_mod(con->gl_unblock_timer, timeout); + } else { + timer_del(con->gl_unblock_timer); } } @@ -466,6 +483,8 @@ static void text_console_resize(QemuConsole *s) TextCell *cells, *c, *c1; int w1, x, y, last_width; + assert(s->scanout.kind == SCANOUT_SURFACE); + last_width = s->width; s->width = surface_width(s->surface) / FONT_WIDTH; s->height = surface_height(s->surface) / FONT_HEIGHT; @@ -1037,6 +1056,48 @@ static void console_putchar(QemuConsole *s, int ch) } } +static void displaychangelistener_display_console(DisplayChangeListener *dcl, + QemuConsole *con) +{ + static const char nodev[] = + "This VM has no graphic display device."; + static DisplaySurface *dummy; + + if (!con) { + if (!dcl->ops->dpy_gfx_switch) { + return; + } + if (!dummy) { + dummy = qemu_create_placeholder_surface(640, 480, nodev); + } + dcl->ops->dpy_gfx_switch(dcl, dummy); + return; + } + + if (con->scanout.kind == SCANOUT_DMABUF && + displaychangelistener_has_dmabuf(dcl)) { + dcl->ops->dpy_gl_scanout_dmabuf(dcl, con->scanout.dmabuf); + } else if (con->scanout.kind == SCANOUT_TEXTURE && + dcl->ops->dpy_gl_scanout_texture) { + dcl->ops->dpy_gl_scanout_texture(dcl, + con->scanout.texture.backing_id, + con->scanout.texture.backing_y_0_top, + con->scanout.texture.backing_width, + con->scanout.texture.backing_height, + con->scanout.texture.x, + con->scanout.texture.y, + con->scanout.texture.width, + con->scanout.texture.height); + } else if (con->scanout.kind == SCANOUT_SURFACE && + dcl->ops->dpy_gfx_switch) { + dcl->ops->dpy_gfx_switch(dcl, con->surface); + } + + dcl->ops->dpy_gfx_update(dcl, 0, 0, + qemu_console_get_width(con, 0), + qemu_console_get_height(con, 0)); +} + void console_select(unsigned int index) { DisplayChangeListener *dcl; @@ -1053,13 +1114,7 @@ void console_select(unsigned int index) if (dcl->con != NULL) { continue; } - if (dcl->ops->dpy_gfx_switch) { - dcl->ops->dpy_gfx_switch(dcl, s->surface); - } - } - if (s->surface) { - dpy_gfx_update(s, 0, 0, surface_width(s->surface), - surface_height(s->surface)); + displaychangelistener_display_console(dcl, s); } } if (ds->have_text) { @@ -1443,24 +1498,36 @@ static bool dpy_compatible_with(QemuConsole *con, return true; } +void qemu_console_set_display_gl_ctx(QemuConsole *con, DisplayGLCtx *gl) +{ + /* display has opengl support */ + assert(con); + if (con->gl) { + error_report("The console already has an OpenGL context."); + exit(1); + } + con->gl = gl; +} + +static bool dpy_gl_compatible_with(QemuConsole *con, DisplayChangeListener *dcl) +{ + if (!con->gl) { + return true; + } + + return con->gl->ops->compatible_dcl == dcl->ops; +} + void register_displaychangelistener(DisplayChangeListener *dcl) { - static const char nodev[] = - "This VM has no graphic display device."; - static DisplaySurface *dummy; QemuConsole *con; assert(!dcl->ds); - if (dcl->ops->dpy_gl_ctx_create) { - /* display has opengl support */ - assert(dcl->con); - if (dcl->con->gl) { - fprintf(stderr, "can't register two opengl displays (%s, %s)\n", - dcl->ops->dpy_name, dcl->con->gl->ops->dpy_name); - exit(1); - } - dcl->con->gl = dcl; + if (dcl->con && !dpy_gl_compatible_with(dcl->con, dcl)) { + error_report("Display %s is incompatible with the GL context", + dcl->ops->dpy_name); + exit(1); } if (dcl->con) { @@ -1477,16 +1544,7 @@ void register_displaychangelistener(DisplayChangeListener *dcl) } else { con = active_console; } - if (dcl->ops->dpy_gfx_switch) { - if (con) { - dcl->ops->dpy_gfx_switch(dcl, con->surface); - } else { - if (!dummy) { - dummy = qemu_create_placeholder_surface(640, 480, nodev); - } - dcl->ops->dpy_gfx_switch(dcl, dummy); - } - } + displaychangelistener_display_console(dcl, con); text_console_update_cursor(NULL); } @@ -1538,7 +1596,7 @@ const QemuUIInfo *dpy_get_ui_info(const QemuConsole *con) return &con->ui_info; } -int dpy_set_ui_info(QemuConsole *con, QemuUIInfo *info) +int dpy_set_ui_info(QemuConsole *con, QemuUIInfo *info, bool delay) { if (con == NULL) { con = active_console; @@ -1558,7 +1616,8 @@ int dpy_set_ui_info(QemuConsole *con, QemuUIInfo *info) * go notify the guest. */ con->ui_info = *info; - timer_mod(con->ui_timer, qemu_clock_get_ms(QEMU_CLOCK_REALTIME) + 1000); + timer_mod(con->ui_timer, + qemu_clock_get_ms(QEMU_CLOCK_REALTIME) + (delay ? 1000 : 0)); return 0; } @@ -1566,13 +1625,9 @@ void dpy_gfx_update(QemuConsole *con, int x, int y, int w, int h) { DisplayState *s = con->ds; DisplayChangeListener *dcl; - int width = w; - int height = h; + int width = qemu_console_get_width(con, x + w); + int height = qemu_console_get_height(con, y + h); - if (con->surface) { - width = surface_width(con->surface); - height = surface_height(con->surface); - } x = MAX(x, 0); y = MAX(y, 0); x = MIN(x, width); @@ -1595,12 +1650,10 @@ void dpy_gfx_update(QemuConsole *con, int x, int y, int w, int h) void dpy_gfx_update_full(QemuConsole *con) { - if (!con->surface) { - return; - } - dpy_gfx_update(con, 0, 0, - surface_width(con->surface), - surface_height(con->surface)); + int w = qemu_console_get_width(con, 0); + int h = qemu_console_get_height(con, 0); + + dpy_gfx_update(con, 0, 0, w, h); } void dpy_gfx_replace_surface(QemuConsole *con, @@ -1627,6 +1680,7 @@ void dpy_gfx_replace_surface(QemuConsole *con, assert(old_surface != surface); + con->scanout.kind = SCANOUT_SURFACE; con->surface = surface; QLIST_FOREACH(dcl, &s->listeners, next) { if (con != (dcl->con ? dcl->con : active_console)) { @@ -1799,8 +1853,15 @@ int dpy_gl_ctx_make_current(QemuConsole *con, QEMUGLContext ctx) void dpy_gl_scanout_disable(QemuConsole *con) { - assert(con->gl); - con->gl->ops->dpy_gl_scanout_disable(con->gl); + DisplayState *s = con->ds; + DisplayChangeListener *dcl; + + if (con->scanout.kind != SCANOUT_SURFACE) { + con->scanout.kind = SCANOUT_NONE; + } + QLIST_FOREACH(dcl, &s->listeners, next) { + dcl->ops->dpy_gl_scanout_disable(dcl); + } } void dpy_gl_scanout_texture(QemuConsole *con, @@ -1811,56 +1872,88 @@ void dpy_gl_scanout_texture(QemuConsole *con, uint32_t x, uint32_t y, uint32_t width, uint32_t height) { - assert(con->gl); - con->gl->ops->dpy_gl_scanout_texture(con->gl, backing_id, + DisplayState *s = con->ds; + DisplayChangeListener *dcl; + + con->scanout.kind = SCANOUT_TEXTURE; + con->scanout.texture = (ScanoutTexture) { + backing_id, backing_y_0_top, backing_width, backing_height, + x, y, width, height + }; + QLIST_FOREACH(dcl, &s->listeners, next) { + dcl->ops->dpy_gl_scanout_texture(dcl, backing_id, backing_y_0_top, backing_width, backing_height, x, y, width, height); + } } void dpy_gl_scanout_dmabuf(QemuConsole *con, QemuDmaBuf *dmabuf) { - assert(con->gl); - con->gl->ops->dpy_gl_scanout_dmabuf(con->gl, dmabuf); + DisplayState *s = con->ds; + DisplayChangeListener *dcl; + + con->scanout.kind = SCANOUT_DMABUF; + con->scanout.dmabuf = dmabuf; + QLIST_FOREACH(dcl, &s->listeners, next) { + dcl->ops->dpy_gl_scanout_dmabuf(dcl, dmabuf); + } } void dpy_gl_cursor_dmabuf(QemuConsole *con, QemuDmaBuf *dmabuf, bool have_hot, uint32_t hot_x, uint32_t hot_y) { - assert(con->gl); + DisplayState *s = con->ds; + DisplayChangeListener *dcl; - if (con->gl->ops->dpy_gl_cursor_dmabuf) { - con->gl->ops->dpy_gl_cursor_dmabuf(con->gl, dmabuf, + QLIST_FOREACH(dcl, &s->listeners, next) { + if (dcl->ops->dpy_gl_cursor_dmabuf) { + dcl->ops->dpy_gl_cursor_dmabuf(dcl, dmabuf, have_hot, hot_x, hot_y); + } } } void dpy_gl_cursor_position(QemuConsole *con, uint32_t pos_x, uint32_t pos_y) { - assert(con->gl); + DisplayState *s = con->ds; + DisplayChangeListener *dcl; - if (con->gl->ops->dpy_gl_cursor_position) { - con->gl->ops->dpy_gl_cursor_position(con->gl, pos_x, pos_y); + QLIST_FOREACH(dcl, &s->listeners, next) { + if (dcl->ops->dpy_gl_cursor_position) { + dcl->ops->dpy_gl_cursor_position(dcl, pos_x, pos_y); + } } } void dpy_gl_release_dmabuf(QemuConsole *con, QemuDmaBuf *dmabuf) { - assert(con->gl); + DisplayState *s = con->ds; + DisplayChangeListener *dcl; - if (con->gl->ops->dpy_gl_release_dmabuf) { - con->gl->ops->dpy_gl_release_dmabuf(con->gl, dmabuf); + QLIST_FOREACH(dcl, &s->listeners, next) { + if (dcl->ops->dpy_gl_release_dmabuf) { + dcl->ops->dpy_gl_release_dmabuf(dcl, dmabuf); + } } } void dpy_gl_update(QemuConsole *con, uint32_t x, uint32_t y, uint32_t w, uint32_t h) { + DisplayState *s = con->ds; + DisplayChangeListener *dcl; + assert(con->gl); - con->gl->ops->dpy_gl_update(con->gl, x, y, w, h); + + graphic_hw_gl_block(con, true); + QLIST_FOREACH(dcl, &s->listeners, next) { + dcl->ops->dpy_gl_update(dcl, x, y, w, h); + } + graphic_hw_gl_block(con, false); } /***********************************************************/ @@ -1929,10 +2022,8 @@ QemuConsole *graphic_console_init(DeviceState *dev, uint32_t head, s = qemu_console_lookup_unused(); if (s) { trace_console_gfx_reuse(s->index); - if (s->surface) { - width = surface_width(s->surface); - height = surface_height(s->surface); - } + width = qemu_console_get_width(s, 0); + height = qemu_console_get_height(s, 0); } else { trace_console_gfx_new(); s = new_console(ds, GRAPHIC_CONSOLE, head); @@ -1947,6 +2038,8 @@ QemuConsole *graphic_console_init(DeviceState *dev, uint32_t head, surface = qemu_create_placeholder_surface(width, height, noinit); dpy_gfx_replace_surface(s, surface); + s->gl_unblock_timer = timer_new_ms(QEMU_CLOCK_REALTIME, + graphic_hw_gl_unblock_timer, s); return s; } @@ -1959,13 +2052,8 @@ void graphic_console_close(QemuConsole *con) static const char unplugged[] = "Guest display has been unplugged"; DisplaySurface *surface; - int width = 640; - int height = 480; - - if (con->surface) { - width = surface_width(con->surface); - height = surface_height(con->surface); - } + int width = qemu_console_get_width(con, 640); + int height = qemu_console_get_height(con, 480); trace_console_gfx_close(con->index); object_property_set_link(OBJECT(con), "device", NULL, &error_abort); @@ -2117,7 +2205,19 @@ int qemu_console_get_width(QemuConsole *con, int fallback) if (con == NULL) { con = active_console; } - return con ? surface_width(con->surface) : fallback; + if (con == NULL) { + return fallback; + } + switch (con->scanout.kind) { + case SCANOUT_DMABUF: + return con->scanout.dmabuf->width; + case SCANOUT_TEXTURE: + return con->scanout.texture.width; + case SCANOUT_SURFACE: + return surface_width(con->surface); + default: + return fallback; + } } int qemu_console_get_height(QemuConsole *con, int fallback) @@ -2125,7 +2225,19 @@ int qemu_console_get_height(QemuConsole *con, int fallback) if (con == NULL) { con = active_console; } - return con ? surface_height(con->surface) : fallback; + if (con == NULL) { + return fallback; + } + switch (con->scanout.kind) { + case SCANOUT_DMABUF: + return con->scanout.dmabuf->height; + case SCANOUT_TEXTURE: + return con->scanout.texture.height; + case SCANOUT_SURFACE: + return surface_height(con->surface); + default: + return fallback; + } } static void vc_chr_accept_input(Chardev *chr) @@ -2191,12 +2303,13 @@ static void text_console_do_init(Chardev *chr, DisplayState *ds) s->total_height = DEFAULT_BACKSCROLL; s->x = 0; s->y = 0; - if (!s->surface) { - if (active_console && active_console->surface) { - g_width = surface_width(active_console->surface); - g_height = surface_height(active_console->surface); + if (s->scanout.kind != SCANOUT_SURFACE) { + if (active_console && active_console->scanout.kind == SCANOUT_SURFACE) { + g_width = qemu_console_get_width(active_console, g_width); + g_height = qemu_console_get_height(active_console, g_height); } s->surface = qemu_create_displaysurface(g_width, g_height); + s->scanout.kind = SCANOUT_SURFACE; } s->hw_ops = &text_console_ops; @@ -2255,6 +2368,7 @@ static void vc_chr_open(Chardev *chr, s = new_console(NULL, TEXT_CONSOLE, 0); } else { s = new_console(NULL, TEXT_CONSOLE_FIXED_SIZE, 0); + s->scanout.kind = SCANOUT_SURFACE; s->surface = qemu_create_displaysurface(width, height); } @@ -2278,13 +2392,13 @@ static void vc_chr_open(Chardev *chr, void qemu_console_resize(QemuConsole *s, int width, int height) { - DisplaySurface *surface; + DisplaySurface *surface = qemu_console_surface(s); assert(s->console_type == GRAPHIC_CONSOLE); - if (s->surface && (s->surface->flags & QEMU_ALLOCATED_FLAG) && - pixman_image_get_width(s->surface->image) == width && - pixman_image_get_height(s->surface->image) == height) { + if (surface && (surface->flags & QEMU_ALLOCATED_FLAG) && + pixman_image_get_width(surface->image) == width && + pixman_image_get_height(surface->image) == height) { return; } @@ -2294,7 +2408,12 @@ void qemu_console_resize(QemuConsole *s, int width, int height) DisplaySurface *qemu_console_surface(QemuConsole *console) { - return console->surface; + switch (console->scanout.kind) { + case SCANOUT_SURFACE: + return console->surface; + default: + return NULL; + } } PixelFormat qemu_default_pixelformat(int bpp) diff --git a/ui/dbus-chardev.c b/ui/dbus-chardev.c new file mode 100644 index 0000000000..940ef937cd --- /dev/null +++ b/ui/dbus-chardev.c @@ -0,0 +1,296 @@ +/* + * QEMU DBus display + * + * Copyright (c) 2021 Marc-André Lureau <marcandre.lureau@redhat.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "qemu/osdep.h" +#include "trace.h" +#include "qapi/error.h" +#include "qemu/config-file.h" +#include "qemu/option.h" + +#include <gio/gunixfdlist.h> + +#include "dbus.h" + +static char * +dbus_display_chardev_path(DBusChardev *chr) +{ + return g_strdup_printf(DBUS_DISPLAY1_ROOT "/Chardev_%s", + CHARDEV(chr)->label); +} + +static void +dbus_display_chardev_export(DBusDisplay *dpy, DBusChardev *chr) +{ + g_autoptr(GDBusObjectSkeleton) sk = NULL; + g_autofree char *path = dbus_display_chardev_path(chr); + + if (chr->exported) { + return; + } + + sk = g_dbus_object_skeleton_new(path); + g_dbus_object_skeleton_add_interface( + sk, G_DBUS_INTERFACE_SKELETON(chr->iface)); + g_dbus_object_manager_server_export(dpy->server, sk); + chr->exported = true; +} + +static void +dbus_display_chardev_unexport(DBusDisplay *dpy, DBusChardev *chr) +{ + g_autofree char *path = dbus_display_chardev_path(chr); + + if (!chr->exported) { + return; + } + + g_dbus_object_manager_server_unexport(dpy->server, path); + chr->exported = false; +} + +static int +dbus_display_chardev_foreach(Object *obj, void *data) +{ + DBusDisplay *dpy = DBUS_DISPLAY(data); + + if (!CHARDEV_IS_DBUS(obj)) { + return 0; + } + + dbus_display_chardev_export(dpy, DBUS_CHARDEV(obj)); + + return 0; +} + +static void +dbus_display_on_notify(Notifier *notifier, void *data) +{ + DBusDisplay *dpy = container_of(notifier, DBusDisplay, notifier); + DBusDisplayEvent *event = data; + + switch (event->type) { + case DBUS_DISPLAY_CHARDEV_OPEN: + dbus_display_chardev_export(dpy, event->chardev); + break; + case DBUS_DISPLAY_CHARDEV_CLOSE: + dbus_display_chardev_unexport(dpy, event->chardev); + break; + } +} + +void +dbus_chardev_init(DBusDisplay *dpy) +{ + dpy->notifier.notify = dbus_display_on_notify; + dbus_display_notifier_add(&dpy->notifier); + + object_child_foreach(container_get(object_get_root(), "/chardevs"), + dbus_display_chardev_foreach, dpy); +} + +static gboolean +dbus_chr_register( + DBusChardev *dc, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + GVariant *arg_stream, + QemuDBusDisplay1Chardev *object) +{ + g_autoptr(GError) err = NULL; + int fd; + + fd = g_unix_fd_list_get(fd_list, g_variant_get_handle(arg_stream), &err); + if (err) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Couldn't get peer FD: %s", err->message); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + if (qemu_chr_add_client(CHARDEV(dc), fd) < 0) { + g_dbus_method_invocation_return_error(invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Couldn't register FD!"); + close(fd); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + g_object_set(dc->iface, + "owner", g_dbus_method_invocation_get_sender(invocation), + NULL); + + qemu_dbus_display1_chardev_complete_register(object, invocation, NULL); + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_chr_send_break( + DBusChardev *dc, + GDBusMethodInvocation *invocation, + QemuDBusDisplay1Chardev *object) +{ + qemu_chr_be_event(CHARDEV(dc), CHR_EVENT_BREAK); + + qemu_dbus_display1_chardev_complete_send_break(object, invocation); + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +dbus_chr_open(Chardev *chr, ChardevBackend *backend, + bool *be_opened, Error **errp) +{ + ERRP_GUARD(); + + DBusChardev *dc = DBUS_CHARDEV(chr); + DBusDisplayEvent event = { + .type = DBUS_DISPLAY_CHARDEV_OPEN, + .chardev = dc, + }; + g_autoptr(ChardevBackend) be = NULL; + g_autoptr(QemuOpts) opts = NULL; + + dc->iface = qemu_dbus_display1_chardev_skeleton_new(); + g_object_set(dc->iface, "name", backend->u.dbus.data->name, NULL); + g_object_connect(dc->iface, + "swapped-signal::handle-register", + dbus_chr_register, dc, + "swapped-signal::handle-send-break", + dbus_chr_send_break, dc, + NULL); + + dbus_display_notify(&event); + + be = g_new0(ChardevBackend, 1); + opts = qemu_opts_create(qemu_find_opts("chardev"), NULL, 0, &error_abort); + qemu_opt_set(opts, "server", "on", &error_abort); + qemu_opt_set(opts, "wait", "off", &error_abort); + CHARDEV_CLASS(object_class_by_name(TYPE_CHARDEV_SOCKET))->parse( + opts, be, errp); + if (*errp) { + return; + } + CHARDEV_CLASS(object_class_by_name(TYPE_CHARDEV_SOCKET))->open( + chr, be, be_opened, errp); +} + +static void +dbus_chr_set_fe_open(Chardev *chr, int fe_open) +{ + DBusChardev *dc = DBUS_CHARDEV(chr); + + g_object_set(dc->iface, "feopened", fe_open, NULL); +} + +static void +dbus_chr_set_echo(Chardev *chr, bool echo) +{ + DBusChardev *dc = DBUS_CHARDEV(chr); + + g_object_set(dc->iface, "echo", echo, NULL); +} + +static void +dbus_chr_be_event(Chardev *chr, QEMUChrEvent event) +{ + DBusChardev *dc = DBUS_CHARDEV(chr); + DBusChardevClass *klass = DBUS_CHARDEV_GET_CLASS(chr); + + switch (event) { + case CHR_EVENT_CLOSED: + if (dc->iface) { + /* on finalize, iface is set to NULL */ + g_object_set(dc->iface, "owner", "", NULL); + } + break; + default: + break; + }; + + klass->parent_chr_be_event(chr, event); +} + +static void +dbus_chr_parse(QemuOpts *opts, ChardevBackend *backend, + Error **errp) +{ + const char *name = qemu_opt_get(opts, "name"); + ChardevDBus *dbus; + + if (name == NULL) { + error_setg(errp, "chardev: dbus: no name given"); + return; + } + + backend->type = CHARDEV_BACKEND_KIND_DBUS; + dbus = backend->u.dbus.data = g_new0(ChardevDBus, 1); + qemu_chr_parse_common(opts, qapi_ChardevDBus_base(dbus)); + dbus->name = g_strdup(name); +} + +static void +char_dbus_class_init(ObjectClass *oc, void *data) +{ + DBusChardevClass *klass = DBUS_CHARDEV_CLASS(oc); + ChardevClass *cc = CHARDEV_CLASS(oc); + + cc->parse = dbus_chr_parse; + cc->open = dbus_chr_open; + cc->chr_set_fe_open = dbus_chr_set_fe_open; + cc->chr_set_echo = dbus_chr_set_echo; + klass->parent_chr_be_event = cc->chr_be_event; + cc->chr_be_event = dbus_chr_be_event; +} + +static void +char_dbus_finalize(Object *obj) +{ + DBusChardev *dc = DBUS_CHARDEV(obj); + DBusDisplayEvent event = { + .type = DBUS_DISPLAY_CHARDEV_CLOSE, + .chardev = dc, + }; + + dbus_display_notify(&event); + g_clear_object(&dc->iface); +} + +static const TypeInfo char_dbus_type_info = { + .name = TYPE_CHARDEV_DBUS, + .parent = TYPE_CHARDEV_SOCKET, + .class_size = sizeof(DBusChardevClass), + .instance_size = sizeof(DBusChardev), + .instance_finalize = char_dbus_finalize, + .class_init = char_dbus_class_init, +}; +module_obj(TYPE_CHARDEV_DBUS); + +static void +register_types(void) +{ + type_register_static(&char_dbus_type_info); +} + +type_init(register_types); diff --git a/ui/dbus-clipboard.c b/ui/dbus-clipboard.c new file mode 100644 index 0000000000..5843d26cd2 --- /dev/null +++ b/ui/dbus-clipboard.c @@ -0,0 +1,457 @@ +/* + * QEMU DBus display + * + * Copyright (c) 2021 Marc-André Lureau <marcandre.lureau@redhat.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "qemu/osdep.h" +#include "qemu/dbus.h" +#include "qemu/main-loop.h" +#include "qom/object_interfaces.h" +#include "sysemu/sysemu.h" +#include "qapi/error.h" +#include "trace.h" + +#include "dbus.h" + +#define MIME_TEXT_PLAIN_UTF8 "text/plain;charset=utf-8" + +static void +dbus_clipboard_complete_request( + DBusDisplay *dpy, + GDBusMethodInvocation *invocation, + QemuClipboardInfo *info, + QemuClipboardType type) +{ + GVariant *v_data = g_variant_new_from_data( + G_VARIANT_TYPE("ay"), + info->types[type].data, + info->types[type].size, + TRUE, + (GDestroyNotify)qemu_clipboard_info_unref, + qemu_clipboard_info_ref(info)); + + qemu_dbus_display1_clipboard_complete_request( + dpy->clipboard, invocation, + MIME_TEXT_PLAIN_UTF8, v_data); +} + +static void +dbus_clipboard_update_info(DBusDisplay *dpy, QemuClipboardInfo *info) +{ + bool self_update = info->owner == &dpy->clipboard_peer; + const char *mime[QEMU_CLIPBOARD_TYPE__COUNT + 1] = { 0, }; + DBusClipboardRequest *req; + int i = 0; + + if (info->owner == NULL) { + if (dpy->clipboard_proxy) { + qemu_dbus_display1_clipboard_call_release( + dpy->clipboard_proxy, + info->selection, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + } + return; + } + + if (self_update || !info->has_serial) { + return; + } + + req = &dpy->clipboard_request[info->selection]; + if (req->invocation && info->types[req->type].data) { + dbus_clipboard_complete_request(dpy, req->invocation, info, req->type); + g_clear_object(&req->invocation); + g_source_remove(req->timeout_id); + req->timeout_id = 0; + return; + } + + if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) { + mime[i++] = MIME_TEXT_PLAIN_UTF8; + } + + if (i > 0) { + if (dpy->clipboard_proxy) { + qemu_dbus_display1_clipboard_call_grab( + dpy->clipboard_proxy, + info->selection, + info->serial, + mime, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + } + } +} + +static void +dbus_clipboard_reset_serial(DBusDisplay *dpy) +{ + if (dpy->clipboard_proxy) { + qemu_dbus_display1_clipboard_call_register( + dpy->clipboard_proxy, + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, NULL, NULL); + } +} + +static void +dbus_clipboard_notify(Notifier *notifier, void *data) +{ + DBusDisplay *dpy = + container_of(notifier, DBusDisplay, clipboard_peer.notifier); + QemuClipboardNotify *notify = data; + + switch (notify->type) { + case QEMU_CLIPBOARD_UPDATE_INFO: + dbus_clipboard_update_info(dpy, notify->info); + return; + case QEMU_CLIPBOARD_RESET_SERIAL: + dbus_clipboard_reset_serial(dpy); + return; + } +} + +static void +dbus_clipboard_qemu_request(QemuClipboardInfo *info, + QemuClipboardType type) +{ + DBusDisplay *dpy = container_of(info->owner, DBusDisplay, clipboard_peer); + g_autofree char *mime = NULL; + g_autoptr(GVariant) v_data = NULL; + g_autoptr(GError) err = NULL; + const char *data = NULL; + const char *mimes[] = { MIME_TEXT_PLAIN_UTF8, NULL }; + size_t n; + + if (type != QEMU_CLIPBOARD_TYPE_TEXT) { + /* unsupported atm */ + return; + } + + if (dpy->clipboard_proxy) { + if (!qemu_dbus_display1_clipboard_call_request_sync( + dpy->clipboard_proxy, + info->selection, + mimes, + G_DBUS_CALL_FLAGS_NONE, -1, &mime, &v_data, NULL, &err)) { + error_report("Failed to request clipboard: %s", err->message); + return; + } + + if (g_strcmp0(mime, MIME_TEXT_PLAIN_UTF8)) { + error_report("Unsupported returned MIME: %s", mime); + return; + } + + data = g_variant_get_fixed_array(v_data, &n, 1); + qemu_clipboard_set_data(&dpy->clipboard_peer, info, type, + n, data, true); + } +} + +static void +dbus_clipboard_request_cancelled(DBusClipboardRequest *req) +{ + if (!req->invocation) { + return; + } + + g_dbus_method_invocation_return_error( + req->invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Cancelled clipboard request"); + + g_clear_object(&req->invocation); + g_source_remove(req->timeout_id); + req->timeout_id = 0; +} + +static void +dbus_clipboard_unregister_proxy(DBusDisplay *dpy) +{ + const char *name = NULL; + int i; + + for (i = 0; i < G_N_ELEMENTS(dpy->clipboard_request); ++i) { + dbus_clipboard_request_cancelled(&dpy->clipboard_request[i]); + } + + if (!dpy->clipboard_proxy) { + return; + } + + name = g_dbus_proxy_get_name(G_DBUS_PROXY(dpy->clipboard_proxy)); + trace_dbus_clipboard_unregister(name); + g_clear_object(&dpy->clipboard_proxy); +} + +static void +dbus_on_clipboard_proxy_name_owner_changed( + DBusDisplay *dpy, + GObject *object, + GParamSpec *pspec) +{ + dbus_clipboard_unregister_proxy(dpy); +} + +static gboolean +dbus_clipboard_register( + DBusDisplay *dpy, + GDBusMethodInvocation *invocation) +{ + g_autoptr(GError) err = NULL; + const char *name = NULL; + + if (dpy->clipboard_proxy) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Clipboard peer already registered!"); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + dpy->clipboard_proxy = + qemu_dbus_display1_clipboard_proxy_new_sync( + g_dbus_method_invocation_get_connection(invocation), + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + g_dbus_method_invocation_get_sender(invocation), + "/org/qemu/Display1/Clipboard", + NULL, + &err); + if (!dpy->clipboard_proxy) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Failed to setup proxy: %s", err->message); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + name = g_dbus_proxy_get_name(G_DBUS_PROXY(dpy->clipboard_proxy)); + trace_dbus_clipboard_register(name); + + g_object_connect(dpy->clipboard_proxy, + "swapped-signal::notify::g-name-owner", + dbus_on_clipboard_proxy_name_owner_changed, dpy, + NULL); + qemu_clipboard_reset_serial(); + + qemu_dbus_display1_clipboard_complete_register(dpy->clipboard, invocation); + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_clipboard_check_caller(DBusDisplay *dpy, GDBusMethodInvocation *invocation) +{ + if (!dpy->clipboard_proxy || + g_strcmp0(g_dbus_proxy_get_name(G_DBUS_PROXY(dpy->clipboard_proxy)), + g_dbus_method_invocation_get_sender(invocation))) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Unregistered caller"); + return FALSE; + } + + return TRUE; +} + +static gboolean +dbus_clipboard_unregister( + DBusDisplay *dpy, + GDBusMethodInvocation *invocation) +{ + if (!dbus_clipboard_check_caller(dpy, invocation)) { + return DBUS_METHOD_INVOCATION_HANDLED; + } + + dbus_clipboard_unregister_proxy(dpy); + + qemu_dbus_display1_clipboard_complete_unregister( + dpy->clipboard, invocation); + + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_clipboard_grab( + DBusDisplay *dpy, + GDBusMethodInvocation *invocation, + gint arg_selection, + guint arg_serial, + const gchar *const *arg_mimes) +{ + QemuClipboardSelection s = arg_selection; + g_autoptr(QemuClipboardInfo) info = NULL; + + if (!dbus_clipboard_check_caller(dpy, invocation)) { + return DBUS_METHOD_INVOCATION_HANDLED; + } + + if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Invalid clipboard selection: %d", arg_selection); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + info = qemu_clipboard_info_new(&dpy->clipboard_peer, s); + if (g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8)) { + info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true; + } + info->serial = arg_serial; + info->has_serial = true; + if (qemu_clipboard_check_serial(info, true)) { + qemu_clipboard_update(info); + } else { + trace_dbus_clipboard_grab_failed(); + } + + qemu_dbus_display1_clipboard_complete_grab(dpy->clipboard, invocation); + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_clipboard_release( + DBusDisplay *dpy, + GDBusMethodInvocation *invocation, + gint arg_selection) +{ + if (!dbus_clipboard_check_caller(dpy, invocation)) { + return DBUS_METHOD_INVOCATION_HANDLED; + } + + qemu_clipboard_peer_release(&dpy->clipboard_peer, arg_selection); + + qemu_dbus_display1_clipboard_complete_release(dpy->clipboard, invocation); + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_clipboard_request_timeout(gpointer user_data) +{ + dbus_clipboard_request_cancelled(user_data); + return G_SOURCE_REMOVE; +} + +static gboolean +dbus_clipboard_request( + DBusDisplay *dpy, + GDBusMethodInvocation *invocation, + gint arg_selection, + const gchar *const *arg_mimes) +{ + QemuClipboardSelection s = arg_selection; + QemuClipboardType type = QEMU_CLIPBOARD_TYPE_TEXT; + QemuClipboardInfo *info = NULL; + + if (!dbus_clipboard_check_caller(dpy, invocation)) { + return DBUS_METHOD_INVOCATION_HANDLED; + } + + if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Invalid clipboard selection: %d", arg_selection); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + if (dpy->clipboard_request[s].invocation) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Pending request"); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + info = qemu_clipboard_info(s); + if (!info || !info->owner || info->owner == &dpy->clipboard_peer) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Empty clipboard"); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + if (!g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8) || + !info->types[type].available) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Unhandled MIME types requested"); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + if (info->types[type].data) { + dbus_clipboard_complete_request(dpy, invocation, info, type); + } else { + qemu_clipboard_request(info, type); + + dpy->clipboard_request[s].invocation = g_object_ref(invocation); + dpy->clipboard_request[s].type = type; + dpy->clipboard_request[s].timeout_id = + g_timeout_add_seconds(5, dbus_clipboard_request_timeout, + &dpy->clipboard_request[s]); + } + + return DBUS_METHOD_INVOCATION_HANDLED; +} + +void +dbus_clipboard_init(DBusDisplay *dpy) +{ + g_autoptr(GDBusObjectSkeleton) clipboard = NULL; + + assert(!dpy->clipboard); + + clipboard = g_dbus_object_skeleton_new(DBUS_DISPLAY1_ROOT "/Clipboard"); + dpy->clipboard = qemu_dbus_display1_clipboard_skeleton_new(); + g_object_connect(dpy->clipboard, + "swapped-signal::handle-register", + dbus_clipboard_register, dpy, + "swapped-signal::handle-unregister", + dbus_clipboard_unregister, dpy, + "swapped-signal::handle-grab", + dbus_clipboard_grab, dpy, + "swapped-signal::handle-release", + dbus_clipboard_release, dpy, + "swapped-signal::handle-request", + dbus_clipboard_request, dpy, + NULL); + + g_dbus_object_skeleton_add_interface( + G_DBUS_OBJECT_SKELETON(clipboard), + G_DBUS_INTERFACE_SKELETON(dpy->clipboard)); + g_dbus_object_manager_server_export(dpy->server, clipboard); + dpy->clipboard_peer.name = "dbus"; + dpy->clipboard_peer.notifier.notify = dbus_clipboard_notify; + dpy->clipboard_peer.request = dbus_clipboard_qemu_request; + qemu_clipboard_peer_register(&dpy->clipboard_peer); +} diff --git a/ui/dbus-console.c b/ui/dbus-console.c new file mode 100644 index 0000000000..e062f721d7 --- /dev/null +++ b/ui/dbus-console.c @@ -0,0 +1,497 @@ +/* + * QEMU DBus display console + * + * Copyright (c) 2021 Marc-André Lureau <marcandre.lureau@redhat.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "qemu/osdep.h" +#include "qapi/error.h" +#include "ui/input.h" +#include "ui/kbd-state.h" +#include "trace.h" + +#include <gio/gunixfdlist.h> + +#include "dbus.h" + +struct _DBusDisplayConsole { + GDBusObjectSkeleton parent_instance; + DisplayChangeListener dcl; + + DBusDisplay *display; + QemuConsole *con; + GHashTable *listeners; + QemuDBusDisplay1Console *iface; + + QemuDBusDisplay1Keyboard *iface_kbd; + QKbdState *kbd; + + QemuDBusDisplay1Mouse *iface_mouse; + gboolean last_set; + guint last_x; + guint last_y; + Notifier mouse_mode_notifier; +}; + +G_DEFINE_TYPE(DBusDisplayConsole, + dbus_display_console, + G_TYPE_DBUS_OBJECT_SKELETON) + +static void +dbus_display_console_set_size(DBusDisplayConsole *ddc, + uint32_t width, uint32_t height) +{ + g_object_set(ddc->iface, + "width", width, + "height", height, + NULL); +} + +static void +dbus_gfx_switch(DisplayChangeListener *dcl, + struct DisplaySurface *new_surface) +{ + DBusDisplayConsole *ddc = container_of(dcl, DBusDisplayConsole, dcl); + + dbus_display_console_set_size(ddc, + surface_width(new_surface), + surface_height(new_surface)); +} + +static void +dbus_gfx_update(DisplayChangeListener *dcl, + int x, int y, int w, int h) +{ +} + +static void +dbus_gl_scanout_disable(DisplayChangeListener *dcl) +{ +} + +static void +dbus_gl_scanout_texture(DisplayChangeListener *dcl, + uint32_t tex_id, + bool backing_y_0_top, + uint32_t backing_width, + uint32_t backing_height, + uint32_t x, uint32_t y, + uint32_t w, uint32_t h) +{ + DBusDisplayConsole *ddc = container_of(dcl, DBusDisplayConsole, dcl); + + dbus_display_console_set_size(ddc, w, h); +} + +static void +dbus_gl_scanout_dmabuf(DisplayChangeListener *dcl, + QemuDmaBuf *dmabuf) +{ + DBusDisplayConsole *ddc = container_of(dcl, DBusDisplayConsole, dcl); + + dbus_display_console_set_size(ddc, + dmabuf->width, + dmabuf->height); +} + +static void +dbus_gl_scanout_update(DisplayChangeListener *dcl, + uint32_t x, uint32_t y, + uint32_t w, uint32_t h) +{ +} + +static const DisplayChangeListenerOps dbus_console_dcl_ops = { + .dpy_name = "dbus-console", + .dpy_gfx_switch = dbus_gfx_switch, + .dpy_gfx_update = dbus_gfx_update, + .dpy_gl_scanout_disable = dbus_gl_scanout_disable, + .dpy_gl_scanout_texture = dbus_gl_scanout_texture, + .dpy_gl_scanout_dmabuf = dbus_gl_scanout_dmabuf, + .dpy_gl_update = dbus_gl_scanout_update, +}; + +static void +dbus_display_console_init(DBusDisplayConsole *object) +{ + DBusDisplayConsole *ddc = DBUS_DISPLAY_CONSOLE(object); + + ddc->listeners = g_hash_table_new_full(g_str_hash, g_str_equal, + NULL, g_object_unref); + ddc->dcl.ops = &dbus_console_dcl_ops; +} + +static void +dbus_display_console_dispose(GObject *object) +{ + DBusDisplayConsole *ddc = DBUS_DISPLAY_CONSOLE(object); + + unregister_displaychangelistener(&ddc->dcl); + g_clear_object(&ddc->iface_kbd); + g_clear_object(&ddc->iface); + g_clear_pointer(&ddc->listeners, g_hash_table_unref); + g_clear_pointer(&ddc->kbd, qkbd_state_free); + + G_OBJECT_CLASS(dbus_display_console_parent_class)->dispose(object); +} + +static void +dbus_display_console_class_init(DBusDisplayConsoleClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(klass); + + gobject_class->dispose = dbus_display_console_dispose; +} + +static void +listener_vanished_cb(DBusDisplayListener *listener) +{ + DBusDisplayConsole *ddc = dbus_display_listener_get_console(listener); + const char *name = dbus_display_listener_get_bus_name(listener); + + trace_dbus_listener_vanished(name); + + g_hash_table_remove(ddc->listeners, name); + qkbd_state_lift_all_keys(ddc->kbd); +} + +static gboolean +dbus_console_set_ui_info(DBusDisplayConsole *ddc, + GDBusMethodInvocation *invocation, + guint16 arg_width_mm, + guint16 arg_height_mm, + gint arg_xoff, + gint arg_yoff, + guint arg_width, + guint arg_height) +{ + QemuUIInfo info = { + .width_mm = arg_width_mm, + .height_mm = arg_height_mm, + .xoff = arg_xoff, + .yoff = arg_yoff, + .width = arg_width, + .height = arg_height, + }; + + if (!dpy_ui_info_supported(ddc->con)) { + g_dbus_method_invocation_return_error(invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_UNSUPPORTED, + "SetUIInfo is not supported"); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + dpy_set_ui_info(ddc->con, &info, false); + qemu_dbus_display1_console_complete_set_uiinfo(ddc->iface, invocation); + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_console_register_listener(DBusDisplayConsole *ddc, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + GVariant *arg_listener) +{ + const char *sender = g_dbus_method_invocation_get_sender(invocation); + GDBusConnection *listener_conn; + g_autoptr(GError) err = NULL; + g_autoptr(GSocket) socket = NULL; + g_autoptr(GSocketConnection) socket_conn = NULL; + g_autofree char *guid = g_dbus_generate_guid(); + DBusDisplayListener *listener; + int fd; + + if (sender && g_hash_table_contains(ddc->listeners, sender)) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_INVALID, + "`%s` is already registered!", + sender); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + fd = g_unix_fd_list_get(fd_list, g_variant_get_handle(arg_listener), &err); + if (err) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Couldn't get peer fd: %s", err->message); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + socket = g_socket_new_from_fd(fd, &err); + if (err) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Couldn't make a socket: %s", err->message); + close(fd); + return DBUS_METHOD_INVOCATION_HANDLED; + } + socket_conn = g_socket_connection_factory_create_connection(socket); + + qemu_dbus_display1_console_complete_register_listener( + ddc->iface, invocation, NULL); + + listener_conn = g_dbus_connection_new_sync( + G_IO_STREAM(socket_conn), + guid, + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_SERVER, + NULL, NULL, &err); + if (err) { + error_report("Failed to setup peer connection: %s", err->message); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + listener = dbus_display_listener_new(sender, listener_conn, ddc); + if (!listener) { + return DBUS_METHOD_INVOCATION_HANDLED; + } + + g_hash_table_insert(ddc->listeners, + (gpointer)dbus_display_listener_get_bus_name(listener), + listener); + g_object_connect(listener_conn, + "swapped-signal::closed", listener_vanished_cb, listener, + NULL); + + trace_dbus_registered_listener(sender); + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_kbd_press(DBusDisplayConsole *ddc, + GDBusMethodInvocation *invocation, + guint arg_keycode) +{ + QKeyCode qcode = qemu_input_key_number_to_qcode(arg_keycode); + + trace_dbus_kbd_press(arg_keycode); + + qkbd_state_key_event(ddc->kbd, qcode, true); + + qemu_dbus_display1_keyboard_complete_press(ddc->iface_kbd, invocation); + + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_kbd_release(DBusDisplayConsole *ddc, + GDBusMethodInvocation *invocation, + guint arg_keycode) +{ + QKeyCode qcode = qemu_input_key_number_to_qcode(arg_keycode); + + trace_dbus_kbd_release(arg_keycode); + + qkbd_state_key_event(ddc->kbd, qcode, false); + + qemu_dbus_display1_keyboard_complete_release(ddc->iface_kbd, invocation); + + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +dbus_kbd_qemu_leds_updated(void *data, int ledstate) +{ + DBusDisplayConsole *ddc = DBUS_DISPLAY_CONSOLE(data); + + qemu_dbus_display1_keyboard_set_modifiers(ddc->iface_kbd, ledstate); +} + +static gboolean +dbus_mouse_rel_motion(DBusDisplayConsole *ddc, + GDBusMethodInvocation *invocation, + int dx, int dy) +{ + trace_dbus_mouse_rel_motion(dx, dy); + + if (qemu_input_is_absolute()) { + g_dbus_method_invocation_return_error( + invocation, DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_INVALID, + "Mouse is not relative"); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + qemu_input_queue_rel(ddc->con, INPUT_AXIS_X, dx); + qemu_input_queue_rel(ddc->con, INPUT_AXIS_Y, dy); + qemu_input_event_sync(); + + qemu_dbus_display1_mouse_complete_rel_motion(ddc->iface_mouse, + invocation); + + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_mouse_set_pos(DBusDisplayConsole *ddc, + GDBusMethodInvocation *invocation, + guint x, guint y) +{ + int width, height; + + trace_dbus_mouse_set_pos(x, y); + + if (!qemu_input_is_absolute()) { + g_dbus_method_invocation_return_error( + invocation, DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_INVALID, + "Mouse is not absolute"); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + width = qemu_console_get_width(ddc->con, 0); + height = qemu_console_get_height(ddc->con, 0); + if (x >= width || y >= height) { + g_dbus_method_invocation_return_error( + invocation, DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_INVALID, + "Invalid mouse position"); + return DBUS_METHOD_INVOCATION_HANDLED; + } + qemu_input_queue_abs(ddc->con, INPUT_AXIS_X, x, 0, width); + qemu_input_queue_abs(ddc->con, INPUT_AXIS_Y, y, 0, height); + qemu_input_event_sync(); + + qemu_dbus_display1_mouse_complete_set_abs_position(ddc->iface_mouse, + invocation); + + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_mouse_press(DBusDisplayConsole *ddc, + GDBusMethodInvocation *invocation, + guint button) +{ + trace_dbus_mouse_press(button); + + qemu_input_queue_btn(ddc->con, button, true); + qemu_input_event_sync(); + + qemu_dbus_display1_mouse_complete_press(ddc->iface_mouse, invocation); + + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_mouse_release(DBusDisplayConsole *ddc, + GDBusMethodInvocation *invocation, + guint button) +{ + trace_dbus_mouse_release(button); + + qemu_input_queue_btn(ddc->con, button, false); + qemu_input_event_sync(); + + qemu_dbus_display1_mouse_complete_release(ddc->iface_mouse, invocation); + + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +dbus_mouse_mode_change(Notifier *notify, void *data) +{ + DBusDisplayConsole *ddc = + container_of(notify, DBusDisplayConsole, mouse_mode_notifier); + + g_object_set(ddc->iface_mouse, + "is-absolute", qemu_input_is_absolute(), + NULL); +} + +int dbus_display_console_get_index(DBusDisplayConsole *ddc) +{ + return qemu_console_get_index(ddc->con); +} + +DBusDisplayConsole * +dbus_display_console_new(DBusDisplay *display, QemuConsole *con) +{ + g_autofree char *path = NULL; + g_autofree char *label = NULL; + char device_addr[256] = ""; + DBusDisplayConsole *ddc; + int idx; + + assert(display); + assert(con); + + label = qemu_console_get_label(con); + idx = qemu_console_get_index(con); + path = g_strdup_printf(DBUS_DISPLAY1_ROOT "/Console_%d", idx); + ddc = g_object_new(DBUS_DISPLAY_TYPE_CONSOLE, + "g-object-path", path, + NULL); + ddc->display = display; + ddc->con = con; + /* handle errors, and skip non graphics? */ + qemu_console_fill_device_address( + con, device_addr, sizeof(device_addr), NULL); + + ddc->iface = qemu_dbus_display1_console_skeleton_new(); + g_object_set(ddc->iface, + "label", label, + "type", qemu_console_is_graphic(con) ? "Graphic" : "Text", + "head", qemu_console_get_head(con), + "width", qemu_console_get_width(con, 0), + "height", qemu_console_get_height(con, 0), + "device-address", device_addr, + NULL); + g_object_connect(ddc->iface, + "swapped-signal::handle-register-listener", + dbus_console_register_listener, ddc, + "swapped-signal::handle-set-uiinfo", + dbus_console_set_ui_info, ddc, + NULL); + g_dbus_object_skeleton_add_interface(G_DBUS_OBJECT_SKELETON(ddc), + G_DBUS_INTERFACE_SKELETON(ddc->iface)); + + ddc->kbd = qkbd_state_init(con); + ddc->iface_kbd = qemu_dbus_display1_keyboard_skeleton_new(); + qemu_add_led_event_handler(dbus_kbd_qemu_leds_updated, ddc); + g_object_connect(ddc->iface_kbd, + "swapped-signal::handle-press", dbus_kbd_press, ddc, + "swapped-signal::handle-release", dbus_kbd_release, ddc, + NULL); + g_dbus_object_skeleton_add_interface(G_DBUS_OBJECT_SKELETON(ddc), + G_DBUS_INTERFACE_SKELETON(ddc->iface_kbd)); + + ddc->iface_mouse = qemu_dbus_display1_mouse_skeleton_new(); + g_object_connect(ddc->iface_mouse, + "swapped-signal::handle-set-abs-position", dbus_mouse_set_pos, ddc, + "swapped-signal::handle-rel-motion", dbus_mouse_rel_motion, ddc, + "swapped-signal::handle-press", dbus_mouse_press, ddc, + "swapped-signal::handle-release", dbus_mouse_release, ddc, + NULL); + g_dbus_object_skeleton_add_interface(G_DBUS_OBJECT_SKELETON(ddc), + G_DBUS_INTERFACE_SKELETON(ddc->iface_mouse)); + + register_displaychangelistener(&ddc->dcl); + ddc->mouse_mode_notifier.notify = dbus_mouse_mode_change; + qemu_add_mouse_mode_change_notifier(&ddc->mouse_mode_notifier); + + return ddc; +} diff --git a/ui/dbus-display1.xml b/ui/dbus-display1.xml new file mode 100644 index 0000000000..c3b2293376 --- /dev/null +++ b/ui/dbus-display1.xml @@ -0,0 +1,761 @@ +<?xml version="1.0" encoding="utf-8"?> +<node> + <!-- + org.qemu.Display1.VM: + + This interface is implemented on ``/org/qemu/Display1/VM``. + --> + <interface name="org.qemu.Display1.VM"> + <!-- + Name: + + The name of the VM. + --> + <property name="Name" type="s" access="read"/> + + <!-- + UUID: + + The UUID of the VM. + --> + <property name="UUID" type="s" access="read"/> + + <!-- + ConsoleIDs: + + The list of consoles available on ``/org/qemu/Display1/Console_$id``. + --> + <property name="ConsoleIDs" type="au" access="read"/> + </interface> + + <!-- + org.qemu.Display1.Console: + + This interface is implemented on ``/org/qemu/Display1/Console_$id``. You + may discover available consoles through introspection or with the + :dbus:prop:`org.qemu.Display1.VM.ConsoleIDs` property. + + A console is attached to a video device head. It may be "Graphic" or + "Text" (see :dbus:prop:`Type` and other properties). + + Interactions with a console may be done with + :dbus:iface:`org.qemu.Display1.Keyboard` and + :dbus:iface:`org.qemu.Display1.Mouse` interfaces when available. + --> + <interface name="org.qemu.Display1.Console"> + <!-- + RegisterListener: + @listener: a Unix socket FD, for peer-to-peer D-Bus communication. + + Register a console listener, which will receive display updates, until + it is disconnected. + + Multiple listeners may be registered simultaneously. + + The listener is expected to implement the + :dbus:iface:`org.qemu.Display1.Listener` interface. + --> + <method name="RegisterListener"> + <arg type="h" name="listener" direction="in"/> + </method> + + <!-- + SetUIInfo: + @width_mm: the physical display width in millimeters. + @height_mm: the physical display height in millimeters. + @xoff: horizontal offset, in pixels. + @yoff: vertical offset, in pixels. + @width: console width, in pixels. + @height: console height, in pixels. + + Modify the dimensions and display settings. + --> + <method name="SetUIInfo"> + <arg name="width_mm" type="q" direction="in"/> + <arg name="height_mm" type="q" direction="in"/> + <arg name="xoff" type="i" direction="in"/> + <arg name="yoff" type="i" direction="in"/> + <arg name="width" type="u" direction="in"/> + <arg name="height" type="u" direction="in"/> + </method> + + <!-- + Label: + + A user-friendly name for the console (for ex: "VGA"). + --> + <property name="Label" type="s" access="read"/> + + <!-- + Head: + + Graphical device head number. + --> + <property name="Head" type="u" access="read"/> + + <!-- + Type: + + Console type ("Graphic" or "Text"). + --> + <property name="Type" type="s" access="read"/> + + <!-- + Width: + + Console width, in pixels. + --> + <property name="Width" type="u" access="read"/> + + <!-- + Height: + + Console height, in pixels. + --> + <property name="Height" type="u" access="read"/> + + <!-- + DeviceAddress: + + The device address (ex: "pci/0000/02.0"). + --> + <property name="DeviceAddress" type="s" access="read"/> + </interface> + + <!-- + org.qemu.Display1.Keyboard: + + This interface in implemented on ``/org/qemu/Display1/Console_$id`` (see + :dbus:iface:`~org.qemu.Display1.Console`). + --> + <interface name="org.qemu.Display1.Keyboard"> + <!-- + Press: + @keycode: QEMU key number (xtkbd + special re-encoding of high bit) + + Send a key press event. + --> + <method name="Press"> + <arg type="u" name="keycode" direction="in"/> + </method> + + <!-- + Release: + @keycode: QEMU key number (xtkbd + special re-encoding of high bit) + + Send a key release event. + --> + <method name="Release"> + <arg type="u" name="keycode" direction="in"/> + </method> + + <!-- + Modifiers: + + The active keyboard modifiers:: + + Scroll = 1 << 0 + Num = 1 << 1 + Caps = 1 << 2 + --> + <property name="Modifiers" type="u" access="read"/> + </interface> + + <!-- + org.qemu.Display1.Mouse: + + This interface in implemented on ``/org/qemu/Display1/Console_$id`` (see + :dbus:iface:`~org.qemu.Display1.Console` documentation). + + .. _dbus-button-values: + + **Button values**:: + + Left = 0 + Middle = 1 + Right = 2 + Wheel-up = 3 + Wheel-down = 4 + Side = 5 + Extra = 6 + --> + <interface name="org.qemu.Display1.Mouse"> + <!-- + Press: + @button: :ref:`button value<dbus-button-values>`. + + Send a mouse button press event. + --> + <method name="Press"> + <arg type="u" name="button" direction="in"/> + </method> + + <!-- + Release: + @button: :ref:`button value<dbus-button-values>`. + + Send a mouse button release event. + --> + <method name="Release"> + <arg type="u" name="button" direction="in"/> + </method> + + <!-- + SetAbsPosition: + @x: X position, in pixels. + @y: Y position, in pixels. + + Set the mouse pointer position. + + Returns an error if not :dbus:prop:`IsAbsolute`. + --> + <method name="SetAbsPosition"> + <arg type="u" name="x" direction="in"/> + <arg type="u" name="y" direction="in"/> + </method> + + <!-- + RelMotion: + @dx: X-delta, in pixels. + @dy: Y-delta, in pixels. + + Move the mouse pointer position, relative to the current position. + + Returns an error if :dbus:prop:`IsAbsolute`. + --> + <method name="RelMotion"> + <arg type="i" name="dx" direction="in"/> + <arg type="i" name="dy" direction="in"/> + </method> + + <!-- + IsAbsolute: + + Whether the mouse is using absolute movements. + --> + <property name="IsAbsolute" type="b" access="read"/> + </interface> + + <!-- + org.qemu.Display1.Listener: + + This client-side interface must be available on + ``/org/qemu/Display1/Listener`` when registering the peer-to-peer + connection with :dbus:meth:`~org.qemu.Display1.Console.Register`. + --> + <interface name="org.qemu.Display1.Listener"> + <!-- + Scanout: + @width: display width, in pixels. + @height: display height, in pixels. + @stride: data stride, in bytes. + @pixman_format: image format (ex: ``PIXMAN_X8R8G8B8``). + @data: image data. + + Resize and update the display content. + + The data to transfer for the display update may be large. The preferred + scanout method is :dbus:meth:`ScanoutDMABUF`, used whenever possible. + --> + <method name="Scanout"> + <arg type="u" name="width" direction="in"/> + <arg type="u" name="height" direction="in"/> + <arg type="u" name="stride" direction="in"/> + <arg type="u" name="pixman_format" direction="in"/> + <arg type="ay" name="data" direction="in"> + <annotation name="org.gtk.GDBus.C.ForceGVariant" value="true"/> + </arg> + </method> + + <!-- + Update: + @x: X update position, in pixels. + @y: Y update position, in pixels. + @width: update width, in pixels. + @height: update height, in pixels. + @stride: data stride, in bytes. + @pixman_format: image format (ex: ``PIXMAN_X8R8G8B8``). + @data: display image data. + + Update the display content. + + This method is only called after a :dbus:meth:`Scanout` call. + --> + <method name="Update"> + <arg type="i" name="x" direction="in"/> + <arg type="i" name="y" direction="in"/> + <arg type="i" name="width" direction="in"/> + <arg type="i" name="height" direction="in"/> + <arg type="u" name="stride" direction="in"/> + <arg type="u" name="pixman_format" direction="in"/> + <arg type="ay" name="data" direction="in"> + <annotation name="org.gtk.GDBus.C.ForceGVariant" value="true"/> + </arg> + </method> + + <!-- + ScanoutDMABUF: + @dmabuf: the DMABUF file descriptor. + @width: display width, in pixels. + @height: display height, in pixels. + @stride: stride, in bytes. + @fourcc: DMABUF fourcc. + @modifier: DMABUF modifier. + @y0_top: whether Y position 0 is the top or not. + + Resize and update the display content with a DMABUF. + --> + <method name="ScanoutDMABUF"> + <arg type="h" name="dmabuf" direction="in"/> + <arg type="u" name="width" direction="in"/> + <arg type="u" name="height" direction="in"/> + <arg type="u" name="stride" direction="in"/> + <arg type="u" name="fourcc" direction="in"/> + <!-- xywh? --> + <arg type="t" name="modifier" direction="in"/> + <arg type="b" name="y0_top" direction="in"/> + </method> + + <!-- + UpdateDMABUF: + @x: the X update position, in pixels. + @y: the Y update position, in pixels. + @width: the update width, in pixels. + @height: the update height, in pixels. + + Update the display content with the current DMABUF and the given region. + --> + <method name="UpdateDMABUF"> + <arg type="i" name="x" direction="in"/> + <arg type="i" name="y" direction="in"/> + <arg type="i" name="width" direction="in"/> + <arg type="i" name="height" direction="in"/> + </method> + + <!-- + Disable: + + Disable the display (turn it off). + --> + <method name="Disable"> + </method> + + <!-- + MouseSet: + @x: X mouse position, in pixels. + @y: Y mouse position, in pixels. + @on: whether the mouse is visible or not. + + Set the mouse position and visibility. + --> + <method name="MouseSet"> + <arg type="i" name="x" direction="in"/> + <arg type="i" name="y" direction="in"/> + <arg type="i" name="on" direction="in"/> + </method> + + <!-- + CursorDefine: + @width: cursor width, in pixels. + @height: cursor height, in pixels. + @hot_x: hot-spot X position, in pixels. + @hot_y: hot-spot Y position, in pixels. + @data: the cursor data. + + Set the mouse cursor shape and hot-spot. The "data" must be ARGB, 32-bit + per pixel. + --> + <method name="CursorDefine"> + <arg type="i" name="width" direction="in"/> + <arg type="i" name="height" direction="in"/> + <arg type="i" name="hot_x" direction="in"/> + <arg type="i" name="hot_y" direction="in"/> + <arg type="ay" name="data" direction="in"> + <annotation name="org.gtk.GDBus.C.ForceGVariant" value="true"/> + </arg> + </method> + </interface> + + <!-- + org.qemu.Display1.Clipboard: + + This interface must be implemented by both the client and the server on + ``/org/qemu/Display1/Clipboard`` to support clipboard sharing between + the client and the guest. + + Once :dbus:meth:`Register`'ed, method calls may be sent and received in both + directions. Unregistered callers will get error replies. + + .. _dbus-clipboard-selection: + + **Selection values**:: + + Clipboard = 0 + Primary = 1 + Secondary = 2 + + .. _dbus-clipboard-serial: + + **Serial counter** + + To solve potential clipboard races, clipboard grabs have an associated + serial counter. It is set to 0 on registration, and incremented by 1 for + each grab. The peer with the highest serial is the clipboard grab owner. + + When a grab with a lower serial is received, it should be discarded. + + When a grab is attempted with the same serial number as the current grab, + the one coming from the client should have higher priority, and the client + should gain clipboard grab ownership. + --> + <interface name="org.qemu.Display1.Clipboard"> + <!-- + Register: + + Register a clipboard session and reinitialize the serial counter. + + The client must register itself, and is granted an exclusive + access for handling the clipboard. + + The server can reinitialize the session as well (to reset the counter). + --> + <method name="Register"/> + + <!-- + Unregister: + + Unregister the clipboard session. + --> + <method name="Unregister"/> + <!-- + Grab: + @selection: a :ref:`selection value<dbus-clipboard-selection>`. + @serial: the current grab :ref:`serial<dbus-clipboard-serial>`. + @mimes: the list of available content MIME types. + + Grab the clipboard, claiming current clipboard content. + --> + <method name="Grab"> + <arg type="u" name="selection"/> + <arg type="u" name="serial"/> + <arg type="as" name="mimes"/> + </method> + + <!-- + Release: + @selection: a :ref:`selection value<dbus-clipboard-selection>`. + + Release the clipboard (does nothing if not the current owner). + --> + <method name="Release"> + <arg type="u" name="selection"/> + </method> + + <!-- + Request: + @selection: a :ref:`selection value<dbus-clipboard-selection>` + @mimes: requested MIME types (by order of preference). + @reply_mime: the returned data MIME type. + @data: the clipboard data. + + Request the clipboard content. + + Return an error if the clipboard is empty, or the requested MIME types + are unavailable. + --> + <method name="Request"> + <arg type="u" name="selection"/> + <arg type="as" name="mimes"/> + <arg type="s" name="reply_mime" direction="out"/> + <arg type="ay" name="data" direction="out"> + <annotation name="org.gtk.GDBus.C.ForceGVariant" value="true"/> + </arg> + </method> + </interface> + + <!-- + org.qemu.Display1.Audio: + + Audio backend may be available on ``/org/qemu/Display1/Audio``. + --> + <interface name="org.qemu.Display1.Audio"> + <!-- + RegisterOutListener: + @listener: a Unix socket FD, for peer-to-peer D-Bus communication. + + Register an audio backend playback handler. + + Multiple listeners may be registered simultaneously. + + The listener is expected to implement the + :dbus:iface:`org.qemu.Display1.AudioOutListener` interface. + --> + <method name="RegisterOutListener"> + <arg type="h" name="listener" direction="in"/> + </method> + + <!-- + RegisterInListener: + @listener: a Unix socket FD, for peer-to-peer D-Bus communication. + + Register an audio backend record handler. + + Multiple listeners may be registered simultaneously. + + The listener is expected to implement the + :dbus:iface:`org.qemu.Display1.AudioInListener` interface. + --> + <method name="RegisterInListener"> + <arg type="h" name="listener" direction="in"/> + </method> + </interface> + + <!-- + org.qemu.Display1.AudioOutListener: + + This client-side interface must be available on + ``/org/qemu/Display1/AudioOutListener`` when registering the peer-to-peer + connection with :dbus:meth:`~org.qemu.Display1.Audio.RegisterOutListener`. + --> + <interface name="org.qemu.Display1.AudioOutListener"> + <!-- + Init: + @id: the stream ID. + @bits: PCM bits per sample. + @is_signed: whether the PCM data is signed. + @is_float: PCM floating point format. + @freq: the PCM frequency in Hz. + @nchannels: the number of channels. + @bytes_per_frame: the bytes per frame. + @bytes_per_second: the bytes per second. + @be: whether using big-endian format. + + Initializes a PCM playback stream. + --> + <method name="Init"> + <arg name="id" type="t" direction="in"/> + <arg name="bits" type="y" direction="in"/> + <arg name="is_signed" type="b" direction="in"/> + <arg name="is_float" type="b" direction="in"/> + <arg name="freq" type="u" direction="in"/> + <arg name="nchannels" type="y" direction="in"/> + <arg name="bytes_per_frame" type="u" direction="in"/> + <arg name="bytes_per_second" type="u" direction="in"/> + <arg name="be" type="b" direction="in"/> + </method> + + <!-- + Fini: + @id: the stream ID. + + Finish & close a playback stream. + --> + <method name="Fini"> + <arg name="id" type="t" direction="in"/> + </method> + + <!-- + SetEnabled: + @id: the stream ID. + + Resume or suspend the playback stream. + --> + <method name="SetEnabled"> + <arg name="id" type="t" direction="in"/> + <arg name="enabled" type="b" direction="in"/> + </method> + + <!-- + SetVolume: + @id: the stream ID. + @mute: whether the stream is muted. + @volume: the volume per-channel. + + Set the stream volume and mute state (volume without unit, 0-255). + --> + <method name="SetVolume"> + <arg name="id" type="t" direction="in"/> + <arg name="mute" type="b" direction="in"/> + <arg name="volume" type="ay" direction="in"> + <annotation name="org.gtk.GDBus.C.ForceGVariant" value="true"/> + </arg> + </method> + + <!-- + Write: + @id: the stream ID. + @data: the PCM data. + + PCM stream to play. + --> + <method name="Write"> + <arg name="id" type="t" direction="in"/> + <arg type="ay" name="data" direction="in"> + <annotation name="org.gtk.GDBus.C.ForceGVariant" value="true"/> + </arg> + </method> + </interface> + + <!-- + org.qemu.Display1.AudioInListener: + + This client-side interface must be available on + ``/org/qemu/Display1/AudioInListener`` when registering the peer-to-peer + connection with :dbus:meth:`~org.qemu.Display1.Audio.RegisterInListener`. + --> + <interface name="org.qemu.Display1.AudioInListener"> + <!-- + Init: + @id: the stream ID. + @bits: PCM bits per sample. + @is_signed: whether the PCM data is signed. + @is_float: PCM floating point format. + @freq: the PCM frequency in Hz. + @nchannels: the number of channels. + @bytes_per_frame: the bytes per frame. + @bytes_per_second: the bytes per second. + @be: whether using big-endian format. + + Initializes a PCM record stream. + --> + <method name="Init"> + <arg name="id" type="t" direction="in"/> + <arg name="bits" type="y" direction="in"/> + <arg name="is_signed" type="b" direction="in"/> + <arg name="is_float" type="b" direction="in"/> + <arg name="freq" type="u" direction="in"/> + <arg name="nchannels" type="y" direction="in"/> + <arg name="bytes_per_frame" type="u" direction="in"/> + <arg name="bytes_per_second" type="u" direction="in"/> + <arg name="be" type="b" direction="in"/> + </method> + + <!-- + Fini: + @id: the stream ID. + + Finish & close a record stream. + --> + <method name="Fini"> + <arg name="id" type="t" direction="in"/> + </method> + + <!-- + SetEnabled: + @id: the stream ID. + + Resume or suspend the record stream. + --> + <method name="SetEnabled"> + <arg name="id" type="t" direction="in"/> + <arg name="enabled" type="b" direction="in"/> + </method> + + <!-- + SetVolume: + @id: the stream ID. + @mute: whether the stream is muted. + @volume: the volume per-channel. + + Set the stream volume and mute state (volume without unit, 0-255). + --> + <method name="SetVolume"> + <arg name="id" type="t" direction="in"/> + <arg name="mute" type="b" direction="in"/> + <arg name="volume" type="ay" direction="in"> + <annotation name="org.gtk.GDBus.C.ForceGVariant" value="true"/> + </arg> + </method> + + <!-- + Read: + @id: the stream ID. + @size: the amount to read, in bytes. + @data: the recorded data (which may be less than requested). + + Read "size" bytes from the record stream. + --> + <method name="Read"> + <arg name="id" type="t" direction="in"/> + <arg name="size" type="t" direction="in"/> + <arg type="ay" name="data" direction="out"> + <annotation name="org.gtk.GDBus.C.ForceGVariant" value="true"/> + </arg> + </method> + </interface> + + <!-- + org.qemu.Display1.Chardev: + + Character devices may be available on ``/org/qemu/Display1/Chardev_$id``. + + They may be used for different kind of streams, which are identified via + their FQDN :dbus:prop:`Name`. + + .. _dbus-chardev-fqdn: + + Here are some known reserved kind names (the ``org.qemu`` prefix is + reserved by QEMU): + + org.qemu.console.serial.0 + A serial console stream. + + org.qemu.monitor.hmp.0 + A QEMU HMP human monitor. + + org.qemu.monitor.qmp.0 + A QEMU QMP monitor. + + org.qemu.usbredir + A usbredir stream. + --> + <interface name="org.qemu.Display1.Chardev"> + <!-- + Register: + @stream: a Unix FD to redirect the stream to. + + Register a file-descriptor for the stream handling. + + The current handler, if any, will be replaced. + --> + <method name="Register"> + <arg type="h" name="stream" direction="in"/> + </method> + + <!-- + SendBreak: + + Send a break event to the character device. + --> + <method name="SendBreak"/> + + <!-- + Name: + + The FQDN name to identify the kind of stream. See :ref:`reserved + names<dbus-chardev-fqdn>`. + --> + <property name="Name" type="s" access="read"/> + + <!-- + FEOpened: + + Whether the front-end side is opened. + --> + <property name="FEOpened" type="b" access="read"/> + + <!-- + Echo: + + Whether the input should be echo'ed (for serial streams). + --> + <property name="Echo" type="b" access="read"/> + + <!-- + Owner: + + The D-Bus unique name of the registered handler. + --> + <property name="Owner" type="s" access="read"/> + </interface> +</node> diff --git a/ui/dbus-error.c b/ui/dbus-error.c new file mode 100644 index 0000000000..85a9194d57 --- /dev/null +++ b/ui/dbus-error.c @@ -0,0 +1,48 @@ +/* + * QEMU DBus display errors + * + * Copyright (c) 2021 Marc-André Lureau <marcandre.lureau@redhat.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "qemu/osdep.h" +#include "dbus.h" + +static const GDBusErrorEntry dbus_display_error_entries[] = { + { DBUS_DISPLAY_ERROR_FAILED, "org.qemu.Display1.Error.Failed" }, + { DBUS_DISPLAY_ERROR_INVALID, "org.qemu.Display1.Error.Invalid" }, + { DBUS_DISPLAY_ERROR_UNSUPPORTED, "org.qemu.Display1.Error.Unsupported" }, +}; + +G_STATIC_ASSERT(G_N_ELEMENTS(dbus_display_error_entries) == + DBUS_DISPLAY_N_ERRORS); + +GQuark +dbus_display_error_quark(void) +{ + static gsize quark; + + g_dbus_error_register_error_domain( + "dbus-display-error-quark", + &quark, + dbus_display_error_entries, + G_N_ELEMENTS(dbus_display_error_entries)); + + return (GQuark)quark; +} diff --git a/ui/dbus-listener.c b/ui/dbus-listener.c new file mode 100644 index 0000000000..81c119b13a --- /dev/null +++ b/ui/dbus-listener.c @@ -0,0 +1,486 @@ +/* + * QEMU DBus display console + * + * Copyright (c) 2021 Marc-André Lureau <marcandre.lureau@redhat.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "qemu/osdep.h" +#include "sysemu/sysemu.h" +#include "dbus.h" +#include <gio/gunixfdlist.h> + +#include "ui/shader.h" +#include "ui/egl-helpers.h" +#include "ui/egl-context.h" +#include "trace.h" + +struct _DBusDisplayListener { + GObject parent; + + char *bus_name; + DBusDisplayConsole *console; + GDBusConnection *conn; + + QemuDBusDisplay1Listener *proxy; + + DisplayChangeListener dcl; + DisplaySurface *ds; + QemuGLShader *gls; + int gl_updates; +}; + +G_DEFINE_TYPE(DBusDisplayListener, dbus_display_listener, G_TYPE_OBJECT) + +static void dbus_update_gl_cb(GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GError) err = NULL; + DBusDisplayListener *ddl = user_data; + + if (!qemu_dbus_display1_listener_call_update_dmabuf_finish(ddl->proxy, + res, &err)) { + error_report("Failed to call update: %s", err->message); + } + + graphic_hw_gl_block(ddl->dcl.con, false); + g_object_unref(ddl); +} + +static void dbus_call_update_gl(DBusDisplayListener *ddl, + int x, int y, int w, int h) +{ + graphic_hw_gl_block(ddl->dcl.con, true); + glFlush(); + qemu_dbus_display1_listener_call_update_dmabuf(ddl->proxy, + x, y, w, h, + G_DBUS_CALL_FLAGS_NONE, + DBUS_DEFAULT_TIMEOUT, NULL, + dbus_update_gl_cb, + g_object_ref(ddl)); +} + +static void dbus_scanout_disable(DisplayChangeListener *dcl) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + + ddl->ds = NULL; + qemu_dbus_display1_listener_call_disable( + ddl->proxy, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); +} + +static void dbus_scanout_dmabuf(DisplayChangeListener *dcl, + QemuDmaBuf *dmabuf) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + g_autoptr(GError) err = NULL; + g_autoptr(GUnixFDList) fd_list = NULL; + + fd_list = g_unix_fd_list_new(); + if (g_unix_fd_list_append(fd_list, dmabuf->fd, &err) != 0) { + error_report("Failed to setup dmabuf fdlist: %s", err->message); + return; + } + + qemu_dbus_display1_listener_call_scanout_dmabuf( + ddl->proxy, + g_variant_new_handle(0), + dmabuf->width, + dmabuf->height, + dmabuf->stride, + dmabuf->fourcc, + dmabuf->modifier, + dmabuf->y0_top, + G_DBUS_CALL_FLAGS_NONE, + -1, + fd_list, + NULL, NULL, NULL); +} + +static void dbus_scanout_texture(DisplayChangeListener *dcl, + uint32_t tex_id, + bool backing_y_0_top, + uint32_t backing_width, + uint32_t backing_height, + uint32_t x, uint32_t y, + uint32_t w, uint32_t h) +{ + QemuDmaBuf dmabuf = { + .width = backing_width, + .height = backing_height, + .y0_top = backing_y_0_top, + }; + + assert(tex_id); + dmabuf.fd = egl_get_fd_for_texture( + tex_id, (EGLint *)&dmabuf.stride, + (EGLint *)&dmabuf.fourcc, + &dmabuf.modifier); + if (dmabuf.fd < 0) { + error_report("%s: failed to get fd for texture", __func__); + return; + } + + dbus_scanout_dmabuf(dcl, &dmabuf); + close(dmabuf.fd); +} + +static void dbus_cursor_dmabuf(DisplayChangeListener *dcl, + QemuDmaBuf *dmabuf, bool have_hot, + uint32_t hot_x, uint32_t hot_y) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + DisplaySurface *ds; + GVariant *v_data = NULL; + egl_fb cursor_fb; + + if (!dmabuf) { + qemu_dbus_display1_listener_call_mouse_set( + ddl->proxy, 0, 0, false, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + return; + } + + egl_dmabuf_import_texture(dmabuf); + if (!dmabuf->texture) { + return; + } + egl_fb_setup_for_tex(&cursor_fb, dmabuf->width, dmabuf->height, + dmabuf->texture, false); + ds = qemu_create_displaysurface(dmabuf->width, dmabuf->height); + egl_fb_read(ds, &cursor_fb); + + v_data = g_variant_new_from_data( + G_VARIANT_TYPE("ay"), + surface_data(ds), + surface_width(ds) * surface_height(ds) * 4, + TRUE, + (GDestroyNotify)qemu_free_displaysurface, + ds); + qemu_dbus_display1_listener_call_cursor_define( + ddl->proxy, + surface_width(ds), + surface_height(ds), + hot_x, + hot_y, + v_data, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + NULL, + NULL); +} + +static void dbus_cursor_position(DisplayChangeListener *dcl, + uint32_t pos_x, uint32_t pos_y) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + + qemu_dbus_display1_listener_call_mouse_set( + ddl->proxy, pos_x, pos_y, true, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); +} + +static void dbus_release_dmabuf(DisplayChangeListener *dcl, + QemuDmaBuf *dmabuf) +{ + dbus_scanout_disable(dcl); +} + +static void dbus_scanout_update(DisplayChangeListener *dcl, + uint32_t x, uint32_t y, + uint32_t w, uint32_t h) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + + dbus_call_update_gl(ddl, x, y, w, h); +} + +static void dbus_gl_refresh(DisplayChangeListener *dcl) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + + graphic_hw_update(dcl->con); + + if (!ddl->ds || qemu_console_is_gl_blocked(ddl->dcl.con)) { + return; + } + + if (ddl->gl_updates) { + dbus_call_update_gl(ddl, 0, 0, + surface_width(ddl->ds), surface_height(ddl->ds)); + ddl->gl_updates = 0; + } +} + +static void dbus_refresh(DisplayChangeListener *dcl) +{ + graphic_hw_update(dcl->con); +} + +static void dbus_gl_gfx_update(DisplayChangeListener *dcl, + int x, int y, int w, int h) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + + if (ddl->ds) { + surface_gl_update_texture(ddl->gls, ddl->ds, x, y, w, h); + } + + ddl->gl_updates++; +} + +static void dbus_gfx_update(DisplayChangeListener *dcl, + int x, int y, int w, int h) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + pixman_image_t *img; + GVariant *v_data; + size_t stride; + + assert(ddl->ds); + stride = w * DIV_ROUND_UP(PIXMAN_FORMAT_BPP(surface_format(ddl->ds)), 8); + + trace_dbus_update(x, y, w, h); + + /* make a copy, since gvariant only handles linear data */ + img = pixman_image_create_bits(surface_format(ddl->ds), + w, h, NULL, stride); + pixman_image_composite(PIXMAN_OP_SRC, ddl->ds->image, NULL, img, + x, y, 0, 0, 0, 0, w, h); + + v_data = g_variant_new_from_data( + G_VARIANT_TYPE("ay"), + pixman_image_get_data(img), + pixman_image_get_stride(img) * h, + TRUE, + (GDestroyNotify)pixman_image_unref, + img); + qemu_dbus_display1_listener_call_update(ddl->proxy, + x, y, w, h, pixman_image_get_stride(img), pixman_image_get_format(img), + v_data, + G_DBUS_CALL_FLAGS_NONE, + DBUS_DEFAULT_TIMEOUT, NULL, NULL, NULL); +} + +static void dbus_gl_gfx_switch(DisplayChangeListener *dcl, + struct DisplaySurface *new_surface) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + + if (ddl->ds) { + surface_gl_destroy_texture(ddl->gls, ddl->ds); + } + ddl->ds = new_surface; + if (ddl->ds) { + int width = surface_width(ddl->ds); + int height = surface_height(ddl->ds); + + surface_gl_create_texture(ddl->gls, ddl->ds); + /* TODO: lazy send dmabuf (there are unnecessary sent otherwise) */ + dbus_scanout_texture(&ddl->dcl, ddl->ds->texture, false, + width, height, 0, 0, width, height); + } +} + +static void dbus_gfx_switch(DisplayChangeListener *dcl, + struct DisplaySurface *new_surface) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + GVariant *v_data = NULL; + + ddl->ds = new_surface; + if (!ddl->ds) { + /* why not call disable instead? */ + return; + } + + v_data = g_variant_new_from_data( + G_VARIANT_TYPE("ay"), + surface_data(ddl->ds), + surface_stride(ddl->ds) * surface_height(ddl->ds), + TRUE, + (GDestroyNotify)pixman_image_unref, + pixman_image_ref(ddl->ds->image)); + qemu_dbus_display1_listener_call_scanout(ddl->proxy, + surface_width(ddl->ds), + surface_height(ddl->ds), + surface_stride(ddl->ds), + surface_format(ddl->ds), + v_data, + G_DBUS_CALL_FLAGS_NONE, + DBUS_DEFAULT_TIMEOUT, NULL, NULL, NULL); +} + +static void dbus_mouse_set(DisplayChangeListener *dcl, + int x, int y, int on) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + + qemu_dbus_display1_listener_call_mouse_set( + ddl->proxy, x, y, on, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); +} + +static void dbus_cursor_define(DisplayChangeListener *dcl, + QEMUCursor *c) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + GVariant *v_data = NULL; + + cursor_get(c); + v_data = g_variant_new_from_data( + G_VARIANT_TYPE("ay"), + c->data, + c->width * c->height * 4, + TRUE, + (GDestroyNotify)cursor_put, + c); + + qemu_dbus_display1_listener_call_cursor_define( + ddl->proxy, + c->width, + c->height, + c->hot_x, + c->hot_y, + v_data, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + NULL, + NULL); +} + +const DisplayChangeListenerOps dbus_gl_dcl_ops = { + .dpy_name = "dbus-gl", + .dpy_gfx_update = dbus_gl_gfx_update, + .dpy_gfx_switch = dbus_gl_gfx_switch, + .dpy_gfx_check_format = console_gl_check_format, + .dpy_refresh = dbus_gl_refresh, + .dpy_mouse_set = dbus_mouse_set, + .dpy_cursor_define = dbus_cursor_define, + + .dpy_gl_scanout_disable = dbus_scanout_disable, + .dpy_gl_scanout_texture = dbus_scanout_texture, + .dpy_gl_scanout_dmabuf = dbus_scanout_dmabuf, + .dpy_gl_cursor_dmabuf = dbus_cursor_dmabuf, + .dpy_gl_cursor_position = dbus_cursor_position, + .dpy_gl_release_dmabuf = dbus_release_dmabuf, + .dpy_gl_update = dbus_scanout_update, +}; + +const DisplayChangeListenerOps dbus_dcl_ops = { + .dpy_name = "dbus", + .dpy_gfx_update = dbus_gfx_update, + .dpy_gfx_switch = dbus_gfx_switch, + .dpy_refresh = dbus_refresh, + .dpy_mouse_set = dbus_mouse_set, + .dpy_cursor_define = dbus_cursor_define, +}; + +static void +dbus_display_listener_dispose(GObject *object) +{ + DBusDisplayListener *ddl = DBUS_DISPLAY_LISTENER(object); + + unregister_displaychangelistener(&ddl->dcl); + g_clear_object(&ddl->conn); + g_clear_pointer(&ddl->bus_name, g_free); + g_clear_object(&ddl->proxy); + g_clear_pointer(&ddl->gls, qemu_gl_fini_shader); + + G_OBJECT_CLASS(dbus_display_listener_parent_class)->dispose(object); +} + +static void +dbus_display_listener_constructed(GObject *object) +{ + DBusDisplayListener *ddl = DBUS_DISPLAY_LISTENER(object); + + if (display_opengl) { + ddl->gls = qemu_gl_init_shader(); + ddl->dcl.ops = &dbus_gl_dcl_ops; + } else { + ddl->dcl.ops = &dbus_dcl_ops; + } + + G_OBJECT_CLASS(dbus_display_listener_parent_class)->constructed(object); +} + +static void +dbus_display_listener_class_init(DBusDisplayListenerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + + object_class->dispose = dbus_display_listener_dispose; + object_class->constructed = dbus_display_listener_constructed; +} + +static void +dbus_display_listener_init(DBusDisplayListener *ddl) +{ +} + +const char * +dbus_display_listener_get_bus_name(DBusDisplayListener *ddl) +{ + return ddl->bus_name ?: "p2p"; +} + +DBusDisplayConsole * +dbus_display_listener_get_console(DBusDisplayListener *ddl) +{ + return ddl->console; +} + +DBusDisplayListener * +dbus_display_listener_new(const char *bus_name, + GDBusConnection *conn, + DBusDisplayConsole *console) +{ + DBusDisplayListener *ddl; + QemuConsole *con; + g_autoptr(GError) err = NULL; + + ddl = g_object_new(DBUS_DISPLAY_TYPE_LISTENER, NULL); + ddl->proxy = + qemu_dbus_display1_listener_proxy_new_sync(conn, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + NULL, + "/org/qemu/Display1/Listener", + NULL, + &err); + if (!ddl->proxy) { + error_report("Failed to setup proxy: %s", err->message); + g_object_unref(conn); + g_object_unref(ddl); + return NULL; + } + + ddl->bus_name = g_strdup(bus_name); + ddl->conn = conn; + ddl->console = console; + + con = qemu_console_lookup_by_index(dbus_display_console_get_index(console)); + assert(con); + ddl->dcl.con = con; + register_displaychangelistener(&ddl->dcl); + + return ddl; +} diff --git a/ui/dbus-module.c b/ui/dbus-module.c new file mode 100644 index 0000000000..c8771fe48c --- /dev/null +++ b/ui/dbus-module.c @@ -0,0 +1,35 @@ +/* + * D-Bus module support. + * + * Copyright (C) 2021 Red Hat, Inc. + * + * 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 or + * (at your option) version 3 of the License. + * + * 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 "qapi/error.h" +#include "ui/dbus-module.h" + +int using_dbus_display; + +static bool +qemu_dbus_display_add_client(int csock, Error **errp) +{ + error_setg(errp, "D-Bus display isn't enabled"); + return false; +} + +struct QemuDBusDisplayOps qemu_dbus_display = { + .add_client = qemu_dbus_display_add_client, +}; diff --git a/ui/dbus.c b/ui/dbus.c new file mode 100644 index 0000000000..b2c1c9fb52 --- /dev/null +++ b/ui/dbus.c @@ -0,0 +1,482 @@ +/* + * QEMU DBus display + * + * Copyright (c) 2021 Marc-André Lureau <marcandre.lureau@redhat.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "qemu/osdep.h" +#include "qemu/cutils.h" +#include "qemu/dbus.h" +#include "qemu/main-loop.h" +#include "qemu/option.h" +#include "qom/object_interfaces.h" +#include "sysemu/sysemu.h" +#include "ui/dbus-module.h" +#include "ui/egl-helpers.h" +#include "ui/egl-context.h" +#include "audio/audio.h" +#include "audio/audio_int.h" +#include "qapi/error.h" +#include "trace.h" + +#include "dbus.h" + +static DBusDisplay *dbus_display; + +static QEMUGLContext dbus_create_context(DisplayGLCtx *dgc, + QEMUGLParams *params) +{ + eglMakeCurrent(qemu_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, + qemu_egl_rn_ctx); + return qemu_egl_create_context(dgc, params); +} + +static const DisplayGLCtxOps dbus_gl_ops = { + .compatible_dcl = &dbus_gl_dcl_ops, + .dpy_gl_ctx_create = dbus_create_context, + .dpy_gl_ctx_destroy = qemu_egl_destroy_context, + .dpy_gl_ctx_make_current = qemu_egl_make_context_current, +}; + +static NotifierList dbus_display_notifiers = + NOTIFIER_LIST_INITIALIZER(dbus_display_notifiers); + +void +dbus_display_notifier_add(Notifier *notifier) +{ + notifier_list_add(&dbus_display_notifiers, notifier); +} + +static void +dbus_display_notifier_remove(Notifier *notifier) +{ + notifier_remove(notifier); +} + +void +dbus_display_notify(DBusDisplayEvent *event) +{ + notifier_list_notify(&dbus_display_notifiers, event); +} + +static void +dbus_display_init(Object *o) +{ + DBusDisplay *dd = DBUS_DISPLAY(o); + g_autoptr(GDBusObjectSkeleton) vm = NULL; + + dd->glctx.ops = &dbus_gl_ops; + dd->iface = qemu_dbus_display1_vm_skeleton_new(); + dd->consoles = g_ptr_array_new_with_free_func(g_object_unref); + + dd->server = g_dbus_object_manager_server_new(DBUS_DISPLAY1_ROOT); + + vm = g_dbus_object_skeleton_new(DBUS_DISPLAY1_ROOT "/VM"); + g_dbus_object_skeleton_add_interface( + vm, G_DBUS_INTERFACE_SKELETON(dd->iface)); + g_dbus_object_manager_server_export(dd->server, vm); + + dbus_clipboard_init(dd); + dbus_chardev_init(dd); +} + +static void +dbus_display_finalize(Object *o) +{ + DBusDisplay *dd = DBUS_DISPLAY(o); + + if (dd->notifier.notify) { + dbus_display_notifier_remove(&dd->notifier); + } + + qemu_clipboard_peer_unregister(&dd->clipboard_peer); + g_clear_object(&dd->clipboard); + + g_clear_object(&dd->server); + g_clear_pointer(&dd->consoles, g_ptr_array_unref); + if (dd->add_client_cancellable) { + g_cancellable_cancel(dd->add_client_cancellable); + } + g_clear_object(&dd->add_client_cancellable); + g_clear_object(&dd->bus); + g_clear_object(&dd->iface); + g_free(dd->dbus_addr); + g_free(dd->audiodev); + dbus_display = NULL; +} + +static bool +dbus_display_add_console(DBusDisplay *dd, int idx, Error **errp) +{ + QemuConsole *con; + DBusDisplayConsole *dbus_console; + + con = qemu_console_lookup_by_index(idx); + assert(con); + + if (qemu_console_is_graphic(con) && + dd->gl_mode != DISPLAYGL_MODE_OFF) { + qemu_console_set_display_gl_ctx(con, &dd->glctx); + } + + dbus_console = dbus_display_console_new(dd, con); + g_ptr_array_insert(dd->consoles, idx, dbus_console); + g_dbus_object_manager_server_export(dd->server, + G_DBUS_OBJECT_SKELETON(dbus_console)); + return true; +} + +static void +dbus_display_complete(UserCreatable *uc, Error **errp) +{ + DBusDisplay *dd = DBUS_DISPLAY(uc); + g_autoptr(GError) err = NULL; + g_autofree char *uuid = qemu_uuid_unparse_strdup(&qemu_uuid); + g_autoptr(GArray) consoles = NULL; + GVariant *console_ids; + int idx; + + if (!object_resolve_path_type("", TYPE_DBUS_DISPLAY, NULL)) { + error_setg(errp, "There is already an instance of %s", + TYPE_DBUS_DISPLAY); + return; + } + + if (dd->p2p) { + /* wait for dbus_display_add_client() */ + dbus_display = dd; + } else if (dd->dbus_addr && *dd->dbus_addr) { + dd->bus = g_dbus_connection_new_for_address_sync(dd->dbus_addr, + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | + G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION, + NULL, NULL, &err); + } else { + dd->bus = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &err); + } + if (err) { + error_setg(errp, "failed to connect to DBus: %s", err->message); + return; + } + + if (dd->audiodev && *dd->audiodev) { + AudioState *audio_state = audio_state_by_name(dd->audiodev); + if (!audio_state) { + error_setg(errp, "Audiodev '%s' not found", dd->audiodev); + return; + } + if (!g_str_equal(audio_state->drv->name, "dbus")) { + error_setg(errp, "Audiodev '%s' is not compatible with DBus", + dd->audiodev); + return; + } + audio_state->drv->set_dbus_server(audio_state, dd->server); + } + + consoles = g_array_new(FALSE, FALSE, sizeof(guint32)); + for (idx = 0;; idx++) { + if (!qemu_console_lookup_by_index(idx)) { + break; + } + if (!dbus_display_add_console(dd, idx, errp)) { + return; + } + g_array_append_val(consoles, idx); + } + + console_ids = g_variant_new_from_data( + G_VARIANT_TYPE("au"), + consoles->data, consoles->len * sizeof(guint32), TRUE, + (GDestroyNotify)g_array_unref, consoles); + g_steal_pointer(&consoles); + g_object_set(dd->iface, + "name", qemu_name ?: "QEMU " QEMU_VERSION, + "uuid", uuid, + "console-ids", console_ids, + NULL); + + if (dd->bus) { + g_dbus_object_manager_server_set_connection(dd->server, dd->bus); + g_bus_own_name_on_connection(dd->bus, "org.qemu", + G_BUS_NAME_OWNER_FLAGS_NONE, + NULL, NULL, NULL, NULL); + } +} + +static void +dbus_display_add_client_ready(GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GError) err = NULL; + g_autoptr(GDBusConnection) conn = NULL; + + g_clear_object(&dbus_display->add_client_cancellable); + + conn = g_dbus_connection_new_finish(res, &err); + if (!conn) { + error_printf("Failed to accept D-Bus client: %s", err->message); + } + + g_dbus_object_manager_server_set_connection(dbus_display->server, conn); +} + + +static bool +dbus_display_add_client(int csock, Error **errp) +{ + g_autoptr(GError) err = NULL; + g_autoptr(GSocket) socket = NULL; + g_autoptr(GSocketConnection) conn = NULL; + g_autofree char *guid = g_dbus_generate_guid(); + + if (!dbus_display) { + error_setg(errp, "p2p connections not accepted in bus mode"); + return false; + } + + if (dbus_display->add_client_cancellable) { + g_cancellable_cancel(dbus_display->add_client_cancellable); + } + + socket = g_socket_new_from_fd(csock, &err); + if (!socket) { + error_setg(errp, "Failed to setup D-Bus socket: %s", err->message); + return false; + } + + conn = g_socket_connection_factory_create_connection(socket); + + dbus_display->add_client_cancellable = g_cancellable_new(); + + g_dbus_connection_new(G_IO_STREAM(conn), + guid, + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_SERVER, + NULL, + dbus_display->add_client_cancellable, + dbus_display_add_client_ready, + NULL); + + return true; +} + +static bool +get_dbus_p2p(Object *o, Error **errp) +{ + DBusDisplay *dd = DBUS_DISPLAY(o); + + return dd->p2p; +} + +static void +set_dbus_p2p(Object *o, bool p2p, Error **errp) +{ + DBusDisplay *dd = DBUS_DISPLAY(o); + + dd->p2p = p2p; +} + +static char * +get_dbus_addr(Object *o, Error **errp) +{ + DBusDisplay *dd = DBUS_DISPLAY(o); + + return g_strdup(dd->dbus_addr); +} + +static void +set_dbus_addr(Object *o, const char *str, Error **errp) +{ + DBusDisplay *dd = DBUS_DISPLAY(o); + + g_free(dd->dbus_addr); + dd->dbus_addr = g_strdup(str); +} + +static char * +get_audiodev(Object *o, Error **errp) +{ + DBusDisplay *dd = DBUS_DISPLAY(o); + + return g_strdup(dd->audiodev); +} + +static void +set_audiodev(Object *o, const char *str, Error **errp) +{ + DBusDisplay *dd = DBUS_DISPLAY(o); + + g_free(dd->audiodev); + dd->audiodev = g_strdup(str); +} + + +static int +get_gl_mode(Object *o, Error **errp) +{ + DBusDisplay *dd = DBUS_DISPLAY(o); + + return dd->gl_mode; +} + +static void +set_gl_mode(Object *o, int val, Error **errp) +{ + DBusDisplay *dd = DBUS_DISPLAY(o); + + dd->gl_mode = val; +} + +static void +dbus_display_class_init(ObjectClass *oc, void *data) +{ + UserCreatableClass *ucc = USER_CREATABLE_CLASS(oc); + + ucc->complete = dbus_display_complete; + object_class_property_add_bool(oc, "p2p", get_dbus_p2p, set_dbus_p2p); + object_class_property_add_str(oc, "addr", get_dbus_addr, set_dbus_addr); + object_class_property_add_str(oc, "audiodev", get_audiodev, set_audiodev); + object_class_property_add_enum(oc, "gl-mode", + "DisplayGLMode", &DisplayGLMode_lookup, + get_gl_mode, set_gl_mode); +} + +#define TYPE_CHARDEV_VC "chardev-vc" + +typedef struct DBusVCClass { + DBusChardevClass parent_class; + + void (*parent_parse)(QemuOpts *opts, ChardevBackend *b, Error **errp); +} DBusVCClass; + +DECLARE_CLASS_CHECKERS(DBusVCClass, DBUS_VC, + TYPE_CHARDEV_VC) + +static void +dbus_vc_parse(QemuOpts *opts, ChardevBackend *backend, + Error **errp) +{ + DBusVCClass *klass = DBUS_VC_CLASS(object_class_by_name(TYPE_CHARDEV_VC)); + const char *name = qemu_opt_get(opts, "name"); + const char *id = qemu_opts_id(opts); + + if (name == NULL) { + if (g_str_has_prefix(id, "compat_monitor")) { + name = "org.qemu.monitor.hmp.0"; + } else if (g_str_has_prefix(id, "serial")) { + name = "org.qemu.console.serial.0"; + } else { + name = ""; + } + if (!qemu_opt_set(opts, "name", name, errp)) { + return; + } + } + + klass->parent_parse(opts, backend, errp); +} + +static void +dbus_vc_class_init(ObjectClass *oc, void *data) +{ + DBusVCClass *klass = DBUS_VC_CLASS(oc); + ChardevClass *cc = CHARDEV_CLASS(oc); + + klass->parent_parse = cc->parse; + cc->parse = dbus_vc_parse; +} + +static const TypeInfo dbus_vc_type_info = { + .name = TYPE_CHARDEV_VC, + .parent = TYPE_CHARDEV_DBUS, + .class_init = dbus_vc_class_init, +}; + +static void +early_dbus_init(DisplayOptions *opts) +{ + DisplayGLMode mode = opts->has_gl ? opts->gl : DISPLAYGL_MODE_OFF; + + if (mode != DISPLAYGL_MODE_OFF) { + if (egl_rendernode_init(opts->u.dbus.rendernode, mode) < 0) { + error_report("dbus: render node init failed"); + exit(1); + } + + display_opengl = 1; + } + + type_register(&dbus_vc_type_info); +} + +static void +dbus_init(DisplayState *ds, DisplayOptions *opts) +{ + DisplayGLMode mode = opts->has_gl ? opts->gl : DISPLAYGL_MODE_OFF; + + if (opts->u.dbus.addr && opts->u.dbus.p2p) { + error_report("dbus: can't accept both addr=X and p2p=yes options"); + exit(1); + } + + using_dbus_display = 1; + + object_new_with_props(TYPE_DBUS_DISPLAY, + object_get_objects_root(), + "dbus-display", &error_fatal, + "addr", opts->u.dbus.addr ?: "", + "audiodev", opts->u.dbus.audiodev ?: "", + "gl-mode", DisplayGLMode_str(mode), + "p2p", yes_no(opts->u.dbus.p2p), + NULL); +} + +static const TypeInfo dbus_display_info = { + .name = TYPE_DBUS_DISPLAY, + .parent = TYPE_OBJECT, + .instance_size = sizeof(DBusDisplay), + .instance_init = dbus_display_init, + .instance_finalize = dbus_display_finalize, + .class_init = dbus_display_class_init, + .interfaces = (InterfaceInfo[]) { + { TYPE_USER_CREATABLE }, + { } + } +}; + +static QemuDisplay qemu_display_dbus = { + .type = DISPLAY_TYPE_DBUS, + .early_init = early_dbus_init, + .init = dbus_init, +}; + +static void register_dbus(void) +{ + qemu_dbus_display = (struct QemuDBusDisplayOps) { + .add_client = dbus_display_add_client, + }; + type_register_static(&dbus_display_info); + qemu_display_register(&qemu_display_dbus); +} + +type_init(register_dbus); + +#ifdef CONFIG_OPENGL +module_dep("ui-opengl"); +#endif diff --git a/ui/dbus.h b/ui/dbus.h new file mode 100644 index 0000000000..64c77cab44 --- /dev/null +++ b/ui/dbus.h @@ -0,0 +1,144 @@ +/* + * QEMU DBus display + * + * Copyright (c) 2021 Marc-André Lureau <marcandre.lureau@redhat.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef UI_DBUS_H_ +#define UI_DBUS_H_ + +#include "chardev/char-socket.h" +#include "qemu/dbus.h" +#include "qom/object.h" +#include "ui/console.h" +#include "ui/clipboard.h" + +#include "dbus-display1.h" + +typedef struct DBusClipboardRequest { + GDBusMethodInvocation *invocation; + QemuClipboardType type; + guint timeout_id; +} DBusClipboardRequest; + +struct DBusDisplay { + Object parent; + + DisplayGLMode gl_mode; + bool p2p; + char *dbus_addr; + char *audiodev; + DisplayGLCtx glctx; + + GDBusConnection *bus; + GDBusObjectManagerServer *server; + QemuDBusDisplay1VM *iface; + GPtrArray *consoles; + GCancellable *add_client_cancellable; + + QemuClipboardPeer clipboard_peer; + QemuDBusDisplay1Clipboard *clipboard; + QemuDBusDisplay1Clipboard *clipboard_proxy; + DBusClipboardRequest clipboard_request[QEMU_CLIPBOARD_SELECTION__COUNT]; + + Notifier notifier; +}; + +#define TYPE_DBUS_DISPLAY "dbus-display" +OBJECT_DECLARE_SIMPLE_TYPE(DBusDisplay, DBUS_DISPLAY) + +void dbus_display_notifier_add(Notifier *notifier); + +#define DBUS_DISPLAY_TYPE_CONSOLE dbus_display_console_get_type() +G_DECLARE_FINAL_TYPE(DBusDisplayConsole, + dbus_display_console, + DBUS_DISPLAY, + CONSOLE, + GDBusObjectSkeleton) + +DBusDisplayConsole * +dbus_display_console_new(DBusDisplay *display, QemuConsole *con); + +int +dbus_display_console_get_index(DBusDisplayConsole *ddc); + +#define DBUS_DISPLAY_TYPE_LISTENER dbus_display_listener_get_type() +G_DECLARE_FINAL_TYPE(DBusDisplayListener, + dbus_display_listener, + DBUS_DISPLAY, + LISTENER, + GObject) + +DBusDisplayListener * +dbus_display_listener_new(const char *bus_name, + GDBusConnection *conn, + DBusDisplayConsole *console); + +DBusDisplayConsole * +dbus_display_listener_get_console(DBusDisplayListener *ddl); + +const char * +dbus_display_listener_get_bus_name(DBusDisplayListener *ddl); + +extern const DisplayChangeListenerOps dbus_gl_dcl_ops; +extern const DisplayChangeListenerOps dbus_dcl_ops; + +#define TYPE_CHARDEV_DBUS "chardev-dbus" + +typedef struct DBusChardevClass { + SocketChardevClass parent_class; + + void (*parent_chr_be_event)(Chardev *s, QEMUChrEvent event); +} DBusChardevClass; + +DECLARE_CLASS_CHECKERS(DBusChardevClass, DBUS_CHARDEV, + TYPE_CHARDEV_DBUS) + +typedef struct DBusChardev { + SocketChardev parent; + + bool exported; + QemuDBusDisplay1Chardev *iface; +} DBusChardev; + +DECLARE_INSTANCE_CHECKER(DBusChardev, DBUS_CHARDEV, TYPE_CHARDEV_DBUS) + +#define CHARDEV_IS_DBUS(chr) \ + object_dynamic_cast(OBJECT(chr), TYPE_CHARDEV_DBUS) + +typedef enum { + DBUS_DISPLAY_CHARDEV_OPEN, + DBUS_DISPLAY_CHARDEV_CLOSE, +} DBusDisplayEventType; + +typedef struct DBusDisplayEvent { + DBusDisplayEventType type; + union { + DBusChardev *chardev; + }; +} DBusDisplayEvent; + +void dbus_display_notify(DBusDisplayEvent *event); + +void dbus_chardev_init(DBusDisplay *dpy); + +void dbus_clipboard_init(DBusDisplay *dpy); + +#endif /* UI_DBUS_H_ */ diff --git a/ui/egl-context.c b/ui/egl-context.c index 368ffa49d8..eb5f520fc4 100644 --- a/ui/egl-context.c +++ b/ui/egl-context.c @@ -1,7 +1,7 @@ #include "qemu/osdep.h" #include "ui/egl-context.h" -QEMUGLContext qemu_egl_create_context(DisplayChangeListener *dcl, +QEMUGLContext qemu_egl_create_context(DisplayGLCtx *dgc, QEMUGLParams *params) { EGLContext ctx; @@ -24,12 +24,12 @@ QEMUGLContext qemu_egl_create_context(DisplayChangeListener *dcl, return ctx; } -void qemu_egl_destroy_context(DisplayChangeListener *dcl, QEMUGLContext ctx) +void qemu_egl_destroy_context(DisplayGLCtx *dgc, QEMUGLContext ctx) { eglDestroyContext(qemu_egl_display, ctx); } -int qemu_egl_make_context_current(DisplayChangeListener *dcl, +int qemu_egl_make_context_current(DisplayGLCtx *dgc, QEMUGLContext ctx) { return eglMakeCurrent(qemu_egl_display, diff --git a/ui/egl-headless.c b/ui/egl-headless.c index a26a2520c4..94082a9da9 100644 --- a/ui/egl-headless.c +++ b/ui/egl-headless.c @@ -38,12 +38,12 @@ static void egl_gfx_switch(DisplayChangeListener *dcl, edpy->ds = new_surface; } -static QEMUGLContext egl_create_context(DisplayChangeListener *dcl, +static QEMUGLContext egl_create_context(DisplayGLCtx *dgc, QEMUGLParams *params) { eglMakeCurrent(qemu_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, qemu_egl_rn_ctx); - return qemu_egl_create_context(dcl, params); + return qemu_egl_create_context(dgc, params); } static void egl_scanout_disable(DisplayChangeListener *dcl) @@ -157,10 +157,6 @@ static const DisplayChangeListenerOps egl_ops = { .dpy_gfx_update = egl_gfx_update, .dpy_gfx_switch = egl_gfx_switch, - .dpy_gl_ctx_create = egl_create_context, - .dpy_gl_ctx_destroy = qemu_egl_destroy_context, - .dpy_gl_ctx_make_current = qemu_egl_make_context_current, - .dpy_gl_scanout_disable = egl_scanout_disable, .dpy_gl_scanout_texture = egl_scanout_texture, .dpy_gl_scanout_dmabuf = egl_scanout_dmabuf, @@ -170,6 +166,13 @@ static const DisplayChangeListenerOps egl_ops = { .dpy_gl_update = egl_scanout_flush, }; +static const DisplayGLCtxOps eglctx_ops = { + .compatible_dcl = &egl_ops, + .dpy_gl_ctx_create = egl_create_context, + .dpy_gl_ctx_destroy = qemu_egl_destroy_context, + .dpy_gl_ctx_make_current = qemu_egl_make_context_current, +}; + static void early_egl_headless_init(DisplayOptions *opts) { display_opengl = 1; @@ -188,6 +191,8 @@ static void egl_headless_init(DisplayState *ds, DisplayOptions *opts) } for (idx = 0;; idx++) { + DisplayGLCtx *ctx; + con = qemu_console_lookup_by_index(idx); if (!con || !qemu_console_is_graphic(con)) { break; @@ -197,6 +202,9 @@ static void egl_headless_init(DisplayState *ds, DisplayOptions *opts) edpy->dcl.con = con; edpy->dcl.ops = &egl_ops; edpy->gls = qemu_gl_init_shader(); + ctx = g_new0(DisplayGLCtx, 1); + ctx->ops = &eglctx_ops; + qemu_console_set_display_gl_ctx(con, ctx); register_displaychangelistener(&edpy->dcl); } } diff --git a/ui/gtk-clipboard.c b/ui/gtk-clipboard.c index 35b7a2c228..e0b8b283fe 100644 --- a/ui/gtk-clipboard.c +++ b/ui/gtk-clipboard.c @@ -74,10 +74,9 @@ static void gd_clipboard_clear(GtkClipboard *clipboard, gd->cbowner[s] = false; } -static void gd_clipboard_notify(Notifier *notifier, void *data) +static void gd_clipboard_update_info(GtkDisplayState *gd, + QemuClipboardInfo *info) { - GtkDisplayState *gd = container_of(notifier, GtkDisplayState, cbpeer.update); - QemuClipboardInfo *info = data; QemuClipboardSelection s = info->selection; bool self_update = info->owner == &gd->cbpeer; @@ -118,6 +117,22 @@ static void gd_clipboard_notify(Notifier *notifier, void *data) */ } +static void gd_clipboard_notify(Notifier *notifier, void *data) +{ + GtkDisplayState *gd = + container_of(notifier, GtkDisplayState, cbpeer.notifier); + QemuClipboardNotify *notify = data; + + switch (notify->type) { + case QEMU_CLIPBOARD_UPDATE_INFO: + gd_clipboard_update_info(gd, notify->info); + return; + case QEMU_CLIPBOARD_RESET_SERIAL: + /* ignore */ + return; + } +} + static void gd_clipboard_request(QemuClipboardInfo *info, QemuClipboardType type) { @@ -172,7 +187,7 @@ static void gd_owner_change(GtkClipboard *clipboard, void gd_clipboard_init(GtkDisplayState *gd) { gd->cbpeer.name = "gtk"; - gd->cbpeer.update.notify = gd_clipboard_notify; + gd->cbpeer.notifier.notify = gd_clipboard_notify; gd->cbpeer.request = gd_clipboard_request; qemu_clipboard_peer_register(&gd->cbpeer); diff --git a/ui/gtk-egl.c b/ui/gtk-egl.c index 45cb67712d..e3bd4bc274 100644 --- a/ui/gtk-egl.c +++ b/ui/gtk-egl.c @@ -119,8 +119,6 @@ void gd_egl_draw(VirtualConsole *vc) glFlush(); } - - graphic_hw_gl_flushed(vc->gfx.dcl.con); } void gd_egl_update(DisplayChangeListener *dcl, @@ -199,14 +197,14 @@ void gd_egl_switch(DisplayChangeListener *dcl, } } -QEMUGLContext gd_egl_create_context(DisplayChangeListener *dcl, +QEMUGLContext gd_egl_create_context(DisplayGLCtx *dgc, QEMUGLParams *params) { - VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + VirtualConsole *vc = container_of(dgc, VirtualConsole, gfx.dgc); eglMakeCurrent(qemu_egl_display, vc->gfx.esurface, vc->gfx.esurface, vc->gfx.ectx); - return qemu_egl_create_context(dcl, params); + return qemu_egl_create_context(dgc, params); } void gd_egl_scanout_disable(DisplayChangeListener *dcl) @@ -362,10 +360,10 @@ void gtk_egl_init(DisplayGLMode mode) display_opengl = 1; } -int gd_egl_make_current(DisplayChangeListener *dcl, +int gd_egl_make_current(DisplayGLCtx *dgc, QEMUGLContext ctx) { - VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + VirtualConsole *vc = container_of(dgc, VirtualConsole, gfx.dgc); return eglMakeCurrent(qemu_egl_display, vc->gfx.esurface, vc->gfx.esurface, ctx); diff --git a/ui/gtk-gl-area.c b/ui/gtk-gl-area.c index 01e4e74ee3..fc5a082eb8 100644 --- a/ui/gtk-gl-area.c +++ b/ui/gtk-gl-area.c @@ -101,8 +101,6 @@ void gd_gl_area_draw(VirtualConsole *vc) surface_gl_setup_viewport(vc->gfx.gls, vc->gfx.ds, ww, wh); surface_gl_render_texture(vc->gfx.gls, vc->gfx.ds); } - - graphic_hw_gl_flushed(vc->gfx.dcl.con); } void gd_gl_area_update(DisplayChangeListener *dcl, @@ -172,10 +170,10 @@ void gd_gl_area_switch(DisplayChangeListener *dcl, } } -QEMUGLContext gd_gl_area_create_context(DisplayChangeListener *dcl, +QEMUGLContext gd_gl_area_create_context(DisplayGLCtx *dgc, QEMUGLParams *params) { - VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + VirtualConsole *vc = container_of(dgc, VirtualConsole, gfx.dgc); GdkWindow *window; GdkGLContext *ctx; GError *err = NULL; @@ -201,7 +199,7 @@ QEMUGLContext gd_gl_area_create_context(DisplayChangeListener *dcl, return ctx; } -void gd_gl_area_destroy_context(DisplayChangeListener *dcl, QEMUGLContext ctx) +void gd_gl_area_destroy_context(DisplayGLCtx *dgc, QEMUGLContext ctx) { /* FIXME */ } @@ -280,7 +278,7 @@ void gtk_gl_area_init(void) display_opengl = 1; } -int gd_gl_area_make_current(DisplayChangeListener *dcl, +int gd_gl_area_make_current(DisplayGLCtx *dgc, QEMUGLContext ctx) { gdk_gl_context_make_current(ctx); @@ -593,7 +593,6 @@ void gd_hw_gl_flushed(void *vcon) close(dmabuf->fence_fd); dmabuf->fence_fd = -1; graphic_hw_gl_block(vc->gfx.dcl.con, false); - graphic_hw_gl_flushed(vc->gfx.dcl.con); } /** DisplayState Callbacks (opengl version) **/ @@ -607,9 +606,6 @@ static const DisplayChangeListenerOps dcl_gl_area_ops = { .dpy_mouse_set = gd_mouse_set, .dpy_cursor_define = gd_cursor_define, - .dpy_gl_ctx_create = gd_gl_area_create_context, - .dpy_gl_ctx_destroy = gd_gl_area_destroy_context, - .dpy_gl_ctx_make_current = gd_gl_area_make_current, .dpy_gl_scanout_texture = gd_gl_area_scanout_texture, .dpy_gl_scanout_disable = gd_gl_area_scanout_disable, .dpy_gl_update = gd_gl_area_scanout_flush, @@ -618,8 +614,14 @@ static const DisplayChangeListenerOps dcl_gl_area_ops = { .dpy_has_dmabuf = gd_has_dmabuf, }; -#ifdef CONFIG_X11 +static const DisplayGLCtxOps gl_area_ctx_ops = { + .compatible_dcl = &dcl_gl_area_ops, + .dpy_gl_ctx_create = gd_gl_area_create_context, + .dpy_gl_ctx_destroy = gd_gl_area_destroy_context, + .dpy_gl_ctx_make_current = gd_gl_area_make_current, +}; +#ifdef CONFIG_X11 static const DisplayChangeListenerOps dcl_egl_ops = { .dpy_name = "gtk-egl", .dpy_gfx_update = gd_egl_update, @@ -629,9 +631,6 @@ static const DisplayChangeListenerOps dcl_egl_ops = { .dpy_mouse_set = gd_mouse_set, .dpy_cursor_define = gd_cursor_define, - .dpy_gl_ctx_create = gd_egl_create_context, - .dpy_gl_ctx_destroy = qemu_egl_destroy_context, - .dpy_gl_ctx_make_current = gd_egl_make_current, .dpy_gl_scanout_disable = gd_egl_scanout_disable, .dpy_gl_scanout_texture = gd_egl_scanout_texture, .dpy_gl_scanout_dmabuf = gd_egl_scanout_dmabuf, @@ -642,6 +641,12 @@ static const DisplayChangeListenerOps dcl_egl_ops = { .dpy_has_dmabuf = gd_has_dmabuf, }; +static const DisplayGLCtxOps egl_ctx_ops = { + .compatible_dcl = &dcl_egl_ops, + .dpy_gl_ctx_create = gd_egl_create_context, + .dpy_gl_ctx_destroy = qemu_egl_destroy_context, + .dpy_gl_ctx_make_current = gd_egl_make_current, +}; #endif #endif /* CONFIG_OPENGL */ @@ -698,7 +703,7 @@ static void gd_set_ui_info(VirtualConsole *vc, gint width, gint height) memset(&info, 0, sizeof(info)); info.width = width; info.height = height; - dpy_set_ui_info(vc->gfx.dcl.con, &info); + dpy_set_ui_info(vc->gfx.dcl.con, &info, true); } #if defined(CONFIG_OPENGL) @@ -2035,6 +2040,7 @@ static GSList *gd_vc_gfx_init(GtkDisplayState *s, VirtualConsole *vc, g_signal_connect(vc->gfx.drawing_area, "realize", G_CALLBACK(gl_area_realize), vc); vc->gfx.dcl.ops = &dcl_gl_area_ops; + vc->gfx.dgc.ops = &gl_area_ctx_ops; } else { #ifdef CONFIG_X11 vc->gfx.drawing_area = gtk_drawing_area_new(); @@ -2049,6 +2055,7 @@ static GSList *gd_vc_gfx_init(GtkDisplayState *s, VirtualConsole *vc, gtk_widget_set_double_buffered(vc->gfx.drawing_area, FALSE); #pragma GCC diagnostic pop vc->gfx.dcl.ops = &dcl_egl_ops; + vc->gfx.dgc.ops = &egl_ctx_ops; vc->gfx.has_dmabuf = qemu_egl_has_dmabuf(); #else abort(); @@ -2083,6 +2090,9 @@ static GSList *gd_vc_gfx_init(GtkDisplayState *s, VirtualConsole *vc, vc->gfx.kbd = qkbd_state_init(con); vc->gfx.dcl.con = con; + if (display_opengl) { + qemu_console_set_display_gl_ctx(con, &vc->gfx.dgc); + } register_displaychangelistener(&vc->gfx.dcl); gd_connect_vc_gfx_signals(vc); diff --git a/ui/meson.build b/ui/meson.build index ee8ef27714..64286ba150 100644 --- a/ui/meson.build +++ b/ui/meson.build @@ -12,7 +12,11 @@ softmmu_ss.add(files( 'kbd-state.c', 'keymaps.c', 'qemu-pixman.c', + 'util.c', )) +if dbus_display + softmmu_ss.add(files('dbus-module.c')) +endif softmmu_ss.add([spice_headers, files('spice-module.c')]) softmmu_ss.add(when: spice_protocol, if_true: files('vdagent.c')) @@ -64,6 +68,30 @@ if config_host.has_key('CONFIG_OPENGL') and gbm.found() ui_modules += {'egl-headless' : egl_headless_ss} endif +if dbus_display + dbus_ss = ss.source_set() + dbus_display1 = custom_target('dbus-display gdbus-codegen', + output: ['dbus-display1.h', 'dbus-display1.c'], + input: files('dbus-display1.xml'), + command: [config_host['GDBUS_CODEGEN'], + '@INPUT@', + '--glib-min-required', '2.64', + '--output-directory', meson.current_build_dir(), + '--interface-prefix', 'org.qemu.', + '--c-namespace', 'QemuDBus', + '--generate-c-code', '@BASENAME@']) + dbus_ss.add(when: [gio, pixman, opengl, 'CONFIG_GIO'], + if_true: [files( + 'dbus-chardev.c', + 'dbus-clipboard.c', + 'dbus-console.c', + 'dbus-error.c', + 'dbus-listener.c', + 'dbus.c', + ), dbus_display1]) + ui_modules += {'dbus' : dbus_ss} +endif + if gtk.found() softmmu_ss.add(when: 'CONFIG_WIN32', if_true: files('win32-kbd-hook.c')) diff --git a/ui/sdl2-gl.c b/ui/sdl2-gl.c index a21d2deed9..39cab8cde7 100644 --- a/ui/sdl2-gl.c +++ b/ui/sdl2-gl.c @@ -58,7 +58,6 @@ static void sdl2_gl_render_surface(struct sdl2_console *scon) surface_gl_render_texture(scon->gls, scon->surface); SDL_GL_SwapWindow(scon->real_window); - graphic_hw_gl_flushed(scon->dcl.con); } void sdl2_gl_update(DisplayChangeListener *dcl, @@ -133,10 +132,10 @@ void sdl2_gl_redraw(struct sdl2_console *scon) } } -QEMUGLContext sdl2_gl_create_context(DisplayChangeListener *dcl, +QEMUGLContext sdl2_gl_create_context(DisplayGLCtx *dgc, QEMUGLParams *params) { - struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + struct sdl2_console *scon = container_of(dgc, struct sdl2_console, dgc); SDL_GLContext ctx; assert(scon->opengl); @@ -168,17 +167,17 @@ QEMUGLContext sdl2_gl_create_context(DisplayChangeListener *dcl, return (QEMUGLContext)ctx; } -void sdl2_gl_destroy_context(DisplayChangeListener *dcl, QEMUGLContext ctx) +void sdl2_gl_destroy_context(DisplayGLCtx *dgc, QEMUGLContext ctx) { SDL_GLContext sdlctx = (SDL_GLContext)ctx; SDL_GL_DeleteContext(sdlctx); } -int sdl2_gl_make_context_current(DisplayChangeListener *dcl, +int sdl2_gl_make_context_current(DisplayGLCtx *dgc, QEMUGLContext ctx) { - struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + struct sdl2_console *scon = container_of(dgc, struct sdl2_console, dgc); SDL_GLContext sdlctx = (SDL_GLContext)ctx; assert(scon->opengl); @@ -241,5 +240,4 @@ void sdl2_gl_scanout_flush(DisplayChangeListener *dcl, egl_fb_blit(&scon->win_fb, &scon->guest_fb, !scon->y0_top); SDL_GL_SwapWindow(scon->real_window); - graphic_hw_gl_flushed(dcl->con); } @@ -561,7 +561,7 @@ static void handle_windowevent(SDL_Event *ev) memset(&info, 0, sizeof(info)); info.width = ev->window.data1; info.height = ev->window.data2; - dpy_set_ui_info(scon->dcl.con, &info); + dpy_set_ui_info(scon->dcl.con, &info, true); } sdl2_redraw(scon); break; @@ -778,13 +778,17 @@ static const DisplayChangeListenerOps dcl_gl_ops = { .dpy_mouse_set = sdl_mouse_warp, .dpy_cursor_define = sdl_mouse_define, - .dpy_gl_ctx_create = sdl2_gl_create_context, - .dpy_gl_ctx_destroy = sdl2_gl_destroy_context, - .dpy_gl_ctx_make_current = sdl2_gl_make_context_current, .dpy_gl_scanout_disable = sdl2_gl_scanout_disable, .dpy_gl_scanout_texture = sdl2_gl_scanout_texture, .dpy_gl_update = sdl2_gl_scanout_flush, }; + +static const DisplayGLCtxOps gl_ctx_ops = { + .compatible_dcl = &dcl_gl_ops, + .dpy_gl_ctx_create = sdl2_gl_create_context, + .dpy_gl_ctx_destroy = sdl2_gl_destroy_context, + .dpy_gl_ctx_make_current = sdl2_gl_make_context_current, +}; #endif static void sdl2_display_early_init(DisplayOptions *o) @@ -860,12 +864,16 @@ static void sdl2_display_init(DisplayState *ds, DisplayOptions *o) #ifdef CONFIG_OPENGL sdl2_console[i].opengl = display_opengl; sdl2_console[i].dcl.ops = display_opengl ? &dcl_gl_ops : &dcl_2d_ops; + sdl2_console[i].dgc.ops = display_opengl ? &gl_ctx_ops : NULL; #else sdl2_console[i].opengl = 0; sdl2_console[i].dcl.ops = &dcl_2d_ops; #endif sdl2_console[i].dcl.con = con; sdl2_console[i].kbd = qkbd_state_init(con); + if (display_opengl) { + qemu_console_set_display_gl_ctx(con, &sdl2_console[i].dgc); + } register_displaychangelistener(&sdl2_console[i].dcl); #if defined(SDL_VIDEO_DRIVER_WINDOWS) || defined(SDL_VIDEO_DRIVER_X11) diff --git a/ui/spice-core.c b/ui/spice-core.c index 31974b8d6c..c3ac20ad43 100644 --- a/ui/spice-core.c +++ b/ui/spice-core.c @@ -884,56 +884,6 @@ bool qemu_spice_have_display_interface(QemuConsole *con) return false; } -/* - * Recursively (in reverse order) appends addresses of PCI devices as it moves - * up in the PCI hierarchy. - * - * @returns true on success, false when the buffer wasn't large enough - */ -static bool append_pci_address(char *buf, size_t buf_size, const PCIDevice *pci) -{ - PCIBus *bus = pci_get_bus(pci); - /* - * equivalent to if (!pci_bus_is_root(bus)), but the function is not built - * with PCI_CONFIG=n, avoid using an #ifdef by checking directly - */ - if (bus->parent_dev != NULL) { - append_pci_address(buf, buf_size, bus->parent_dev); - } - - size_t len = strlen(buf); - ssize_t written = snprintf(buf + len, buf_size - len, "/%02x.%x", - PCI_SLOT(pci->devfn), PCI_FUNC(pci->devfn)); - - return written > 0 && written < buf_size - len; -} - -bool qemu_spice_fill_device_address(QemuConsole *con, - char *device_address, - size_t size) -{ - DeviceState *dev = DEVICE(object_property_get_link(OBJECT(con), - "device", - &error_abort)); - PCIDevice *pci = (PCIDevice *) object_dynamic_cast(OBJECT(dev), - TYPE_PCI_DEVICE); - - if (pci == NULL) { - warn_report("Setting device address of a display device to SPICE: " - "Not a PCI device."); - return false; - } - - strncpy(device_address, "pci/0000", size); - if (!append_pci_address(device_address, size, pci)) { - warn_report("Setting device address of a display device to SPICE: " - "Too many PCI devices in the chain."); - return false; - } - - return true; -} - int qemu_spice_add_display_interface(QXLInstance *qxlin, QemuConsole *con) { if (g_slist_find(spice_consoles, con)) { diff --git a/ui/spice-display.c b/ui/spice-display.c index f59c69882d..1043f47f94 100644 --- a/ui/spice-display.c +++ b/ui/spice-display.c @@ -692,7 +692,7 @@ static int interface_client_monitors_config(QXLInstance *sin, } trace_qemu_spice_ui_info(ssd->qxl.id, info.width, info.height); - dpy_set_ui_info(ssd->dcl.con, &info); + dpy_set_ui_info(ssd->dcl.con, &info, false); return 1; } @@ -830,7 +830,6 @@ static void qemu_spice_gl_unblock_bh(void *opaque) SimpleSpiceDisplay *ssd = opaque; qemu_spice_gl_block(ssd, false); - graphic_hw_gl_flushed(ssd->dcl.con); } static void qemu_spice_gl_block_timer(void *opaque) @@ -909,12 +908,12 @@ static void spice_gl_switch(DisplayChangeListener *dcl, } } -static QEMUGLContext qemu_spice_gl_create_context(DisplayChangeListener *dcl, +static QEMUGLContext qemu_spice_gl_create_context(DisplayGLCtx *dgc, QEMUGLParams *params) { eglMakeCurrent(qemu_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, qemu_egl_rn_ctx); - return qemu_egl_create_context(dcl, params); + return qemu_egl_create_context(dgc, params); } static void qemu_spice_gl_scanout_disable(DisplayChangeListener *dcl) @@ -1106,10 +1105,6 @@ static const DisplayChangeListenerOps display_listener_gl_ops = { .dpy_mouse_set = display_mouse_set, .dpy_cursor_define = display_mouse_define, - .dpy_gl_ctx_create = qemu_spice_gl_create_context, - .dpy_gl_ctx_destroy = qemu_egl_destroy_context, - .dpy_gl_ctx_make_current = qemu_egl_make_context_current, - .dpy_gl_scanout_disable = qemu_spice_gl_scanout_disable, .dpy_gl_scanout_texture = qemu_spice_gl_scanout_texture, .dpy_gl_scanout_dmabuf = qemu_spice_gl_scanout_dmabuf, @@ -1119,6 +1114,13 @@ static const DisplayChangeListenerOps display_listener_gl_ops = { .dpy_gl_update = qemu_spice_gl_update, }; +static const DisplayGLCtxOps gl_ctx_ops = { + .compatible_dcl = &display_listener_gl_ops, + .dpy_gl_ctx_create = qemu_spice_gl_create_context, + .dpy_gl_ctx_destroy = qemu_egl_destroy_context, + .dpy_gl_ctx_make_current = qemu_egl_make_context_current, +}; + #endif /* HAVE_SPICE_GL */ static void qemu_spice_display_init_one(QemuConsole *con) @@ -1131,6 +1133,7 @@ static void qemu_spice_display_init_one(QemuConsole *con) #ifdef HAVE_SPICE_GL if (spice_opengl) { ssd->dcl.ops = &display_listener_gl_ops; + ssd->dgc.ops = &gl_ctx_ops; ssd->gl_unblock_bh = qemu_bh_new(qemu_spice_gl_unblock_bh, ssd); ssd->gl_unblock_timer = timer_new_ms(QEMU_CLOCK_REALTIME, qemu_spice_gl_block_timer, ssd); @@ -1145,17 +1148,23 @@ static void qemu_spice_display_init_one(QemuConsole *con) qemu_spice_add_display_interface(&ssd->qxl, con); #if SPICE_SERVER_VERSION >= 0x000e02 /* release 0.14.2 */ + Error *err = NULL; char device_address[256] = ""; - if (qemu_spice_fill_device_address(con, device_address, 256)) { + if (qemu_console_fill_device_address(con, device_address, 256, &err)) { spice_qxl_set_device_info(&ssd->qxl, device_address, qemu_console_get_head(con), 1); + } else { + error_report_err(err); } #endif qemu_spice_create_host_memslot(ssd); + if (spice_opengl) { + qemu_console_set_display_gl_ctx(con, &ssd->dgc); + } register_displaychangelistener(&ssd->dcl); } diff --git a/ui/trace-events b/ui/trace-events index b9c0dd0fa1..f78b5e6606 100644 --- a/ui/trace-events +++ b/ui/trace-events @@ -135,3 +135,18 @@ vdagent_recv_msg(const char *name, uint32_t size) "msg %s, size %d" 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" +vdagent_cb_serial_discard(uint32_t current, uint32_t received) "current=%u, received=%u" + +# dbus.c +dbus_registered_listener(const char *bus_name) "peer %s" +dbus_listener_vanished(const char *bus_name) "peer %s" +dbus_kbd_press(unsigned int keycode) "keycode %u" +dbus_kbd_release(unsigned int keycode) "keycode %u" +dbus_mouse_press(unsigned int button) "button %u" +dbus_mouse_release(unsigned int button) "button %u" +dbus_mouse_set_pos(unsigned int x, unsigned int y) "x=%u, y=%u" +dbus_mouse_rel_motion(int dx, int dy) "dx=%d, dy=%d" +dbus_update(int x, int y, int w, int h) "x=%d, y=%d, w=%d, h=%d" +dbus_clipboard_grab_failed(void) "" +dbus_clipboard_register(const char *bus_name) "peer %s" +dbus_clipboard_unregister(const char *bus_name) "peer %s" diff --git a/ui/util.c b/ui/util.c new file mode 100644 index 0000000000..7e8fc1ea53 --- /dev/null +++ b/ui/util.c @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2021 Red Hat, Inc. + * + * 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 or + * (at your option) version 3 of the License. + * + * 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 "hw/pci/pci.h" +#include "hw/pci/pci_bus.h" +#include "qapi/error.h" +#include "ui/console.h" + +/* + * Recursively (in reverse order) appends addresses of PCI devices as it moves + * up in the PCI hierarchy. + * + * @returns true on success, false when the buffer wasn't large enough + */ +static bool append_pci_address(char *buf, size_t buf_size, const PCIDevice *pci) +{ + PCIBus *bus = pci_get_bus(pci); + /* + * equivalent to if (!pci_bus_is_root(bus)), but the function is not built + * with PCI_CONFIG=n, avoid using an #ifdef by checking directly + */ + if (bus->parent_dev != NULL) { + append_pci_address(buf, buf_size, bus->parent_dev); + } + + size_t len = strlen(buf); + ssize_t written = snprintf(buf + len, buf_size - len, "/%02x.%x", + PCI_SLOT(pci->devfn), PCI_FUNC(pci->devfn)); + + return written > 0 && written < buf_size - len; +} + +bool qemu_console_fill_device_address(QemuConsole *con, + char *device_address, + size_t size, + Error **errp) +{ + ERRP_GUARD(); + DeviceState *dev = DEVICE(object_property_get_link(OBJECT(con), + "device", + &error_abort)); + PCIDevice *pci = (PCIDevice *) object_dynamic_cast(OBJECT(dev), + TYPE_PCI_DEVICE); + + if (pci == NULL) { + error_setg(errp, "Setting device address of a display device: " + "Not a PCI device."); + return false; + } + + strncpy(device_address, "pci/0000", size); + if (!append_pci_address(device_address, size, pci)) { + error_setg(errp, "Setting device address of a display device: " + "Too many PCI devices in the chain."); + return false; + } + + return true; +} diff --git a/ui/vdagent.c b/ui/vdagent.c index 19e8fbfc96..7ea4bc5d9a 100644 --- a/ui/vdagent.c +++ b/ui/vdagent.c @@ -17,6 +17,14 @@ #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 @@ -51,6 +59,7 @@ struct VDAgentChardev { /* clipboard */ QemuClipboardPeer cbpeer; + uint32_t last_serial[QEMU_CLIPBOARD_SELECTION__COUNT]; uint32_t cbpending[QEMU_CLIPBOARD_SELECTION__COUNT]; }; typedef struct VDAgentChardev VDAgentChardev; @@ -79,8 +88,10 @@ static const char *cap_name[] = { [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 +#if CHECK_SPICE_PROTOCOL_VERSION(0, 14, 0) [VD_AGENT_CAP_GRAPHICS_DEVICE_INFO] = "graphics-device-info", +#endif +#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 @@ -102,7 +113,7 @@ static const char *msg_name[] = { [VD_AGENT_CLIENT_DISCONNECTED] = "client-disconnected", [VD_AGENT_MAX_CLIPBOARD] = "max-clipboard", [VD_AGENT_AUDIO_VOLUME_SYNC] = "audio-volume-sync", -#if 0 +#if CHECK_SPICE_PROTOCOL_VERSION(0, 14, 0) [VD_AGENT_GRAPHICS_DEVICE_INFO] = "graphics-device-info", #endif }; @@ -120,7 +131,7 @@ static const char *type_name[] = { [VD_AGENT_CLIPBOARD_IMAGE_BMP] = "bmp", [VD_AGENT_CLIPBOARD_IMAGE_TIFF] = "tiff", [VD_AGENT_CLIPBOARD_IMAGE_JPG] = "jpg", -#if 0 +#if CHECK_SPICE_PROTOCOL_VERSION(0, 14, 3) [VD_AGENT_CLIPBOARD_FILE_LIST] = "files", #endif }; @@ -193,6 +204,9 @@ static void vdagent_send_caps(VDAgentChardev *vd) 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 } vdagent_send_msg(vd, msg); @@ -323,7 +337,8 @@ static void vdagent_send_clipboard_grab(VDAgentChardev *vd, { g_autofree VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) + - sizeof(uint32_t) * (QEMU_CLIPBOARD_TYPE__COUNT + 1)); + 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; @@ -336,6 +351,19 @@ static void vdagent_send_clipboard_grab(VDAgentChardev *vd, return; } +#if CHECK_SPICE_PROTOCOL_VERSION(0, 14, 1) + if (vd->caps & (1 << VD_AGENT_CAP_CLIPBOARD_GRAB_SERIAL)) { + 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); + } +#endif + 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) { @@ -407,10 +435,9 @@ static void vdagent_send_empty_clipboard_data(VDAgentChardev *vd, vdagent_send_clipboard_data(vd, info, type); } -static void vdagent_clipboard_notify(Notifier *notifier, void *data) +static void vdagent_clipboard_update_info(VDAgentChardev *vd, + QemuClipboardInfo *info) { - VDAgentChardev *vd = container_of(notifier, VDAgentChardev, cbpeer.update); - QemuClipboardInfo *info = data; QemuClipboardSelection s = info->selection; QemuClipboardType type; bool self_update = info->owner == &vd->cbpeer; @@ -439,6 +466,31 @@ static void vdagent_clipboard_notify(Notifier *notifier, void *data) } } +static void vdagent_clipboard_reset_serial(VDAgentChardev *vd) +{ + Chardev *chr = CHARDEV(vd); + + /* reopen the agent connection to reset the serial state */ + qemu_chr_be_event(chr, CHR_EVENT_CLOSED); + qemu_chr_be_event(chr, CHR_EVENT_OPENED); +} + +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) { @@ -472,6 +524,24 @@ static void vdagent_clipboard_recv_grab(VDAgentChardev *vd, uint8_t s, uint32_t trace_vdagent_cb_grab_selection(GET_NAME(sel_name, s)); info = qemu_clipboard_info_new(&vd->cbpeer, s); +#if CHECK_SPICE_PROTOCOL_VERSION(0, 14, 1) + if (vd->caps & (1 << VD_AGENT_CAP_CLIPBOARD_GRAB_SERIAL)) { + 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]) { + /* discard lower-ordering guest grab */ + return; + } + vd->last_serial[s] = info->serial; + data += sizeof(uint32_t); + size -= sizeof(uint32_t); + } +#endif if (size > sizeof(uint32_t) * 10) { /* * spice has 6 types as of 2021. Limiting to 10 entries @@ -648,9 +718,10 @@ static void vdagent_chr_recv_caps(VDAgentChardev *vd, VDAgentMessage *msg) if (have_mouse(vd) && vd->mouse_hs) { qemu_input_handler_activate(vd->mouse_hs); } - if (have_clipboard(vd) && vd->cbpeer.update.notify == NULL) { + if (have_clipboard(vd) && vd->cbpeer.notifier.notify == NULL) { + memset(vd->last_serial, 0, sizeof(vd->last_serial)); vd->cbpeer.name = "vdagent"; - vd->cbpeer.update.notify = vdagent_clipboard_notify; + vd->cbpeer.notifier.notify = vdagent_clipboard_notify; vd->cbpeer.request = vdagent_clipboard_request; qemu_clipboard_peer_register(&vd->cbpeer); } @@ -789,7 +860,7 @@ static void vdagent_disconnect(VDAgentChardev *vd) if (vd->mouse_hs) { qemu_input_handler_deactivate(vd->mouse_hs); } - if (vd->cbpeer.update.notify) { + if (vd->cbpeer.notifier.notify) { qemu_clipboard_peer_unregister(&vd->cbpeer); memset(&vd->cbpeer, 0, sizeof(vd->cbpeer)); } @@ -797,11 +868,8 @@ static void vdagent_disconnect(VDAgentChardev *vd) static void vdagent_chr_set_fe_open(struct Chardev *chr, int fe_open) { - VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(chr); - if (!fe_open) { trace_vdagent_close(); - vdagent_disconnect(vd); return; } diff --git a/ui/vnc-clipboard.c b/ui/vnc-clipboard.c index 67284b556c..d48f75eb1a 100644 --- a/ui/vnc-clipboard.c +++ b/ui/vnc-clipboard.c @@ -189,10 +189,8 @@ static void vnc_clipboard_provide(VncState *vs, vnc_flush(vs); } -static void vnc_clipboard_notify(Notifier *notifier, void *data) +static void vnc_clipboard_update_info(VncState *vs, QemuClipboardInfo *info) { - VncState *vs = container_of(notifier, VncState, cbpeer.update); - QemuClipboardInfo *info = data; QemuClipboardType type; bool self_update = info->owner == &vs->cbpeer; uint32_t flags = 0; @@ -223,6 +221,21 @@ static void vnc_clipboard_notify(Notifier *notifier, void *data) } } +static void vnc_clipboard_notify(Notifier *notifier, void *data) +{ + VncState *vs = container_of(notifier, VncState, cbpeer.notifier); + QemuClipboardNotify *notify = data; + + switch (notify->type) { + case QEMU_CLIPBOARD_UPDATE_INFO: + vnc_clipboard_update_info(vs, notify->info); + return; + case QEMU_CLIPBOARD_RESET_SERIAL: + /* ignore */ + return; + } +} + static void vnc_clipboard_request(QemuClipboardInfo *info, QemuClipboardType type) { @@ -316,9 +329,9 @@ void vnc_server_cut_text_caps(VncState *vs) caps[1] = 0; vnc_clipboard_send(vs, 2, caps); - if (!vs->cbpeer.update.notify) { + if (!vs->cbpeer.notifier.notify) { vs->cbpeer.name = "vnc"; - vs->cbpeer.update.notify = vnc_clipboard_notify; + vs->cbpeer.notifier.notify = vnc_clipboard_notify; vs->cbpeer.request = vnc_clipboard_request; qemu_clipboard_peer_register(&vs->cbpeer); } @@ -1354,7 +1354,7 @@ void vnc_disconnect_finish(VncState *vs) /* last client gone */ vnc_update_server_surface(vs->vd); } - if (vs->cbpeer.update.notify) { + if (vs->cbpeer.notifier.notify) { qemu_clipboard_peer_unregister(&vs->cbpeer); } @@ -2596,7 +2596,7 @@ static int protocol_client_msg(VncState *vs, uint8_t *data, size_t len) memset(&info, 0, sizeof(info)); info.width = w; info.height = h; - dpy_set_ui_info(vs->vd->dcl.con, &info); + dpy_set_ui_info(vs->vd->dcl.con, &info, false); vnc_desktop_resize_ext(vs, 4 /* Request forwarded */); } else { vnc_desktop_resize_ext(vs, 3 /* Invalid screen layout */); |