diff --git a/src/adapter/ezsp/driver/driver.ts b/src/adapter/ezsp/driver/driver.ts index 6bd0251d7d..ec17a79327 100644 --- a/src/adapter/ezsp/driver/driver.ts +++ b/src/adapter/ezsp/driver/driver.ts @@ -565,6 +565,8 @@ export class Driver extends EventEmitter { const frame = this.makeApsFrame(requestCmd as number, false); const payload = this.makeZDOframe(requestCmd as number, {transId: frame.sequence, ...params}); const waiter = this.waitFor(networkAddress, responseCmd as number, frame.sequence).start(); + // if the request takes longer than the timeout, avoid an unhandled promise rejection. + waiter.promise.catch(() => {}); const res = await this.request(networkAddress, frame, payload); if (!res) { debug.error(`zdoRequest error`); diff --git a/src/adapter/ezsp/driver/ezsp.ts b/src/adapter/ezsp/driver/ezsp.ts index 6675b83170..022a08092d 100644 --- a/src/adapter/ezsp/driver/ezsp.ts +++ b/src/adapter/ezsp/driver/ezsp.ts @@ -263,6 +263,7 @@ export class Ezsp extends EventEmitter { } public async connect(path: string, options: Record): Promise { + let lastError = null; for (let i = 1; i < 5; i += 1) { try { await this.serialDriver.connect(path, options); @@ -271,10 +272,11 @@ export class Ezsp extends EventEmitter { debug.error(`Connection attempt ${i} error: ${error.stack}`); await Wait(5000); debug.log(`Next attempt ${i+1}`); + lastError = error; } } if (!this.serialDriver.isInitialized()) { - throw new Error("Failure to connect"); + throw new Error("Failure to connect", {cause: lastError}); } if (WATCHDOG_WAKE_PERIOD) { this.watchdogTimer = setInterval( diff --git a/src/adapter/ezsp/driver/uart.ts b/src/adapter/ezsp/driver/uart.ts index 9480ffce52..9e6802f461 100644 --- a/src/adapter/ezsp/driver/uart.ts +++ b/src/adapter/ezsp/driver/uart.ts @@ -83,29 +83,35 @@ export class SerialDriver extends EventEmitter { this.parser = new Parser(); this.serialPort.pipe(this.parser); this.parser.on('parsed', this.onParsed.bind(this)); - - return new Promise((resolve, reject): void => { - this.serialPort.open(async (error): Promise => { - if (error) { - this.initialized = false; - if (this.serialPort.isOpen) { - this.serialPort.close(); + try { + await new Promise((resolve, reject): void => { + this.serialPort.open(async (error): Promise => { + if (error) { + reject(new Error(`Error while opening serialport '${error}'`)); + } else { + resolve(null); } - reject(new Error(`Error while opening serialport '${error}'`)); - } else { - debug('Serialport opened'); - this.serialPort.once('close', this.onPortClose.bind(this)); - this.serialPort.once('error', (error) => { - debug(`Serialport error: ${error}`); - }); - // reset - await this.reset(); - this.initialized = true; - this.emit('connected'); - resolve(); - } + }); }); - }); + + debug('Serialport opened'); + this.serialPort.once('close', this.onPortClose.bind(this)); + this.serialPort.once('error', (error) => { + debug(`Serialport error: ${error}`); + }); + + // reset + await this.reset(); + this.initialized = true; + this.emit('connected'); + } catch (e) { + this.initialized = false; + if (this.serialPort.isOpen) { + this.serialPort.close(); + } + throw e; + } + } private async openSocketPort(path: string): Promise { @@ -196,7 +202,7 @@ export class SerialDriver extends EventEmitter { case (data[0] === 0xC2): debug(`<-- Error: ${data.toString('hex')}`); // send reset - this.reset(); + this.reset().catch((e) => debug(`Failed to reset: ${e}`)); break; default: debug("UNKNOWN FRAME RECEIVED: %r", data); diff --git a/src/controller/controller.ts b/src/controller/controller.ts index ce574c76ac..e5a0655f44 100644 --- a/src/controller/controller.ts +++ b/src/controller/controller.ts @@ -248,8 +248,10 @@ class Controller extends events.EventEmitter { // Zigbee 3 networks automatically close after max 255 seconds, keep network open. this.permitJoinNetworkClosedTimer = setInterval(async (): Promise => { - await this.adapter.permitJoin(254, !device ? null : device.networkAddress); - await this.greenPower.permitJoin(254, !device ? null : device.networkAddress); + await catcho(async () => { + await this.adapter.permitJoin(254, !device ? null : device.networkAddress); + await this.greenPower.permitJoin(254, !device ? null : device.networkAddress); + }, "Failed to keep permit join alive"); }, 200 * 1000); if (typeof time === 'number') { diff --git a/src/utils/queue.ts b/src/utils/queue.ts index 00ebdcbc8f..f19deda67b 100644 --- a/src/utils/queue.ts +++ b/src/utils/queue.ts @@ -1,13 +1,11 @@ -interface Job { +interface Job { key: string | number; - func: () => Promise; running: boolean; - resolve: (result: T) => void; - reject: (error: Error) => void; + start: () => void; } class Queue { - private jobs: Job[]; + private jobs: Job[]; private readonly concurrent: number; constructor(concurrent = 1) { @@ -15,33 +13,41 @@ class Queue { this.concurrent = concurrent; } - public execute(func: () => Promise, key: string | number = null): Promise { - return new Promise((resolve, reject): void => { - this.jobs.push({key, func, running: false, resolve, reject}); + public async execute(func: () => Promise, key: string | number = null): Promise { + const job : Job = {key, running: false, start: null}; + // Minor optimization/workaround: various tests like the idea that a job that is + // immediately runnable is run without an event loop spin. This also helps with stack + // traces in some cases, so avoid an `await` if we can help it. + this.jobs.push(job); + if (this.getNext() !== job) { + await new Promise((resolve): void => { + job.start = (): void => { + job.running = true; + resolve(null); + }; + this.executeNext(); + }); + } else { + job.running = true; + } + + try { + return await func(); + } finally { + this.jobs.splice(this.jobs.indexOf(job), 1); this.executeNext(); - }); + } } - private async executeNext(): Promise { + private executeNext(): void { const job = this.getNext(); if (job) { - job.running = true; - - try { - const result = await job.func(); - this.jobs.splice(this.jobs.indexOf(job), 1); - job.resolve(result); - this.executeNext(); - } catch (error) { - this.jobs.splice(this.jobs.indexOf(job), 1); - job.reject(error); - this.executeNext(); - } + job.start(); } } - private getNext(): Job { + private getNext(): Job { if (this.jobs.filter((j) => j.running).length > (this.concurrent - 1)) { return null; } @@ -66,4 +72,4 @@ class Queue { } } -export default Queue; \ No newline at end of file +export default Queue; diff --git a/src/utils/waitress.ts b/src/utils/waitress.ts index b412b3b96f..a4d48cc5c9 100644 --- a/src/utils/waitress.ts +++ b/src/utils/waitress.ts @@ -58,10 +58,13 @@ class Waitress { const start = (): {promise: Promise; ID: number} => { const waiter = this.waiters.get(ID); if (waiter && !waiter.resolved && !waiter.timer) { + // Capture the stack trace from the caller of start() + const error = new Error(); + Error.captureStackTrace(error); waiter.timer = setTimeout((): void => { - const message = this.timeoutFormatter(matcher, timeout); + error.message = this.timeoutFormatter(matcher, timeout); waiter.timedout = true; - waiter.reject(new Error(message)); + waiter.reject(error); }, timeout); }