-
-
Notifications
You must be signed in to change notification settings - Fork 204
fix(ble): stop a missing D-Bus socket killing the server #2992
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dirkwa
wants to merge
7
commits into
SignalK:master
Choose a base branch
from
dirkwa:fix-ble-dbus-uncaught-error
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
0d6ddd9
fix(ble): stop a missing D-Bus socket killing the server
dirkwa ad413fe
feat(plugin-ci): fail plugins that crash the server after start()
dirkwa ffe7985
fix(ble): reject pending adapter calls when the D-Bus bus fails
dirkwa 206be9f
fix(plugin-ci): drain async crashes on every lifecycle exit
dirkwa e1d769f
fix(plugin-ci): report delayed failures on the stop and restart paths
dirkwa 4755ea4
fix(ble): remove the per-call D-Bus error listener once the call settles
dirkwa 829d544
fix(ble): detach the per-call listener when a bus error rejects the call
dirkwa File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| /** | ||
| * Safe wrapper around `@naugehyde/node-ble`'s `createBluetooth()`. | ||
| * | ||
| * `createBluetooth()` opens a D-Bus system-bus connection eagerly and returns | ||
| * synchronously, without attaching an `error` listener to it. The underlying | ||
| * `@jellybrick/dbus-next` connection reports transport failures by emitting | ||
| * `error` on that connection rather than by rejecting the pending call, so a | ||
| * missing or unreachable `/var/run/dbus/system_bus_socket` surfaces as an | ||
| * `error` event with no listener — which Node escalates into an uncaught | ||
| * exception. Because it arrives on the event emitter and not through the | ||
| * promise chain, an `await createBluetooth()...` inside `try/catch` never sees | ||
| * it: the process dies with `connect ENOENT /var/run/dbus/system_bus_socket` | ||
| * a few seconds after the server has otherwise started cleanly. | ||
| * | ||
| * This happens on any host without a reachable system bus — most commonly a | ||
| * container that does not bind-mount the socket, but also a stripped-down | ||
| * Linux install with no D-Bus daemon running. | ||
| * | ||
| * Attaching a listener before handing the session back keeps the failure on | ||
| * the promise path, where the existing `try/catch` blocks already handle it. | ||
| */ | ||
|
|
||
| import { createDebug } from '../../debug' | ||
| const debug = createDebug('signalk-server:api:ble:safe') | ||
|
|
||
| export interface BluetoothSession { | ||
| bluetooth: any | ||
| destroy: () => void | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| /** | ||
| * The only part of the dbus-next connection this module touches. node-ble | ||
| * ships no types, so the surface is declared here rather than pulling in an | ||
| * `any` and losing the check on the call below. | ||
| */ | ||
| interface ErrorEmitter { | ||
| on(event: 'error', listener: (err: unknown) => void): void | ||
| } | ||
|
|
||
| /** | ||
| * Creates a node-ble session whose D-Bus connection can never raise an | ||
| * unhandled `error` event. | ||
| * | ||
| * The listener is attached synchronously, before the caller gets a chance to | ||
| * await anything, so there is no window in which an early transport error can | ||
| * escape. Errors are logged to debug only: every caller already treats "no | ||
| * usable adapter" as a normal outcome, and a missing system bus is the | ||
| * expected case on non-BlueZ hosts rather than something worth logging loudly. | ||
| */ | ||
| export function createBluetoothSafe(): BluetoothSession { | ||
| // eslint-disable-next-line @typescript-eslint/no-require-imports | ||
| const { createBluetooth } = require('@naugehyde/node-ble') | ||
| const session: BluetoothSession = createBluetooth() | ||
|
|
||
| // node-ble keeps the dbus-next connection on the Bluetooth instance; guard | ||
| // the lookup so an upstream rename degrades to today's behaviour rather | ||
| // than throwing from inside the safety wrapper itself. | ||
| const bus = (session.bluetooth as { dbus?: ErrorEmitter } | undefined)?.dbus | ||
| if (bus && typeof bus.on === 'function') { | ||
| bus.on('error', (err: unknown) => { | ||
| debug( | ||
| `D-Bus system bus error: ${err instanceof Error ? err.message : String(err)}` | ||
| ) | ||
| }) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } else { | ||
| debug('Could not attach D-Bus error listener — node-ble internals changed') | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| return session | ||
| } | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| import { expect } from 'chai' | ||
| import { EventEmitter } from 'node:events' | ||
| import Module from 'node:module' | ||
|
|
||
| /** | ||
| * Regression test for the BLE local provider taking the whole server down. | ||
| * | ||
| * `@naugehyde/node-ble`'s `createBluetooth()` opens a D-Bus system-bus | ||
| * connection eagerly and hands it back without an `error` listener. dbus-next | ||
| * reports transport failures by emitting `error` on that connection, so on a | ||
| * host with no reachable `/var/run/dbus/system_bus_socket` (any container that | ||
| * does not mount it) the failure arrives as an unhandled `error` event and | ||
| * Node escalates it to an uncaught exception — several seconds after the | ||
| * server has otherwise started cleanly. The `try/catch` around the awaited | ||
| * calls cannot catch it because it never travels the promise path. | ||
| * | ||
| * `createBluetoothSafe()` attaches the listener synchronously, before the | ||
| * caller can await anything, closing that window. | ||
| */ | ||
|
|
||
| // Stand-in for the dbus-next connection: an emitter whose only interesting | ||
| // behaviour is that an unlistened 'error' throws, exactly as Node's does. | ||
| class FakeBus extends EventEmitter {} | ||
|
|
||
| // Module.prototype.require is not in @types/node's public surface, so the | ||
| // patch point is described structurally rather than reached through `any`. | ||
| type Requirer = (this: unknown, id: string, ...rest: unknown[]) => unknown | ||
| interface PatchableModule { | ||
| prototype: { require: Requirer } | ||
| } | ||
|
|
||
| const patchable = Module as unknown as PatchableModule | ||
| const requireStub: Requirer = patchable.prototype.require | ||
|
|
||
| const withStubbedNodeBle = (fn: () => void) => { | ||
| patchable.prototype.require = function ( | ||
| this: unknown, | ||
| id: string, | ||
| ...rest: unknown[] | ||
| ) { | ||
| if (id === '@naugehyde/node-ble') { | ||
| return { | ||
| createBluetooth: () => { | ||
| const dbus = new FakeBus() | ||
| return { bluetooth: { dbus }, destroy: () => undefined } | ||
| } | ||
| } | ||
| } | ||
| return requireStub.call(this, id, ...rest) | ||
| } | ||
| try { | ||
| fn() | ||
| } finally { | ||
| patchable.prototype.require = requireStub | ||
| } | ||
| } | ||
|
|
||
| describe('BLE D-Bus transport errors', () => { | ||
| it('does not let a system-bus error escape as an uncaught exception', () => { | ||
| withStubbedNodeBle(() => { | ||
| // Imported inside the stub so the wrapper picks up the fake module. | ||
| const { createBluetoothSafe } = | ||
| // eslint-disable-next-line @typescript-eslint/no-require-imports | ||
| require('../src/api/ble/safeBluetooth') as typeof import('../src/api/ble/safeBluetooth') | ||
|
|
||
| const session = createBluetoothSafe() | ||
| const bus = (session.bluetooth as { dbus: FakeBus }).dbus | ||
|
|
||
| expect(bus.listenerCount('error')).to.equal( | ||
| 1, | ||
| 'expected an error listener to be attached synchronously' | ||
| ) | ||
|
|
||
| // The real failure: ENOENT on the system bus socket. Without a | ||
| // listener attached, this emit throws and takes the process down. | ||
| const emit = () => | ||
| bus.emit( | ||
| 'error', | ||
| Object.assign( | ||
| new Error('connect ENOENT /var/run/dbus/system_bus_socket'), | ||
| { | ||
| code: 'ENOENT' | ||
| } | ||
| ) | ||
| ) | ||
|
|
||
| expect(emit).to.not.throw() | ||
| }) | ||
| }) | ||
|
|
||
| it('still returns a usable session when the bus is healthy', () => { | ||
| withStubbedNodeBle(() => { | ||
| const { createBluetoothSafe } = | ||
| // eslint-disable-next-line @typescript-eslint/no-require-imports | ||
| require('../src/api/ble/safeBluetooth') as typeof import('../src/api/ble/safeBluetooth') | ||
|
|
||
| const session = createBluetoothSafe() | ||
| expect(session.bluetooth).to.be.an('object') | ||
| expect(session.destroy).to.be.a('function') | ||
| }) | ||
| }) | ||
| }) |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.