UI/chardev-related patch queue

-----BEGIN PGP SIGNATURE-----
 
 iQJQBAABCAA6FiEEh6m9kz+HxgbSdvYt2ujhCXWWnOUFAmeg+mwcHG1hcmNhbmRy
 ZS5sdXJlYXVAcmVkaGF0LmNvbQAKCRDa6OEJdZac5X9JD/4ie4unhYkWEaJLR5ks
 eVRE2ZrwrO1HF2HkFHgs9UN/G6Pl4o/YaPzICQkManJOhbJvOcp8hReOrSETrOLg
 iiYHr3DH+H1nRzPgH+Nuvj3IRnl2EdypfgHbWVmvMQQ7u0vwpUiraTHEqy2PvTqO
 ougTl0lf4v4NB1CHWDTbs6IT4/hMwXM4/pP1ztXvdWeJxKUUTKb9SSOlmjkdT/Ou
 kZqDr/aonWxvQs6t3HeauNkiIIq21pVAIDUoDr338hTK4/EPhxOwaTpZ0b2RATA6
 ldpcS7VNfsMe8aJI3nsRaRz5NkWNDnQgejGkIxxXo3xj8c/rhZMyqrrqYaqFleVW
 0ahh6eY0qxc+Z7HJ+SxU8oDUzNjOw+14NeUlHTd+qRnZVasWXZlB7wYTxlbLKCHP
 KtbAm8KsdWrKokMkupRCHiI0je3QXlhX3TGEUS5HHcknjhvmkEzCcEYy0gYuyLRq
 +e79xdC/IyylZvzM/SXQXWEtb3GmBhi5pQmcRftTgISNxryXFfYXeOOQhgvJQS2L
 8/Ul/rIEvhecj1me/wzOK1bDGzFae8xYSM2z7v/EAm4I59N8N8aomnN3sHeaeLlG
 UwWGpq9Z3igoWaM88/h8EktA0Kk8s9YBXZoKvGwVQPglEqEeWEwvrGKEM2Le7kYF
 eHM+osrJFf2iD42v6AnYVARhIA==
 =1pl1
 -----END PGP SIGNATURE-----

Merge tag 'ui-pull-request' of https://gitlab.com/marcandre.lureau/qemu into staging

UI/chardev-related patch queue

# -----BEGIN PGP SIGNATURE-----
#
# iQJQBAABCAA6FiEEh6m9kz+HxgbSdvYt2ujhCXWWnOUFAmeg+mwcHG1hcmNhbmRy
# ZS5sdXJlYXVAcmVkaGF0LmNvbQAKCRDa6OEJdZac5X9JD/4ie4unhYkWEaJLR5ks
# eVRE2ZrwrO1HF2HkFHgs9UN/G6Pl4o/YaPzICQkManJOhbJvOcp8hReOrSETrOLg
# iiYHr3DH+H1nRzPgH+Nuvj3IRnl2EdypfgHbWVmvMQQ7u0vwpUiraTHEqy2PvTqO
# ougTl0lf4v4NB1CHWDTbs6IT4/hMwXM4/pP1ztXvdWeJxKUUTKb9SSOlmjkdT/Ou
# kZqDr/aonWxvQs6t3HeauNkiIIq21pVAIDUoDr338hTK4/EPhxOwaTpZ0b2RATA6
# ldpcS7VNfsMe8aJI3nsRaRz5NkWNDnQgejGkIxxXo3xj8c/rhZMyqrrqYaqFleVW
# 0ahh6eY0qxc+Z7HJ+SxU8oDUzNjOw+14NeUlHTd+qRnZVasWXZlB7wYTxlbLKCHP
# KtbAm8KsdWrKokMkupRCHiI0je3QXlhX3TGEUS5HHcknjhvmkEzCcEYy0gYuyLRq
# +e79xdC/IyylZvzM/SXQXWEtb3GmBhi5pQmcRftTgISNxryXFfYXeOOQhgvJQS2L
# 8/Ul/rIEvhecj1me/wzOK1bDGzFae8xYSM2z7v/EAm4I59N8N8aomnN3sHeaeLlG
# UwWGpq9Z3igoWaM88/h8EktA0Kk8s9YBXZoKvGwVQPglEqEeWEwvrGKEM2Le7kYF
# eHM+osrJFf2iD42v6AnYVARhIA==
# =1pl1
# -----END PGP SIGNATURE-----
# gpg: Signature made Mon 03 Feb 2025 12:18:36 EST
# gpg:                using RSA key 87A9BD933F87C606D276F62DDAE8E10975969CE5
# gpg:                issuer "marcandre.lureau@redhat.com"
# gpg: Good signature from "Marc-André Lureau <marcandre.lureau@redhat.com>" [full]
# gpg:                 aka "Marc-André Lureau <marcandre.lureau@gmail.com>" [full]
# Primary key fingerprint: 87A9 BD93 3F87 C606 D276  F62D DAE8 E109 7596 9CE5

* tag 'ui-pull-request' of https://gitlab.com/marcandre.lureau/qemu:
  dbus: add -audio dbus nsamples option
  plugins: fix -Werror=maybe-uninitialized false-positive
  ui/dbus: clarify the kind of win32 handle that is shared
  ui/dbus: on win32, allow ANONYMOUS with p2p
  qemu-options.hx: describe hub chardev and aggregation of several backends
  tests/unit/test-char: add unit tests for hub chardev backend
  chardev/char-hub: implement backend chardev aggregator
  chardev/char-pty: send CHR_EVENT_CLOSED on disconnect

Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
This commit is contained in:
Stefan Hajnoczi 2025-02-03 13:42:02 -05:00
commit d922088eb4
15 changed files with 923 additions and 19 deletions

View File

@ -43,9 +43,10 @@
#define DBUS_DISPLAY1_AUDIO_PATH DBUS_DISPLAY1_ROOT "/Audio"
#define DBUS_AUDIO_NSAMPLES 1024 /* could be configured? */
#define DBUS_DEFAULT_AUDIO_NSAMPLES 480
typedef struct DBusAudio {
Audiodev *dev;
GDBusObjectManagerServer *server;
bool p2p;
GDBusObjectSkeleton *audio;
@ -151,6 +152,18 @@ dbus_init_out_listener(QemuDBusDisplay1AudioOutListener *listener,
G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
}
static guint
dbus_audio_get_nsamples(DBusAudio *da)
{
AudiodevDBusOptions *opts = &da->dev->u.dbus;
if (opts->has_nsamples && opts->nsamples) {
return opts->nsamples;
} else {
return DBUS_DEFAULT_AUDIO_NSAMPLES;
}
}
static int
dbus_init_out(HWVoiceOut *hw, struct audsettings *as, void *drv_opaque)
{
@ -160,7 +173,7 @@ dbus_init_out(HWVoiceOut *hw, struct audsettings *as, void *drv_opaque)
QemuDBusDisplay1AudioOutListener *listener = NULL;
audio_pcm_init_info(&hw->info, as);
hw->samples = DBUS_AUDIO_NSAMPLES;
hw->samples = dbus_audio_get_nsamples(da);
audio_rate_start(&vo->rate);
g_hash_table_iter_init(&iter, da->out_listeners);
@ -274,7 +287,7 @@ dbus_init_in(HWVoiceIn *hw, struct audsettings *as, void *drv_opaque)
QemuDBusDisplay1AudioInListener *listener = NULL;
audio_pcm_init_info(&hw->info, as);
hw->samples = DBUS_AUDIO_NSAMPLES;
hw->samples = dbus_audio_get_nsamples(da);
audio_rate_start(&vo->rate);
g_hash_table_iter_init(&iter, da->in_listeners);
@ -399,6 +412,7 @@ dbus_audio_init(Audiodev *dev, Error **errp)
{
DBusAudio *da = g_new0(DBusAudio, 1);
da->dev = dev;
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,
@ -524,11 +538,17 @@ dbus_audio_register_listener(AudioState *s,
);
}
GDBusConnectionFlags flags =
G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_SERVER;
#ifdef WIN32
flags |= G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_ALLOW_ANONYMOUS;
#endif
listener_conn =
g_dbus_connection_new_sync(
G_IO_STREAM(socket_conn),
guid,
G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_SERVER,
flags,
NULL, NULL, &err);
if (err) {
error_report("Failed to setup peer connection: %s", err->message);
@ -646,6 +666,7 @@ dbus_audio_set_server(AudioState *s, GDBusObjectManagerServer *server, bool p2p)
"swapped-signal::handle-register-out-listener",
dbus_audio_register_out_listener, s,
NULL);
qemu_dbus_display1_audio_set_nsamples(da->iface, dbus_audio_get_nsamples(da));
g_dbus_object_skeleton_add_interface(G_DBUS_OBJECT_SKELETON(da->audio),
G_DBUS_INTERFACE_SKELETON(da->iface));

301
chardev/char-hub.c Normal file
View File

@ -0,0 +1,301 @@
/*
* QEMU Character Hub Device
*
* Author: Roman Penyaev <r.peniaev@gmail.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 "qemu/option.h"
#include "chardev/char.h"
#include "chardev-internal.h"
/*
* Character hub device aggregates input from multiple backend devices
* and forwards it to a single frontend device. Additionally, hub
* device takes the output from the frontend device and sends it back
* to all the connected backend devices.
*/
/*
* Write to all backends. Different backend devices accept data with
* various rate, so it is quite possible that one device returns less,
* then others. In this case we return minimum to the caller,
* expecting caller will repeat operation soon. When repeat happens
* send to the devices which consume data faster must be avoided
* for obvious reasons not to send data, which was already sent.
* Called with chr_write_lock held.
*/
static int hub_chr_write(Chardev *chr, const uint8_t *buf, int len)
{
HubChardev *d = HUB_CHARDEV(chr);
int r, i, ret = len;
unsigned int written;
/* Invalidate index on every write */
d->be_eagain_ind = -1;
for (i = 0; i < d->be_cnt; i++) {
if (!d->backends[i].be.chr->be_open) {
/* Skip closed backend */
continue;
}
written = d->be_written[i] - d->be_min_written;
if (written) {
/* Written in the previous call so take into account */
ret = MIN(written, ret);
continue;
}
r = qemu_chr_fe_write(&d->backends[i].be, buf, len);
if (r < 0) {
if (errno == EAGAIN) {
/* Set index and expect to be called soon on watch wake up */
d->be_eagain_ind = i;
}
return r;
}
d->be_written[i] += r;
ret = MIN(r, ret);
}
d->be_min_written += ret;
return ret;
}
static int hub_chr_can_read(void *opaque)
{
HubCharBackend *backend = opaque;
CharBackend *fe = backend->hub->parent.be;
if (fe && fe->chr_can_read) {
return fe->chr_can_read(fe->opaque);
}
return 0;
}
static void hub_chr_read(void *opaque, const uint8_t *buf, int size)
{
HubCharBackend *backend = opaque;
CharBackend *fe = backend->hub->parent.be;
if (fe && fe->chr_read) {
fe->chr_read(fe->opaque, buf, size);
}
}
static void hub_chr_event(void *opaque, QEMUChrEvent event)
{
HubCharBackend *backend = opaque;
HubChardev *d = backend->hub;
CharBackend *fe = d->parent.be;
if (event == CHR_EVENT_OPENED) {
/*
* Catch up with what was already written while this backend
* was closed
*/
d->be_written[backend->be_ind] = d->be_min_written;
if (d->be_event_opened_cnt++) {
/* Ignore subsequent open events from other backends */
return;
}
} else if (event == CHR_EVENT_CLOSED) {
if (!d->be_event_opened_cnt) {
/* Don't go below zero. Probably assert is better */
return;
}
if (--d->be_event_opened_cnt) {
/* Serve only the last one close event */
return;
}
}
if (fe && fe->chr_event) {
fe->chr_event(fe->opaque, event);
}
}
static GSource *hub_chr_add_watch(Chardev *s, GIOCondition cond)
{
HubChardev *d = HUB_CHARDEV(s);
Chardev *chr;
ChardevClass *cc;
if (d->be_eagain_ind == -1) {
return NULL;
}
assert(d->be_eagain_ind < d->be_cnt);
chr = qemu_chr_fe_get_driver(&d->backends[d->be_eagain_ind].be);
cc = CHARDEV_GET_CLASS(chr);
if (!cc->chr_add_watch) {
return NULL;
}
return cc->chr_add_watch(chr, cond);
}
static bool hub_chr_attach_chardev(HubChardev *d, Chardev *chr,
Error **errp)
{
bool ret;
if (d->be_cnt >= MAX_HUB) {
error_setg(errp, "hub: too many uses of chardevs '%s'"
" (maximum is " stringify(MAX_HUB) ")",
d->parent.label);
return false;
}
ret = qemu_chr_fe_init(&d->backends[d->be_cnt].be, chr, errp);
if (ret) {
d->backends[d->be_cnt].hub = d;
d->backends[d->be_cnt].be_ind = d->be_cnt;
d->be_cnt += 1;
}
return ret;
}
static void char_hub_finalize(Object *obj)
{
HubChardev *d = HUB_CHARDEV(obj);
int i;
for (i = 0; i < d->be_cnt; i++) {
qemu_chr_fe_deinit(&d->backends[i].be, false);
}
}
static void hub_chr_update_read_handlers(Chardev *chr)
{
HubChardev *d = HUB_CHARDEV(chr);
int i;
for (i = 0; i < d->be_cnt; i++) {
qemu_chr_fe_set_handlers_full(&d->backends[i].be,
hub_chr_can_read,
hub_chr_read,
hub_chr_event,
NULL,
&d->backends[i],
chr->gcontext, true, false);
}
}
static void qemu_chr_open_hub(Chardev *chr,
ChardevBackend *backend,
bool *be_opened,
Error **errp)
{
ChardevHub *hub = backend->u.hub.data;
HubChardev *d = HUB_CHARDEV(chr);
strList *list = hub->chardevs;
d->be_eagain_ind = -1;
if (list == NULL) {
error_setg(errp, "hub: 'chardevs' list is not defined");
return;
}
while (list) {
Chardev *s;
s = qemu_chr_find(list->value);
if (s == NULL) {
error_setg(errp, "hub: chardev can't be found by id '%s'",
list->value);
return;
}
if (CHARDEV_IS_HUB(s) || CHARDEV_IS_MUX(s)) {
error_setg(errp, "hub: multiplexers and hub devices can't be "
"stacked, check chardev '%s', chardev should not "
"be a hub device or have 'mux=on' enabled",
list->value);
return;
}
if (!hub_chr_attach_chardev(d, s, errp)) {
return;
}
list = list->next;
}
/* Closed until an explicit event from backend */
*be_opened = false;
}
static void qemu_chr_parse_hub(QemuOpts *opts, ChardevBackend *backend,
Error **errp)
{
ChardevHub *hub;
strList **tail;
int i;
backend->type = CHARDEV_BACKEND_KIND_HUB;
hub = backend->u.hub.data = g_new0(ChardevHub, 1);
qemu_chr_parse_common(opts, qapi_ChardevHub_base(hub));
tail = &hub->chardevs;
for (i = 0; i < MAX_HUB; i++) {
char optbuf[16];
const char *dev;
snprintf(optbuf, sizeof(optbuf), "chardevs.%u", i);
dev = qemu_opt_get(opts, optbuf);
if (!dev) {
break;
}
QAPI_LIST_APPEND(tail, g_strdup(dev));
}
}
static void char_hub_class_init(ObjectClass *oc, void *data)
{
ChardevClass *cc = CHARDEV_CLASS(oc);
cc->parse = qemu_chr_parse_hub;
cc->open = qemu_chr_open_hub;
cc->chr_write = hub_chr_write;
cc->chr_add_watch = hub_chr_add_watch;
/* We handle events from backends only */
cc->chr_be_event = NULL;
cc->chr_update_read_handler = hub_chr_update_read_handlers;
}
static const TypeInfo char_hub_type_info = {
.name = TYPE_CHARDEV_HUB,
.parent = TYPE_CHARDEV,
.class_init = char_hub_class_init,
.instance_size = sizeof(HubChardev),
.instance_finalize = char_hub_finalize,
};
static void register_types(void)
{
type_register_static(&char_hub_type_info);
}
type_init(register_types);

View File

@ -181,6 +181,9 @@ static void pty_chr_state(Chardev *chr, int connected)
if (!connected) {
remove_fd_in_watch(chr);
if (s->connected) {
qemu_chr_be_event(chr, CHR_EVENT_CLOSED);
}
s->connected = 0;
/* (re-)connect poll interval for idle guests: once per second.
* We check more frequently in case the guests sends data to
@ -215,7 +218,6 @@ static void char_pty_finalize(Object *obj)
pty_chr_state(chr, 0);
object_unref(OBJECT(s->ioc));
pty_chr_timer_cancel(s);
qemu_chr_be_event(chr, CHR_EVENT_CLOSED);
}
#if defined HAVE_PTY_H

View File

@ -943,7 +943,26 @@ QemuOptsList qemu_chardev_opts = {
},{
.name = "chardev",
.type = QEMU_OPT_STRING,
},
/*
* Multiplexer options. Follows QAPI array syntax.
* See MAX_HUB macro to obtain array capacity.
*/
{
.name = "chardevs.0",
.type = QEMU_OPT_STRING,
},{
.name = "chardevs.1",
.type = QEMU_OPT_STRING,
},{
.name = "chardevs.2",
.type = QEMU_OPT_STRING,
},{
.name = "chardevs.3",
.type = QEMU_OPT_STRING,
},
{
.name = "append",
.type = QEMU_OPT_BOOL,
},{
@ -1106,8 +1125,8 @@ ChardevReturn *qmp_chardev_change(const char *id, ChardevBackend *backend,
return NULL;
}
if (CHARDEV_IS_MUX(chr)) {
error_setg(errp, "Mux device hotswap not supported yet");
if (CHARDEV_IS_MUX(chr) || CHARDEV_IS_HUB(chr)) {
error_setg(errp, "For mux or hub device hotswap is not supported yet");
return NULL;
}

View File

@ -29,13 +29,16 @@
#include "chardev/char-fe.h"
#include "qom/object.h"
#define MAX_HUB 4
#define MAX_MUX 4
#define MUX_BUFFER_SIZE 32 /* Must be a power of 2. */
#define MUX_BUFFER_MASK (MUX_BUFFER_SIZE - 1)
struct MuxChardev {
Chardev parent;
/* Linked frontends */
CharBackend *backends[MAX_MUX];
/* Linked backend */
CharBackend chr;
unsigned long mux_bitset;
int focus;
@ -53,11 +56,57 @@ struct MuxChardev {
int64_t timestamps_start;
};
typedef struct MuxChardev MuxChardev;
typedef struct HubChardev HubChardev;
typedef struct HubCharBackend HubCharBackend;
/*
* Back-pointer on a hub, actual backend and its index in
* `hub->backends` array
*/
struct HubCharBackend {
HubChardev *hub;
CharBackend be;
unsigned int be_ind;
};
struct HubChardev {
Chardev parent;
/* Linked backends */
HubCharBackend backends[MAX_HUB];
/*
* Number of backends attached to this hub. Once attached, a
* backend can't be detached, so the counter is only increasing.
* To safely remove a backend, hub has to be removed first.
*/
unsigned int be_cnt;
/*
* Number of CHR_EVEN_OPENED events from all backends. Needed to
* send CHR_EVEN_CLOSED only when counter goes to zero.
*/
unsigned int be_event_opened_cnt;
/*
* Counters of written bytes from a single frontend device
* to multiple backend devices.
*/
unsigned int be_written[MAX_HUB];
unsigned int be_min_written;
/*
* Index of a backend device which got EAGAIN on last write,
* -1 is invalid index.
*/
int be_eagain_ind;
};
typedef struct HubChardev HubChardev;
DECLARE_INSTANCE_CHECKER(MuxChardev, MUX_CHARDEV,
TYPE_CHARDEV_MUX)
DECLARE_INSTANCE_CHECKER(HubChardev, HUB_CHARDEV,
TYPE_CHARDEV_HUB)
#define CHARDEV_IS_MUX(chr) \
object_dynamic_cast(OBJECT(chr), TYPE_CHARDEV_MUX)
#define CHARDEV_IS_HUB(chr) \
object_dynamic_cast(OBJECT(chr), TYPE_CHARDEV_HUB)
bool mux_chr_attach_frontend(MuxChardev *d, CharBackend *b,
unsigned int *tag, Error **errp);

View File

@ -3,6 +3,7 @@ chardev_ss.add(files(
'char-file.c',
'char-io.c',
'char-mux.c',
'char-hub.c',
'char-null.c',
'char-pipe.c',
'char-ringbuf.c',

View File

@ -603,7 +603,7 @@ static int l2_cmp(gconstpointer a, gconstpointer b)
static void log_stats(void)
{
int i;
Cache *icache, *dcache, *l2_cache;
Cache *icache, *dcache, *l2_cache = NULL;
g_autoptr(GString) rep = g_string_new("core #, data accesses, data misses,"
" dmiss rate, insn accesses,"

View File

@ -232,6 +232,7 @@ OBJECT_DECLARE_TYPE(Chardev, ChardevClass, CHARDEV)
#define TYPE_CHARDEV_NULL "chardev-null"
#define TYPE_CHARDEV_MUX "chardev-mux"
#define TYPE_CHARDEV_HUB "chardev-hub"
#define TYPE_CHARDEV_RINGBUF "chardev-ringbuf"
#define TYPE_CHARDEV_PTY "chardev-pty"
#define TYPE_CHARDEV_CONSOLE "chardev-console"

View File

@ -65,6 +65,26 @@
'*in': 'AudiodevPerDirectionOptions',
'*out': 'AudiodevPerDirectionOptions' } }
##
# @AudiodevDBusOptions:
#
# Options of the D-Bus audio backend.
#
# @in: options of the capture stream
#
# @out: options of the playback stream
#
# @nsamples: set the number of samples per read/write calls (default to 480,
# 10ms at 48kHz).
#
# Since: 10.0
##
{ 'struct': 'AudiodevDBusOptions',
'data': {
'*in': 'AudiodevPerDirectionOptions',
'*out': 'AudiodevPerDirectionOptions',
'*nsamples': 'uint32'} }
##
# @AudiodevAlsaPerDirectionOptions:
#
@ -490,7 +510,7 @@
'if': 'CONFIG_AUDIO_ALSA' },
'coreaudio': { 'type': 'AudiodevCoreaudioOptions',
'if': 'CONFIG_AUDIO_COREAUDIO' },
'dbus': { 'type': 'AudiodevGenericOptions',
'dbus': { 'type': 'AudiodevDBusOptions',
'if': 'CONFIG_DBUS_DISPLAY' },
'dsound': { 'type': 'AudiodevDsoundOptions',
'if': 'CONFIG_AUDIO_DSOUND' },

View File

@ -332,6 +332,19 @@
'data': { 'chardev': 'str' },
'base': 'ChardevCommon' }
##
# @ChardevHub:
#
# Configuration info for hub chardevs.
#
# @chardevs: List of chardev IDs, which should be added to this hub
#
# Since: 10.0
##
{ 'struct': 'ChardevHub',
'data': { 'chardevs': ['str'] },
'base': 'ChardevCommon' }
##
# @ChardevStdio:
#
@ -479,6 +492,8 @@
#
# @mux: (since 1.5)
#
# @hub: (since 10.0)
#
# @msmouse: emulated Microsoft serial mouse (since 1.5)
#
# @wctablet: emulated Wacom Penpartner serial tablet (since 2.9)
@ -521,6 +536,7 @@
'pty',
'null',
'mux',
'hub',
'msmouse',
'wctablet',
{ 'name': 'braille', 'if': 'CONFIG_BRLAPI' },
@ -595,6 +611,16 @@
{ 'struct': 'ChardevMuxWrapper',
'data': { 'data': 'ChardevMux' } }
##
# @ChardevHubWrapper:
#
# @data: Configuration info for hub chardevs
#
# Since: 10.0
##
{ 'struct': 'ChardevHubWrapper',
'data': { 'data': 'ChardevHub' } }
##
# @ChardevStdioWrapper:
#
@ -703,6 +729,7 @@
'pty': 'ChardevPtyWrapper',
'null': 'ChardevCommonWrapper',
'mux': 'ChardevMuxWrapper',
'hub': 'ChardevHubWrapper',
'msmouse': 'ChardevCommonWrapper',
'wctablet': 'ChardevCommonWrapper',
'braille': { 'type': 'ChardevCommonWrapper',

View File

@ -3733,7 +3733,7 @@ SRST
The general form of a character device option is:
``-chardev backend,id=id[,mux=on|off][,options]``
Backend is one of: ``null``, ``socket``, ``udp``, ``msmouse``,
Backend is one of: ``null``, ``socket``, ``udp``, ``msmouse``, ``hub``,
``vc``, ``ringbuf``, ``file``, ``pipe``, ``console``, ``serial``,
``pty``, ``stdio``, ``braille``, ``parallel``,
``spicevmc``, ``spiceport``. The specific backend will determine the
@ -3790,9 +3790,10 @@ The general form of a character device option is:
the QEMU monitor, and ``-nographic`` also multiplexes the console
and the monitor to stdio.
There is currently no support for multiplexing in the other
direction (where a single QEMU front end takes input and output from
multiple chardevs).
If you need to aggregate data in the opposite direction (where one
QEMU frontend interface receives input and output from multiple
backend chardev devices), please refer to the paragraph below
regarding chardev ``hub`` aggregator device configuration.
Every backend supports the ``logfile`` option, which supplies the
path to a file to record all data transmitted via the backend. The
@ -3892,6 +3893,46 @@ The available backends are:
Forward QEMU's emulated msmouse events to the guest. ``msmouse``
does not take any options.
``-chardev hub,id=id,chardevs.0=id[,chardevs.N=id]``
Explicitly create chardev backend hub device with the possibility
to aggregate input from multiple backend devices and forward it to
a single frontend device. Additionally, ``hub`` device takes the
output from the frontend device and sends it back to all the
connected backend devices. This allows for seamless interaction
between different backend devices and a single frontend
interface. Aggregation supported for up to 4 chardev
devices. (Since 10.0)
For example, the following is a use case of 2 backend devices:
virtual console ``vc0`` and a pseudo TTY ``pty0`` connected to
a single virtio hvc console frontend device with a hub ``hub0``
help. Virtual console renders text to an image, which can be
shared over the VNC protocol. In turn, pty backend provides
bidirectional communication to the virtio hvc console over the
pseudo TTY file. The example configuration can be as follows:
::
-chardev pty,path=/tmp/pty,id=pty0 \
-chardev vc,id=vc0 \
-chardev hub,id=hub0,chardevs.0=pty0,chardevs.1=vc0 \
-device virtconsole,chardev=hub0 \
-vnc 0.0.0.0:0
Once QEMU starts VNC client and any TTY emulator can be used to
control a single hvc console:
::
# Start TTY emulator
tio /tmp/pty
# Start VNC client and switch to virtual console Ctrl-Alt-2
vncviewer :0
Several frontend devices is not supported. Stacking of multiplexers
and hub devices is not supported as well.
``-chardev vc,id=id[[,width=width][,height=height]][[,cols=cols][,rows=rows]]``
Connect to a QEMU text console. ``vc`` may optionally be given a
specific size.

View File

@ -359,6 +359,403 @@ static void char_mux_test(void)
qmp_chardev_remove("mux-label", &error_abort);
}
static void char_hub_test(void)
{
QemuOpts *opts;
Chardev *hub, *chr1, *chr2, *base;
char *data;
FeHandler h = { 0, false, 0, false, };
Error *error = NULL;
CharBackend chr_be;
int ret, i;
#define RB_SIZE 128
/*
* Create invalid hub
* 1. Create hub without a 'chardevs.N' defined (expect error)
*/
opts = qemu_opts_create(qemu_find_opts("chardev"), "hub0",
1, &error_abort);
qemu_opt_set(opts, "backend", "hub", &error_abort);
hub = qemu_chr_new_from_opts(opts, NULL, &error);
g_assert_cmpstr(error_get_pretty(error), ==,
"hub: 'chardevs' list is not defined");
error_free(error);
error = NULL;
qemu_opts_del(opts);
/*
* Create invalid hub
* 1. Create chardev with embedded mux: 'mux=on'
* 2. Create hub which refers mux
* 3. Create hub which refers chardev already attached
* to the mux (already in use, expect error)
*/
opts = qemu_opts_create(qemu_find_opts("chardev"), "chr0",
1, &error_abort);
qemu_opt_set(opts, "mux", "on", &error_abort);
qemu_opt_set(opts, "backend", "ringbuf", &error_abort);
qemu_opt_set(opts, "size", stringify(RB_SIZE), &error_abort);
base = qemu_chr_new_from_opts(opts, NULL, &error_abort);
g_assert_nonnull(base);
qemu_opts_del(opts);
opts = qemu_opts_create(qemu_find_opts("chardev"), "hub0",
1, &error_abort);
qemu_opt_set(opts, "backend", "hub", &error_abort);
qemu_opt_set(opts, "chardevs.0", "chr0", &error_abort);
hub = qemu_chr_new_from_opts(opts, NULL, &error);
g_assert_cmpstr(error_get_pretty(error), ==,
"hub: multiplexers and hub devices can't be "
"stacked, check chardev 'chr0', chardev should "
"not be a hub device or have 'mux=on' enabled");
error_free(error);
error = NULL;
qemu_opts_del(opts);
opts = qemu_opts_create(qemu_find_opts("chardev"), "hub0",
1, &error_abort);
qemu_opt_set(opts, "backend", "hub", &error_abort);
qemu_opt_set(opts, "chardevs.0", "chr0-base", &error_abort);
hub = qemu_chr_new_from_opts(opts, NULL, &error);
g_assert_cmpstr(error_get_pretty(error), ==,
"chardev 'chr0-base' is already in use");
error_free(error);
error = NULL;
qemu_opts_del(opts);
/* Finalize chr0 */
qmp_chardev_remove("chr0", &error_abort);
/*
* Create invalid hub with more than maximum allowed backends
* 1. Create more than maximum allowed 'chardevs.%d' options for
* hub (expect error)
*/
opts = qemu_opts_create(qemu_find_opts("chardev"), "hub0",
1, &error_abort);
for (i = 0; i < 10; i++) {
char key[32], val[32];
snprintf(key, sizeof(key), "chardevs.%d", i);
snprintf(val, sizeof(val), "chr%d", i);
qemu_opt_set(opts, key, val, &error);
if (error) {
char buf[64];
snprintf(buf, sizeof(buf), "Invalid parameter 'chardevs.%d'", i);
g_assert_cmpstr(error_get_pretty(error), ==, buf);
error_free(error);
break;
}
}
g_assert_nonnull(error);
error = NULL;
qemu_opts_del(opts);
/*
* Create hub with 2 backend chardevs and 1 frontend and perform
* data aggregation
* 1. Create 2 ringbuf backend chardevs
* 2. Create 1 frontend
* 3. Create hub which refers 2 backend chardevs
* 4. Attach hub to a frontend
* 5. Attach hub to a frontend second time (expect error)
* 6. Perform data aggregation
* 7. Remove chr1 ("chr1 is busy", expect error)
* 8. Remove hub0 ("hub0 is busy", expect error);
* 9. Finilize frontend, hub and backend chardevs in correct order
*/
/* Create first chardev */
opts = qemu_opts_create(qemu_find_opts("chardev"), "chr1",
1, &error_abort);
qemu_opt_set(opts, "backend", "ringbuf", &error_abort);
qemu_opt_set(opts, "size", stringify(RB_SIZE), &error_abort);
chr1 = qemu_chr_new_from_opts(opts, NULL, &error_abort);
g_assert_nonnull(chr1);
qemu_opts_del(opts);
/* Create second chardev */
opts = qemu_opts_create(qemu_find_opts("chardev"), "chr2",
1, &error_abort);
qemu_opt_set(opts, "backend", "ringbuf", &error_abort);
qemu_opt_set(opts, "size", stringify(RB_SIZE), &error_abort);
chr2 = qemu_chr_new_from_opts(opts, NULL, &error_abort);
g_assert_nonnull(chr2);
qemu_opts_del(opts);
/* Create hub0 and refer 2 backend chardevs */
opts = qemu_opts_create(qemu_find_opts("chardev"), "hub0",
1, &error_abort);
qemu_opt_set(opts, "backend", "hub", &error_abort);
qemu_opt_set(opts, "chardevs.0", "chr1", &error_abort);
qemu_opt_set(opts, "chardevs.1", "chr2", &error_abort);
hub = qemu_chr_new_from_opts(opts, NULL, &error_abort);
g_assert_nonnull(hub);
qemu_opts_del(opts);
/* Attach hub to a frontend */
qemu_chr_fe_init(&chr_be, hub, &error_abort);
qemu_chr_fe_set_handlers(&chr_be,
fe_can_read,
fe_read,
fe_event,
NULL,
&h,
NULL, true);
/* Fails second time */
qemu_chr_fe_init(&chr_be, hub, &error);
g_assert_cmpstr(error_get_pretty(error), ==, "chardev 'hub0' is already in use");
error_free(error);
error = NULL;
/* Write to backend, chr1 */
base = qemu_chr_find("chr1");
g_assert_cmpint(qemu_chr_be_can_write(base), !=, 0);
qemu_chr_be_write(base, (void *)"hello", 6);
g_assert_cmpint(h.read_count, ==, 6);
g_assert_cmpstr(h.read_buf, ==, "hello");
h.read_count = 0;
/* Write to backend, chr2 */
base = qemu_chr_find("chr2");
g_assert_cmpint(qemu_chr_be_can_write(base), !=, 0);
qemu_chr_be_write(base, (void *)"olleh", 6);
g_assert_cmpint(h.read_count, ==, 6);
g_assert_cmpstr(h.read_buf, ==, "olleh");
h.read_count = 0;
/* Write to frontend, chr_be */
ret = qemu_chr_fe_write(&chr_be, (void *)"heyhey", 6);
g_assert_cmpint(ret, ==, 6);
data = qmp_ringbuf_read("chr1", RB_SIZE, false, 0, &error_abort);
g_assert_cmpint(strlen(data), ==, 6);
g_assert_cmpstr(data, ==, "heyhey");
g_free(data);
data = qmp_ringbuf_read("chr2", RB_SIZE, false, 0, &error_abort);
g_assert_cmpint(strlen(data), ==, 6);
g_assert_cmpstr(data, ==, "heyhey");
g_free(data);
/* Can't be removed, depends on hub0 */
qmp_chardev_remove("chr1", &error);
g_assert_cmpstr(error_get_pretty(error), ==, "Chardev 'chr1' is busy");
error_free(error);
error = NULL;
/* Can't be removed, depends on frontend chr_be */
qmp_chardev_remove("hub0", &error);
g_assert_cmpstr(error_get_pretty(error), ==, "Chardev 'hub0' is busy");
error_free(error);
error = NULL;
/* Finalize frontend */
qemu_chr_fe_deinit(&chr_be, false);
/* Finalize hub0 */
qmp_chardev_remove("hub0", &error_abort);
/* Finalize backend chardevs */
qmp_chardev_remove("chr1", &error_abort);
qmp_chardev_remove("chr2", &error_abort);
#ifndef _WIN32
/*
* Create 3 backend chardevs to simulate EAGAIN and watcher.
* Mainly copied from char_pipe_test().
* 1. Create 2 ringbuf backend chardevs
* 2. Create 1 pipe backend chardev
* 3. Create 1 frontend
* 4. Create hub which refers 2 backend chardevs
* 5. Attach hub to a frontend
* 6. Perform data aggregation and check watcher
* 7. Finilize frontend, hub and backend chardevs in correct order
*/
{
gchar *tmp_path = g_dir_make_tmp("qemu-test-char.XXXXXX", NULL);
gchar *in, *out, *pipe = g_build_filename(tmp_path, "pipe", NULL);
Chardev *chr3;
int fd, len;
char buf[128];
in = g_strdup_printf("%s.in", pipe);
if (mkfifo(in, 0600) < 0) {
abort();
}
out = g_strdup_printf("%s.out", pipe);
if (mkfifo(out, 0600) < 0) {
abort();
}
/* Create first chardev */
opts = qemu_opts_create(qemu_find_opts("chardev"), "chr1",
1, &error_abort);
qemu_opt_set(opts, "backend", "ringbuf", &error_abort);
qemu_opt_set(opts, "size", stringify(RB_SIZE), &error_abort);
chr1 = qemu_chr_new_from_opts(opts, NULL, &error_abort);
g_assert_nonnull(chr1);
qemu_opts_del(opts);
/* Create second chardev */
opts = qemu_opts_create(qemu_find_opts("chardev"), "chr2",
1, &error_abort);
qemu_opt_set(opts, "backend", "ringbuf", &error_abort);
qemu_opt_set(opts, "size", stringify(RB_SIZE), &error_abort);
chr2 = qemu_chr_new_from_opts(opts, NULL, &error_abort);
g_assert_nonnull(chr2);
qemu_opts_del(opts);
/* Create third chardev */
opts = qemu_opts_create(qemu_find_opts("chardev"), "chr3",
1, &error_abort);
qemu_opt_set(opts, "backend", "pipe", &error_abort);
qemu_opt_set(opts, "path", pipe, &error_abort);
chr3 = qemu_chr_new_from_opts(opts, NULL, &error_abort);
g_assert_nonnull(chr3);
/* Create hub0 and refer 3 backend chardevs */
opts = qemu_opts_create(qemu_find_opts("chardev"), "hub0",
1, &error_abort);
qemu_opt_set(opts, "backend", "hub", &error_abort);
qemu_opt_set(opts, "chardevs.0", "chr1", &error_abort);
qemu_opt_set(opts, "chardevs.1", "chr2", &error_abort);
qemu_opt_set(opts, "chardevs.2", "chr3", &error_abort);
hub = qemu_chr_new_from_opts(opts, NULL, &error_abort);
g_assert_nonnull(hub);
qemu_opts_del(opts);
/* Attach hub to a frontend */
qemu_chr_fe_init(&chr_be, hub, &error_abort);
qemu_chr_fe_set_handlers(&chr_be,
fe_can_read,
fe_read,
fe_event,
NULL,
&h,
NULL, true);
/* Write to frontend, chr_be */
ret = qemu_chr_fe_write(&chr_be, (void *)"thisis", 6);
g_assert_cmpint(ret, ==, 6);
data = qmp_ringbuf_read("chr1", RB_SIZE, false, 0, &error_abort);
g_assert_cmpint(strlen(data), ==, 6);
g_assert_cmpstr(data, ==, "thisis");
g_free(data);
data = qmp_ringbuf_read("chr2", RB_SIZE, false, 0, &error_abort);
g_assert_cmpint(strlen(data), ==, 6);
g_assert_cmpstr(data, ==, "thisis");
g_free(data);
fd = open(out, O_RDWR);
ret = read(fd, buf, sizeof(buf));
g_assert_cmpint(ret, ==, 6);
buf[ret] = 0;
g_assert_cmpstr(buf, ==, "thisis");
close(fd);
/* Add watch. 0 indicates no watches if nothing to wait for */
ret = qemu_chr_fe_add_watch(&chr_be, G_IO_OUT | G_IO_HUP,
NULL, NULL);
g_assert_cmpint(ret, ==, 0);
/*
* Write to frontend, chr_be, until EAGAIN. Make sure length is
* power of two to fit nicely the whole pipe buffer.
*/
len = 0;
while ((ret = qemu_chr_fe_write(&chr_be, (void *)"thisisit", 8))
!= -1) {
len += ret;
}
g_assert_cmpint(errno, ==, EAGAIN);
/* Further all writes should cause EAGAIN */
ret = qemu_chr_fe_write(&chr_be, (void *)"b", 1);
g_assert_cmpint(ret, ==, -1);
g_assert_cmpint(errno, ==, EAGAIN);
/*
* Add watch. Non 0 indicates we have a blocked chardev, which
* can wakes us up when write is possible.
*/
ret = qemu_chr_fe_add_watch(&chr_be, G_IO_OUT | G_IO_HUP,
NULL, NULL);
g_assert_cmpint(ret, !=, 0);
g_source_remove(ret);
/* Drain pipe and ring buffers */
fd = open(out, O_RDWR);
while ((ret = read(fd, buf, MIN(sizeof(buf), len))) != -1 && len > 0) {
len -= ret;
}
close(fd);
data = qmp_ringbuf_read("chr1", RB_SIZE, false, 0, &error_abort);
g_assert_cmpint(strlen(data), ==, 128);
g_free(data);
data = qmp_ringbuf_read("chr2", RB_SIZE, false, 0, &error_abort);
g_assert_cmpint(strlen(data), ==, 128);
g_free(data);
/*
* Now we are good to go, first repeat "lost" sequence, which
* was already consumed and drained by the ring buffers, but
* pipe have not recieved that yet.
*/
ret = qemu_chr_fe_write(&chr_be, (void *)"thisisit", 8);
g_assert_cmpint(ret, ==, 8);
ret = qemu_chr_fe_write(&chr_be, (void *)"streamisrestored", 16);
g_assert_cmpint(ret, ==, 16);
data = qmp_ringbuf_read("chr1", RB_SIZE, false, 0, &error_abort);
g_assert_cmpint(strlen(data), ==, 16);
/* Only last 16 bytes, see big comment above */
g_assert_cmpstr(data, ==, "streamisrestored");
g_free(data);
data = qmp_ringbuf_read("chr2", RB_SIZE, false, 0, &error_abort);
g_assert_cmpint(strlen(data), ==, 16);
/* Only last 16 bytes, see big comment above */
g_assert_cmpstr(data, ==, "streamisrestored");
g_free(data);
fd = open(out, O_RDWR);
ret = read(fd, buf, sizeof(buf));
g_assert_cmpint(ret, ==, 24);
buf[ret] = 0;
/* Both 8 and 16 bytes */
g_assert_cmpstr(buf, ==, "thisisitstreamisrestored");
close(fd);
g_free(in);
g_free(out);
g_free(tmp_path);
g_free(pipe);
/* Finalize frontend */
qemu_chr_fe_deinit(&chr_be, false);
/* Finalize hub0 */
qmp_chardev_remove("hub0", &error_abort);
/* Finalize backend chardevs */
qmp_chardev_remove("chr1", &error_abort);
qmp_chardev_remove("chr2", &error_abort);
qmp_chardev_remove("chr3", &error_abort);
}
#endif
}
static void websock_server_read(void *opaque, const uint8_t *buf, int size)
{
@ -1507,6 +1904,7 @@ int main(int argc, char **argv)
g_test_add_func("/char/invalid", char_invalid_test);
g_test_add_func("/char/ringbuf", char_ringbuf_test);
g_test_add_func("/char/mux", char_mux_test);
g_test_add_func("/char/hub", char_hub_test);
#ifdef _WIN32
g_test_add_func("/char/console/subprocess", char_console_test_subprocess);
g_test_add_func("/char/console", char_console_test);

View File

@ -305,10 +305,16 @@ dbus_console_register_listener(DBusDisplayConsole *ddc,
#endif
);
GDBusConnectionFlags flags =
G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_SERVER;
#ifdef WIN32
flags |= G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_ALLOW_ANONYMOUS;
#endif
listener_conn = g_dbus_connection_new_sync(
G_IO_STREAM(socket_conn),
guid,
G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_SERVER,
flags,
NULL, NULL, &err);
if (err) {
error_report("Failed to setup peer connection: %s", err->message);

View File

@ -527,14 +527,14 @@
<interface name="org.qemu.Display1.Listener.Win32.Map">
<!--
ScanoutMap:
@handle: the shared map handle value.
@handle: the shared file mapping handle value (not a file handle)
@offset: mapping offset.
@width: display width, in pixels.
@height: display height, in pixels.
@stride: stride, in bytes.
@pixman_format: image format (ex: ``PIXMAN_X8R8G8B8``).
Resize and update the display content with a shared map.
Resize and update the display content with a shared file mapping object.
-->
<method name="ScanoutMap">
<arg type="t" name="handle" direction="in"/>
@ -773,6 +773,18 @@
<?endif?>
</method>
<!--
NSamples:
The number of samples per read/write frames. (for example the default is
480, or 10ms at 48kHz)
(earlier version of the display interface do not provide this property)
-->
<property name="NSamples" type="u" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
<!--
Interfaces:

View File

@ -317,11 +317,17 @@ dbus_display_add_client(int csock, Error **errp)
conn = g_socket_connection_factory_create_connection(socket);
dbus_display->add_client_cancellable = g_cancellable_new();
GDBusConnectionFlags flags =
G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_SERVER |
G_DBUS_CONNECTION_FLAGS_DELAY_MESSAGE_PROCESSING;
#ifdef WIN32
flags |= G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_ALLOW_ANONYMOUS;
#endif
g_dbus_connection_new(G_IO_STREAM(conn),
guid,
G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_SERVER |
G_DBUS_CONNECTION_FLAGS_DELAY_MESSAGE_PROCESSING,
flags,
NULL,
dbus_display->add_client_cancellable,
dbus_display_add_client_ready,