* [PATCH v7 0/4] chardev: implement backend chardev multiplexing
@ 2025-01-18 16:40 Roman Penyaev
2025-01-18 16:40 ` [PATCH v7 1/4] chardev/char-pty: send CHR_EVENT_CLOSED on disconnect Roman Penyaev
` (3 more replies)
0 siblings, 4 replies; 13+ messages in thread
From: Roman Penyaev @ 2025-01-18 16:40 UTC (permalink / raw)
Cc: Roman Penyaev, Marc-André Lureau, Markus Armbruster,
Kevin Wolf, qemu-devel
Mux is a character backend (host side) device, which multiplexes
multiple frontends with one backend device. The following is a
few lines from the QEMU manpage [1]:
A multiplexer is a "1:N" device, and here the "1" end is your
specified chardev backend, and the "N" end is the various parts
of QEMU that can talk to a chardev.
But sadly multiple backends are not supported.
This work implements a new chardev backend `hub` device, which
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. This allows for seamless interaction between
different backend devices and a single frontend interface.
The motivation is the EVE project [2], where it would be very
convenient to have a virtio console frontend device on the guest that
can be controlled from multiple backend devices, namely VNC and local
TTY emulator. The following is an example of the QEMU command line:
-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
Which creates two backend devices:
* Text virtual console (`vc0`)
* A pseudo TTY (`pty0`) connected to the single virtio hvc console with the
help of a new backend aggregator (`hub0`)
`vc0` renders text to an image, which can be shared over the VNC
protocol. `pty0` is a pseudo TTY backend which provides bidirectional
communication to the virtio hvc console.
Once QEMU starts, the VNC client and any TTY emulator can be used to
control a single hvc console. For example, these two different
consoles should have similar input and output due to the buffer
aggregation:
# Start TTY emulator
tio /tmp/pty
# Start VNC client and switch to virtual console Ctrl-Alt-2
vncviewer :0
'chardevs.N' list syntax is used for the sake of compatibility with
the representation of JSON lists in 'key=val' pairs format of the
util/keyval.c, despite the fact that modern QAPI way of parsing,
namely qobject_input_visitor_new_str(), is not used. Choice of keeping
QAPI list sytax may help to smoothly switch to modern parsing in the
future.
v6 .. v7:
After discussing v6 it was decided to:
* Rename "multiplexer" to "aggregator"
* Rename "mux-be" device type to "hub"
* Drop all changes related to the original multiplexer implementation
Code changes:
* Added counting of CHR_EVENT_OPENED and CHR_EVENT_CLOSED events
coming from backend devices. This prevents frontend devices from
closing if one of the backend devices has been disconnected. The
logic is simple: "the last one turns off the light".
v5 .. v6:
* Rebased on latest master
* Changed how chardev is attached to a multiplexer: with version 6
mux should specify list elements with ID of chardevs:
chardevs.0=ID[,chardevs.N=ID]
'chardevs.N' list syntax is used for the sake of compatibility with
the representation of JSON lists in 'key=val' pairs format of the
util/keyval.c, despite the fact that modern QAPI way of parsing,
namely qobject_input_visitor_new_str(), is not used. Choice of keeping
QAPI list sytax may help to smoothly switch to modern parsing in the
future.
v4 .. v5:
* Spelling fixes in qemu-options description
* Memory leaks fixes in mux-be tests
* Add sanity checks to chardev to avoid stacking of mux devices
* Add corresponding unit test case to cover the creation of stacked
muxers: `-chardev mux-be,mux-id-be=ID`, which is forbidden
* Reflect the fact that stacking is not supported in the documentation
v3 .. v4:
* Rebase on latest chardev changes
* Add unit tests which test corner cases:
* Inability to remove mux with active frontend
* Inability to add more chardevs to a mux than `MUX_MAX`
* Inability to mix mux-fe and mux-be for the same chardev
v2 .. v3:
* Split frontend and backend multiplexer implementations and
move them to separate files: char-mux-fe.c and char-mux-be.c
v1 .. v2:
* Separate type for the backend multiplexer `mux-be`
* Handle EAGAIN on write to the backend device
* Support of watch of previously failed backend device
* Proper json support of the `mux-be-id` option
* Unit test for the `mux-be` multiplexer
[1] https://www.qemu.org/docs/master/system/qemu-manpage.html#hxtool-6
[2] https://github.com/lf-edge/eve
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
Cc: "Marc-André Lureau" <marcandre.lureau@redhat.com>
Cc: Markus Armbruster <armbru@redhat.com>
Cc: Kevin Wolf <kwolf@redhat.com>
Cc: qemu-devel@nongnu.org
Roman Penyaev (4):
chardev/char-pty: send CHR_EVENT_CLOSED on disconnect
chardev/char-hub: implement backend chardev aggregator
tests/unit/test-char: add unit tests for hub chardev backend
qemu-options.hx: describe hub chardev and aggregation of several
backends
chardev/char-fe.c | 9 +
chardev/char-hub.c | 334 +++++++++++++++++++++++++++++++
chardev/char-pty.c | 3 +-
chardev/char.c | 26 ++-
chardev/chardev-internal.h | 56 +++++-
chardev/meson.build | 1 +
include/chardev/char.h | 1 +
qapi/char.json | 27 +++
qemu-options.hx | 48 ++++-
tests/unit/test-char.c | 399 +++++++++++++++++++++++++++++++++++++
10 files changed, 896 insertions(+), 8 deletions(-)
create mode 100644 chardev/char-hub.c
--
2.43.0
^ permalink raw reply [flat|nested] 13+ messages in thread
* [PATCH v7 1/4] chardev/char-pty: send CHR_EVENT_CLOSED on disconnect
2025-01-18 16:40 [PATCH v7 0/4] chardev: implement backend chardev multiplexing Roman Penyaev
@ 2025-01-18 16:40 ` Roman Penyaev
2025-01-21 15:14 ` Alex Bennée
2025-01-18 16:40 ` [PATCH v7 2/4] chardev/char-hub: implement backend chardev aggregator Roman Penyaev
` (2 subsequent siblings)
3 siblings, 1 reply; 13+ messages in thread
From: Roman Penyaev @ 2025-01-18 16:40 UTC (permalink / raw)
Cc: Roman Penyaev, Marc-André Lureau, qemu-devel
Change makes code symmetric to the code, which handles
the "connected" state, i.e. send CHR_EVENT_CLOSED when
state changes from "connected" to "disconnected".
This behavior is similar to char-socket, for example.
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
Cc: "Marc-André Lureau" <marcandre.lureau@redhat.com>
Cc: qemu-devel@nongnu.org
---
chardev/char-pty.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/chardev/char-pty.c b/chardev/char-pty.c
index cbb21b76ae8d..10a6ee94d55c 100644
--- a/chardev/char-pty.c
+++ b/chardev/char-pty.c
@@ -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 +217,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
--
2.43.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH v7 2/4] chardev/char-hub: implement backend chardev aggregator
2025-01-18 16:40 [PATCH v7 0/4] chardev: implement backend chardev multiplexing Roman Penyaev
2025-01-18 16:40 ` [PATCH v7 1/4] chardev/char-pty: send CHR_EVENT_CLOSED on disconnect Roman Penyaev
@ 2025-01-18 16:40 ` Roman Penyaev
[not found] ` <CAMxuvaxUDzGN1H-zUccQrEz0KvG+a_9t1iKVz4YJQrsdwr=WKA@mail.gmail.com>
2025-01-22 14:44 ` Alex Bennée
2025-01-18 16:40 ` [PATCH v7 3/4] tests/unit/test-char: add unit tests for hub chardev backend Roman Penyaev
2025-01-18 16:40 ` [PATCH v7 4/4] qemu-options.hx: describe hub chardev and aggregation of several backends Roman Penyaev
3 siblings, 2 replies; 13+ messages in thread
From: Roman Penyaev @ 2025-01-18 16:40 UTC (permalink / raw)
Cc: Roman Penyaev, Marc-André Lureau, qemu-devel
This patch implements a new chardev backend `hub` device, which
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. This allows for seamless interaction between
different backend devices and a single frontend interface.
The idea of the change is trivial: keep list of backend devices
(up to 4), init them on demand and forward data buffer back and
forth.
The following is QEMU command line example:
-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
Which creates 2 backend devices: text virtual console (`vc0`) and a
pseudo TTY (`pty0`) connected to the single virtio hvc console with
the backend aggregator (`hub0`) help. `vc0` renders text to an image,
which can be shared over the VNC protocol. `pty0` is a pseudo TTY
backend which provides biderectional communication to the virtio hvc
console.
'chardevs.N' list syntax is used for the sake of compatibility with
the representation of JSON lists in 'key=val' pairs format of the
util/keyval.c, despite the fact that modern QAPI way of parsing,
namely qobject_input_visitor_new_str(), is not used. Choice of keeping
QAPI list sytax may help to smoothly switch to modern parsing in the
future.
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
Cc: "Marc-André Lureau" <marcandre.lureau@redhat.com>
Cc: qemu-devel@nongnu.org
---
chardev/char-fe.c | 9 +
chardev/char-hub.c | 334 +++++++++++++++++++++++++++++++++++++
chardev/char.c | 26 ++-
chardev/chardev-internal.h | 56 ++++++-
chardev/meson.build | 1 +
include/chardev/char.h | 1 +
qapi/char.json | 27 +++
7 files changed, 451 insertions(+), 3 deletions(-)
create mode 100644 chardev/char-hub.c
diff --git a/chardev/char-fe.c b/chardev/char-fe.c
index 158a5f4f551e..cfd0577c3f46 100644
--- a/chardev/char-fe.c
+++ b/chardev/char-fe.c
@@ -200,6 +200,12 @@ bool qemu_chr_fe_init(CharBackend *b, Chardev *s, Error **errp)
if (!mux_chr_attach_frontend(d, b, &tag, errp)) {
return false;
}
+ } else if (CHARDEV_IS_HUB(s)) {
+ HubChardev *d = HUB_CHARDEV(s);
+
+ if (!hub_chr_attach_frontend(d, b, errp)) {
+ return false;
+ }
} else if (s->be) {
error_setg(errp, "chardev '%s' is already in use", s->label);
return false;
@@ -226,6 +232,9 @@ void qemu_chr_fe_deinit(CharBackend *b, bool del)
if (CHARDEV_IS_MUX(b->chr)) {
MuxChardev *d = MUX_CHARDEV(b->chr);
mux_chr_detach_frontend(d, b->tag);
+ } else if (CHARDEV_IS_HUB(b->chr)) {
+ HubChardev *d = HUB_CHARDEV(b->chr);
+ hub_chr_detach_frontend(d);
}
if (del) {
Object *obj = OBJECT(b->chr);
diff --git a/chardev/char-hub.c b/chardev/char-hub.c
new file mode 100644
index 000000000000..9b53df51de44
--- /dev/null
+++ b/chardev/char-hub.c
@@ -0,0 +1,336 @@
+/*
+ * 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.
+ */
+static int hub_chr_write_to_all(HubChardev *d, const uint8_t *buf, int len)
+{
+ 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 && errno == EAGAIN) {
+ /*
+ * Fail immediately if write would block. Expect to be called
+ * soon on watch wake up.
+ */
+ d->be_eagain_ind = i;
+ return r;
+ } else if (r < 0) {
+ /*
+ * Ignore all other errors and pretend the entire buffer is
+ * written to avoid this chardev being watched. This device
+ * becomes disabled until the following write succeeds, but
+ * writing continues to others.
+ */
+ r = len;
+ }
+ d->be_written[i] += r;
+ ret = MIN(r, ret);
+ }
+ d->be_min_written += ret;
+
+ return ret;
+}
+
+/* Called with chr_write_lock held. */
+static int hub_chr_write(Chardev *chr, const uint8_t *buf, int len)
+{
+ HubChardev *d = HUB_CHARDEV(chr);
+ return hub_chr_write_to_all(d, buf, len);
+}
+
+static int hub_chr_can_read(void *opaque)
+{
+ HubCharBackend *backend = opaque;
+ CharBackend *fe = backend->hub->frontend;
+
+ 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->frontend;
+
+
+ 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->frontend;
+
+ 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);
+ CharBackend *fe = d->frontend;
+ int i;
+
+ if (fe) {
+ fe->chr = NULL;
+ }
+ 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);
+ }
+}
+
+bool hub_chr_attach_frontend(HubChardev *d, CharBackend *b, Error **errp)
+{
+ if (d->frontend) {
+ error_setg(errp, "hub: multiplexed chardev '%s' is already used "
+ "for multiplexing", d->parent.label);
+ return false;
+ }
+ d->frontend = b;
+
+ return true;
+}
+
+void hub_chr_detach_frontend(HubChardev *d)
+{
+ d->frontend = NULL;
+}
+
+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);
diff --git a/chardev/char.c b/chardev/char.c
index 7705da5ad02b..31536f4b7356 100644
--- a/chardev/char.c
+++ b/chardev/char.c
@@ -334,6 +334,9 @@ static bool qemu_chr_is_busy(Chardev *s)
if (CHARDEV_IS_MUX(s)) {
MuxChardev *d = MUX_CHARDEV(s);
return d->mux_bitset != 0;
+ } else if (CHARDEV_IS_HUB(s)) {
+ HubChardev *d = HUB_CHARDEV(s);
+ return d->frontend != NULL;
} else {
return s->be != NULL;
}
@@ -943,7 +946,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 +1128,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;
}
diff --git a/chardev/chardev-internal.h b/chardev/chardev-internal.h
index 853807f3cb88..ff5432008aad 100644
--- a/chardev/chardev-internal.h
+++ b/chardev/chardev-internal.h
@@ -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,59 @@ 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 frontend */
+ CharBackend *frontend;
+ /* 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)
-#define CHARDEV_IS_MUX(chr) \
+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);
@@ -65,6 +116,9 @@ bool mux_chr_detach_frontend(MuxChardev *d, unsigned int tag);
void mux_set_focus(Chardev *chr, unsigned int focus);
void mux_chr_send_all_event(Chardev *chr, QEMUChrEvent event);
+bool hub_chr_attach_frontend(HubChardev *d, CharBackend *b, Error **errp);
+void hub_chr_detach_frontend(HubChardev *d);
+
Object *get_chardevs_root(void);
#endif /* CHARDEV_INTERNAL_H */
diff --git a/chardev/meson.build b/chardev/meson.build
index 70070a8279a9..56ee39ac0b01 100644
--- a/chardev/meson.build
+++ b/chardev/meson.build
@@ -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',
diff --git a/include/chardev/char.h b/include/chardev/char.h
index 01df55f9e8c8..429852f8d9d3 100644
--- a/include/chardev/char.h
+++ b/include/chardev/char.h
@@ -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"
diff --git a/qapi/char.json b/qapi/char.json
index e04535435034..f02b66c06b3e 100644
--- a/qapi/char.json
+++ b/qapi/char.json
@@ -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',
--
2.43.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH v7 3/4] tests/unit/test-char: add unit tests for hub chardev backend
2025-01-18 16:40 [PATCH v7 0/4] chardev: implement backend chardev multiplexing Roman Penyaev
2025-01-18 16:40 ` [PATCH v7 1/4] chardev/char-pty: send CHR_EVENT_CLOSED on disconnect Roman Penyaev
2025-01-18 16:40 ` [PATCH v7 2/4] chardev/char-hub: implement backend chardev aggregator Roman Penyaev
@ 2025-01-18 16:40 ` Roman Penyaev
2025-01-18 16:40 ` [PATCH v7 4/4] qemu-options.hx: describe hub chardev and aggregation of several backends Roman Penyaev
3 siblings, 0 replies; 13+ messages in thread
From: Roman Penyaev @ 2025-01-18 16:40 UTC (permalink / raw)
Cc: Roman Penyaev, Marc-André Lureau, qemu-devel
This commit introduces a new test function `char_hub_test` to validate
the functionality and constraints of the "hub" chardev backend in QEMU.
The test includes multiple scenarios:
1. Invalid hub creation:
- Creating a hub without defining `chardevs.N` (expects an error).
- Creating a hub with an embedded multiplexer (`mux=on`) or a chardev
already in use (expects errors).
2. Max backend limit:
- Ensures the hub does not accept more backends than the maximum
allowed, with appropriate error handling.
3. Valid hub creation and data aggregation:
- Successfully creating a hub with two ring buffer backends.
- Verifying data aggregation from backends to a frontend and vice versa.
- Ensuring correct error handling for attempts to attach a hub multiple
times or remove busy chardevs.
4. Extended EAGAIN simulation (non-Windows only):
- Simulates a setup with three backends, including a pipe, to test
EAGAIN handling and watcher behavior.
- Verifies data flow and recovery in scenarios involving buffer
overflows and drained pipes.
The test also ensures correct cleanup of chardevs in all cases, covering
both valid and invalid configurations.
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
Cc: "Marc-André Lureau" <marcandre.lureau@redhat.com>
Cc: qemu-devel@nongnu.org
---
chardev/char-hub.c | 4 +-
tests/unit/test-char.c | 399 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 401 insertions(+), 2 deletions(-)
diff --git a/chardev/char-hub.c b/chardev/char-hub.c
index 9b53df51de44..fb514d92111b 100644
--- a/chardev/char-hub.c
+++ b/chardev/char-hub.c
@@ -223,8 +223,8 @@ static void hub_chr_update_read_handlers(Chardev *chr)
bool hub_chr_attach_frontend(HubChardev *d, CharBackend *b, Error **errp)
{
if (d->frontend) {
- error_setg(errp, "hub: multiplexed chardev '%s' is already used "
- "for multiplexing", d->parent.label);
+ error_setg(errp, "hub: chardev '%s' is already used",
+ d->parent.label);
return false;
}
d->frontend = b;
diff --git a/tests/unit/test-char.c b/tests/unit/test-char.c
index 98a60d86b143..e0b15f0b5d0d 100644
--- a/tests/unit/test-char.c
+++ b/tests/unit/test-char.c
@@ -359,6 +359,404 @@ 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), ==, "hub: chardev "
+ "'hub0' is already used");
+ 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 +1905,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);
--
2.43.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH v7 4/4] qemu-options.hx: describe hub chardev and aggregation of several backends
2025-01-18 16:40 [PATCH v7 0/4] chardev: implement backend chardev multiplexing Roman Penyaev
` (2 preceding siblings ...)
2025-01-18 16:40 ` [PATCH v7 3/4] tests/unit/test-char: add unit tests for hub chardev backend Roman Penyaev
@ 2025-01-18 16:40 ` Roman Penyaev
2025-01-21 15:02 ` Alex Bennée
3 siblings, 1 reply; 13+ messages in thread
From: Roman Penyaev @ 2025-01-18 16:40 UTC (permalink / raw)
Cc: Roman Penyaev, Marc-André Lureau, qemu-devel
This adds a few lines describing `hub` aggregator configuration
for aggregation of several backend devices with a single frontend
device.
Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
Cc: "Marc-André Lureau" <marcandre.lureau@redhat.com>
Cc: qemu-devel@nongnu.org
---
qemu-options.hx | 48 ++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 44 insertions(+), 4 deletions(-)
diff --git a/qemu-options.hx b/qemu-options.hx
index 7090d59f6f10..fdc46f7e68b3 100644
--- a/qemu-options.hx
+++ b/qemu-options.hx
@@ -3720,7 +3720,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
@@ -3777,9 +3777,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
@@ -3879,6 +3880,45 @@ 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.
--
2.43.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* Re: [PATCH v7 2/4] chardev/char-hub: implement backend chardev aggregator
[not found] ` <CAMxuvaxUDzGN1H-zUccQrEz0KvG+a_9t1iKVz4YJQrsdwr=WKA@mail.gmail.com>
@ 2025-01-21 10:56 ` Roman Penyaev
0 siblings, 0 replies; 13+ messages in thread
From: Roman Penyaev @ 2025-01-21 10:56 UTC (permalink / raw)
To: Marc-André Lureau; +Cc: qemu-devel
Hi Marc-André,
On Mon, Jan 20, 2025 at 12:21 PM Marc-André Lureau
<marcandre.lureau@redhat.com> wrote:
>
> Hi
>
> On Sat, Jan 18, 2025 at 8:41 PM Roman Penyaev <r.peniaev@gmail.com> wrote:
> >
> > This patch implements a new chardev backend `hub` device, which
> > 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. This allows for seamless interaction between
> > different backend devices and a single frontend interface.
> >
> > The idea of the change is trivial: keep list of backend devices
> > (up to 4), init them on demand and forward data buffer back and
> > forth.
> >
> > The following is QEMU command line example:
> >
> > -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
> >
> > Which creates 2 backend devices: text virtual console (`vc0`) and a
> > pseudo TTY (`pty0`) connected to the single virtio hvc console with
> > the backend aggregator (`hub0`) help. `vc0` renders text to an image,
> > which can be shared over the VNC protocol. `pty0` is a pseudo TTY
> > backend which provides biderectional communication to the virtio hvc
> > console.
> >
> > 'chardevs.N' list syntax is used for the sake of compatibility with
> > the representation of JSON lists in 'key=val' pairs format of the
> > util/keyval.c, despite the fact that modern QAPI way of parsing,
> > namely qobject_input_visitor_new_str(), is not used. Choice of keeping
> > QAPI list sytax may help to smoothly switch to modern parsing in the
>
> syntax
Will fix. Thanks.
>
> > future.
> >
> > Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
> > Cc: "Marc-André Lureau" <marcandre.lureau@redhat.com>
> > Cc: qemu-devel@nongnu.org
> > ---
> > chardev/char-fe.c | 9 +
> > chardev/char-hub.c | 334 +++++++++++++++++++++++++++++++++++++
> > chardev/char.c | 26 ++-
> > chardev/chardev-internal.h | 56 ++++++-
> > chardev/meson.build | 1 +
> > include/chardev/char.h | 1 +
> > qapi/char.json | 27 +++
> > 7 files changed, 451 insertions(+), 3 deletions(-)
> > create mode 100644 chardev/char-hub.c
> >
> > diff --git a/chardev/char-fe.c b/chardev/char-fe.c
> > index 158a5f4f551e..cfd0577c3f46 100644
> > --- a/chardev/char-fe.c
> > +++ b/chardev/char-fe.c
> > @@ -200,6 +200,12 @@ bool qemu_chr_fe_init(CharBackend *b, Chardev *s, Error **errp)
> > if (!mux_chr_attach_frontend(d, b, &tag, errp)) {
> > return false;
> > }
> > + } else if (CHARDEV_IS_HUB(s)) {
> > + HubChardev *d = HUB_CHARDEV(s);
> > +
> > + if (!hub_chr_attach_frontend(d, b, errp)) {
> > + return false;
> > + }
> > } else if (s->be) {
> > error_setg(errp, "chardev '%s' is already in use", s->label);
> > return false;
> > @@ -226,6 +232,9 @@ void qemu_chr_fe_deinit(CharBackend *b, bool del)
> > if (CHARDEV_IS_MUX(b->chr)) {
> > MuxChardev *d = MUX_CHARDEV(b->chr);
> > mux_chr_detach_frontend(d, b->tag);
> > + } else if (CHARDEV_IS_HUB(b->chr)) {
> > + HubChardev *d = HUB_CHARDEV(b->chr);
> > + hub_chr_detach_frontend(d);
> > }
>
> you don't need this extra attach/detach logic, you can rely on parent
> implementation. See attached patch
Ah, correct. What a service, thank you :) I was focused on the attach
logic that I completely forgot the hub is a char device itself.
Will squash your changes.
>
>
> > if (del) {
> > Object *obj = OBJECT(b->chr);
> > diff --git a/chardev/char-hub.c b/chardev/char-hub.c
> > new file mode 100644
> > index 000000000000..9b53df51de44
> > --- /dev/null
> > +++ b/chardev/char-hub.c
> > @@ -0,0 +1,336 @@
> > +/*
> > + * 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.
> > + */
> > +static int hub_chr_write_to_all(HubChardev *d, const uint8_t *buf, int len)
> > +{
> > + 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 && errno == EAGAIN) {
> > + /*
> > + * Fail immediately if write would block. Expect to be called
> > + * soon on watch wake up.
> > + */
> > + d->be_eagain_ind = i;
> > + return r;
> > + } else if (r < 0) {
> > + /*
> > + * Ignore all other errors and pretend the entire buffer is
> > + * written to avoid this chardev being watched. This device
> > + * becomes disabled until the following write succeeds, but
> > + * writing continues to others.
> > + */
>
> I wonder if this behaviour is desirable. Why silence the error? What
> if all backends fail?
The intention was to cover the case, when one of the backend devices is
closed, but I need to continue serving writes for the other devices. Later I
discovered the OPENED/CLOSED events and `->be_open` flag. So you
are absolutely correct, this "else if" branch is not needed any more and
can be removed. Thanks.
>
> > + r = len;
> > + }
> > + d->be_written[i] += r;
> > + ret = MIN(r, ret);
> > + }
> > + d->be_min_written += ret;
> > +
> > + return ret;
> > +}
> > +
> > +/* Called with chr_write_lock held. */
> > +static int hub_chr_write(Chardev *chr, const uint8_t *buf, int len)
> > +{
> > + HubChardev *d = HUB_CHARDEV(chr);
> > + return hub_chr_write_to_all(d, buf, len);
>
> no need for an extra function, you can just inline here
Will do, not a big deal.
>
> > +}
> > +
> > +static int hub_chr_can_read(void *opaque)
> > +{
> > + HubCharBackend *backend = opaque;
> > + CharBackend *fe = backend->hub->frontend;
> > +
> > + 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->frontend;
> > +
> > +
> > + 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->frontend;
> > +
> > + 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;
> > + }
>
> I wonder if it really makes sense to open when the first chardev opens
> and close with the last one.
>
> I would rather have a default "safe" behaviour that opens when all are
> open, and close when the first one is closed.
>
> Or a loosy behaviour that would just stay open, regardless of backend status.
>
> Or a "selected" behaviour where one backend dictates the hub state.
The concept is similar to an fd handle: a file is considered opened if there
is a valid reference (fd handle) on it. Backend devices can appear and
disappear at any time, there are no main or selected backend devices, all
are equal.
Closing the frontend when the first backend is closed definitely breaks the
user scenario, since there is no way to predict when the user will open or close
the backend device (attach/detach to a pty device or a connection to a socket
can happen at any time).
Keeping the frontend device open seems to do no harm, but it seemed wrong
to me resource wise on the guest and also makes it impossible to implement
logic on the guest that would determine when the opposite side is open/closed
(this can be relevant for virtualserialport scenario). So I wanted to
do it "correctly".
Do you foresee any issues? I noticed that OPEN/CLOSED events can often
be repeated or not sent at all (the first patch solves this problem
for char-pty).
Although, tests/unit/test-char.c tries to verify the correctness by
counting these
events.
I tested the reference logic using pty/socket/vc backends paired with
virtconsole/virtserialport frontends.
>
>
> > + }
> > +
> > + 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);
> > + CharBackend *fe = d->frontend;
> > + int i;
> > +
> > + if (fe) {
> > + fe->chr = NULL;
> > + }
> > + 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);
> > + }
> > +}
> > +
> > +bool hub_chr_attach_frontend(HubChardev *d, CharBackend *b, Error **errp)
> > +{
> > + if (d->frontend) {
> > + error_setg(errp, "hub: multiplexed chardev '%s' is already used "
> > + "for multiplexing", d->parent.label);
> > + return false;
> > + }
> > + d->frontend = b;
> > +
> > + return true;
> > +}
> > +
> > +void hub_chr_detach_frontend(HubChardev *d)
> > +{
> > + d->frontend = NULL;
> > +}
> > +
> > +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);
> > diff --git a/chardev/char.c b/chardev/char.c
> > index 7705da5ad02b..31536f4b7356 100644
> > --- a/chardev/char.c
> > +++ b/chardev/char.c
> > @@ -334,6 +334,9 @@ static bool qemu_chr_is_busy(Chardev *s)
> > if (CHARDEV_IS_MUX(s)) {
> > MuxChardev *d = MUX_CHARDEV(s);
> > return d->mux_bitset != 0;
> > + } else if (CHARDEV_IS_HUB(s)) {
> > + HubChardev *d = HUB_CHARDEV(s);
> > + return d->frontend != NULL;
> > } else {
> > return s->be != NULL;
> > }
> > @@ -943,7 +946,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 +1128,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;
> > }
> >
> > diff --git a/chardev/chardev-internal.h b/chardev/chardev-internal.h
> > index 853807f3cb88..ff5432008aad 100644
> > --- a/chardev/chardev-internal.h
> > +++ b/chardev/chardev-internal.h
> > @@ -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,59 @@ 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 frontend */
> > + CharBackend *frontend;
> > + /* 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)
> > -#define CHARDEV_IS_MUX(chr) \
> > +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);
> > @@ -65,6 +116,9 @@ bool mux_chr_detach_frontend(MuxChardev *d, unsigned int tag);
> > void mux_set_focus(Chardev *chr, unsigned int focus);
> > void mux_chr_send_all_event(Chardev *chr, QEMUChrEvent event);
> >
> > +bool hub_chr_attach_frontend(HubChardev *d, CharBackend *b, Error **errp);
> > +void hub_chr_detach_frontend(HubChardev *d);
> > +
> > Object *get_chardevs_root(void);
> >
> > #endif /* CHARDEV_INTERNAL_H */
> > diff --git a/chardev/meson.build b/chardev/meson.build
> > index 70070a8279a9..56ee39ac0b01 100644
> > --- a/chardev/meson.build
> > +++ b/chardev/meson.build
> > @@ -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',
> > diff --git a/include/chardev/char.h b/include/chardev/char.h
> > index 01df55f9e8c8..429852f8d9d3 100644
> > --- a/include/chardev/char.h
> > +++ b/include/chardev/char.h
> > @@ -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"
> > diff --git a/qapi/char.json b/qapi/char.json
> > index e04535435034..f02b66c06b3e 100644
> > --- a/qapi/char.json
> > +++ b/qapi/char.json
> > @@ -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',
> > --
> > 2.43.0
> >
>
> looks ok to me otherwise
I'll resend the series, thanks.
--
Roman
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH v7 4/4] qemu-options.hx: describe hub chardev and aggregation of several backends
2025-01-18 16:40 ` [PATCH v7 4/4] qemu-options.hx: describe hub chardev and aggregation of several backends Roman Penyaev
@ 2025-01-21 15:02 ` Alex Bennée
2025-01-21 15:23 ` Roman Penyaev
0 siblings, 1 reply; 13+ messages in thread
From: Alex Bennée @ 2025-01-21 15:02 UTC (permalink / raw)
To: Roman Penyaev; +Cc: Marc-André Lureau, qemu-devel
Roman Penyaev <r.peniaev@gmail.com> writes:
> This adds a few lines describing `hub` aggregator configuration
> for aggregation of several backend devices with a single frontend
> device.
>
> Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
> Cc: "Marc-André Lureau" <marcandre.lureau@redhat.com>
> Cc: qemu-devel@nongnu.org
> ---
> qemu-options.hx | 48 ++++++++++++++++++++++++++++++++++++++++++++----
> 1 file changed, 44 insertions(+), 4 deletions(-)
>
> diff --git a/qemu-options.hx b/qemu-options.hx
> index 7090d59f6f10..fdc46f7e68b3 100644
> --- a/qemu-options.hx
> +++ b/qemu-options.hx
> @@ -3720,7 +3720,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
> @@ -3777,9 +3777,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
> @@ -3879,6 +3880,45 @@ 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.
> +
Not sure why this breaks but I'm seeing:
FAILED: docs/docs.stamp
/usr/bin/env CONFDIR=etc/qemu /home/alex/lsrc/qemu.git/builds/all/pyvenv/bin/sphinx-build -q -W -Dkerneldoc_werror=1 -j auto -Dversion=9.2.50 -Drelease= -Ddepfile=docs/docs.d -Ddepfile_stamp=docs/docs.stamp -b html -d /home/alex/lsrc/qemu.git/builds/all/docs/manual.p /home/alex/lsrc/qemu.git/docs /home/alex/lsrc/qemu.git/builds/all/docs/manual
Warning, treated as error:
/home/alex/lsrc/qemu.git/qemu-options.hx:3884:'any' reference target not found: hub
> ``-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.
--
Alex Bennée
Virtualisation Tech Lead @ Linaro
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH v7 1/4] chardev/char-pty: send CHR_EVENT_CLOSED on disconnect
2025-01-18 16:40 ` [PATCH v7 1/4] chardev/char-pty: send CHR_EVENT_CLOSED on disconnect Roman Penyaev
@ 2025-01-21 15:14 ` Alex Bennée
0 siblings, 0 replies; 13+ messages in thread
From: Alex Bennée @ 2025-01-21 15:14 UTC (permalink / raw)
To: Roman Penyaev; +Cc: Marc-André Lureau, qemu-devel
Roman Penyaev <r.peniaev@gmail.com> writes:
> Change makes code symmetric to the code, which handles
> the "connected" state, i.e. send CHR_EVENT_CLOSED when
> state changes from "connected" to "disconnected".
>
> This behavior is similar to char-socket, for example.
>
> Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
> Cc: "Marc-André Lureau" <marcandre.lureau@redhat.com>
> Cc: qemu-devel@nongnu.org
> ---
> chardev/char-pty.c | 3 ++-
> 1 file changed, 2 insertions(+), 1 deletion(-)
>
> diff --git a/chardev/char-pty.c b/chardev/char-pty.c
> index cbb21b76ae8d..10a6ee94d55c 100644
> --- a/chardev/char-pty.c
> +++ b/chardev/char-pty.c
> @@ -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 +217,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);
> }
It would be nice to clean up connected/s->connected to bools at some
point. However this is fine:
Reviewed-by: Alex Bennée <alex.bennee@linaro.org>
--
Alex Bennée
Virtualisation Tech Lead @ Linaro
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH v7 4/4] qemu-options.hx: describe hub chardev and aggregation of several backends
2025-01-21 15:02 ` Alex Bennée
@ 2025-01-21 15:23 ` Roman Penyaev
2025-01-21 15:49 ` Roman Penyaev
2025-01-21 16:02 ` Alex Bennée
0 siblings, 2 replies; 13+ messages in thread
From: Roman Penyaev @ 2025-01-21 15:23 UTC (permalink / raw)
To: Alex Bennée; +Cc: Marc-André Lureau, qemu-devel
On Tue, Jan 21, 2025 at 4:02 PM Alex Bennée <alex.bennee@linaro.org> wrote:
>
> Roman Penyaev <r.peniaev@gmail.com> writes:
>
> > This adds a few lines describing `hub` aggregator configuration
> > for aggregation of several backend devices with a single frontend
> > device.
> >
> > Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
> > Cc: "Marc-André Lureau" <marcandre.lureau@redhat.com>
> > Cc: qemu-devel@nongnu.org
> > ---
> > qemu-options.hx | 48 ++++++++++++++++++++++++++++++++++++++++++++----
> > 1 file changed, 44 insertions(+), 4 deletions(-)
> >
> > diff --git a/qemu-options.hx b/qemu-options.hx
> > index 7090d59f6f10..fdc46f7e68b3 100644
> > --- a/qemu-options.hx
> > +++ b/qemu-options.hx
> > @@ -3720,7 +3720,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
> > @@ -3777,9 +3777,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
> > @@ -3879,6 +3880,45 @@ 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.
> > +
>
> Not sure why this breaks but I'm seeing:
>
> FAILED: docs/docs.stamp
> /usr/bin/env CONFDIR=etc/qemu /home/alex/lsrc/qemu.git/builds/all/pyvenv/bin/sphinx-build -q -W -Dkerneldoc_werror=1 -j auto -Dversion=9.2.50 -Drelease= -Ddepfile=docs/docs.d -Ddepfile_stamp=docs/docs.stamp -b html -d /home/alex/lsrc/qemu.git/builds/all/docs/manual.p /home/alex/lsrc/qemu.git/docs /home/alex/lsrc/qemu.git/builds/all/docs/manual
>
> Warning, treated as error:
> /home/alex/lsrc/qemu.git/qemu-options.hx:3884:'any' reference target not found: hub
This is odd, my make is silent. Can you please check this?
diff --git a/qemu-options.hx b/qemu-options.hx
index fdc46f7e68b3..e6d9de142aaf 100644
--- a/qemu-options.hx
+++ b/qemu-options.hx
@@ -3880,7 +3880,8 @@ 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 hub,id=id,chardevs.0=id[,chardevs.N=id]``
+ Explicitly create
--
Roman
^ permalink raw reply related [flat|nested] 13+ messages in thread
* Re: [PATCH v7 4/4] qemu-options.hx: describe hub chardev and aggregation of several backends
2025-01-21 15:23 ` Roman Penyaev
@ 2025-01-21 15:49 ` Roman Penyaev
2025-01-21 16:02 ` Alex Bennée
1 sibling, 0 replies; 13+ messages in thread
From: Roman Penyaev @ 2025-01-21 15:49 UTC (permalink / raw)
To: Alex Bennée; +Cc: Marc-André Lureau, qemu-devel
On Tue, Jan 21, 2025 at 4:23 PM Roman Penyaev <r.peniaev@gmail.com> wrote:
>
> On Tue, Jan 21, 2025 at 4:02 PM Alex Bennée <alex.bennee@linaro.org> wrote:
> >
> > Roman Penyaev <r.peniaev@gmail.com> writes:
> >
> > > This adds a few lines describing `hub` aggregator configuration
> > > for aggregation of several backend devices with a single frontend
> > > device.
> > >
> > > Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
> > > Cc: "Marc-André Lureau" <marcandre.lureau@redhat.com>
> > > Cc: qemu-devel@nongnu.org
> > > ---
> > > qemu-options.hx | 48 ++++++++++++++++++++++++++++++++++++++++++++----
> > > 1 file changed, 44 insertions(+), 4 deletions(-)
> > >
> > > diff --git a/qemu-options.hx b/qemu-options.hx
> > > index 7090d59f6f10..fdc46f7e68b3 100644
> > > --- a/qemu-options.hx
> > > +++ b/qemu-options.hx
> > > @@ -3720,7 +3720,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
> > > @@ -3777,9 +3777,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
> > > @@ -3879,6 +3880,45 @@ 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.
> > > +
> >
> > Not sure why this breaks but I'm seeing:
> >
> > FAILED: docs/docs.stamp
> > /usr/bin/env CONFDIR=etc/qemu /home/alex/lsrc/qemu.git/builds/all/pyvenv/bin/sphinx-build -q -W -Dkerneldoc_werror=1 -j auto -Dversion=9.2.50 -Drelease= -Ddepfile=docs/docs.d -Ddepfile_stamp=docs/docs.stamp -b html -d /home/alex/lsrc/qemu.git/builds/all/docs/manual.p /home/alex/lsrc/qemu.git/docs /home/alex/lsrc/qemu.git/builds/all/docs/manual
> >
> > Warning, treated as error:
> > /home/alex/lsrc/qemu.git/qemu-options.hx:3884:'any' reference target not found: hub
>
> This is odd, my make is silent. Can you please check this?
Docs generation was disabled on my side. My bad. The problem is in the
`hub`, which should be ``hub``. Thanks for noticing that.
--
Roman
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH v7 4/4] qemu-options.hx: describe hub chardev and aggregation of several backends
2025-01-21 15:23 ` Roman Penyaev
2025-01-21 15:49 ` Roman Penyaev
@ 2025-01-21 16:02 ` Alex Bennée
1 sibling, 0 replies; 13+ messages in thread
From: Alex Bennée @ 2025-01-21 16:02 UTC (permalink / raw)
To: Roman Penyaev; +Cc: Marc-André Lureau, qemu-devel
Roman Penyaev <r.peniaev@gmail.com> writes:
> On Tue, Jan 21, 2025 at 4:02 PM Alex Bennée <alex.bennee@linaro.org> wrote:
>>
>> Roman Penyaev <r.peniaev@gmail.com> writes:
>>
>> > This adds a few lines describing `hub` aggregator configuration
>> > for aggregation of several backend devices with a single frontend
>> > device.
>> >
>> > Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
>> > Cc: "Marc-André Lureau" <marcandre.lureau@redhat.com>
>> > Cc: qemu-devel@nongnu.org
>> > ---
>> > qemu-options.hx | 48 ++++++++++++++++++++++++++++++++++++++++++++----
>> > 1 file changed, 44 insertions(+), 4 deletions(-)
>> >
>> > diff --git a/qemu-options.hx b/qemu-options.hx
>> > index 7090d59f6f10..fdc46f7e68b3 100644
>> > --- a/qemu-options.hx
>> > +++ b/qemu-options.hx
>> > @@ -3720,7 +3720,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
>> > @@ -3777,9 +3777,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
>> > @@ -3879,6 +3880,45 @@ 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.
>> > +
>>
>> Not sure why this breaks but I'm seeing:
>>
>> FAILED: docs/docs.stamp
>> /usr/bin/env CONFDIR=etc/qemu
>> /home/alex/lsrc/qemu.git/builds/all/pyvenv/bin/sphinx-build -q -W
>> -Dkerneldoc_werror=1 -j auto -Dversion=9.2.50 -Drelease=
>> -Ddepfile=docs/docs.d -Ddepfile_stamp=docs/docs.stamp -b html -d
>> /home/alex/lsrc/qemu.git/builds/all/docs/manual.p
>> /home/alex/lsrc/qemu.git/docs
>> /home/alex/lsrc/qemu.git/builds/all/docs/manual
>>
>> Warning, treated as error:
>> /home/alex/lsrc/qemu.git/qemu-options.hx:3884:'any' reference target not found: hub
>
> This is odd, my make is silent. Can you please check this?
>
> diff --git a/qemu-options.hx b/qemu-options.hx
> index fdc46f7e68b3..e6d9de142aaf 100644
> --- a/qemu-options.hx
> +++ b/qemu-options.hx
> @@ -3880,7 +3880,8 @@ 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 hub,id=id,chardevs.0=id[,chardevs.N=id]``
> + Explicitly create
No joy. I did replicate from the command line and s/-q/-v/
writing output... [ 43%] specs/fsi .. specs/ppc-spapr-hotplug
Traceback (most recent call last):
File "/usr/lib/python3/dist-packages/sphinx/cmd/build.py", line 281, in build_main
app.build(args.force_all, args.filenames)
File "/usr/lib/python3/dist-packages/sphinx/application.py", line 341, in build
self.builder.build_update()
File "/usr/lib/python3/dist-packages/sphinx/builders/__init__.py", line 310, in build_update
self.build(to_build,
File "/usr/lib/python3/dist-packages/sphinx/builders/__init__.py", line 376, in build
self.write(docnames, list(updated_docnames), method)
File "/usr/lib/python3/dist-packages/sphinx/builders/__init__.py", line 568, in write
self._write_parallel(sorted(docnames),
File "/usr/lib/python3/dist-packages/sphinx/builders/__init__.py", line 612, in _write_parallel
doctree = self.env.get_and_resolve_doctree(docname, self)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/sphinx/environment/__init__.py", line 591, in get_and_resolve_doctree
self.apply_post_transforms(doctree, docname)
File "/usr/lib/python3/dist-packages/sphinx/environment/__init__.py", line 637, in apply_post_transforms
transformer.apply_transforms()
File "/usr/lib/python3/dist-packages/sphinx/transforms/__init__.py", line 80, in apply_transforms
super().apply_transforms()
File "/usr/lib/python3/dist-packages/docutils/transforms/__init__.py", line 173, in apply_transforms
transform.apply(**kwargs)
File "/usr/lib/python3/dist-packages/sphinx/transforms/post_transforms/__init__.py", line 35, in apply
self.run(**kwargs)
File "/usr/lib/python3/dist-packages/sphinx/transforms/post_transforms/__init__.py", line 97, in run
self.warn_missing_reference(refdoc, typ, target, node, domain)
File "/usr/lib/python3/dist-packages/sphinx/transforms/post_transforms/__init__.py", line 204, in warn_missing_reference
logger.warning(msg, location=node, type='ref', subtype=typ)
File "/usr/lib/python3.11/logging/__init__.py", line 1855, in warning
self.log(WARNING, msg, *args, **kwargs)
File "/usr/lib/python3/dist-packages/sphinx/util/logging.py", line 123, in log
super().log(level, msg, *args, **kwargs)
File "/usr/lib/python3.11/logging/__init__.py", line 1887, in log
self.logger.log(level, msg, *args, **kwargs)
File "/usr/lib/python3.11/logging/__init__.py", line 1559, in log
self._log(level, msg, args, **kwargs)
File "/usr/lib/python3.11/logging/__init__.py", line 1634, in _log
self.handle(record)
File "/usr/lib/python3.11/logging/__init__.py", line 1644, in handle
self.callHandlers(record)
File "/usr/lib/python3.11/logging/__init__.py", line 1706, in callHandlers
hdlr.handle(record)
File "/usr/lib/python3.11/logging/__init__.py", line 974, in handle
rv = self.filter(record)
^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.11/logging/__init__.py", line 830, in filter
result = f.filter(record)
^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/sphinx/util/logging.py", line 426, in filter
raise exc
sphinx.errors.SphinxWarning: /home/alex/lsrc/qemu.git/qemu-options.hx:3884:'any' reference target not found: hub
Warning, treated as error:
/home/alex/lsrc/qemu.git/qemu-options.hx:3884:'any' reference target not found: hub
So it appears deep in sphinx itself and not any of our config bits... I
can't work out why its looking up hub as a reference though.
--
Alex Bennée
Virtualisation Tech Lead @ Linaro
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH v7 2/4] chardev/char-hub: implement backend chardev aggregator
2025-01-18 16:40 ` [PATCH v7 2/4] chardev/char-hub: implement backend chardev aggregator Roman Penyaev
[not found] ` <CAMxuvaxUDzGN1H-zUccQrEz0KvG+a_9t1iKVz4YJQrsdwr=WKA@mail.gmail.com>
@ 2025-01-22 14:44 ` Alex Bennée
2025-01-22 15:17 ` Roman Penyaev
1 sibling, 1 reply; 13+ messages in thread
From: Alex Bennée @ 2025-01-22 14:44 UTC (permalink / raw)
To: Roman Penyaev; +Cc: Marc-André Lureau, qemu-devel
Roman Penyaev <r.peniaev@gmail.com> writes:
> This patch implements a new chardev backend `hub` device, which
> 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. This allows for seamless interaction between
> different backend devices and a single frontend interface.
>
> The idea of the change is trivial: keep list of backend devices
> (up to 4), init them on demand and forward data buffer back and
> forth.
>
> The following is QEMU command line example:
>
> -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
>
> Which creates 2 backend devices: text virtual console (`vc0`) and a
> pseudo TTY (`pty0`) connected to the single virtio hvc console with
> the backend aggregator (`hub0`) help. `vc0` renders text to an image,
> which can be shared over the VNC protocol. `pty0` is a pseudo TTY
> backend which provides biderectional communication to the virtio hvc
> console.
>
<snip>
> +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;
So I was looking at this to see if I could implement what I wanted which
was a tee-like copy of a serial port output while maintaining the C-a
support of the mux.
Normally I just use the shortcut -serial mon:stdio
However that form is a special case so I tried the following and ran
into the above:
-chardev stdio,mux=on,id=char0 \
-chardev file,path=console.log,id=clog \
-mon chardev=char0,mode=readline \
-chardev hub,id=hub0,chardevs.0=char0,chardevs.1=clog
Giving:
qemu-system-aarch64: -chardev -hub,id=hub0,chardevs.0=char0,chardevs.1=clog: hub: -multiplexers and hub devices can't be stacked, check chardev
-'char0', chardev should not be a hub device or have 'mux=on' -enabled
So what stops this sort of chain?
--
Alex Bennée
Virtualisation Tech Lead @ Linaro
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH v7 2/4] chardev/char-hub: implement backend chardev aggregator
2025-01-22 14:44 ` Alex Bennée
@ 2025-01-22 15:17 ` Roman Penyaev
0 siblings, 0 replies; 13+ messages in thread
From: Roman Penyaev @ 2025-01-22 15:17 UTC (permalink / raw)
To: Alex Bennée; +Cc: Marc-André Lureau, qemu-devel
On Wed, Jan 22, 2025 at 3:44 PM Alex Bennée <alex.bennee@linaro.org> wrote:
>
> Roman Penyaev <r.peniaev@gmail.com> writes:
>
> > This patch implements a new chardev backend `hub` device, which
> > 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. This allows for seamless interaction between
> > different backend devices and a single frontend interface.
> >
> > The idea of the change is trivial: keep list of backend devices
> > (up to 4), init them on demand and forward data buffer back and
> > forth.
> >
> > The following is QEMU command line example:
> >
> > -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
> >
> > Which creates 2 backend devices: text virtual console (`vc0`) and a
> > pseudo TTY (`pty0`) connected to the single virtio hvc console with
> > the backend aggregator (`hub0`) help. `vc0` renders text to an image,
> > which can be shared over the VNC protocol. `pty0` is a pseudo TTY
> > backend which provides biderectional communication to the virtio hvc
> > console.
> >
> <snip>
> > +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;
>
> So I was looking at this to see if I could implement what I wanted which
> was a tee-like copy of a serial port output while maintaining the C-a
> support of the mux.
>
> Normally I just use the shortcut -serial mon:stdio
>
> However that form is a special case so I tried the following and ran
> into the above:
>
> -chardev stdio,mux=on,id=char0 \
> -chardev file,path=console.log,id=clog \
> -mon chardev=char0,mode=readline \
> -chardev hub,id=hub0,chardevs.0=char0,chardevs.1=clog
>
> Giving:
> qemu-system-aarch64: -chardev -hub,id=hub0,chardevs.0=char0,chardevs.1=clog: hub: -multiplexers and hub devices can't be stacked, check chardev
> -'char0', chardev should not be a hub device or have 'mux=on' -enabled
>
> So what stops this sort of chain?
Hi Alex,
You define 'char0' as a mux device (the "mux=on" option). That is not supported
simply to avoid circle dependencies. To be frank I never considered
this use-case,
although chains might be useful. If you need this use-case we can
think of a more
proper (or advance) check.
--
Roman
^ permalink raw reply [flat|nested] 13+ messages in thread
end of thread, other threads:[~2025-01-22 15:19 UTC | newest]
Thread overview: 13+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-01-18 16:40 [PATCH v7 0/4] chardev: implement backend chardev multiplexing Roman Penyaev
2025-01-18 16:40 ` [PATCH v7 1/4] chardev/char-pty: send CHR_EVENT_CLOSED on disconnect Roman Penyaev
2025-01-21 15:14 ` Alex Bennée
2025-01-18 16:40 ` [PATCH v7 2/4] chardev/char-hub: implement backend chardev aggregator Roman Penyaev
[not found] ` <CAMxuvaxUDzGN1H-zUccQrEz0KvG+a_9t1iKVz4YJQrsdwr=WKA@mail.gmail.com>
2025-01-21 10:56 ` Roman Penyaev
2025-01-22 14:44 ` Alex Bennée
2025-01-22 15:17 ` Roman Penyaev
2025-01-18 16:40 ` [PATCH v7 3/4] tests/unit/test-char: add unit tests for hub chardev backend Roman Penyaev
2025-01-18 16:40 ` [PATCH v7 4/4] qemu-options.hx: describe hub chardev and aggregation of several backends Roman Penyaev
2025-01-21 15:02 ` Alex Bennée
2025-01-21 15:23 ` Roman Penyaev
2025-01-21 15:49 ` Roman Penyaev
2025-01-21 16:02 ` Alex Bennée
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.