From: Luiz Augusto von Dentz <luiz.dentz@gmail.com>
To: linux-bluetooth@vger.kernel.org
Subject: [PATCH BlueZ v1 14/27] emulator: Add virtio console and time travel client to btvirt
Date: Thu, 22 Oct 2026 11:46:47 -0400 [thread overview]
Message-ID: <20261022154701.3959500-15-luiz.dentz@gmail.com> (raw)
In-Reply-To: <20261022154701.3959500-1-luiz.dentz@gmail.com>
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Add --console, a vhost-user virtio console with named ports, each
relayed to a Unix socket <path>-<name>, so a test driver outside the
guest, such as pytest-bluezenv, can reach User Mode Linux guests the way
it uses virtio-serial ports under QEMU. While the driver has no request
outstanding on a sync port, time is paced to real time so guests do not
run ahead of it.
Add --time-client, so the console relay takes part in an external time
travel calendar, e.g. one simulating the controllers: it keeps a request
one quantum (100 us) ahead and only injects input while the calendar
lets it run, as guests may run ahead of the calendar's current time with
FREE_UNTIL. Add --time-skew for btvirt's own calendar.
Assisted-by: Claude:claude-opus-5-5
---
Makefile.tools | 2 +
emulator/main.c | 69 ++++-
emulator/ttclient.c | 313 ++++++++++++++++++++
emulator/ttclient.h | 50 ++++
emulator/vconsole.c | 701 ++++++++++++++++++++++++++++++++++++++++++++
emulator/vconsole.h | 26 ++
6 files changed, 1158 insertions(+), 3 deletions(-)
create mode 100644 emulator/ttclient.c
create mode 100644 emulator/ttclient.h
create mode 100644 emulator/vconsole.c
create mode 100644 emulator/vconsole.h
diff --git a/Makefile.tools b/Makefile.tools
index bc92bc02b83d..304dbf135e98 100644
--- a/Makefile.tools
+++ b/Makefile.tools
@@ -106,6 +106,8 @@ emulator_btvirt_SOURCES = emulator/main.c monitor/bt.h \
emulator/vhost.h emulator/vhost.c \
emulator/virtio.h emulator/virtio.c \
emulator/timetravel.h emulator/timetravel.c \
+ emulator/vconsole.h emulator/vconsole.c \
+ emulator/ttclient.h emulator/ttclient.c \
emulator/timeout.c
emulator_btvirt_LDADD = lib/libbluetooth-internal.la src/libshared-mainloop.la
diff --git a/emulator/main.c b/emulator/main.c
index a3b9456bb642..794d381764b5 100644
--- a/emulator/main.c
+++ b/emulator/main.c
@@ -29,6 +29,8 @@
#include "vhci.h"
#include "virtio.h"
#include "timetravel.h"
+#include "vconsole.h"
+#include "ttclient.h"
#include "le.h"
static void signal_callback(int signum, void *user_data)
@@ -55,6 +57,14 @@ static void usage(void)
"\t server socket\n"
"\t--time[=path=/tmp] Create time travel calendar socket\n"
"\t for User Mode Linux guests\n"
+ "\t--time-skew=<msec> Let guests run up to msec apart\n"
+ "\t (default: 0, lock-step)\n"
+ "\t--time-client=<id>:<socket>\n"
+ "\t Inject console input as client of a\n"
+ "\t time travel calendar\n"
+ "\t--console=<path>:<name>[=sync]...\n"
+ "\t Create vhost-user virtio console with\n"
+ "\t named ports relayed to <path>-<name>\n"
"\t-l[num] Number of local controllers\n"
"\t-L Create LE only controller\n"
"\t-U[num] Number of test LE controllers\n"
@@ -74,6 +84,9 @@ static const struct option main_options[] = {
{ "tcp", optional_argument, NULL, 't' },
{ "vhost", optional_argument, NULL, 'V' },
{ "time", optional_argument, NULL, 'I' },
+ { "console", required_argument, NULL, 'O' },
+ { "time-skew", required_argument, NULL, 'K' },
+ { "time-client", required_argument, NULL, 'J' },
{ "local", optional_argument, NULL, 'l' },
{ "le", no_argument, NULL, 'L' },
{ "bredr", no_argument, NULL, 'B' },
@@ -136,6 +149,10 @@ int main(int argc, char *argv[])
uint16_t tcp_port = 0;
const char *vhost_path = NULL;
const char *time_path = NULL;
+ const char *consoles[16];
+ int console_count = 0;
+ unsigned int time_skew = 0;
+ const char *time_client = NULL;
bool serial_enabled = false;
int letest_count = 0;
int vhci_count = 0;
@@ -150,8 +167,9 @@ int main(int argc, char *argv[])
for (;;) {
int opt;
- opt = getopt_long(argc, argv, "dSs::t::V::I::l::LBAC:U::vh",
- main_options, NULL);
+ opt = getopt_long(argc, argv,
+ "dSs::t::V::I::O:K:J:l::LBAC:U::vh",
+ main_options, NULL);
if (opt < 0)
break;
@@ -173,6 +191,19 @@ int main(int argc, char *argv[])
else
tcp_port = 45550;
break;
+ case 'J':
+ time_client = optarg;
+ break;
+ case 'K':
+ time_skew = atoi(optarg);
+ break;
+ case 'O':
+ if (console_count == ARRAY_SIZE(consoles)) {
+ fprintf(stderr, "Too many consoles\n");
+ return EXIT_FAILURE;
+ }
+ consoles[console_count++] = optarg;
+ break;
case 'I':
time_path = optarg ? optarg : "/tmp";
break;
@@ -226,7 +257,7 @@ int main(int argc, char *argv[])
if (letest_count < 1 && vhci_count < 1 && !server_enabled &&
!tcp_port && !serial_enabled && !vhost_path &&
- !time_path) {
+ !time_path && !console_count) {
fprintf(stderr, "No emulator specified\n");
return EXIT_FAILURE;
}
@@ -243,6 +274,8 @@ int main(int argc, char *argv[])
return EXIT_FAILURE;
}
+ tt_set_skew(time_skew);
+
if (debug_enabled)
tt_set_debug(server_debug, "time");
}
@@ -337,6 +370,36 @@ int main(int argc, char *argv[])
fprintf(stderr, "Listening TCP on 127.0.0.1:%d\n", tcp_port);
}
+ for (i = 0; i < console_count; i++) {
+ struct vcon *vcon;
+
+ vcon = vcon_open(consoles[i]);
+ if (!vcon)
+ return EXIT_FAILURE;
+
+ if (debug_enabled)
+ vcon_set_debug(vcon, server_debug, "console");
+ }
+
+ if (time_client) {
+ char *end;
+ unsigned long id = strtoul(time_client, &end, 0);
+
+ if (*end != ':' || !end[1]) {
+ fprintf(stderr, "Invalid time client: %s\n",
+ time_client);
+ return EXIT_FAILURE;
+ }
+
+ if (debug_enabled)
+ ttc_set_debug(server_debug, "time-client");
+
+ if (!ttc_open(end + 1, id, 0)) {
+ fprintf(stderr, "Failed to connect to %s\n", end + 1);
+ return EXIT_FAILURE;
+ }
+ }
+
if (vhost_path) {
struct vtio *vtio;
char path[PATH_MAX];
diff --git a/emulator/ttclient.c b/emulator/ttclient.c
new file mode 100644
index 000000000000..c134b5be9af0
--- /dev/null
+++ b/emulator/ttclient.c
@@ -0,0 +1,313 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright (C) 2026 Intel Corporation
+ *
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <sys/epoll.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#include "src/shared/mainloop.h"
+#include "src/shared/util.h"
+
+#include "ttclient.h"
+
+/* Protocol of include/uapi/linux/um_timetravel.h in Linux */
+#define TT_ACK 0
+#define TT_START 1
+#define TT_REQUEST 2
+#define TT_WAIT 3
+#define TT_RUN 6
+#define TT_FREE_UNTIL 7
+#define TT_BROADCAST 9
+
+struct tt_msg {
+ uint32_t op;
+ uint32_t seq;
+ uint64_t time;
+} __attribute__((packed));
+
+static struct {
+ int fd;
+ uint32_t seq;
+ uint64_t now;
+ uint64_t quantum;
+ bool running;
+ int pace_id;
+ ttc_run_func_t run;
+ void *run_data;
+ ttc_pace_func_t pace;
+ void *pace_data;
+ ttc_debug_func_t debug;
+ void *debug_data;
+} ttc = { .fd = -1 };
+
+static void ttc_debug(const char *format, ...)
+{
+ char str[128];
+ va_list ap;
+
+ if (!ttc.debug)
+ return;
+
+ va_start(ap, format);
+ vsnprintf(str, sizeof(str), format, ap);
+ va_end(ap);
+
+ ttc.debug(str, ttc.debug_data);
+}
+
+/* Read a message, closing any file descriptors it carries (shared memory
+ * offered with the START acknowledgment, which this client does not use).
+ */
+static bool read_msg(struct tt_msg *msg)
+{
+ char control[CMSG_SPACE(sizeof(int) * 2)];
+ struct iovec iov = { msg, sizeof(*msg) };
+ struct msghdr mh = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = control,
+ .msg_controllen = sizeof(control),
+ };
+ struct cmsghdr *cmsg;
+ ssize_t len;
+
+ do {
+ len = recvmsg(ttc.fd, &mh, MSG_CMSG_CLOEXEC);
+ } while (len < 0 && errno == EINTR);
+
+ for (cmsg = CMSG_FIRSTHDR(&mh); cmsg; cmsg = CMSG_NXTHDR(&mh, cmsg)) {
+ int fds[2];
+ unsigned int i, n;
+
+ if (cmsg->cmsg_level != SOL_SOCKET ||
+ cmsg->cmsg_type != SCM_RIGHTS)
+ continue;
+
+ n = MIN((cmsg->cmsg_len - CMSG_LEN(0)) / sizeof(int), 2U);
+ memcpy(fds, CMSG_DATA(cmsg), n * sizeof(int));
+ for (i = 0; i < n; i++)
+ close(fds[i]);
+ }
+
+ return len == sizeof(*msg);
+}
+
+static void send_msg(uint32_t op, uint32_t seq, uint64_t time)
+{
+ struct tt_msg msg = { .op = op, .seq = seq, .time = time };
+
+ if (write(ttc.fd, &msg, sizeof(msg)) < 0)
+ ttc_debug("write failed: %s", strerror(errno));
+}
+
+/* Messages the calendar may send at any time */
+static void handle_async(struct tt_msg *msg)
+{
+
+ switch (msg->op) {
+ case TT_FREE_UNTIL:
+ case TT_BROADCAST:
+ send_msg(TT_ACK, msg->seq, 0);
+ break;
+ case TT_RUN:
+ ttc.now = msg->time;
+ ttc.running = true;
+ send_msg(TT_ACK, msg->seq, 0);
+ break;
+ }
+}
+
+/* Send a request and wait for its acknowledgment */
+static bool request(uint32_t op, uint64_t time, uint64_t *ack_time)
+{
+ uint32_t seq = ++ttc.seq;
+ struct tt_msg msg;
+
+ send_msg(op, seq, time);
+
+ while (read_msg(&msg)) {
+ if (msg.op == TT_ACK && msg.seq == seq) {
+ if (ack_time)
+ *ack_time = msg.time;
+ return true;
+ }
+
+ handle_async(&msg);
+ }
+
+ ttc_debug("calendar connection lost");
+ return false;
+}
+
+static void run(void);
+
+void ttc_process(void)
+{
+ struct tt_msg msg;
+
+ if (ttc.fd < 0)
+ return;
+
+ /* Only messages the calendar may send while the client runs */
+ while (recv(ttc.fd, &msg, sizeof(msg), MSG_DONTWAIT | MSG_PEEK) ==
+ sizeof(msg)) {
+ if (msg.op != TT_FREE_UNTIL && msg.op != TT_BROADCAST)
+ break;
+
+ if (!read_msg(&msg))
+ break;
+
+ handle_async(&msg);
+ }
+}
+
+/* Ask to run again one quantum later and let the guests run until then */
+static void wait_next(void)
+{
+ if (!request(TT_REQUEST, ttc.now + ttc.quantum, NULL) ||
+ !request(TT_WAIT, (uint64_t) -1, NULL)) {
+ mainloop_remove_fd(ttc.fd);
+ return;
+ }
+
+ ttc.running = false;
+}
+
+static void pace_callback(int id, void *user_data)
+{
+ mainloop_remove_timeout(id);
+ ttc.pace_id = 0;
+ wait_next();
+}
+
+static void run(void)
+{
+ if (ttc.run)
+ ttc.run(ttc.run_data);
+
+ /* Without anything asked of the guests, hold time for one quantum
+ * of real time, so it advances no faster than real time.
+ */
+ if (ttc.pace && ttc.pace(ttc.pace_data)) {
+ ttc.pace_id = mainloop_add_timeout(
+ MAX(ttc.quantum / 1000000ULL, 1ULL),
+ pace_callback, NULL, NULL);
+ if (ttc.pace_id > 0)
+ return;
+ }
+
+ wait_next();
+}
+
+static void read_callback(int fd, uint32_t events, void *user_data)
+{
+ struct tt_msg msg;
+
+ if ((events & (EPOLLERR | EPOLLHUP)) || !read_msg(&msg)) {
+ ttc_debug("calendar disconnected");
+ mainloop_remove_fd(fd);
+ return;
+ }
+
+ handle_async(&msg);
+
+ if (msg.op == TT_RUN)
+ run();
+}
+
+static void ttc_destroy(void *user_data)
+{
+ if (ttc.pace_id > 0)
+ mainloop_remove_timeout(ttc.pace_id);
+
+ close(ttc.fd);
+ ttc.fd = -1;
+ ttc.running = false;
+}
+
+bool ttc_open(const char *path, unsigned int id, unsigned int quantum_us)
+{
+ struct sockaddr_un addr;
+ uint64_t ack;
+
+ if (ttc.fd >= 0 || strlen(path) >= sizeof(addr.sun_path))
+ return false;
+
+ ttc.fd = socket(PF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (ttc.fd < 0)
+ return false;
+
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ memcpy(addr.sun_path, path, strlen(path));
+
+ if (connect(ttc.fd, (struct sockaddr *) &addr, sizeof(addr)) < 0)
+ goto fail;
+
+ ttc.quantum = (uint64_t) (quantum_us ? quantum_us : 100) * 1000ULL;
+
+ if (!request(TT_START, id, &ack))
+ goto fail;
+
+ ttc_debug("started, id %u", (unsigned int) (ack & 0xffff));
+
+ if (mainloop_add_fd(ttc.fd, EPOLLIN, read_callback, NULL,
+ ttc_destroy) < 0)
+ goto fail;
+
+ /* A client runs once started */
+ ttc.running = true;
+ run();
+
+ return true;
+
+fail:
+ close(ttc.fd);
+ ttc.fd = -1;
+ return false;
+}
+
+bool ttc_enabled(void)
+{
+ return ttc.fd >= 0;
+}
+
+bool ttc_running(void)
+{
+ return ttc.fd < 0 || ttc.running;
+}
+
+void ttc_set_run(ttc_run_func_t func, void *user_data)
+{
+ ttc.run = func;
+ ttc.run_data = user_data;
+}
+
+void ttc_set_pace(ttc_pace_func_t func, void *user_data)
+{
+ ttc.pace = func;
+ ttc.pace_data = user_data;
+}
+
+void ttc_set_debug(ttc_debug_func_t callback, void *user_data)
+{
+ ttc.debug = callback;
+ ttc.debug_data = user_data;
+}
diff --git a/emulator/ttclient.h b/emulator/ttclient.h
new file mode 100644
index 000000000000..237a42cc6db9
--- /dev/null
+++ b/emulator/ttclient.h
@@ -0,0 +1,50 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright (C) 2026 Intel Corporation
+ *
+ *
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+
+/* Time travel client: takes part in the scheduling of a calendar (the time
+ * controller of User Mode Linux guests started with time-travel=ext), so
+ * that input from outside the simulation is injected at simulated times
+ * no guest has run past.
+ *
+ * The client keeps a request one quantum ahead of its time, so the
+ * calendar never lets guests run (or run ahead with FREE_UNTIL) beyond
+ * it, and only runs when the calendar grants it. Requesting the current
+ * time instead is not safe: with FREE_UNTIL, guests may already be past
+ * the calendar's current time.
+ */
+
+/* Quantum in microseconds, default (0) 100 */
+bool ttc_open(const char *path, unsigned int id, unsigned int quantum_us);
+bool ttc_enabled(void);
+
+/* The calendar granted the client to run: injecting events is safe */
+bool ttc_running(void);
+
+/* Answer pending calendar messages, e.g. while waiting for a guest that
+ * itself waits for the calendar
+ */
+void ttc_process(void);
+
+/* Called each time the client runs */
+typedef void (*ttc_run_func_t)(void *user_data);
+void ttc_set_run(ttc_run_func_t func, void *user_data);
+
+/* While the pace function returns true, the client lets the guests' time
+ * advance no faster than real time, e.g. while a test driver outside the
+ * simulation has not asked anything of the guests.
+ */
+typedef bool (*ttc_pace_func_t)(void *user_data);
+void ttc_set_pace(ttc_pace_func_t func, void *user_data);
+
+typedef void (*ttc_debug_func_t)(const char *str, void *user_data);
+void ttc_set_debug(ttc_debug_func_t callback, void *user_data);
diff --git a/emulator/vconsole.c b/emulator/vconsole.c
new file mode 100644
index 000000000000..7d288458a759
--- /dev/null
+++ b/emulator/vconsole.c
@@ -0,0 +1,701 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright (C) 2026 Intel Corporation
+ *
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/epoll.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#include "src/shared/mainloop.h"
+#include "src/shared/queue.h"
+#include "src/shared/util.h"
+
+#include "vhost.h"
+#include "timetravel.h"
+#include "ttclient.h"
+#include "vconsole.h"
+
+/* Virtio console, see include/uapi/linux/virtio_console.h in Linux */
+#define VCON_F_MULTIPORT 1
+
+#define VCON_DEVICE_READY 0
+#define VCON_DEVICE_ADD 1
+#define VCON_PORT_READY 3
+#define VCON_PORT_OPEN 6
+#define VCON_PORT_NAME 7
+
+/* Queues: port 0 receive/transmit, control receive/transmit, then a
+ * receive/transmit pair for each further port.
+ */
+#define VCON_CTRL_RX 2
+#define VCON_CTRL_TX 3
+
+#define VCON_MAX_PORTS 3
+#define VCON_MAX_NAME 64
+/* The Linux driver posts receive buffers of a page */
+#define VCON_CHUNK 4096
+/* Data from the guest kept while no peer is connected */
+#define VCON_MAX_BACKLOG (1024 * 1024)
+
+struct vcon_config {
+ uint16_t cols;
+ uint16_t rows;
+ uint32_t max_nr_ports;
+ uint32_t emerg_wr;
+} __attribute__((packed));
+
+struct vcon_ctrl {
+ uint32_t id;
+ uint16_t event;
+ uint16_t value;
+} __attribute__((packed));
+
+struct packet {
+ size_t len;
+ uint8_t data[];
+};
+
+struct port {
+ struct vcon *vcon;
+ unsigned int id;
+ char name[VCON_MAX_NAME];
+ bool sync;
+ int listen_fd;
+ int client_fd;
+ /* Data for the guest, waiting for receive buffers */
+ struct queue *to_guest;
+ /* Data from the guest, waiting for a peer */
+ struct queue *to_peer;
+ size_t to_peer_len;
+ /* The peer sent a request the guest has not answered yet */
+ bool outstanding;
+ /* The driver discards data of ports nobody opened in the guest */
+ bool guest_open;
+};
+
+struct vcon {
+ char path[108];
+ int fd;
+ struct vhost *vu;
+ struct vhost_ops ops;
+ struct vcon_config config;
+ struct port ports[VCON_MAX_PORTS];
+ unsigned int num_ports;
+ /* Control messages waiting for receive buffers */
+ struct queue *ctrl;
+ vcon_debug_func_t debug_callback;
+ void *debug_data;
+};
+
+static struct queue *vcons;
+
+static void vcon_debug(struct vcon *vcon, const char *format, ...)
+{
+ char str[160];
+ va_list ap;
+
+ if (!vcon->debug_callback)
+ return;
+
+ va_start(ap, format);
+ vsnprintf(str, sizeof(str), format, ap);
+ va_end(ap);
+
+ vcon->debug_callback(str, vcon->debug_data);
+}
+
+static unsigned int rx_queue(unsigned int port)
+{
+ return port ? 2 + 2 * port : 0;
+}
+
+static struct packet *packet_new(const void *data, size_t len)
+{
+ struct packet *pkt;
+
+ pkt = malloc(sizeof(*pkt) + len);
+ if (!pkt)
+ return NULL;
+
+ pkt->len = len;
+ memcpy(pkt->data, data, len);
+
+ return pkt;
+}
+
+/* Push pending packets to a receive queue while it has buffers */
+static void flush_queue(struct vcon *vcon, struct queue *pending,
+ unsigned int index)
+{
+ struct packet *pkt;
+
+ if (!vcon->vu || queue_isempty(pending))
+ return;
+
+ /* As time travel client, only inject while the calendar lets it run,
+ * so no guest has run past that time.
+ */
+ if (!ttc_running())
+ return;
+
+ while ((pkt = queue_peek_head(pending))) {
+ struct iovec iov = { pkt->data, pkt->len };
+
+ if (!vhost_push(vcon->vu, index, &iov, 1))
+ break;
+
+
+ queue_pop_head(pending);
+ free(pkt);
+ }
+}
+
+static void send_ctrl(struct vcon *vcon, unsigned int id, uint16_t event,
+ uint16_t value, const char *name)
+{
+ uint8_t buf[sizeof(struct vcon_ctrl) + VCON_MAX_NAME];
+ struct vcon_ctrl *ctrl = (void *) buf;
+ size_t len = sizeof(*ctrl);
+ struct packet *pkt;
+
+ ctrl->id = cpu_to_le32(id);
+ ctrl->event = cpu_to_le16(event);
+ ctrl->value = cpu_to_le16(value);
+
+ if (name) {
+ memcpy(buf + len, name, strlen(name));
+ len += strlen(name);
+ }
+
+ pkt = packet_new(buf, len);
+ if (!pkt)
+ return;
+
+ queue_push_tail(vcon->ctrl, pkt);
+ flush_queue(vcon, vcon->ctrl, VCON_CTRL_RX);
+}
+
+static void flush_port(struct port *port)
+{
+ if (port->guest_open)
+ flush_queue(port->vcon, port->to_guest, rx_queue(port->id));
+}
+
+static void handle_ctrl(struct vcon *vcon, const struct vcon_ctrl *ctrl)
+{
+ unsigned int id = le32_to_cpu(ctrl->id);
+ struct port *port;
+ unsigned int i;
+
+ switch (le16_to_cpu(ctrl->event)) {
+ case VCON_DEVICE_READY:
+ for (i = 0; i < vcon->num_ports; i++)
+ send_ctrl(vcon, i, VCON_DEVICE_ADD, 1, NULL);
+ break;
+ case VCON_PORT_READY:
+ if (id >= vcon->num_ports)
+ break;
+ send_ctrl(vcon, id, VCON_PORT_NAME, 1, vcon->ports[id].name);
+ /* Data from the guest is kept until a peer connects */
+ send_ctrl(vcon, id, VCON_PORT_OPEN, 1, NULL);
+ break;
+ case VCON_PORT_OPEN:
+ if (id >= vcon->num_ports)
+ break;
+ port = &vcon->ports[id];
+ port->guest_open = le16_to_cpu(ctrl->value);
+ vcon_debug(vcon, "%s: guest %s", port->name,
+ port->guest_open ? "opened" : "closed");
+ flush_port(port);
+ break;
+ }
+}
+
+static void write_peer(struct port *port, const uint8_t *data, size_t len)
+{
+ while (len) {
+ ssize_t n = write(port->client_fd, data, len);
+
+ if (n < 0)
+ return;
+
+ data += n;
+ len -= n;
+ }
+}
+
+static void from_guest(struct port *port, const uint8_t *data, size_t len)
+{
+ struct packet *pkt;
+
+ /* The guest answered */
+ port->outstanding = false;
+
+ if (port->client_fd >= 0) {
+ write_peer(port, data, len);
+ return;
+ }
+
+ /* Answers of a sync port belong to the peer that asked, other ports
+ * keep the guest's output (e.g. logs) until a peer connects.
+ */
+ if (port->sync || port->to_peer_len + len > VCON_MAX_BACKLOG)
+ return;
+
+ pkt = packet_new(data, len);
+ if (!pkt)
+ return;
+
+ queue_push_tail(port->to_peer, pkt);
+ port->to_peer_len += len;
+}
+
+static void queue_notify(struct vhost *vu, unsigned int index,
+ void *user_data)
+{
+ struct vcon *vcon = user_data;
+ static uint8_t buf[65536];
+ unsigned int i;
+ ssize_t len;
+
+ if (index == VCON_CTRL_TX) {
+ while ((len = vhost_pop(vu, index, buf, sizeof(buf)))) {
+ if (len >= (ssize_t) sizeof(struct vcon_ctrl))
+ handle_ctrl(vcon, (void *) buf);
+ }
+ goto done;
+ }
+
+ if (index == VCON_CTRL_RX) {
+ flush_queue(vcon, vcon->ctrl, VCON_CTRL_RX);
+ goto done;
+ }
+
+ for (i = 0; i < vcon->num_ports; i++) {
+ struct port *port = &vcon->ports[i];
+
+ if (index == rx_queue(i)) {
+ flush_port(port);
+ break;
+ }
+
+ if (index == rx_queue(i) + 1) {
+ while ((len = vhost_pop(vu, index, buf, sizeof(buf)))) {
+ if (len > 0)
+ from_guest(port, buf, len);
+ }
+ break;
+ }
+ }
+
+done:
+ tt_schedule();
+}
+
+static void run_vcon(void *data, void *user_data)
+{
+ struct vcon *vcon = data;
+ unsigned int i;
+
+ if (!vcon->vu)
+ return;
+
+ flush_queue(vcon, vcon->ctrl, VCON_CTRL_RX);
+
+ for (i = 0; i < vcon->num_ports; i++)
+ flush_port(&vcon->ports[i]);
+
+ vhost_flush_notify(vcon->vu);
+}
+
+/* The calendar lets the time travel client run: deliver what is pending */
+static void run(void *user_data)
+{
+ queue_foreach(vcons, run_vcon, NULL);
+}
+
+static bool can_notify(struct vhost *vu, void *user_data)
+{
+ return ttc_running();
+}
+
+static void vhost_wait(struct vhost *vu, void *user_data)
+{
+ tt_process();
+ ttc_process();
+}
+
+static void disconnect(struct vhost *vu, void *user_data)
+{
+ struct vcon *vcon = user_data;
+ unsigned int i;
+
+ vcon_debug(vcon, "guest disconnected");
+
+ vhost_free(vcon->vu);
+ vcon->vu = NULL;
+ queue_remove_all(vcon->ctrl, NULL, NULL, free);
+
+ for (i = 0; i < vcon->num_ports; i++)
+ vcon->ports[i].guest_open = false;
+
+ tt_schedule();
+}
+
+static void vu_debug(const char *str, void *user_data)
+{
+ struct vcon *vcon = user_data;
+
+ vcon_debug(vcon, "vhost-user: %s", str);
+}
+
+static void accept_guest(int fd, uint32_t events, void *user_data)
+{
+ struct vcon *vcon = user_data;
+ int nfd;
+
+ if (events & (EPOLLERR | EPOLLHUP)) {
+ mainloop_remove_fd(fd);
+ return;
+ }
+
+ nfd = accept(fd, NULL, NULL);
+ if (nfd < 0)
+ return;
+
+ if (vcon->vu) {
+ vcon_debug(vcon, "already serving a guest");
+ close(nfd);
+ return;
+ }
+
+ vcon->vu = vhost_new(nfd, &vcon->ops, vcon);
+ if (!vcon->vu) {
+ close(nfd);
+ return;
+ }
+
+ vhost_set_debug(vcon->vu, vu_debug, vcon);
+
+ vcon_debug(vcon, "guest connected");
+}
+
+static void peer_read(int fd, uint32_t events, void *user_data)
+{
+ struct port *port = user_data;
+ uint8_t buf[VCON_CHUNK];
+ struct packet *pkt;
+ ssize_t len;
+
+ len = read(fd, buf, sizeof(buf));
+ if (len <= 0) {
+ mainloop_remove_fd(fd);
+ return;
+ }
+
+ pkt = packet_new(buf, len);
+ if (!pkt)
+ return;
+
+ if (port->sync)
+ port->outstanding = true;
+
+ queue_push_tail(port->to_guest, pkt);
+ flush_port(port);
+
+ tt_schedule();
+}
+
+static void peer_destroy(void *user_data)
+{
+ struct port *port = user_data;
+
+ vcon_debug(port->vcon, "%s: peer disconnected", port->name);
+
+ close(port->client_fd);
+ port->client_fd = -1;
+ port->outstanding = false;
+
+ /* Requests of the peer that left are not delivered anymore */
+ queue_remove_all(port->to_guest, NULL, NULL, free);
+
+ tt_schedule();
+}
+
+static void accept_peer(int fd, uint32_t events, void *user_data)
+{
+ struct port *port = user_data;
+ struct packet *pkt;
+ int nfd;
+
+ if (events & (EPOLLERR | EPOLLHUP)) {
+ mainloop_remove_fd(fd);
+ return;
+ }
+
+ nfd = accept(fd, NULL, NULL);
+ if (nfd < 0)
+ return;
+
+ if (port->client_fd >= 0) {
+ close(nfd);
+ return;
+ }
+
+ port->client_fd = nfd;
+
+ if (mainloop_add_fd(nfd, EPOLLIN, peer_read, port, peer_destroy) < 0) {
+ close(nfd);
+ port->client_fd = -1;
+ return;
+ }
+
+ vcon_debug(port->vcon, "%s: peer connected", port->name);
+
+ while ((pkt = queue_pop_head(port->to_peer))) {
+ write_peer(port, pkt->data, pkt->len);
+ free(pkt);
+ }
+
+ port->to_peer_len = 0;
+}
+
+static int listen_unix(const char *path)
+{
+ struct sockaddr_un addr;
+ int fd;
+
+ if (strlen(path) >= sizeof(addr.sun_path))
+ return -1;
+
+ fd = socket(PF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (fd < 0)
+ return -1;
+
+ unlink(path);
+
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ memcpy(addr.sun_path, path, strlen(path));
+
+ if (bind(fd, (struct sockaddr *) &addr, sizeof(addr)) < 0 ||
+ listen(fd, 5) < 0) {
+ close(fd);
+ return -1;
+ }
+
+ return fd;
+}
+
+static void sync_state(void *data, void *user_data)
+{
+ struct vcon *vcon = data;
+ unsigned int *state = user_data;
+ unsigned int i;
+
+ for (i = 0; i < vcon->num_ports; i++) {
+ struct port *port = &vcon->ports[i];
+
+ if (!port->sync || port->client_fd < 0)
+ continue;
+
+ state[0]++;
+ if (port->outstanding)
+ state[1]++;
+ }
+}
+
+/* Hold time while a peer is connected to a sync port but has no request
+ * outstanding on any of them: it is between requests, so the guests must
+ * not run ahead of it, time then advances at most at real time.
+ */
+static bool hold(void *user_data)
+{
+ unsigned int state[2] = { 0, 0 };
+
+ queue_foreach(vcons, sync_state, state);
+
+ return state[0] && !state[1];
+}
+
+static void flush(void *user_data)
+{
+ struct vcon *vcon = user_data;
+
+ if (vcon->vu)
+ vhost_process(vcon->vu);
+}
+
+static bool parse_spec(struct vcon *vcon, const char *spec)
+{
+ char *str, *tok, *save = NULL;
+ bool ok = false;
+
+ str = strdup(spec);
+ if (!str)
+ return false;
+
+ tok = strtok_r(str, ":", &save);
+ if (!tok || strlen(tok) >= sizeof(vcon->path))
+ goto done;
+
+ memcpy(vcon->path, tok, strlen(tok) + 1);
+
+ while ((tok = strtok_r(NULL, ":", &save))) {
+ struct port *port;
+ char *opt;
+
+ if (vcon->num_ports == VCON_MAX_PORTS)
+ goto done;
+
+ port = &vcon->ports[vcon->num_ports];
+
+ opt = strchr(tok, '=');
+ if (opt) {
+ *opt++ = '\0';
+ if (strcmp(opt, "sync"))
+ goto done;
+ port->sync = true;
+ }
+
+ if (!*tok || strlen(tok) >= sizeof(port->name))
+ goto done;
+
+ memcpy(port->name, tok, strlen(tok) + 1);
+ port->id = vcon->num_ports++;
+ }
+
+ ok = vcon->num_ports > 0;
+
+done:
+ free(str);
+ return ok;
+}
+
+struct vcon *vcon_open(const char *spec)
+{
+ struct vcon *vcon;
+ unsigned int i;
+
+ vcon = new0(struct vcon, 1);
+ vcon->fd = -1;
+
+ for (i = 0; i < VCON_MAX_PORTS; i++) {
+ vcon->ports[i].listen_fd = -1;
+ vcon->ports[i].client_fd = -1;
+ }
+
+ if (!parse_spec(vcon, spec)) {
+ free(vcon);
+ return NULL;
+ }
+
+ vcon->ctrl = queue_new();
+ vcon->config.max_nr_ports = cpu_to_le32(vcon->num_ports);
+ vcon->ops.features = 1ULL << VCON_F_MULTIPORT;
+ vcon->ops.num_queues = 2 + 2 * vcon->num_ports;
+ vcon->ops.config = &vcon->config;
+ vcon->ops.config_len = sizeof(vcon->config);
+ vcon->ops.queue_notify = queue_notify;
+ vcon->ops.disconnect = disconnect;
+ vcon->ops.wait = vhost_wait;
+ vcon->ops.can_notify = can_notify;
+
+ vcon->fd = listen_unix(vcon->path);
+ if (vcon->fd < 0 || mainloop_add_fd(vcon->fd, EPOLLIN, accept_guest,
+ vcon, NULL) < 0)
+ goto fail;
+
+ for (i = 0; i < vcon->num_ports; i++) {
+ struct port *port = &vcon->ports[i];
+ char path[sizeof(vcon->path) + VCON_MAX_NAME + 1];
+
+ port->vcon = vcon;
+ port->to_guest = queue_new();
+ port->to_peer = queue_new();
+
+ snprintf(path, sizeof(path), "%s-%s", vcon->path, port->name);
+ port->listen_fd = listen_unix(path);
+ if (port->listen_fd < 0 || mainloop_add_fd(port->listen_fd,
+ EPOLLIN, accept_peer, port, NULL) < 0)
+ goto fail;
+ }
+
+ if (!vcons) {
+ vcons = queue_new();
+ tt_set_hold(hold, NULL);
+ ttc_set_pace(hold, NULL);
+ ttc_set_run(run, NULL);
+ }
+
+ queue_push_tail(vcons, vcon);
+ tt_add_flush(flush, vcon);
+
+ return vcon;
+
+fail:
+ fprintf(stderr, "Failed to open console %s\n", vcon->path);
+ vcon_close(vcon);
+ return NULL;
+}
+
+void vcon_close(struct vcon *vcon)
+{
+ unsigned int i;
+
+ if (!vcon)
+ return;
+
+ queue_remove(vcons, vcon);
+
+ for (i = 0; i < vcon->num_ports; i++) {
+ struct port *port = &vcon->ports[i];
+
+ if (port->client_fd >= 0)
+ mainloop_remove_fd(port->client_fd);
+ if (port->listen_fd >= 0) {
+ mainloop_remove_fd(port->listen_fd);
+ close(port->listen_fd);
+ }
+ queue_destroy(port->to_guest, free);
+ queue_destroy(port->to_peer, free);
+ }
+
+ vhost_free(vcon->vu);
+
+ if (vcon->fd >= 0) {
+ mainloop_remove_fd(vcon->fd);
+ close(vcon->fd);
+ }
+
+ tt_remove_flush(flush, vcon);
+ queue_destroy(vcon->ctrl, free);
+ free(vcon);
+}
+
+bool vcon_set_debug(struct vcon *vcon, vcon_debug_func_t callback,
+ void *user_data)
+{
+ if (!vcon)
+ return false;
+
+ vcon->debug_callback = callback;
+ vcon->debug_data = user_data;
+
+ return true;
+}
diff --git a/emulator/vconsole.h b/emulator/vconsole.h
new file mode 100644
index 000000000000..bfebe395005f
--- /dev/null
+++ b/emulator/vconsole.h
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright (C) 2026 Intel Corporation
+ *
+ *
+ */
+
+#include <stdbool.h>
+
+struct vcon;
+
+/* Serve a virtio console with named ports on the vhost-user socket path.
+ * Each port is relayed to a Unix socket "<path>-<name>" for a peer outside
+ * the guest. spec is "<path>:<name>[=sync]:<name>[=sync]...", a sync port
+ * carries requests from the peer: time is held while the peer has none
+ * outstanding on any sync port.
+ */
+struct vcon *vcon_open(const char *spec);
+void vcon_close(struct vcon *vcon);
+
+typedef void (*vcon_debug_func_t)(const char *str, void *user_data);
+bool vcon_set_debug(struct vcon *vcon, vcon_debug_func_t callback,
+ void *user_data);
--
2.56.0
next prev parent reply other threads:[~2026-10-22 15:47 UTC|newest]
Thread overview: 30+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-22 15:46 [PATCH BlueZ v1 00/27] Run the testers under User Mode Linux with time travel Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 01/27] tools/test-runner: Do not report filesystems already mounted Luiz Augusto von Dentz
2026-10-22 20:02 ` Run the testers under User Mode Linux with time travel bluez.test.bot
2026-10-22 15:46 ` [PATCH BlueZ v1 02/27] shared/tester: Time out teardown Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 03/27] mgmt-tester: Wait for LL Privacy setup to complete Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 04/27] iso-tester: Fix leaking socket watches Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 05/27] l2cap-tester: Fix race in LE Client - Close socket 2 Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 06/27] iso-tester: Fix busy waiting in PAST tests Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 07/27] doc: Enable IPv6 in tester.config Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 08/27] 6lowpan-tester: Find the interface of the controller Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 09/27] emulator: Remove -T option from btvirt Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 10/27] doc: Add btvirt documentation Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 11/27] emulator: Add vhost-user backend Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 12/27] emulator: Add vhost-user virtio server to btvirt Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 13/27] emulator: Add time travel calendar " Luiz Augusto von Dentz
2026-10-22 15:46 ` Luiz Augusto von Dentz [this message]
2026-10-22 15:46 ` [PATCH BlueZ v1 15/27] tools/test-runner: Add User Mode Linux support Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 16/27] doc: Add virtio documentation Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 17/27] shared/mainloop: Add idle callbacks Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 18/27] shared/timeout: Run timeouts of 0 once idle Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 19/27] shared/tester: Remove GLib dependency Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 20/27] emulator/hciemu: " Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 21/27] tools: Build testers that do not need GLib with the mainloop Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 22/27] shared/mainloop: Add deferred callbacks Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 23/27] tools: Add socket watches for testers without GLib Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 24/27] tools: Run sco-tester, rfcomm-tester and 6lowpan-tester on the mainloop Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 25/27] tools: Run l2cap-tester and iso-tester " Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 26/27] doc/virtio: Update measurements of the testers Luiz Augusto von Dentz
2026-10-22 15:47 ` [PATCH BlueZ v1 27/27] doc: Split tester.config for QEMU and User Mode Linux Luiz Augusto von Dentz
2026-10-29 18:40 ` [PATCH BlueZ v1 00/27] Run the testers under User Mode Linux with time travel patchwork-bot+bluetooth
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261022154701.3959500-15-luiz.dentz@gmail.com \
--to=luiz.dentz@gmail.com \
--cc=linux-bluetooth@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox