Skip to content

Commit

Permalink
fix: Prevent extension errors from crashing Zigbee2MQTT #20477
Browse files Browse the repository at this point in the history
  • Loading branch information
Koenkk committed Jan 6, 2024
1 parent b17c22b commit 212c709
Show file tree
Hide file tree
Showing 3 changed files with 20 additions and 10 deletions.
5 changes: 1 addition & 4 deletions lib/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,7 @@ export class Controller {

constructor(restartCallback: () => void, exitCallback: (code: number, restart: boolean) => void) {
logger.init();
this.eventBus = new EventBus( /* istanbul ignore next */ (error) => {
logger.error(`Error: ${error.message}`);
logger.debug(error.stack);
});
this.eventBus = new EventBus();
this.zigbee = new Zigbee(this.eventBus);
this.mqtt = new MQTT(this.eventBus);
this.state = new State(this.eventBus, this.zigbee);
Expand Down
15 changes: 9 additions & 6 deletions lib/eventBus.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import events from 'events';
events.captureRejections = true;
import logger from './util/logger';

// eslint-disable-next-line
type ListenerKey = object;
Expand All @@ -8,9 +8,8 @@ export default class EventBus {
private callbacksByExtension: { [s: string]: { event: string, callback: (...args: unknown[]) => void }[] } = {};
private emitter = new events.EventEmitter();

constructor(onError: (error: Error) => void) {
constructor() {
this.emitter.setMaxListeners(100);
this.emitter.on('error', onError);
}

public emitAdapterDisconnected(): void {
Expand Down Expand Up @@ -163,9 +162,13 @@ export default class EventBus {

private on(event: string, callback: (...args: unknown[]) => (Promise<void> | void), key: ListenerKey): void {
if (!this.callbacksByExtension[key.constructor.name]) this.callbacksByExtension[key.constructor.name] = [];
const wrappedCallback = (...args: unknown[]): void => {
// Wrap callback as it may return a Promise which can throw an exception
Promise.resolve(callback(...args)).catch();
const wrappedCallback = async (...args: unknown[]): Promise<void> => {
try {
await callback(...args);
} catch (error) {
logger.error(`EventBus error '${key.constructor.name}/${event}': ${error.message}`);
logger.debug(error.stack);
}
};
this.callbacksByExtension[key.constructor.name].push({event, callback: wrappedCallback});
this.emitter.on(event, wrappedCallback);
Expand Down
10 changes: 10 additions & 0 deletions test/controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -730,4 +730,14 @@ describe('Controller', () => {
await controller.stop();
expect(controller.state.state[device.ieeeAddr]).toStrictEqual(undefined);
});

it('EventBus should handle errors', async () => {
const eventbus = controller.eventBus;
const callback = jest.fn().mockImplementation(async () => {throw new Error('Whoops!')});
eventbus.onStateChange('test', callback);
eventbus.emitStateChange({});
await flushPromises();
expect(callback).toHaveBeenCalledTimes(1);
expect(logger.error).toHaveBeenCalledWith(`EventBus error 'String/stateChange': Whoops!`);
});
});

0 comments on commit 212c709

Please sign in to comment.