[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Re: [PATCH v6 6/8] chardev/char-mux: implement backend chardev multiplex
From: |
Marc-André Lureau |
Subject: |
Re: [PATCH v6 6/8] chardev/char-mux: implement backend chardev multiplexing |
Date: |
Mon, 30 Dec 2024 15:41:32 +0400 |
Hi
On Mon, Dec 23, 2024 at 5:24 PM Roman Penyaev <r.peniaev@gmail.com> wrote:
>
> This patch implements multiplexing capability of several backend
> devices, which opens up an opportunity to use a single frontend
> device on the guest, which can be manipulated from several
> backend devices.
>
> 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.
>
> Patch implements another multiplexer type `mux-be`. The following
> is QEMU command line example:
>
> -chardev socket,path=/tmp/sock,server=on,wait=off,id=sock0 \
> -chardev vc,id=vc0 \
> -chardev mux-be,id=mux0,chardevs.0=sock0,chardevs.1=vc0 \
> -device virtconsole,chardev=mux0 \
> -vnc 0.0.0.0:0
>
> Which creates 2 backend devices: text virtual console (`vc0`) and a
> socket (`sock0`) connected to the single virtio hvc console with the
> backend multiplexer (`mux0`) help. `vc0` renders text to an image,
> which can be shared over the VNC protocol. `sock0` is a socket
> 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-mux-be.c | 321 +++++++++++++++++++++++++++++++++++++
> chardev/char.c | 30 +++-
> chardev/chardev-internal.h | 38 ++++-
> chardev/meson.build | 1 +
> include/chardev/char.h | 1 +
> qapi/char.json | 27 ++++
> 7 files changed, 423 insertions(+), 4 deletions(-)
> create mode 100644 chardev/char-mux-be.c
>
> diff --git a/chardev/char-fe.c b/chardev/char-fe.c
> index 8853f7fb4686..a156053ebef3 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_fe_chr_attach_frontend(d, b, &tag, errp)) {
> return false;
> }
> + } else if (CHARDEV_IS_MUX_BE(s)) {
> + MuxBeChardev *d = MUX_BE_CHARDEV(s);
> +
> + if (!mux_be_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_FE(b->chr)) {
> MuxFeChardev *d = MUX_FE_CHARDEV(b->chr);
> mux_fe_chr_detach_frontend(d, b->tag);
> + } else if (CHARDEV_IS_MUX_BE(b->chr)) {
> + MuxBeChardev *d = MUX_BE_CHARDEV(b->chr);
> + mux_be_chr_detach_frontend(d);
> }
> if (del) {
> Object *obj = OBJECT(b->chr);
> diff --git a/chardev/char-mux-be.c b/chardev/char-mux-be.c
> new file mode 100644
> index 000000000000..f24c00dee2fe
> --- /dev/null
> +++ b/chardev/char-mux-be.c
> @@ -0,0 +1,321 @@
> +/*
> + * QEMU Character Backend Multiplexer
> + *
> + * 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"
> +
> +/*
> + * MUX-BE driver for multiplexing 1 frontend device with N 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 mux_be_chr_write_to_all(MuxBeChardev *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++) {
> + 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], 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;
But next attempt to write will loop over the same backend again, which
will see the "same" write multiple times.
> + } 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;
> +}
I am not sure what is the correct way to handle write here. This
mux-be behaviour is different from mux-fe, since it handles all
backend I/Os, and does not select one... it's more of a "mixer",
right, Is this wanted?
> +
> +/* Called with chr_write_lock held. */
> +static int mux_be_chr_write(Chardev *chr, const uint8_t *buf, int len)
> +{
> + MuxBeChardev *d = MUX_BE_CHARDEV(chr);
> + return mux_be_chr_write_to_all(d, buf, len);
> +}
> +
> +static void mux_be_chr_send_event(MuxBeChardev *d, QEMUChrEvent event)
> +{
> + CharBackend *fe = d->frontend;
> +
> + if (fe && fe->chr_event) {
> + fe->chr_event(fe->opaque, event);
> + }
> +}
> +
> +static void mux_be_chr_be_event(Chardev *chr, QEMUChrEvent event)
> +{
> + MuxBeChardev *d = MUX_BE_CHARDEV(chr);
> +
> + mux_be_chr_send_event(d, event);
> +}
> +
> +static int mux_be_chr_can_read(void *opaque)
> +{
> + MuxBeChardev *d = MUX_BE_CHARDEV(opaque);
> + CharBackend *fe = d->frontend;
> +
> + if (fe && fe->chr_can_read) {
> + return fe->chr_can_read(fe->opaque);
> + }
> +
> + return 0;
> +}
> +
> +static void mux_be_chr_read(void *opaque, const uint8_t *buf, int size)
> +{
> + MuxBeChardev *d = MUX_BE_CHARDEV(opaque);
> + CharBackend *fe = d->frontend;
> +
> + if (fe && fe->chr_read) {
> + fe->chr_read(fe->opaque, buf, size);
> + }
> +}
> +
> +void mux_be_chr_send_all_event(MuxBeChardev *d, QEMUChrEvent event)
> +{
> + mux_be_chr_send_event(d, event);
> +}
> +
> +static void mux_be_chr_event(void *opaque, QEMUChrEvent event)
> +{
> + mux_chr_send_all_event(CHARDEV(opaque), event);
> +}
> +
> +static GSource *mux_be_chr_add_watch(Chardev *s, GIOCondition cond)
> +{
> + MuxBeChardev *d = MUX_BE_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]);
> + cc = CHARDEV_GET_CLASS(chr);
> + if (!cc->chr_add_watch) {
> + return NULL;
> + }
> +
> + return cc->chr_add_watch(chr, cond);
> +}
> +
> +static bool mux_be_chr_attach_chardev(MuxBeChardev *d, Chardev *chr,
> + Error **errp)
> +{
> + bool ret;
> +
> + if (d->be_cnt >= MAX_MUX) {
> + error_setg(errp, "mux-be: too many uses of multiplexed chardev '%s'"
> + " (maximum is " stringify(MAX_MUX) ")",
> + d->parent.label);
> + return false;
> + }
> + ret = qemu_chr_fe_init(&d->backends[d->be_cnt], chr, errp);
> + if (ret) {
> + /* Catch up with what was already written */
> + d->be_written[d->be_cnt] = d->be_min_written;
> + d->be_cnt += 1;
> + }
> +
> + return ret;
> +}
> +
> +static void char_mux_be_finalize(Object *obj)
> +{
> + MuxBeChardev *d = MUX_BE_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], false);
> + }
> +}
> +
> +static void mux_be_chr_update_read_handlers(Chardev *chr)
> +{
> + MuxBeChardev *d = MUX_BE_CHARDEV(chr);
> + int i;
> +
> + for (i = 0; i < d->be_cnt; i++) {
> + /* Fix up the real driver with mux routines */
> + qemu_chr_fe_set_handlers_full(&d->backends[i],
> + mux_be_chr_can_read,
> + mux_be_chr_read,
> + mux_be_chr_event,
> + NULL,
> + chr,
> + chr->gcontext, true, false);
> + }
> +}
> +
> +bool mux_be_chr_attach_frontend(MuxBeChardev *d, CharBackend *b, Error
> **errp)
> +{
> + if (d->frontend) {
> + error_setg(errp, "mux-be: multiplexed chardev '%s' is already used "
> + "for multiplexing", d->parent.label);
> + return false;
> + }
> + d->frontend = b;
> +
> + return true;
> +}
> +
> +void mux_be_chr_detach_frontend(MuxBeChardev *d)
> +{
> + d->frontend = NULL;
> +}
> +
> +static void qemu_chr_open_mux_be(Chardev *chr,
> + ChardevBackend *backend,
> + bool *be_opened,
> + Error **errp)
> +{
> + ChardevMuxBe *mux = backend->u.mux_be.data;
> + MuxBeChardev *d = MUX_BE_CHARDEV(chr);
> + strList *list = mux->chardevs;
> +
> + d->be_eagain_ind = -1;
> +
> + if (list == NULL) {
> + error_setg(errp, "mux-be: 'chardevs' list is not defined");
> + return;
> + }
> +
> + while (list) {
> + Chardev *s;
> +
> + s = qemu_chr_find(list->value);
> + if (s == NULL) {
> + error_setg(errp, "mux-be: chardev can't be found by id '%s'",
> + list->value);
> + return;
> + }
> + if (CHARDEV_IS_MUX_BE(s) || CHARDEV_IS_MUX_FE(s)) {
> + error_setg(errp, "mux-be: multiplexers can't be stacked, check "
> + "chardev '%s', chardev should not be a multiplexer or
> "
> + "have 'mux=on' enabled", list->value);
> + return;
> + }
> + if (!mux_be_chr_attach_chardev(d, s, errp)) {
> + return;
> + }
> + list = list->next;
> + }
> +
> + /*
> + * Only default to opened state if we've realized the initial
> + * set of muxes
> + */
> + *be_opened = mux_is_opened();
> +}
> +
> +static void qemu_chr_parse_mux_be(QemuOpts *opts, ChardevBackend *backend,
> + Error **errp)
> +{
> + ChardevMuxBe *mux;
> + strList **tail;
> + int i;
> +
> + backend->type = CHARDEV_BACKEND_KIND_MUX_BE;
> + mux = backend->u.mux_be.data = g_new0(ChardevMuxBe, 1);
> + qemu_chr_parse_common(opts, qapi_ChardevMuxBe_base(mux));
> +
> + tail = &mux->chardevs;
> +
> + for (i = 0; i < MAX_MUX; 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_mux_be_class_init(ObjectClass *oc, void *data)
> +{
> + ChardevClass *cc = CHARDEV_CLASS(oc);
> +
> + cc->parse = qemu_chr_parse_mux_be;
> + cc->open = qemu_chr_open_mux_be;
> + cc->chr_write = mux_be_chr_write;
> + cc->chr_add_watch = mux_be_chr_add_watch;
> + cc->chr_be_event = mux_be_chr_be_event;
> + cc->chr_update_read_handler = mux_be_chr_update_read_handlers;
> +}
> +
> +static const TypeInfo char_mux_be_type_info = {
> + .name = TYPE_CHARDEV_MUX_BE,
> + .parent = TYPE_CHARDEV,
> + .class_init = char_mux_be_class_init,
> + .instance_size = sizeof(MuxBeChardev),
> + .instance_finalize = char_mux_be_finalize,
> +};
> +
> +static void register_types(void)
> +{
> + type_register_static(&char_mux_be_type_info);
> +}
> +
> +type_init(register_types);
> diff --git a/chardev/char.c b/chardev/char.c
> index 08e7f23bddb2..3e85cfc5a4ba 100644
> --- a/chardev/char.c
> +++ b/chardev/char.c
> @@ -341,6 +341,9 @@ static bool qemu_chr_is_busy(Chardev *s)
> if (CHARDEV_IS_MUX_FE(s)) {
> MuxFeChardev *d = MUX_FE_CHARDEV(s);
> return d->mux_bitset != 0;
> + } else if (CHARDEV_IS_MUX_BE(s)) {
> + MuxBeChardev *d = MUX_BE_CHARDEV(s);
> + return d->frontend != NULL;
> } else {
> return s->be != NULL;
> }
> @@ -949,7 +952,26 @@ QemuOptsList qemu_chardev_opts = {
> },{
> .name = "chardev",
> .type = QEMU_OPT_STRING,
> + },
> + /*
> + * Multiplexer options. Follows QAPI array syntax.
> + * See MAX_MUX 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,
> },{
> @@ -1112,7 +1134,7 @@ ChardevReturn *qmp_chardev_change(const char *id,
> ChardevBackend *backend,
> return NULL;
> }
>
> - if (CHARDEV_IS_MUX_FE(chr)) {
> + if (CHARDEV_IS_MUX_FE(chr) || CHARDEV_IS_MUX_BE(chr)) {
> error_setg(errp, "Mux device hotswap not supported yet");
> return NULL;
> }
> @@ -1300,7 +1322,7 @@ static int chardev_options_parsed_cb(Object *child,
> void *opaque)
> {
> Chardev *chr = (Chardev *)child;
>
> - if (!chr->be_open && CHARDEV_IS_MUX_FE(chr)) {
> + if (!chr->be_open && (CHARDEV_IS_MUX_FE(chr) || CHARDEV_IS_MUX_BE(chr)))
> {
> open_muxes(chr);
> }
>
> @@ -1327,8 +1349,10 @@ void mux_chr_send_all_event(Chardev *chr, QEMUChrEvent
> event)
>
> if (CHARDEV_IS_MUX_FE(chr)) {
> MuxFeChardev *d = MUX_FE_CHARDEV(chr);
> -
> mux_fe_chr_send_all_event(d, event);
> + } else if (CHARDEV_IS_MUX_BE(chr)) {
> + MuxBeChardev *d = MUX_BE_CHARDEV(chr);
> + mux_be_chr_send_all_event(d, event);
> }
> }
>
> diff --git a/chardev/chardev-internal.h b/chardev/chardev-internal.h
> index 94c8d07ac235..8c98a3fb0767 100644
> --- a/chardev/chardev-internal.h
> +++ b/chardev/chardev-internal.h
> @@ -35,7 +35,9 @@
>
> struct MuxFeChardev {
> Chardev parent;
> + /* Linked frontends */
> CharBackend *backends[MAX_MUX];
> + /* Linked backend */
> CharBackend chr;
> unsigned long mux_bitset;
> int focus;
> @@ -54,10 +56,41 @@ struct MuxFeChardev {
> };
> typedef struct MuxFeChardev MuxFeChardev;
>
> +struct MuxBeChardev {
> + Chardev parent;
> + /* Linked frontend */
> + CharBackend *frontend;
> + /* Linked backends */
> + CharBackend backends[MAX_MUX];
> + /*
> + * Number of backends attached to this mux. Once attached, a
> + * backend can't be detached, so the counter is only increasing.
> + * To safely remove a backend, mux has to be removed first.
> + */
> + unsigned int be_cnt;
> + /*
> + * Counters of written bytes from a single frontend device
> + * to multiple backend devices.
> + */
> + unsigned int be_written[MAX_MUX];
> + 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 MuxBeChardev MuxBeChardev;
> +
> DECLARE_INSTANCE_CHECKER(MuxFeChardev, MUX_FE_CHARDEV,
> TYPE_CHARDEV_MUX_FE)
> -#define CHARDEV_IS_MUX_FE(chr) \
> +DECLARE_INSTANCE_CHECKER(MuxBeChardev, MUX_BE_CHARDEV,
> + TYPE_CHARDEV_MUX_BE)
> +
> +#define CHARDEV_IS_MUX_FE(chr) \
> object_dynamic_cast(OBJECT(chr), TYPE_CHARDEV_MUX_FE)
> +#define CHARDEV_IS_MUX_BE(chr) \
> + object_dynamic_cast(OBJECT(chr), TYPE_CHARDEV_MUX_BE)
>
> void mux_chr_send_all_event(Chardev *chr, QEMUChrEvent event);
>
> @@ -67,6 +100,9 @@ void mux_fe_chr_send_all_event(MuxFeChardev *d,
> QEMUChrEvent event);
> bool mux_fe_chr_attach_frontend(MuxFeChardev *d, CharBackend *b,
> unsigned int *tag, Error **errp);
> bool mux_fe_chr_detach_frontend(MuxFeChardev *d, unsigned int tag);
> +void mux_be_chr_send_all_event(MuxBeChardev *d, QEMUChrEvent event);
> +bool mux_be_chr_attach_frontend(MuxBeChardev *d, CharBackend *b, Error
> **errp);
> +void mux_be_chr_detach_frontend(MuxBeChardev *d);
>
> Object *get_chardevs_root(void);
>
> diff --git a/chardev/meson.build b/chardev/meson.build
> index 778444a00ca6..3a9f5565372b 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-fe.c',
> + 'char-mux-be.c',
> 'char-null.c',
> 'char-pipe.c',
> 'char-ringbuf.c',
> diff --git a/include/chardev/char.h b/include/chardev/char.h
> index 0bec974f9d73..c58c11c4eeaf 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_FE "chardev-mux"
> +#define TYPE_CHARDEV_MUX_BE "chardev-mux-be"
> #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..2ad77a7f6435 100644
> --- a/qapi/char.json
> +++ b/qapi/char.json
> @@ -332,6 +332,19 @@
> 'data': { 'chardev': 'str' },
> 'base': 'ChardevCommon' }
>
> +##
> +# @ChardevMuxBe:
> +#
> +# Configuration info for mux-be chardevs.
> +#
> +# @chardevs: List of chardev IDs, which should be added to this mux
> +#
> +# Since: 9.3
> +##
> +{ 'struct': 'ChardevMuxBe',
> + 'data': { 'chardevs': ['str'] },
> + 'base': 'ChardevCommon' }
> +
> ##
> # @ChardevStdio:
> #
> @@ -479,6 +492,8 @@
> #
> # @mux: (since 1.5)
> #
> +# @mux-be: (since 9.3)
> +#
> # @msmouse: emulated Microsoft serial mouse (since 1.5)
> #
> # @wctablet: emulated Wacom Penpartner serial tablet (since 2.9)
> @@ -521,6 +536,7 @@
> 'pty',
> 'null',
> 'mux',
> + 'mux-be',
> 'msmouse',
> 'wctablet',
> { 'name': 'braille', 'if': 'CONFIG_BRLAPI' },
> @@ -595,6 +611,16 @@
> { 'struct': 'ChardevMuxWrapper',
> 'data': { 'data': 'ChardevMux' } }
>
> +##
> +# @ChardevMuxBeWrapper:
> +#
> +# @data: Configuration info for mux-be chardevs
> +#
> +# Since: 9.3
> +##
> +{ 'struct': 'ChardevMuxBeWrapper',
> + 'data': { 'data': 'ChardevMuxBe' } }
> +
> ##
> # @ChardevStdioWrapper:
> #
> @@ -703,6 +729,7 @@
> 'pty': 'ChardevPtyWrapper',
> 'null': 'ChardevCommonWrapper',
> 'mux': 'ChardevMuxWrapper',
> + 'mux-be': 'ChardevMuxBeWrapper',
> 'msmouse': 'ChardevCommonWrapper',
> 'wctablet': 'ChardevCommonWrapper',
> 'braille': { 'type': 'ChardevCommonWrapper',
> --
> 2.34.1
>
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- Re: [PATCH v6 6/8] chardev/char-mux: implement backend chardev multiplexing,
Marc-André Lureau <=