Fold InputReader into the loop and make UPower construction async - #23
Merged
Conversation
Completes the BlueZ event-loop migration: the controllers now run entirely on the single EventLoop thread with no per-device threads and no in-callback blocking D-Bus calls. InputReader is now an EventSource: - The device is opened and its descriptors/feature reports are read in the constructor (non-blocking fd); dispatch() reads and decodes one report per readable event. valid() reports whether initialisation succeeded. - Its worker thread, stop eventfd and private epoll are gone. The controllers add the reader to the loop on device arrival and retire it on removal. EventLoop::retire() added: unregisters a source and destroys it at a safe point (after the current dispatch pass), so a udev "remove" handler can drop the reader for the device that just went away without a use-after-free on an fd still in the current poll set. UPower construction is now asynchronous: - UPowerClient fetches properties via GetAllAsync and enumerates devices via callMethodAsync; UPowerDisplayDevice fetches properties via GetAllAsync. The single loop thread is no longer blocked by these round-trips when a controller with a battery connects. Pending calls are cancelled by unregisterProxy() if the object is destroyed first. UPowerClient keeps its mutex because it is also used by the standalone upower example, which still runs on the connection's own async event-loop thread. Verified at runtime: the clients start, the loop drives the D-Bus callbacks, the async UPower GetAll reply is delivered (onPropertiesChanged fires), and SIGTERM shuts down cleanly. The HID input data path needs real controller hardware and is build-verified only. Signed-off-by: Joel Winarske <joel.winarske@linux.com>
Adversarial review of the event-loop work surfaced two high-severity bugs and several smaller issues: - POLLHUP busy-spin: poll() reports POLLHUP/POLLERR level-triggered and unmaskable, so a hung-up device fd stayed "ready" every iteration and InputReader::dispatch() returned early without reading or removing it — spinning the loop at ~100% CPU until the udev "remove" event arrived (and unbounded if it never did). EventLoop::run() now stops polling a source as soon as its dispatch reports POLLHUP/POLLERR. Covered by a new regression test (a closed pipe reproduces the hangup deterministically). - Use-after-free in apply_pending(): a source added and retired within the same iteration had its object destroyed via to_retire_ and then its dangling pointer re-inserted via to_add_. Adds now skip any source also scheduled for removal that round, and retired objects are destroyed last. - stop() now uses release/acquire ordering for running_/exit_code_ instead of relaxed. - UPowerClient / UPowerDisplayDevice: restore the try/catch around property parsing that the sync->async conversion dropped, so a Variant type mismatch cannot escape the async reply slot. - xbox main: construct SignalSource before createSystemBusConnection, matching ps5/horipad and the "block signals before any thread starts" invariant. - horipad InputReader: clamp the report memcpy to bytes_read (like ps5/xbox) rather than buffer.size(). - SignalSource: unblock the signal mask if signalfd() fails, so the process is not left unkillable via SIGINT/SIGTERM. Signed-off-by: Joel Winarske <joel.winarske@linux.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Final increment of the event-loop source contract, on top of #22. The BlueZ controllers now run entirely on the single
EventLoopthread — no per-device threads and no in-callback blocking D-Bus calls remain.InputReader is now an EventSource
dispatch()reads and decodes one report per readable event.valid()reports whether initialisation succeeded.add()s the reader to the loop on device arrival (only whenvalid()) andretire()s it on removal.EventLoop::retire()
New primitive: unregisters a source and destroys it at a safe point (after the current dispatch pass). This closes a real use-after-free hazard — a udev "remove" and the reader's own fd (
POLLHUP) can be ready in the samepoll(), so a handler that reset the reader mid-pass could free an fd still referenced by this iteration's poll set.retire()holds ownership until the nextapply_pending().UPower construction is now async
UPowerClientfetches properties viaGetAllAsyncand enumerates devices viacallMethodAsync;UPowerDisplayDevicefetches viaGetAllAsync. So when a controller with a battery connects, the loop thread is no longer blocked on those round-trips. Pending calls are cancelled byunregisterProxy()if the object is destroyed first.UPowerClientkeeps itsdevices_mutex_: it's also used by the standalonesrc/upowerexample, which still runs on the connection's own async event-loop thread.Net −148 lines (the reader threads/eventfds deleted).
Validation
GetAllreply is delivered (onPropertiesChanged: org.freedesktop.UPowerlogged by the standaloneupower_clientagainst live UPower), and SIGTERM shuts down cleanly (exit 0). No regression in the standaloneupowershutdown.dispatch()vs a thread) changed.--Werrorand clang-tidy-19 clean; full build green (gcc/clang). TwoNOLINT(performance-unnecessary-value-param)mark the sdbus-mandated by-value error argument (a const-ref signature fails to compile viafunction_traits).Stacked on #22 (now merged); rebased onto
main.Contract complete
With this, the scoped
fd() + dispatch()source contract is fully realized for BlueZ: D-Bus, udev, HID input, and signals all run on oneEventLoopthread, and every controller mutex introduced back in #18 is gone. The ~14 pure-D-Bus clients still useenterEventLoopAsync()+monitorLoop()— migrating them was called out as optional/low-value in the scoping and is not included.