Skip to content

Run the BlueZ controllers on a single EventLoop, drop the mutexes#22

Merged
jwinarske merged 1 commit into
mainfrom
jw/event-loop-bluez
Jul 12, 2026
Merged

Run the BlueZ controllers on a single EventLoop, drop the mutexes#22
jwinarske merged 1 commit into
mainfrom
jw/event-loop-bluez

Conversation

@jwinarske

Copy link
Copy Markdown
Owner

Step 2 of the event-loop source contract, building on the EventLoop from #21. Moves the three BlueZ game-controller clients onto a single-threaded event loop and deletes the concurrency that PR #18 had to harden.

What changed

  • UdevMonitor is now an EventSource — the netlink monitor is set up in the constructor; fd() exposes the socket and dispatch() drains queued device events. Its worker thread and self-pipe are gone.
  • SignalSource (new, in src/utils/) — a signalfd EventSource that stops the loop on SIGINT/SIGTERM. It's constructed before any thread starts so the signals are blocked process-wide and delivered only through the loop.
  • The three controller mains replace enterEventLoopAsync() + monitorLoop() with EventLoop::run(), registering the controller (itself a UdevMonitor) and a SignalSource.
  • All the mutexes are removed from the three controllers (adapters_/devices_/input1_/upower_clients_ and input_reader_). The D-Bus object-manager callbacks and the udev callback now run on the same loop thread, so the state they share needs no locking — this retires the exact cross-thread surface behind PR Fix reliability, security, and performance issues across clients #18's race fixes. Net −122 lines.

Validation

  • Runtime (verified): each client starts, EventLoop::run() drives the D-Bus callbacks on the loop thread (object-manager/property callbacks observed firing), and SIGTERM is delivered through the signalfd — SignalSource: received signal 15, stopping → clean exit 0. Caught and fixed a real bug along the way: with the old main ordering, spdlog's periodic-flush thread was started before signals were blocked and took the default SIGTERM disposition, killing the process; constructing SignalSource first fixes it.
  • udev source: build-verified; dispatch is the identical EventSource mechanism the SignalSource exercises at runtime (couldn't inject a real uevent in the CI/sandbox — udevadm trigger is unavailable there).
  • HID input data path: build-verified only — exercising it needs a real PS5/Xbox/HoriPad controller. InputReader is unchanged here (still on its own read thread), so that path is as it was in Fix reliability, security, and performance issues across clients #18.
  • clang-format --Werror and clang-tidy-19 clean; full default build green.

Scope / next

This increment does the structural unification (udev + D-Bus + signals on one loop). It intentionally does not yet:

  • fold InputReader into the loop as a source (it keeps its read thread; it only logs and touches no shared owner state), or
  • convert the in-callback UPower construction (GetAll + EnumerateDevices) to the async sdbus API.

Those two are the next increment — the async conversion is the approach chosen for keeping the single loop from blocking on in-callback D-Bus round-trips. Until then, UPower client construction runs synchronously on the loop thread (a brief stall on controller connect), which is acceptable for these monitor daemons.

Stacked originally on #21 (now merged); rebased onto main.

Convert UdevMonitor from a self-threaded object into an EventSource and drive
the D-Bus connection, the udev monitor, and signal delivery from one EventLoop
per controller. Because the object-manager callbacks and the udev callback now
run on the same loop thread, the per-map mutexes and the input_reader_ mutex are
removed.

- UdevMonitor: set up the netlink monitor in the constructor, expose fd() and
  drain events in dispatch(); no worker thread or self-pipe.
- SignalSource: a signalfd EventSource that stops the loop on SIGINT/SIGTERM,
  constructed before any thread starts so the signals are blocked process-wide
  and delivered only through the loop's signalfd (otherwise spdlog's periodic
  flush thread can take the default SIGTERM disposition and kill the process).
- The three controller mains replace enterEventLoopAsync() + monitorLoop() with
  EventLoop::run(), registering the controller (a UdevMonitor) and a
  SignalSource.

InputReader keeps its own read thread for now (it only logs and touches no
shared owner state); folding it into the loop and converting the in-callback
UPower construction to async calls is the next step.

Verified at runtime: the clients start, the loop drives the D-Bus callbacks, and
SIGTERM is delivered through the signalfd for a clean exit. The HID input data
path needs real controller hardware and is build-verified only.

Signed-off-by: Joel Winarske <joel.winarske@linux.com>
@jwinarske
jwinarske merged commit fc248bc into main Jul 12, 2026
6 checks passed
@jwinarske
jwinarske deleted the jw/event-loop-bluez branch July 12, 2026 22:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant