diff --git a/lib/controller.ts b/lib/controller.ts index ba3d7cc0..8039e554 100644 --- a/lib/controller.ts +++ b/lib/controller.ts @@ -144,7 +144,8 @@ export class Controller { return await this.exit(1); } - for (const extension of this.extensions) { + // copy current Set of extensions to ignore possible external extensions added while looping + for (const extension of new Set(this.extensions)) { await this.startExtension(extension); } diff --git a/test/assets/external_extensions/mjs/exampleExtension.mjs b/test/assets/external_extensions/mjs/exampleExtension.mjs index 76a37123..fbc365c2 100644 --- a/test/assets/external_extensions/mjs/exampleExtension.mjs +++ b/test/assets/external_extensions/mjs/exampleExtension.mjs @@ -2,13 +2,16 @@ export default class Example { constructor(zigbee, mqtt, state, publishEntityState, eventBus) { this.mqtt = mqtt; this.mqtt.publish('example/extension', 'call from constructor'); + this.counter = 0; } start() { this.mqtt.publish('example/extension', 'call from start'); + this.mqtt.publish('example/extension/counter', `start ${this.counter++}`); } stop() { this.mqtt.publish('example/extension', 'call from stop'); + this.mqtt.publish('example/extension/counter', `stop ${--this.counter}`); } } diff --git a/test/extensions/externalExtensions.test.ts b/test/extensions/externalExtensions.test.ts index 077bd32d..0a6d205b 100644 --- a/test/extensions/externalExtensions.test.ts +++ b/test/extensions/externalExtensions.test.ts @@ -126,6 +126,16 @@ describe('Extension: ExternalExtensions', () => { ); }); + it('starts extensions only once', async () => { + useAssets('mjs'); + + await controller.start(); + await flushPromises(); + + expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/example/extension/counter', 'start 0', {retain: false, qos: 0}); + expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/example/extension/counter', 'start 1', {retain: false, qos: 0}); + }); + it('loads all valid extensions, relocates & skips ones with errors', async () => { useAssets('mjs'); @@ -169,7 +179,9 @@ describe('Extension: ExternalExtensions', () => { {retain: true, qos: 0}, ); - extensionCode = extensionCode.replace("'call from start'", "'call from start - edited'"); + extensionCode = extensionCode + .replace("'call from start'", "'call from start - edited'") + .replace("'call from stop'", "'call from stop - edited'"); mockMQTTPublishAsync.mockClear(); await (controller.getExtension('ExternalExtensions')! as ExternalExtensions).onMQTTMessage({ @@ -177,6 +189,7 @@ describe('Extension: ExternalExtensions', () => { message: {name: extensionName, code: extensionCode}, }); + expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'call from stop', {retain: false, qos: 0}); expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'call from start - edited', {retain: false, qos: 0}); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/extensions', @@ -187,7 +200,9 @@ describe('Extension: ExternalExtensions', () => { {retain: true, qos: 0}, ); - extensionCode = extensionCode.replace("'call from start - edited'", "'call from start'"); + extensionCode = extensionCode + .replace("'call from start - edited'", "'call from start'") + .replace("'call from stop - edited'", "'call from stop'"); mockMQTTPublishAsync.mockClear(); await (controller.getExtension('ExternalExtensions')! as ExternalExtensions).onMQTTMessage({ @@ -195,6 +210,7 @@ describe('Extension: ExternalExtensions', () => { message: {name: 'exampleExtension.1.js', code: extensionCode}, }); + expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'call from stop - edited', {retain: false, qos: 0}); expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'call from start', {retain: false, qos: 0}); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/extensions',