Linux bluetooth development
 help / color / mirror / Atom feed
From: Luiz Augusto von Dentz <luiz.dentz@gmail.com>
To: linux-bluetooth@vger.kernel.org
Subject: [PATCH BlueZ v1 23/27] tools: Add socket watches for testers without GLib
Date: Thu, 22 Oct 2026 11:46:56 -0400	[thread overview]
Message-ID: <20261022154701.3959500-24-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 tester_io, watches on file descriptors on top of the mainloop for
the testers using GIOChannel watches, keeping their semantics: several
watches per descriptor holding a reference to it, the descriptor closed
with the last reference if requested, tester_io_close() reported as
TESTER_IO_NVAL, and the watches called after the other ready descriptors
in the order they were added, which the testers rely on to see the
events of the emulated controllers first.

Unlike GLib, a hang up nobody watches for drops the watches instead of
waking up the mainloop for good, and an error nobody watches for, e.g. a
connection attempt timing out, is polled again after a millisecond,
which also lets time advance under User Mode Linux time travel.

Assisted-by: Claude:claude-opus-5-5
---
 tools/tester-io.c | 400 ++++++++++++++++++++++++++++++++++++++++++++++
 tools/tester-io.h |  52 ++++++
 2 files changed, 452 insertions(+)
 create mode 100644 tools/tester-io.c
 create mode 100644 tools/tester-io.h

diff --git a/tools/tester-io.c b/tools/tester-io.c
new file mode 100644
index 000000000000..e9675225682b
--- /dev/null
+++ b/tools/tester-io.c
@@ -0,0 +1,400 @@
+// 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 <stdlib.h>
+#include <unistd.h>
+
+#include "src/shared/util.h"
+#include "src/shared/queue.h"
+#include "src/shared/mainloop.h"
+#include "src/shared/timeout.h"
+#include "tools/tester-io.h"
+
+/* Watch ids have the high bit set, so tester_remove_source() can tell them
+ * from timeout ids.
+ */
+#define WATCH_ID_BIT	0x80000000u
+
+struct tester_io {
+	int ref_count;
+	int fd;
+	bool close_on_unref;
+	bool registered;
+	bool dispatching;
+	bool scheduled;
+	unsigned int backoff_id;
+	uint32_t pending;
+	struct queue *watches;
+};
+
+struct watch {
+	unsigned int id;
+	struct tester_io *io;
+	uint32_t cond;
+	tester_io_func_t func;
+	void *user_data;
+	bool removed;
+};
+
+/* The tester_io with watches, in the order of their first watch */
+static struct queue *ios;
+static int dispatch_id;
+static unsigned int next_id;
+static unsigned int current_id;
+
+struct tester_io *tester_io_new(int fd)
+{
+	struct tester_io *io;
+
+	io = new0(struct tester_io, 1);
+	io->ref_count = 1;
+	io->fd = fd;
+	io->watches = queue_new();
+
+	return io;
+}
+
+struct tester_io *tester_io_ref(struct tester_io *io)
+{
+	if (io)
+		io->ref_count++;
+
+	return io;
+}
+
+void tester_io_unref(struct tester_io *io)
+{
+	if (!io || --io->ref_count > 0)
+		return;
+
+	queue_destroy(io->watches, free);
+
+	if (io->close_on_unref && io->fd >= 0)
+		close(io->fd);
+
+	free(io);
+}
+
+int tester_io_get_fd(struct tester_io *io)
+{
+	return io ? io->fd : -1;
+}
+
+void tester_io_set_close_on_unref(struct tester_io *io, bool close_on_unref)
+{
+	if (io)
+		io->close_on_unref = close_on_unref;
+}
+
+static void collect_cond(void *data, void *user_data)
+{
+	struct watch *watch = data;
+	uint32_t *events = user_data;
+
+	if (!watch->removed)
+		*events |= watch->cond;
+}
+
+static void io_event(int fd, uint32_t events, void *user_data);
+
+static bool match_registered_fd(const void *data, const void *match_data)
+{
+	const struct tester_io *io = data;
+
+	return io->registered && io->fd == PTR_TO_INT(match_data);
+}
+
+static void unregister(struct tester_io *io)
+{
+	if (!io->registered)
+		return;
+
+	io->registered = false;
+	mainloop_remove_fd(io->fd);
+}
+
+/* Poll for what the watches want, except while their callbacks are
+ * pending, and stop polling without any.
+ */
+static void update_events(struct tester_io *io)
+{
+	uint32_t events = 0;
+
+	queue_foreach(io->watches, collect_cond, &events);
+	events &= ~TESTER_IO_NVAL;
+
+	if (queue_isempty(io->watches) || io->scheduled || io->backoff_id ||
+								io->fd < 0) {
+		unregister(io);
+		return;
+	}
+
+	if (!io->registered) {
+		if (mainloop_add_fd(io->fd, events, io_event, io, NULL) < 0) {
+			struct tester_io *stale;
+
+			/* The descriptor of another io was closed directly,
+			 * and the same number was given to this one.
+			 */
+			stale = queue_find(ios, match_registered_fd,
+							INT_TO_PTR(io->fd));
+			if (!stale || stale == io)
+				return;
+
+			unregister(stale);
+
+			if (mainloop_add_fd(io->fd, events, io_event, io,
+								NULL) < 0)
+				return;
+		}
+
+		io->registered = true;
+		return;
+	}
+
+	mainloop_modify_fd(io->fd, events);
+}
+
+static bool match_removed(const void *data, const void *match_data)
+{
+	const struct watch *watch = data;
+
+	return watch->removed;
+}
+
+/* Drops the removed watches and their references to io. The caller holds
+ * a reference of its own, so io is freed by the caller's unref at most.
+ */
+static void cleanup(struct tester_io *io)
+{
+	struct watch *watch;
+
+	while ((watch = queue_remove_if(io->watches, match_removed, NULL))) {
+		free(watch);
+		io->ref_count--;
+	}
+
+	if (queue_isempty(io->watches)) {
+		io->scheduled = false;
+		io->pending = 0;
+		timeout_remove(io->backoff_id);
+		io->backoff_id = 0;
+		queue_remove(ios, io);
+	}
+
+	update_events(io);
+}
+
+static bool backoff_done(void *user_data)
+{
+	struct tester_io *io = user_data;
+
+	io->backoff_id = 0;
+	update_events(io);
+
+	return false;
+}
+
+static void remove_watch(void *data, void *user_data)
+{
+	struct watch *watch = data;
+
+	watch->removed = true;
+}
+
+static void dispatch(struct tester_io *io)
+{
+	const struct queue_entry *entry;
+	unsigned int last = next_id;
+	uint32_t events = io->pending;
+	uint32_t wanted = 0;
+
+	io->scheduled = false;
+	io->pending = 0;
+
+	tester_io_ref(io);
+	io->dispatching = true;
+
+	queue_foreach(io->watches, collect_cond, &wanted);
+
+	/* Watches added by a callback wait for the next event */
+	for (entry = queue_get_entries(io->watches); entry;
+							entry = entry->next) {
+		struct watch *watch = entry->data;
+
+		if (watch->removed || (watch->id & ~WATCH_ID_BIT) > last ||
+						!(events & watch->cond))
+			continue;
+
+		current_id = watch->id;
+
+		if (!watch->func(io, events & watch->cond, watch->user_data))
+			watch->removed = true;
+
+		current_id = 0;
+	}
+
+	/* A hang up nobody watches for would be reported over and over,
+	 * waking up the mainloop for good: drop the watches instead, as with
+	 * a closed descriptor. An error may be transient, e.g. a connection
+	 * attempt timing out, so keep watching after one, as GLib does.
+	 */
+	if (events & (TESTER_IO_HUP | TESTER_IO_NVAL) & ~wanted)
+		queue_foreach(io->watches, remove_watch, NULL);
+	else if (events & TESTER_IO_ERR & ~wanted && !io->backoff_id)
+		/* Polling would report the error right away again, so look
+		 * again a bit later, which lets simulated time advance too.
+		 */
+		io->backoff_id = timeout_add(1, backoff_done, io, NULL);
+
+	if (io->fd < 0)
+		queue_foreach(io->watches, remove_watch, NULL);
+
+	io->dispatching = false;
+	cleanup(io);
+	tester_io_unref(io);
+}
+
+static bool match_scheduled(const void *data, const void *match_data)
+{
+	const struct tester_io *io = data;
+
+	return io->scheduled;
+}
+
+static void dispatch_all(int id, void *user_data)
+{
+	struct tester_io *io;
+
+	mainloop_remove_timeout(id);
+	dispatch_id = 0;
+
+	while ((io = queue_find(ios, match_scheduled, NULL)))
+		dispatch(io);
+}
+
+/* Like GLib, which dispatches the ready sources in the order they were
+ * added, call the watches after the other descriptors ready at the same
+ * time, e.g. those of the emulated controllers, were handled, in the order
+ * of the first watch of each descriptor, and before idle callbacks: stop
+ * polling and dispatch once the ready descriptors were handled.
+ */
+static void schedule_dispatch(struct tester_io *io, uint32_t events)
+{
+	io->pending |= events;
+
+	if (io->scheduled)
+		return;
+
+	io->scheduled = true;
+	unregister(io);
+
+	if (!dispatch_id)
+		dispatch_id = mainloop_add_deferred(dispatch_all, NULL, NULL);
+}
+
+static void io_event(int fd, uint32_t events, void *user_data)
+{
+	struct tester_io *io = user_data;
+
+	schedule_dispatch(io, events);
+}
+
+unsigned int tester_io_add_watch(struct tester_io *io, uint32_t cond,
+				tester_io_func_t func, void *user_data)
+{
+	struct watch *watch;
+
+	if (!io || !func)
+		return 0;
+
+	if (!ios)
+		ios = queue_new();
+
+	watch = new0(struct watch, 1);
+	watch->id = ++next_id | WATCH_ID_BIT;
+	watch->io = tester_io_ref(io);
+	watch->cond = cond;
+	watch->func = func;
+	watch->user_data = user_data;
+
+	if (queue_isempty(io->watches))
+		queue_push_tail(ios, io);
+
+	queue_push_tail(io->watches, watch);
+	update_events(io);
+
+	return watch->id;
+}
+
+void tester_io_close(struct tester_io *io)
+{
+	if (!io || io->fd < 0)
+		return;
+
+	unregister(io);
+	close(io->fd);
+	io->fd = -1;
+
+	if (!queue_isempty(io->watches))
+		schedule_dispatch(io, TESTER_IO_NVAL);
+}
+
+static bool match_watch_id(const void *data, const void *match_data)
+{
+	const struct watch *watch = data;
+
+	return watch->id == PTR_TO_UINT(match_data) && !watch->removed;
+}
+
+static bool match_io_watch(const void *data, const void *match_data)
+{
+	const struct tester_io *io = data;
+
+	return queue_find(io->watches, match_watch_id, match_data);
+}
+
+unsigned int tester_current_source(void)
+{
+	return current_id;
+}
+
+void tester_remove_source(unsigned int id)
+{
+	struct tester_io *io;
+	struct watch *watch;
+
+	if (!id)
+		return;
+
+	if (!(id & WATCH_ID_BIT)) {
+		timeout_remove(id);
+		return;
+	}
+
+	io = queue_find(ios, match_io_watch, UINT_TO_PTR(id));
+	if (!io)
+		return;
+
+	watch = queue_find(io->watches, match_watch_id, UINT_TO_PTR(id));
+	watch->removed = true;
+
+	/* Cleaned up once the callbacks are done */
+	if (io->dispatching)
+		return;
+
+	tester_io_ref(io);
+	cleanup(io);
+	tester_io_unref(io);
+}
diff --git a/tools/tester-io.h b/tools/tester-io.h
new file mode 100644
index 000000000000..848251702cf0
--- /dev/null
+++ b/tools/tester-io.h
@@ -0,0 +1,52 @@
+/* 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>
+#include <poll.h>
+#include <sys/epoll.h>
+
+/* Watches on file descriptors for the testers, in the way of GLib's
+ * GIOChannel watches, on top of the mainloop: several watches per file
+ * descriptor, each holding a reference to it, and the file descriptor
+ * closed with the last reference if requested. Like GLib, watches are
+ * called after the other ready descriptors were handled.
+ */
+
+#define TESTER_IO_IN	EPOLLIN
+#define TESTER_IO_OUT	EPOLLOUT
+#define TESTER_IO_ERR	EPOLLERR
+#define TESTER_IO_HUP	EPOLLHUP
+/* Reported once the descriptor was closed with tester_io_close() */
+#define TESTER_IO_NVAL	POLLNVAL
+
+struct tester_io;
+
+typedef bool (*tester_io_func_t)(struct tester_io *io, uint32_t cond,
+							void *user_data);
+
+struct tester_io *tester_io_new(int fd);
+struct tester_io *tester_io_ref(struct tester_io *io);
+void tester_io_unref(struct tester_io *io);
+int tester_io_get_fd(struct tester_io *io);
+void tester_io_set_close_on_unref(struct tester_io *io, bool close_on_unref);
+
+/* Closes the descriptor, which the watches see as TESTER_IO_NVAL */
+void tester_io_close(struct tester_io *io);
+
+/* The watch is called while one of cond is set, until it returns false */
+unsigned int tester_io_add_watch(struct tester_io *io, uint32_t cond,
+				tester_io_func_t func, void *user_data);
+
+/* Removes a watch, or a timeout added with timeout_add() */
+void tester_remove_source(unsigned int id);
+
+/* The id of the watch being called, 0 outside of a watch */
+unsigned int tester_current_source(void);
-- 
2.56.0


  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 ` [PATCH BlueZ v1 14/27] emulator: Add virtio console and time travel client " Luiz Augusto von Dentz
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 ` Luiz Augusto von Dentz [this message]
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-24-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