Skip to content

Contain consumer exceptions in D-Bus signal handlers, and stop disposing from finalizers - #83

Open
Kinchul wants to merge 7 commits into
SuessLabs:developfrom
fiveco:vk/signal-handler-exception-safety
Open

Contain consumer exceptions in D-Bus signal handlers, and stop disposing from finalizers#83
Kinchul wants to merge 7 commits into
SuessLabs:developfrom
fiveco:vk/signal-handler-exception-safety

Conversation

@Kinchul

@Kinchul Kinchul commented Aug 7, 2026

Copy link
Copy Markdown

Why

Tmds.DBus reads signals on one receive loop. When a handler throws, ReceiveMessages catches
it, calls Disconnect() and clears the connection's signal handlers. With AutoConnect the
connection then transparently reconnects for method calls only: property reads keep
working, every event is silent, and nothing is logged. On our gateway this presented as BLE
scans that found nothing for hours, with a healthy-looking process.

A consumer callback throwing is not the library's fault, but taking the whole connection down
silently is a bad failure mode for it to have.

What changed

  • Every library-owned signal handler contains consumer exceptions and logs them, so one bad
    subscriber can no longer kill signal delivery for the process.
  • Removed the Adapter, Device, GattServer, Agent and AgentManager finalizers. They
    called the public Dispose, so the finalizer thread disposed managed watchers and issued
    D-Bus traffic — on a connection it may not own and which may already be disposed. Cleanup
    belongs to the owner; these types are all IDisposable.
  • GattServer.Dispose no longer fails when BlueZ answers org.bluez.Error.DoesNotExist:
    a bluetoothd restart makes that routine, and unregistering an object that is already gone
    has reached its goal.
  • GattServer.Dispose traces its success with Debug.WriteLine instead of reporting it on
    stderr.
  • Added BlueZManager.GetAdapterProxiesAsync and made Adapter.CreateAsync public, so a
    caller can pick an adapter without GetAdaptersAsync registering three watchers for every
    adapter on the bus, including the ones it discards. GetAdaptersAsync is unchanged.

Behaviour is otherwise unchanged; no public API is removed.

DamianSuess and others added 7 commits May 11, 2026 08:44
BlueZ answers org.bluez.Error.DoesNotExist when the advertisement or the
application it is asked to unregister is already gone, which a bluetoothd or
DBus restart makes routine. Unregistering has then reached its goal, so the
error is swallowed instead of failing the teardown and, through
Dispose(), the caller that only wanted the server gone.
Dispose() reported its own success on stderr, so every teardown surfaced as an
error line in the consumer's log. Debug.WriteLine matches the other traces of
the class and compiles out of release builds.
~Agent() ran Dispose(), which issues an UnregisterObject call on a connection
the agent does not own and may already have been disposed by its owner.
~AgentManager() called a Dispose() whose only statement was SuppressFinalize.
Both objects are IDisposable, so their cleanup belongs to their owner.
Adapter and Device finalizers called the public Dispose, so the finalizer thread
disposed managed watchers and issued DBus traffic on the shared system
connection. Removed both finalizers and the now-dead GC.SuppressFinalize calls.
Callers that never dispose these objects now leak a signal-handler delegate
instead of having it released non-deterministically.

Added BlueZManager.GetAdapterProxiesAsync so a caller can select an adapter
without GetAdaptersAsync registering three watchers for every adapter on the
bus, including the ones it discards. Adapter.CreateAsync is now public so an
external assembly can build the adapter it keeps. GetAdaptersAsync is unchanged.
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.

2 participants