From patchwork Thu Jan 23 08:53:21 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Roman Penyaev X-Patchwork-Id: 13948012 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.lore.kernel.org (Postfix) with ESMTPS id 70C97C0218B for ; Thu, 23 Jan 2025 08:54:45 +0000 (UTC) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1tasy2-0007bC-D7; Thu, 23 Jan 2025 03:53:38 -0500 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1tasxz-0007an-B0 for qemu-devel@nongnu.org; Thu, 23 Jan 2025 03:53:35 -0500 Received: from mail-ed1-x533.google.com ([2a00:1450:4864:20::533]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1tasxx-0000XQ-RZ for qemu-devel@nongnu.org; Thu, 23 Jan 2025 03:53:35 -0500 Received: by mail-ed1-x533.google.com with SMTP id 4fb4d7f45d1cf-5d3e6274015so1321963a12.0 for ; Thu, 23 Jan 2025 00:53:33 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1737622412; x=1738227212; darn=nongnu.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=OzRKVy23X6eiO9l+v9dWXa+pvd4BNlaJONdpbeQcmxA=; b=eH3o/01trXqWgWFbLT8U/o5mZJQW8Ym4nAoNNACNdgKKRMavG0KEulAA/M9Lu/tQ8N MERUwZ/EoQV1ILFQsHCF56m09J2bwpudoxgySFI4GZEk1N6heHT88stiodJBhWC5RbpR 52I9xfLPuQzopCxhIgFePGg1psjreMfq62WGEPfCKZHH/9L3u8JaxJUf0aHGo8b2gt8j BGdgHRVU8Fpr0aEPCpeYH6sDjx5ciyxYBhzOjoaFT+eXMqTnJXKHsNZhVLB0A5c8jbfu ygDDtsOIf/5G+trfVt235qULoseXg8sRvldL3hUDsi1bOnn1hEIrLrh+qVtEyXMj89Gd jFvg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1737622412; x=1738227212; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=OzRKVy23X6eiO9l+v9dWXa+pvd4BNlaJONdpbeQcmxA=; b=lmPoTMMYbu5PXgVBVEIlc593Sg/LUl2pqhCxJ4NR3j4xHqLvmqmS+mbI/gXJjoIIaT CMNvoojgDvwsVvREJDSfDdEpZ7gWaREgEznUhuShR1BbC/3V3zmQHOx9zuUq7i1db8OP sJv204vvo2vkLDAWWNdLofGABHM7dCWBWPBXvAXzzHKUcUcepHiefWwXDUi8J78k83sU lXdRE7pAqRfBKt0cln22IPqe7d4TihIBxXKwMB+wKRJR5lTr5mhnKmsUCT9K9xlav5sZ Hqd+By0Cutm0fDxXS7X4nMrTtC7SOailv3E1TgbIKhGbR+SeAku0OJ6FJmnjMgbSLQg5 ObEA== X-Forwarded-Encrypted: i=1; AJvYcCXxgulcYHgBHmnNQ7oG/OI5TOa66X9dki1wNDStyJM6QwMCC+STQ6q6yHp7GY/pUazOWQYnpkdqYbgA@nongnu.org X-Gm-Message-State: AOJu0YzutUTuOF6mVeQ0wF5ErDNTljPwHu6m4NL5B9OkbYFt7bQ3HMQq Y78YC5uPWS6GVP7MNRpF7MlcEfH4NBnv02T/sM80543PEwQOU1u6 X-Gm-Gg: ASbGncvcE6B4AA4CGSbwUN/fCjUmIWCcYvk4g1id05DDBpDP5xIWyUl318265lNXele umuXSoc3ltkAZqjt/HN2RFic0Y/u05MrmktudShZxa+ZntqWg78OKQvxwC39v0VWb/uy8fU2FfT YLu5ZR6TTEUeCrrhuRepe1gYep8ly2lMMaXwQkwOY3lFo35szoAMu32euAeUy6zThFLePcUElR1 6HxPIXmI3pKi4/C8CwO60wSo1fNNk9M8wLwf1HqL8cEU5ZeF0/6thASFLJkGLcTBwi9S5oImCV0 JhtjouGI1q/k0Eazh7C/QOSRaReR X-Google-Smtp-Source: AGHT+IF+1rfk7Rhk6Ahzkdt/YZjfObNNFHqN0F2VTkzYVIJnQ8rR9U36x04EbOxYgBG6EuWRs5Hhzw== X-Received: by 2002:a05:6402:3510:b0:5d6:37e5:792a with SMTP id 4fb4d7f45d1cf-5db7d2e86dbmr18842505a12.2.1737622411879; Thu, 23 Jan 2025 00:53:31 -0800 (PST) Received: from think.fkb.profitbricks.net ([2a02:8109:8384:1400:b763:14a0:8818:4012]) by smtp.gmail.com with ESMTPSA id 4fb4d7f45d1cf-5dbcfb8ff37sm4424676a12.72.2025.01.23.00.53.30 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 23 Jan 2025 00:53:30 -0800 (PST) From: Roman Penyaev To: Cc: Roman Penyaev , =?utf-8?q?Marc-Andr=C3=A9_Lureau?= , =?utf-8?q?Alex_Benn=C3=A9e?= , qemu-devel@nongnu.org Subject: [PATCH v9 1/4] chardev/char-pty: send CHR_EVENT_CLOSED on disconnect Date: Thu, 23 Jan 2025 09:53:21 +0100 Message-ID: <20250123085327.965501-2-r.peniaev@gmail.com> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20250123085327.965501-1-r.peniaev@gmail.com> References: <20250123085327.965501-1-r.peniaev@gmail.com> MIME-Version: 1.0 Received-SPF: pass client-ip=2a00:1450:4864:20::533; envelope-from=r.peniaev@gmail.com; helo=mail-ed1-x533.google.com X-Spam_score_int: -20 X-Spam_score: -2.1 X-Spam_bar: -- X-Spam_report: (-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, FREEMAIL_FROM=0.001, RCVD_IN_DNSWL_NONE=-0.0001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+qemu-devel=archiver.kernel.org@nongnu.org Sender: qemu-devel-bounces+qemu-devel=archiver.kernel.org@nongnu.org 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 Reviewed-by: "Marc-André Lureau" Reviewed-by: "Alex Bennée" Cc: qemu-devel@nongnu.org --- chardev/char-pty.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/chardev/char-pty.c b/chardev/char-pty.c index cbb21b76ae8d..6a2c1dc13a3f 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 +218,6 @@ static void char_pty_finalize(Object *obj) pty_chr_state(chr, 0); object_unref(OBJECT(s->ioc)); pty_chr_timer_cancel(s); - qemu_chr_be_event(chr, CHR_EVENT_CLOSED); } #if defined HAVE_PTY_H From patchwork Thu Jan 23 08:53:22 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Roman Penyaev X-Patchwork-Id: 13948014 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.lore.kernel.org (Postfix) with ESMTPS id 3E7A7C0218D for ; Thu, 23 Jan 2025 08:54:44 +0000 (UTC) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1tasy6-0007cU-4M; Thu, 23 Jan 2025 03:53:42 -0500 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1tasy3-0007bv-TK for qemu-devel@nongnu.org; Thu, 23 Jan 2025 03:53:39 -0500 Received: from mail-ed1-x52d.google.com ([2a00:1450:4864:20::52d]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1tasy0-0000Xf-9D for qemu-devel@nongnu.org; Thu, 23 Jan 2025 03:53:39 -0500 Received: by mail-ed1-x52d.google.com with SMTP id 4fb4d7f45d1cf-5dbfab8a2b0so1282392a12.3 for ; Thu, 23 Jan 2025 00:53:35 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1737622414; x=1738227214; darn=nongnu.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=qsnSD0SX7k1GQLAmtBDfuKtdC4HSylj4EOYxeynrGu4=; b=PFcKbiPDUiiR52berPd0bA9blgYClzeo12+8iDFQr6wCMbSQutOadX6bXditlQasTI 32qZEjEkkoxGogVBe1t5XG4F04wSvYj3i8loScKPXoyQPaMKA1fW1Qt50xf9vBMwGNNU DPfPMueelWV0ftKZVNHnziZWBx5SDFv9b1zyJOqn2Afkyqg1sT9POtTfho+iZ6Ni7xsY FrSA0Q0N0Fk0apf5lzDXjPQel8AM3ZIsXeU3D0ynoTRknKlsxna8K2Y+l8taDrLGfFgm 9OXZJWgO6QKmywCuzGzwSpxq0h/lE2lF7BhBVSCTH/vPgfGagXcHADJsBMgrZzKI6HvV gQNg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1737622414; x=1738227214; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=qsnSD0SX7k1GQLAmtBDfuKtdC4HSylj4EOYxeynrGu4=; b=OVFgl6/SupqP2sHRLUdcxDgYxMDqD4fMD40zbT7MryGZWXGX1TMmlMzGJZyVDlvkhS YvVIFaTwv0e5i2d7Tlnh03b01lYgoo7Y2XRrLtHGgudhzoMguBKEcRvyqtt7RmxHlLBE g4yrr0wslpEBlJdPLqsYlY+6fhTYdEY4JBlgnNR6/HYYxPGXoPmsR1mNtNm9fkqH3fJV 1dQnZWzx6yolFVkeXGxjrEC/6s8DSNC1HKH2Oi37WSyLz6kxSzhGTonJNSEr3jiHs/d2 ueEPaLk2vN2Y4KNMKYIs3/mSoicwRZwXMkR2HTBknoQvLeFEL3C3R66T4wjovwbLLH7E oO5Q== X-Forwarded-Encrypted: i=1; AJvYcCXlglcnyAgww0gSPJLgYZSHE0O4LW36RJBcukv9WjUhjbV5czDePdRRNmvYL/STugOkyevjbFx5VWsv@nongnu.org X-Gm-Message-State: AOJu0YyQPJfolC+qwhQQjWfZgqljgSQpcu77y00fkA74sYrj2v6hLHf9 fFXzm75DIXVztU6zp257pAqSmsLDWYAxBjK4maA4p9Iuqaoq/dEX X-Gm-Gg: ASbGncvF1i9ByJWUIZy2aTepzFzfIpDiFBWzZ8Ln6Y/10Itw6dn2Ab4iL+KeAM8o/co yE0B/8+tD0Tritp+6HZTAXvad6hNvZPjl8ibSGYEf8EYKFQnpfVgrexgvCREVw4yaAr4AaXluJT OSSUWLTbTIrmu6V3bw7ousBt4/BW7VPquvXNtiOy2op8K2l+tu9P/ti8FM4RN6QVCVINNJJ/R2k 88284fXtPuMv3m+m08dEHdocAGIFMNCdc//ulg68/wJE8dM4WYkcybWdY2+eEl4+IvoMvWBSgU1 SGE5TfEGeYYoUEAUBTGB6gThMNcE X-Google-Smtp-Source: AGHT+IF+DczfbyTI+1hRdetFXZq04EFsamIQZua7NOOs45Ltnh92BIf6uDDUMlxGlZ79gnt0IB1inw== X-Received: by 2002:a05:6402:34c7:b0:5d8:253:b7df with SMTP id 4fb4d7f45d1cf-5db7db086cfmr23306747a12.27.1737622413736; Thu, 23 Jan 2025 00:53:33 -0800 (PST) Received: from think.fkb.profitbricks.net ([2a02:8109:8384:1400:b763:14a0:8818:4012]) by smtp.gmail.com with ESMTPSA id 4fb4d7f45d1cf-5dbcfb8ff37sm4424676a12.72.2025.01.23.00.53.32 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 23 Jan 2025 00:53:32 -0800 (PST) From: Roman Penyaev To: Cc: Roman Penyaev , =?utf-8?q?Marc-Andr=C3=A9_Lureau?= , qemu-devel@nongnu.org Subject: [PATCH v9 2/4] chardev/char-hub: implement backend chardev aggregator Date: Thu, 23 Jan 2025 09:53:22 +0100 Message-ID: <20250123085327.965501-3-r.peniaev@gmail.com> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20250123085327.965501-1-r.peniaev@gmail.com> References: <20250123085327.965501-1-r.peniaev@gmail.com> MIME-Version: 1.0 Received-SPF: pass client-ip=2a00:1450:4864:20::52d; envelope-from=r.peniaev@gmail.com; helo=mail-ed1-x52d.google.com X-Spam_score_int: -20 X-Spam_score: -2.1 X-Spam_bar: -- X-Spam_report: (-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, FREEMAIL_FROM=0.001, RCVD_IN_DNSWL_NONE=-0.0001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+qemu-devel=archiver.kernel.org@nongnu.org Sender: qemu-devel-bounces+qemu-devel=archiver.kernel.org@nongnu.org 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 syntax may help to smoothly switch to modern parsing in the future. Signed-off-by: Roman Penyaev Reviewed-by: "Marc-André Lureau" Cc: qemu-devel@nongnu.org Acked-by: Markus Armbruster --- chardev/char-hub.c | 301 +++++++++++++++++++++++++++++++++++++ chardev/char.c | 23 ++- chardev/chardev-internal.h | 51 ++++++- chardev/meson.build | 1 + include/chardev/char.h | 1 + qapi/char.json | 27 ++++ 6 files changed, 401 insertions(+), 3 deletions(-) create mode 100644 chardev/char-hub.c diff --git a/chardev/char-hub.c b/chardev/char-hub.c new file mode 100644 index 000000000000..3a4aae328972 --- /dev/null +++ b/chardev/char-hub.c @@ -0,0 +1,301 @@ +/* + * QEMU Character Hub Device + * + * Author: Roman Penyaev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include "qapi/error.h" +#include "qemu/option.h" +#include "chardev/char.h" +#include "chardev-internal.h" + +/* + * Character hub device aggregates input from multiple backend devices + * and forwards it to a single frontend device. Additionally, hub + * device takes the output from the frontend device and sends it back + * to all the connected backend devices. + */ + +/* + * Write to all backends. Different backend devices accept data with + * various rate, so it is quite possible that one device returns less, + * then others. In this case we return minimum to the caller, + * expecting caller will repeat operation soon. When repeat happens + * send to the devices which consume data faster must be avoided + * for obvious reasons not to send data, which was already sent. + * Called with chr_write_lock held. + */ +static int hub_chr_write(Chardev *chr, const uint8_t *buf, int len) +{ + HubChardev *d = HUB_CHARDEV(chr); + int r, i, ret = len; + unsigned int written; + + /* Invalidate index on every write */ + d->be_eagain_ind = -1; + + for (i = 0; i < d->be_cnt; i++) { + if (!d->backends[i].be.chr->be_open) { + /* Skip closed backend */ + continue; + } + written = d->be_written[i] - d->be_min_written; + if (written) { + /* Written in the previous call so take into account */ + ret = MIN(written, ret); + continue; + } + r = qemu_chr_fe_write(&d->backends[i].be, buf, len); + if (r < 0) { + if (errno == EAGAIN) { + /* Set index and expect to be called soon on watch wake up */ + d->be_eagain_ind = i; + } + return r; + } + d->be_written[i] += r; + ret = MIN(r, ret); + } + d->be_min_written += ret; + + + return ret; +} + +static int hub_chr_can_read(void *opaque) +{ + HubCharBackend *backend = opaque; + CharBackend *fe = backend->hub->parent.be; + + if (fe && fe->chr_can_read) { + return fe->chr_can_read(fe->opaque); + } + + return 0; +} + +static void hub_chr_read(void *opaque, const uint8_t *buf, int size) +{ + HubCharBackend *backend = opaque; + CharBackend *fe = backend->hub->parent.be; + + if (fe && fe->chr_read) { + fe->chr_read(fe->opaque, buf, size); + } +} + +static void hub_chr_event(void *opaque, QEMUChrEvent event) +{ + HubCharBackend *backend = opaque; + HubChardev *d = backend->hub; + CharBackend *fe = d->parent.be; + + if (event == CHR_EVENT_OPENED) { + /* + * Catch up with what was already written while this backend + * was closed + */ + d->be_written[backend->be_ind] = d->be_min_written; + + if (d->be_event_opened_cnt++) { + /* Ignore subsequent open events from other backends */ + return; + } + } else if (event == CHR_EVENT_CLOSED) { + if (!d->be_event_opened_cnt) { + /* Don't go below zero. Probably assert is better */ + return; + } + if (--d->be_event_opened_cnt) { + /* Serve only the last one close event */ + return; + } + } + + if (fe && fe->chr_event) { + fe->chr_event(fe->opaque, event); + } +} + +static GSource *hub_chr_add_watch(Chardev *s, GIOCondition cond) +{ + HubChardev *d = HUB_CHARDEV(s); + Chardev *chr; + ChardevClass *cc; + + if (d->be_eagain_ind == -1) { + return NULL; + } + + assert(d->be_eagain_ind < d->be_cnt); + chr = qemu_chr_fe_get_driver(&d->backends[d->be_eagain_ind].be); + cc = CHARDEV_GET_CLASS(chr); + if (!cc->chr_add_watch) { + return NULL; + } + + return cc->chr_add_watch(chr, cond); +} + +static bool hub_chr_attach_chardev(HubChardev *d, Chardev *chr, + Error **errp) +{ + bool ret; + + if (d->be_cnt >= MAX_HUB) { + error_setg(errp, "hub: too many uses of chardevs '%s'" + " (maximum is " stringify(MAX_HUB) ")", + d->parent.label); + return false; + } + ret = qemu_chr_fe_init(&d->backends[d->be_cnt].be, chr, errp); + if (ret) { + d->backends[d->be_cnt].hub = d; + d->backends[d->be_cnt].be_ind = d->be_cnt; + d->be_cnt += 1; + } + + return ret; +} + +static void char_hub_finalize(Object *obj) +{ + HubChardev *d = HUB_CHARDEV(obj); + int i; + + for (i = 0; i < d->be_cnt; i++) { + qemu_chr_fe_deinit(&d->backends[i].be, false); + } +} + +static void hub_chr_update_read_handlers(Chardev *chr) +{ + HubChardev *d = HUB_CHARDEV(chr); + int i; + + for (i = 0; i < d->be_cnt; i++) { + qemu_chr_fe_set_handlers_full(&d->backends[i].be, + hub_chr_can_read, + hub_chr_read, + hub_chr_event, + NULL, + &d->backends[i], + chr->gcontext, true, false); + } +} + +static void qemu_chr_open_hub(Chardev *chr, + ChardevBackend *backend, + bool *be_opened, + Error **errp) +{ + ChardevHub *hub = backend->u.hub.data; + HubChardev *d = HUB_CHARDEV(chr); + strList *list = hub->chardevs; + + d->be_eagain_ind = -1; + + if (list == NULL) { + error_setg(errp, "hub: 'chardevs' list is not defined"); + return; + } + + while (list) { + Chardev *s; + + s = qemu_chr_find(list->value); + if (s == NULL) { + error_setg(errp, "hub: chardev can't be found by id '%s'", + list->value); + return; + } + if (CHARDEV_IS_HUB(s) || CHARDEV_IS_MUX(s)) { + error_setg(errp, "hub: multiplexers and hub devices can't be " + "stacked, check chardev '%s', chardev should not " + "be a hub device or have 'mux=on' enabled", + list->value); + return; + } + if (!hub_chr_attach_chardev(d, s, errp)) { + return; + } + list = list->next; + } + + /* Closed until an explicit event from backend */ + *be_opened = false; +} + +static void qemu_chr_parse_hub(QemuOpts *opts, ChardevBackend *backend, + Error **errp) +{ + ChardevHub *hub; + strList **tail; + int i; + + backend->type = CHARDEV_BACKEND_KIND_HUB; + hub = backend->u.hub.data = g_new0(ChardevHub, 1); + qemu_chr_parse_common(opts, qapi_ChardevHub_base(hub)); + + tail = &hub->chardevs; + + for (i = 0; i < MAX_HUB; i++) { + char optbuf[16]; + const char *dev; + + snprintf(optbuf, sizeof(optbuf), "chardevs.%u", i); + dev = qemu_opt_get(opts, optbuf); + if (!dev) { + break; + } + + QAPI_LIST_APPEND(tail, g_strdup(dev)); + } +} + +static void char_hub_class_init(ObjectClass *oc, void *data) +{ + ChardevClass *cc = CHARDEV_CLASS(oc); + + cc->parse = qemu_chr_parse_hub; + cc->open = qemu_chr_open_hub; + cc->chr_write = hub_chr_write; + cc->chr_add_watch = hub_chr_add_watch; + /* We handle events from backends only */ + cc->chr_be_event = NULL; + cc->chr_update_read_handler = hub_chr_update_read_handlers; +} + +static const TypeInfo char_hub_type_info = { + .name = TYPE_CHARDEV_HUB, + .parent = TYPE_CHARDEV, + .class_init = char_hub_class_init, + .instance_size = sizeof(HubChardev), + .instance_finalize = char_hub_finalize, +}; + +static void register_types(void) +{ + type_register_static(&char_hub_type_info); +} + +type_init(register_types); diff --git a/chardev/char.c b/chardev/char.c index 7705da5ad02b..5a9e9762adca 100644 --- a/chardev/char.c +++ b/chardev/char.c @@ -943,7 +943,26 @@ QemuOptsList qemu_chardev_opts = { },{ .name = "chardev", .type = QEMU_OPT_STRING, + }, + /* + * Multiplexer options. Follows QAPI array syntax. + * See MAX_HUB macro to obtain array capacity. + */ + { + .name = "chardevs.0", + .type = QEMU_OPT_STRING, + },{ + .name = "chardevs.1", + .type = QEMU_OPT_STRING, },{ + .name = "chardevs.2", + .type = QEMU_OPT_STRING, + },{ + .name = "chardevs.3", + .type = QEMU_OPT_STRING, + }, + + { .name = "append", .type = QEMU_OPT_BOOL, },{ @@ -1106,8 +1125,8 @@ ChardevReturn *qmp_chardev_change(const char *id, ChardevBackend *backend, return NULL; } - if (CHARDEV_IS_MUX(chr)) { - error_setg(errp, "Mux device hotswap not supported yet"); + if (CHARDEV_IS_MUX(chr) || CHARDEV_IS_HUB(chr)) { + error_setg(errp, "For mux or hub device hotswap is not supported yet"); return NULL; } diff --git a/chardev/chardev-internal.h b/chardev/chardev-internal.h index 853807f3cb88..9752dd75f714 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,57 @@ struct MuxChardev { int64_t timestamps_start; }; typedef struct MuxChardev MuxChardev; +typedef struct HubChardev HubChardev; +typedef struct HubCharBackend HubCharBackend; + +/* + * Back-pointer on a hub, actual backend and its index in + * `hub->backends` array + */ +struct HubCharBackend { + HubChardev *hub; + CharBackend be; + unsigned int be_ind; +}; + +struct HubChardev { + Chardev parent; + /* Linked backends */ + HubCharBackend backends[MAX_HUB]; + /* + * Number of backends attached to this hub. Once attached, a + * backend can't be detached, so the counter is only increasing. + * To safely remove a backend, hub has to be removed first. + */ + unsigned int be_cnt; + /* + * Number of CHR_EVEN_OPENED events from all backends. Needed to + * send CHR_EVEN_CLOSED only when counter goes to zero. + */ + unsigned int be_event_opened_cnt; + /* + * Counters of written bytes from a single frontend device + * to multiple backend devices. + */ + unsigned int be_written[MAX_HUB]; + unsigned int be_min_written; + /* + * Index of a backend device which got EAGAIN on last write, + * -1 is invalid index. + */ + int be_eagain_ind; +}; +typedef struct HubChardev HubChardev; DECLARE_INSTANCE_CHECKER(MuxChardev, MUX_CHARDEV, TYPE_CHARDEV_MUX) -#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); 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', From patchwork Thu Jan 23 08:53:23 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Roman Penyaev X-Patchwork-Id: 13948011 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.lore.kernel.org (Postfix) with ESMTPS id 3987EC02182 for ; Thu, 23 Jan 2025 08:54:43 +0000 (UTC) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1tasy3-0007bu-Vn; Thu, 23 Jan 2025 03:53:40 -0500 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1tasy2-0007bf-TP for qemu-devel@nongnu.org; Thu, 23 Jan 2025 03:53:38 -0500 Received: from mail-ed1-x530.google.com ([2a00:1450:4864:20::530]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1tasy0-0000Xm-Br for qemu-devel@nongnu.org; Thu, 23 Jan 2025 03:53:38 -0500 Received: by mail-ed1-x530.google.com with SMTP id 4fb4d7f45d1cf-5dbe8e62407so1458476a12.2 for ; Thu, 23 Jan 2025 00:53:36 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1737622415; x=1738227215; darn=nongnu.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=evrpWoPDrk0VSd3yZnzmN4ctLMqHpfEvnKKxl3/cp2I=; b=GYu5l089hajPSiLMswA8Rj+/4jecRCTQZfcXDnVHkFuvelAtcETmvTpe5yJQiklv7J Q2ECgiEpWgVXvkRJ3P67v5ZJhwlnGiS4stMmh80nCV6oyd6V/1Uv4Yj8Bd5/JmhclKsN 9Tv4O50ENfILg3WIQQrqFVpNB+qBy+BEsYr5UVkjgFQscNIXYieoxpzSe2evWGzAVedp oDZ20gE2y9sDVUKRr7ajDkw0dIvZje11P9M42uEnW3RN6IK42Co6z2KfsDYwmlMnAED4 ++cfHVXNVVRS2eYVcP9GPuCms/0h9NuqldrtOXfuGavvdA5tAOmm13S0p9W9w1VRfc8O sstA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1737622415; x=1738227215; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=evrpWoPDrk0VSd3yZnzmN4ctLMqHpfEvnKKxl3/cp2I=; b=Dh+0EVY7oWT4iXLz4Haca5QhMA8sApVqdyEivnWr8LAHntBBdSLByNdvkgIBdbwfS3 p/SbBarilhyqZsjcWs69+QtWn6otghwPgQbbKogaLOk74KsUGoJVtn0yiP4yhqBZDb88 rufbupriIv4nK+hisa7y6GUVQsLlazXZXH4jRovcAU2+yV/sWbBgxsOBXrZ2DGdUtb9j K2ZQpqRMEbR/N7vQtMJx4i1np2yJacC7b5JTr79/JOKEsC0RqTzF6Q/2Cg5hbqk66u0w wWzwY3SsBnf08Bma27REqI0nF6RcdmOxHdQg0C+VKD0hur6rlPjpAzbjbZ3vSZoJtSCB Dwag== X-Forwarded-Encrypted: i=1; AJvYcCWIfcMGm1KW83S1HtPnnfTaRJDPtdcx4atd51R4dhPSt7BqdPQ/MsVKI4O8VHFGacv3feQxoOqhMwYL@nongnu.org X-Gm-Message-State: AOJu0Yzjf9Env5sjQlI2bEgoIkT11Z92+1bqQ0Z4VNVKK7P6AyKscURr G7RQM2V72oulRl3FXl61axz5J8yYBE5NyUgl1qtRwdkVJ++dH8bo X-Gm-Gg: ASbGncvOk0OgCiOzJrRip4T8ew7EIb9zngl5AULXrOyKsEFgx6BRIbTjkclHiiejYmi xuJViAmYbiUvL4XehwcD6cBcxVLhWMcsykpmrnY9hwraoModC8kOnDR3GgHGSfTFZRZ0cPd2yX/ ZpXWdnmk+16TO2vEi0w2fxugFTT9UHtgXuDHMbGe++iU/j6xkSU3rphG41tW+Rt8njBHv2ADRWh CarWQ34T3CkIF5EKVRiuHgZVO2QSxju8VpL05+RqpeHD6IhEAOhCOPLY/omSl/65n/NgK6iRcj3 VjkYfof4IOMZiC43DHIiDZksgmMnhpUv2n4hsc8= X-Google-Smtp-Source: AGHT+IGb1xDAoeES2Nhjl/F0J/l5fQEvTVOoC5fh9a/ITJda6rSZbn+KoSjW5M6pyex3OFu6+tLPyw== X-Received: by 2002:a05:6402:4304:b0:5d0:aa2d:6eee with SMTP id 4fb4d7f45d1cf-5db7db06eb9mr22493920a12.26.1737622414520; Thu, 23 Jan 2025 00:53:34 -0800 (PST) Received: from think.fkb.profitbricks.net ([2a02:8109:8384:1400:b763:14a0:8818:4012]) by smtp.gmail.com with ESMTPSA id 4fb4d7f45d1cf-5dbcfb8ff37sm4424676a12.72.2025.01.23.00.53.33 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 23 Jan 2025 00:53:34 -0800 (PST) From: Roman Penyaev To: Cc: Roman Penyaev , =?utf-8?q?Marc-Andr=C3=A9_Lureau?= , qemu-devel@nongnu.org Subject: [PATCH v9 3/4] tests/unit/test-char: add unit tests for hub chardev backend Date: Thu, 23 Jan 2025 09:53:23 +0100 Message-ID: <20250123085327.965501-4-r.peniaev@gmail.com> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20250123085327.965501-1-r.peniaev@gmail.com> References: <20250123085327.965501-1-r.peniaev@gmail.com> MIME-Version: 1.0 Received-SPF: pass client-ip=2a00:1450:4864:20::530; envelope-from=r.peniaev@gmail.com; helo=mail-ed1-x530.google.com X-Spam_score_int: -20 X-Spam_score: -2.1 X-Spam_bar: -- X-Spam_report: (-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, FREEMAIL_FROM=0.001, RCVD_IN_DNSWL_NONE=-0.0001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+qemu-devel=archiver.kernel.org@nongnu.org Sender: qemu-devel-bounces+qemu-devel=archiver.kernel.org@nongnu.org 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 Reviewed-by: "Marc-André Lureau" Cc: qemu-devel@nongnu.org --- tests/unit/test-char.c | 398 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 398 insertions(+) diff --git a/tests/unit/test-char.c b/tests/unit/test-char.c index 98a60d86b143..85b350a9b7c1 100644 --- a/tests/unit/test-char.c +++ b/tests/unit/test-char.c @@ -359,6 +359,403 @@ static void char_mux_test(void) qmp_chardev_remove("mux-label", &error_abort); } +static void char_hub_test(void) +{ + QemuOpts *opts; + Chardev *hub, *chr1, *chr2, *base; + char *data; + FeHandler h = { 0, false, 0, false, }; + Error *error = NULL; + CharBackend chr_be; + int ret, i; + +#define RB_SIZE 128 + + /* + * Create invalid hub + * 1. Create hub without a 'chardevs.N' defined (expect error) + */ + opts = qemu_opts_create(qemu_find_opts("chardev"), "hub0", + 1, &error_abort); + qemu_opt_set(opts, "backend", "hub", &error_abort); + hub = qemu_chr_new_from_opts(opts, NULL, &error); + g_assert_cmpstr(error_get_pretty(error), ==, + "hub: 'chardevs' list is not defined"); + error_free(error); + error = NULL; + qemu_opts_del(opts); + + /* + * Create invalid hub + * 1. Create chardev with embedded mux: 'mux=on' + * 2. Create hub which refers mux + * 3. Create hub which refers chardev already attached + * to the mux (already in use, expect error) + */ + opts = qemu_opts_create(qemu_find_opts("chardev"), "chr0", + 1, &error_abort); + qemu_opt_set(opts, "mux", "on", &error_abort); + qemu_opt_set(opts, "backend", "ringbuf", &error_abort); + qemu_opt_set(opts, "size", stringify(RB_SIZE), &error_abort); + base = qemu_chr_new_from_opts(opts, NULL, &error_abort); + g_assert_nonnull(base); + qemu_opts_del(opts); + + opts = qemu_opts_create(qemu_find_opts("chardev"), "hub0", + 1, &error_abort); + qemu_opt_set(opts, "backend", "hub", &error_abort); + qemu_opt_set(opts, "chardevs.0", "chr0", &error_abort); + hub = qemu_chr_new_from_opts(opts, NULL, &error); + g_assert_cmpstr(error_get_pretty(error), ==, + "hub: multiplexers and hub devices can't be " + "stacked, check chardev 'chr0', chardev should " + "not be a hub device or have 'mux=on' enabled"); + error_free(error); + error = NULL; + qemu_opts_del(opts); + + opts = qemu_opts_create(qemu_find_opts("chardev"), "hub0", + 1, &error_abort); + qemu_opt_set(opts, "backend", "hub", &error_abort); + qemu_opt_set(opts, "chardevs.0", "chr0-base", &error_abort); + hub = qemu_chr_new_from_opts(opts, NULL, &error); + g_assert_cmpstr(error_get_pretty(error), ==, + "chardev 'chr0-base' is already in use"); + error_free(error); + error = NULL; + qemu_opts_del(opts); + + /* Finalize chr0 */ + qmp_chardev_remove("chr0", &error_abort); + + /* + * Create invalid hub with more than maximum allowed backends + * 1. Create more than maximum allowed 'chardevs.%d' options for + * hub (expect error) + */ + opts = qemu_opts_create(qemu_find_opts("chardev"), "hub0", + 1, &error_abort); + for (i = 0; i < 10; i++) { + char key[32], val[32]; + + snprintf(key, sizeof(key), "chardevs.%d", i); + snprintf(val, sizeof(val), "chr%d", i); + qemu_opt_set(opts, key, val, &error); + if (error) { + char buf[64]; + + snprintf(buf, sizeof(buf), "Invalid parameter 'chardevs.%d'", i); + g_assert_cmpstr(error_get_pretty(error), ==, buf); + error_free(error); + break; + } + } + g_assert_nonnull(error); + error = NULL; + qemu_opts_del(opts); + + /* + * Create hub with 2 backend chardevs and 1 frontend and perform + * data aggregation + * 1. Create 2 ringbuf backend chardevs + * 2. Create 1 frontend + * 3. Create hub which refers 2 backend chardevs + * 4. Attach hub to a frontend + * 5. Attach hub to a frontend second time (expect error) + * 6. Perform data aggregation + * 7. Remove chr1 ("chr1 is busy", expect error) + * 8. Remove hub0 ("hub0 is busy", expect error); + * 9. Finilize frontend, hub and backend chardevs in correct order + */ + + /* Create first chardev */ + opts = qemu_opts_create(qemu_find_opts("chardev"), "chr1", + 1, &error_abort); + qemu_opt_set(opts, "backend", "ringbuf", &error_abort); + qemu_opt_set(opts, "size", stringify(RB_SIZE), &error_abort); + chr1 = qemu_chr_new_from_opts(opts, NULL, &error_abort); + g_assert_nonnull(chr1); + qemu_opts_del(opts); + + /* Create second chardev */ + opts = qemu_opts_create(qemu_find_opts("chardev"), "chr2", + 1, &error_abort); + qemu_opt_set(opts, "backend", "ringbuf", &error_abort); + qemu_opt_set(opts, "size", stringify(RB_SIZE), &error_abort); + chr2 = qemu_chr_new_from_opts(opts, NULL, &error_abort); + g_assert_nonnull(chr2); + qemu_opts_del(opts); + + /* Create hub0 and refer 2 backend chardevs */ + opts = qemu_opts_create(qemu_find_opts("chardev"), "hub0", + 1, &error_abort); + qemu_opt_set(opts, "backend", "hub", &error_abort); + qemu_opt_set(opts, "chardevs.0", "chr1", &error_abort); + qemu_opt_set(opts, "chardevs.1", "chr2", &error_abort); + hub = qemu_chr_new_from_opts(opts, NULL, &error_abort); + g_assert_nonnull(hub); + qemu_opts_del(opts); + + /* Attach hub to a frontend */ + qemu_chr_fe_init(&chr_be, hub, &error_abort); + qemu_chr_fe_set_handlers(&chr_be, + fe_can_read, + fe_read, + fe_event, + NULL, + &h, + NULL, true); + + /* Fails second time */ + qemu_chr_fe_init(&chr_be, hub, &error); + g_assert_cmpstr(error_get_pretty(error), ==, "chardev 'hub0' is already in use"); + error_free(error); + error = NULL; + + /* Write to backend, chr1 */ + base = qemu_chr_find("chr1"); + g_assert_cmpint(qemu_chr_be_can_write(base), !=, 0); + + qemu_chr_be_write(base, (void *)"hello", 6); + g_assert_cmpint(h.read_count, ==, 6); + g_assert_cmpstr(h.read_buf, ==, "hello"); + h.read_count = 0; + + /* Write to backend, chr2 */ + base = qemu_chr_find("chr2"); + g_assert_cmpint(qemu_chr_be_can_write(base), !=, 0); + + qemu_chr_be_write(base, (void *)"olleh", 6); + g_assert_cmpint(h.read_count, ==, 6); + g_assert_cmpstr(h.read_buf, ==, "olleh"); + h.read_count = 0; + + /* Write to frontend, chr_be */ + ret = qemu_chr_fe_write(&chr_be, (void *)"heyhey", 6); + g_assert_cmpint(ret, ==, 6); + + data = qmp_ringbuf_read("chr1", RB_SIZE, false, 0, &error_abort); + g_assert_cmpint(strlen(data), ==, 6); + g_assert_cmpstr(data, ==, "heyhey"); + g_free(data); + + data = qmp_ringbuf_read("chr2", RB_SIZE, false, 0, &error_abort); + g_assert_cmpint(strlen(data), ==, 6); + g_assert_cmpstr(data, ==, "heyhey"); + g_free(data); + + /* Can't be removed, depends on hub0 */ + qmp_chardev_remove("chr1", &error); + g_assert_cmpstr(error_get_pretty(error), ==, "Chardev 'chr1' is busy"); + error_free(error); + error = NULL; + + /* Can't be removed, depends on frontend chr_be */ + qmp_chardev_remove("hub0", &error); + g_assert_cmpstr(error_get_pretty(error), ==, "Chardev 'hub0' is busy"); + error_free(error); + error = NULL; + + /* Finalize frontend */ + qemu_chr_fe_deinit(&chr_be, false); + + /* Finalize hub0 */ + qmp_chardev_remove("hub0", &error_abort); + + /* Finalize backend chardevs */ + qmp_chardev_remove("chr1", &error_abort); + qmp_chardev_remove("chr2", &error_abort); + +#ifndef _WIN32 + /* + * Create 3 backend chardevs to simulate EAGAIN and watcher. + * Mainly copied from char_pipe_test(). + * 1. Create 2 ringbuf backend chardevs + * 2. Create 1 pipe backend chardev + * 3. Create 1 frontend + * 4. Create hub which refers 2 backend chardevs + * 5. Attach hub to a frontend + * 6. Perform data aggregation and check watcher + * 7. Finilize frontend, hub and backend chardevs in correct order + */ + { + gchar *tmp_path = g_dir_make_tmp("qemu-test-char.XXXXXX", NULL); + gchar *in, *out, *pipe = g_build_filename(tmp_path, "pipe", NULL); + Chardev *chr3; + int fd, len; + char buf[128]; + + in = g_strdup_printf("%s.in", pipe); + if (mkfifo(in, 0600) < 0) { + abort(); + } + out = g_strdup_printf("%s.out", pipe); + if (mkfifo(out, 0600) < 0) { + abort(); + } + + /* Create first chardev */ + opts = qemu_opts_create(qemu_find_opts("chardev"), "chr1", + 1, &error_abort); + qemu_opt_set(opts, "backend", "ringbuf", &error_abort); + qemu_opt_set(opts, "size", stringify(RB_SIZE), &error_abort); + chr1 = qemu_chr_new_from_opts(opts, NULL, &error_abort); + g_assert_nonnull(chr1); + qemu_opts_del(opts); + + /* Create second chardev */ + opts = qemu_opts_create(qemu_find_opts("chardev"), "chr2", + 1, &error_abort); + qemu_opt_set(opts, "backend", "ringbuf", &error_abort); + qemu_opt_set(opts, "size", stringify(RB_SIZE), &error_abort); + chr2 = qemu_chr_new_from_opts(opts, NULL, &error_abort); + g_assert_nonnull(chr2); + qemu_opts_del(opts); + + /* Create third chardev */ + opts = qemu_opts_create(qemu_find_opts("chardev"), "chr3", + 1, &error_abort); + qemu_opt_set(opts, "backend", "pipe", &error_abort); + qemu_opt_set(opts, "path", pipe, &error_abort); + chr3 = qemu_chr_new_from_opts(opts, NULL, &error_abort); + g_assert_nonnull(chr3); + + /* Create hub0 and refer 3 backend chardevs */ + opts = qemu_opts_create(qemu_find_opts("chardev"), "hub0", + 1, &error_abort); + qemu_opt_set(opts, "backend", "hub", &error_abort); + qemu_opt_set(opts, "chardevs.0", "chr1", &error_abort); + qemu_opt_set(opts, "chardevs.1", "chr2", &error_abort); + qemu_opt_set(opts, "chardevs.2", "chr3", &error_abort); + hub = qemu_chr_new_from_opts(opts, NULL, &error_abort); + g_assert_nonnull(hub); + qemu_opts_del(opts); + + /* Attach hub to a frontend */ + qemu_chr_fe_init(&chr_be, hub, &error_abort); + qemu_chr_fe_set_handlers(&chr_be, + fe_can_read, + fe_read, + fe_event, + NULL, + &h, + NULL, true); + + /* Write to frontend, chr_be */ + ret = qemu_chr_fe_write(&chr_be, (void *)"thisis", 6); + g_assert_cmpint(ret, ==, 6); + + data = qmp_ringbuf_read("chr1", RB_SIZE, false, 0, &error_abort); + g_assert_cmpint(strlen(data), ==, 6); + g_assert_cmpstr(data, ==, "thisis"); + g_free(data); + + data = qmp_ringbuf_read("chr2", RB_SIZE, false, 0, &error_abort); + g_assert_cmpint(strlen(data), ==, 6); + g_assert_cmpstr(data, ==, "thisis"); + g_free(data); + + fd = open(out, O_RDWR); + ret = read(fd, buf, sizeof(buf)); + g_assert_cmpint(ret, ==, 6); + buf[ret] = 0; + g_assert_cmpstr(buf, ==, "thisis"); + close(fd); + + /* Add watch. 0 indicates no watches if nothing to wait for */ + ret = qemu_chr_fe_add_watch(&chr_be, G_IO_OUT | G_IO_HUP, + NULL, NULL); + g_assert_cmpint(ret, ==, 0); + + /* + * Write to frontend, chr_be, until EAGAIN. Make sure length is + * power of two to fit nicely the whole pipe buffer. + */ + len = 0; + while ((ret = qemu_chr_fe_write(&chr_be, (void *)"thisisit", 8)) + != -1) { + len += ret; + } + g_assert_cmpint(errno, ==, EAGAIN); + + /* Further all writes should cause EAGAIN */ + ret = qemu_chr_fe_write(&chr_be, (void *)"b", 1); + g_assert_cmpint(ret, ==, -1); + g_assert_cmpint(errno, ==, EAGAIN); + + /* + * Add watch. Non 0 indicates we have a blocked chardev, which + * can wakes us up when write is possible. + */ + ret = qemu_chr_fe_add_watch(&chr_be, G_IO_OUT | G_IO_HUP, + NULL, NULL); + g_assert_cmpint(ret, !=, 0); + g_source_remove(ret); + + /* Drain pipe and ring buffers */ + fd = open(out, O_RDWR); + while ((ret = read(fd, buf, MIN(sizeof(buf), len))) != -1 && len > 0) { + len -= ret; + } + close(fd); + + data = qmp_ringbuf_read("chr1", RB_SIZE, false, 0, &error_abort); + g_assert_cmpint(strlen(data), ==, 128); + g_free(data); + + data = qmp_ringbuf_read("chr2", RB_SIZE, false, 0, &error_abort); + g_assert_cmpint(strlen(data), ==, 128); + g_free(data); + + /* + * Now we are good to go, first repeat "lost" sequence, which + * was already consumed and drained by the ring buffers, but + * pipe have not recieved that yet. + */ + ret = qemu_chr_fe_write(&chr_be, (void *)"thisisit", 8); + g_assert_cmpint(ret, ==, 8); + + ret = qemu_chr_fe_write(&chr_be, (void *)"streamisrestored", 16); + g_assert_cmpint(ret, ==, 16); + + data = qmp_ringbuf_read("chr1", RB_SIZE, false, 0, &error_abort); + g_assert_cmpint(strlen(data), ==, 16); + /* Only last 16 bytes, see big comment above */ + g_assert_cmpstr(data, ==, "streamisrestored"); + g_free(data); + + data = qmp_ringbuf_read("chr2", RB_SIZE, false, 0, &error_abort); + g_assert_cmpint(strlen(data), ==, 16); + /* Only last 16 bytes, see big comment above */ + g_assert_cmpstr(data, ==, "streamisrestored"); + g_free(data); + + fd = open(out, O_RDWR); + ret = read(fd, buf, sizeof(buf)); + g_assert_cmpint(ret, ==, 24); + buf[ret] = 0; + /* Both 8 and 16 bytes */ + g_assert_cmpstr(buf, ==, "thisisitstreamisrestored"); + close(fd); + + g_free(in); + g_free(out); + g_free(tmp_path); + g_free(pipe); + + /* Finalize frontend */ + qemu_chr_fe_deinit(&chr_be, false); + + /* Finalize hub0 */ + qmp_chardev_remove("hub0", &error_abort); + + /* Finalize backend chardevs */ + qmp_chardev_remove("chr1", &error_abort); + qmp_chardev_remove("chr2", &error_abort); + qmp_chardev_remove("chr3", &error_abort); + } +#endif +} static void websock_server_read(void *opaque, const uint8_t *buf, int size) { @@ -1507,6 +1904,7 @@ int main(int argc, char **argv) g_test_add_func("/char/invalid", char_invalid_test); g_test_add_func("/char/ringbuf", char_ringbuf_test); g_test_add_func("/char/mux", char_mux_test); + g_test_add_func("/char/hub", char_hub_test); #ifdef _WIN32 g_test_add_func("/char/console/subprocess", char_console_test_subprocess); g_test_add_func("/char/console", char_console_test); From patchwork Thu Jan 23 08:53:24 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Roman Penyaev X-Patchwork-Id: 13948013 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.lore.kernel.org (Postfix) with ESMTPS id 9C6ADC0218E for ; Thu, 23 Jan 2025 08:54:44 +0000 (UTC) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1tasy6-0007cT-1S; Thu, 23 Jan 2025 03:53:42 -0500 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1tasy4-0007c3-8R for qemu-devel@nongnu.org; Thu, 23 Jan 2025 03:53:40 -0500 Received: from mail-ed1-x530.google.com ([2a00:1450:4864:20::530]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1tasy2-0000Y2-B2 for qemu-devel@nongnu.org; Thu, 23 Jan 2025 03:53:40 -0500 Received: by mail-ed1-x530.google.com with SMTP id 4fb4d7f45d1cf-5d96944401dso1211013a12.0 for ; Thu, 23 Jan 2025 00:53:37 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1737622417; x=1738227217; darn=nongnu.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=xJoEBFx5uK1qVNB0I6L/KByUsPzfPdocJz4RTcufOaM=; b=DzDZmWTLyBf4EtX8niNWR5hWH2Y7XW4B5EOKlLyROe1xj9MLq7ks/UcYWFhqDcefDP 75DnAFPUKbRzJOY8OH3SpnCKubSS2N8R88p/AOrwnELPG/sW/V5Jtn9ykLsJdRSfbm8D wBTtbdr8vN4EIpWKJAtfiRszCYX4187NYdC4aru4MPEMJnOAtCxBYJ9FG/JMEgfJHq6q to9kPWU8w7LQPmMF9STo/KclHUcTBOqKoOPuVj4DKQSv0lg4/+9JllflspvYHSOsH7Sw h6aemmklmtkr63u551+VfB5b+dv+RkhDumU/sa2iN+deg8R1hQGPCUuftrDH6wKUu9Dx 2bhA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1737622417; x=1738227217; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=xJoEBFx5uK1qVNB0I6L/KByUsPzfPdocJz4RTcufOaM=; b=JDGN/sKC9bPEMlAcKSCRRQJU834zS/ioqVi9LPO0jx8i6JqtSRwzupAr4XA0B/v2fl HlD2an0oDilGZ0InG/0wGQ2yE0j4KfpUGuf0KanWvzsrvneh4snqOGxEkLjvtu4yfRxn spzvdxWjk42TkPacaFLtiMoFffZxX8XSCoAqAYa4/6QmwuY6Z3OVmraZy3rdS4eY9epR hqBk0oaPkfr5VhHyfTdAyS0GcpvEz2SoaToLsr+8X72LeufehFiyo0Q+sTxzpJpilgE5 pzKNo2LKtKs2tvzGQL/NO9jj8qnESq08bRGyEtLm7bd4iJGRmIrMth1OWte4qaalDxub Zhdg== X-Forwarded-Encrypted: i=1; AJvYcCVNjMa/WqZuvoqLFEZzFZ9/e7fdchdCwEGnRvJmFbNkngY7IIPc2RdVXurcXeuHB7F/q/8IGPU7OlAR@nongnu.org X-Gm-Message-State: AOJu0YxJL2ES5pG29+qOC25fABieTPcgRqVuvr7tUlYB5qu3MzCUfThj Pns8ylOowSzdVL28GmeDTClirHNhBMrQ5XwWQ6UjkFdlNI/m5HD6 X-Gm-Gg: ASbGncv873XvH94jdOtjSzFX9oHpAGIz592ur6FSvlmrvWHDW9L8cn3+LWuSPHeYTjX O8mE+opjaSTtQK0i8oXBNGLiacSa2nEz5h/B59VEmr0ns7GoF3PUYWKsgdGva1GVw7SVIg5IgcI biEPM9LYN8+S0xJC8rWP+wABDPOAesd8VvD1m3EQ+tfiLBh+w/tu38EgvcfOFq6cp5EQV7bTITx C8gYTONMLVVPPlKYM96rZQ3V6qgB/DvuGnQFKbwbrYxjBNNmx5dz3kcQhNZKDbEBvemyfwlZEOw +NZX0YxjqdzOYcAx1jVdJtiooI2l X-Google-Smtp-Source: AGHT+IHEdC4yg9mCCEpN0eFC6tZWwS0r+icFXihH3CuKR1Cts2dxzw6HjODPmmJuD+9gso1CfDlxyQ== X-Received: by 2002:a05:6402:51cb:b0:5d9:fbb5:9ee with SMTP id 4fb4d7f45d1cf-5db7d2f8ae3mr24760321a12.13.1737622416707; Thu, 23 Jan 2025 00:53:36 -0800 (PST) Received: from think.fkb.profitbricks.net ([2a02:8109:8384:1400:b763:14a0:8818:4012]) by smtp.gmail.com with ESMTPSA id 4fb4d7f45d1cf-5dbcfb8ff37sm4424676a12.72.2025.01.23.00.53.34 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 23 Jan 2025 00:53:34 -0800 (PST) From: Roman Penyaev To: Cc: Roman Penyaev , =?utf-8?q?Marc-Andr=C3=A9_Lureau?= , qemu-devel@nongnu.org Subject: [PATCH v9 4/4] qemu-options.hx: describe hub chardev and aggregation of several backends Date: Thu, 23 Jan 2025 09:53:24 +0100 Message-ID: <20250123085327.965501-5-r.peniaev@gmail.com> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20250123085327.965501-1-r.peniaev@gmail.com> References: <20250123085327.965501-1-r.peniaev@gmail.com> MIME-Version: 1.0 Received-SPF: pass client-ip=2a00:1450:4864:20::530; envelope-from=r.peniaev@gmail.com; helo=mail-ed1-x530.google.com X-Spam_score_int: -20 X-Spam_score: -2.1 X-Spam_bar: -- X-Spam_report: (-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, FREEMAIL_FROM=0.001, RCVD_IN_DNSWL_NONE=-0.0001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+qemu-devel=archiver.kernel.org@nongnu.org Sender: qemu-devel-bounces+qemu-devel=archiver.kernel.org@nongnu.org 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 Reviewed-by: "Marc-André Lureau" Cc: qemu-devel@nongnu.org --- qemu-options.hx | 49 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/qemu-options.hx b/qemu-options.hx index 7090d59f6f10..8e28bd41a9c3 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,46 @@ The available backends are: Forward QEMU's emulated msmouse events to the guest. ``msmouse`` does not take any options. +``-chardev hub,id=id,chardevs.0=id[,chardevs.N=id]`` + Explicitly create chardev backend hub device with the possibility + to aggregate input from multiple backend devices and forward it to + a single frontend device. Additionally, ``hub`` device takes the + output from the frontend device and sends it back to all the + connected backend devices. This allows for seamless interaction + between different backend devices and a single frontend + interface. Aggregation supported for up to 4 chardev + devices. (Since 10.0) + + For example, the following is a use case of 2 backend devices: + virtual console ``vc0`` and a pseudo TTY ``pty0`` connected to + a single virtio hvc console frontend device with a hub ``hub0`` + help. Virtual console renders text to an image, which can be + shared over the VNC protocol. In turn, pty backend provides + bidirectional communication to the virtio hvc console over the + pseudo TTY file. The example configuration can be as follows: + + :: + + -chardev pty,path=/tmp/pty,id=pty0 \ + -chardev vc,id=vc0 \ + -chardev hub,id=hub0,chardevs.0=pty0,chardevs.1=vc0 \ + -device virtconsole,chardev=hub0 \ + -vnc 0.0.0.0:0 + + Once QEMU starts VNC client and any TTY emulator can be used to + control a single hvc console: + + :: + + # Start TTY emulator + tio /tmp/pty + + # Start VNC client and switch to virtual console Ctrl-Alt-2 + vncviewer :0 + + Several frontend devices is not supported. Stacking of multiplexers + and hub devices is not supported as well. + ``-chardev vc,id=id[[,width=width][,height=height]][[,cols=cols][,rows=rows]]`` Connect to a QEMU text console. ``vc`` may optionally be given a specific size.