Skip to content

Commit 4b3fc99

Browse files
author
Marco Crespi
committed
fix(hci): Fix possible deadlock when connecting
1 parent 85bd95e commit 4b3fc99

File tree

8 files changed

+122
-70
lines changed

8 files changed

+122
-70
lines changed

lib/bindings/hci/misc/Hci.js

Lines changed: 44 additions & 27 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/bindings/hci/misc/Hci.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/models/Peripheral.d.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,20 @@ export declare abstract class Peripheral {
4242
get state(): PeripheralState;
4343
constructor(adapter: Adapter, uuid: string, addressType: AddressType, address: string, advertisement?: any, rssi?: number);
4444
/**
45-
* Connect to this peripheral. Does nothing if already connected.
45+
* Connect to this peripheral. Throws an error when connecting fails.
4646
*/
4747
abstract connect(): Promise<void>;
4848
/**
49-
* Disconnect from this peripheral. Does nothing if not connected.
49+
* Disconnect from this peripheral. Does nothing if not connected. This method **never** throws an error.
50+
* When connecting to a peripheral you should always wrap your calls in try-catch and call this method at the end.
51+
* ```
52+
* try {
53+
* peripheral.connect()
54+
* } catch (err) {
55+
* ...
56+
* } finally {
57+
* peripheral.disconnect();
58+
* }```
5059
*/
5160
abstract disconnect(): Promise<void>;
5261
/**

lib/models/Peripheral.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/bindings/hci/misc/Hci.ts

Lines changed: 52 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -317,37 +317,38 @@ export class Hci extends EventEmitter {
317317
const timeoutError = new Error(`HCI command timed out`);
318318

319319
return new Promise<Buffer | void>((resolve, reject) => {
320-
let isDone = false;
320+
let timeout: NodeJS.Timeout;
321+
let onComplete: (status: number, responseData?: Buffer) => void;
321322

322-
const resolveHandler = (response?: Buffer) => {
323-
if (isDone) {
324-
return;
323+
const cleanup = () => {
324+
if (statusOnly) {
325+
this.off('cmdStatus', onComplete);
326+
} else {
327+
this.off('cmdComplete', onComplete);
328+
}
329+
330+
if (timeout) {
331+
clearTimeout(timeout);
332+
timeout = null;
325333
}
326-
isDone = true;
327334

328335
this.currentCmd = null;
329336
if (release) {
330337
release();
331338
}
339+
};
332340

341+
const resolveHandler = (response?: Buffer) => {
342+
cleanup();
333343
resolve(response);
334344
};
335345

336346
const rejectHandler = (error?: any) => {
337-
if (isDone) {
338-
return;
339-
}
340-
isDone = true;
341-
342-
this.currentCmd = null;
343-
if (release) {
344-
release();
345-
}
346-
347+
cleanup();
347348
reject(error);
348349
};
349350

350-
const onComplete = (status: number, responseData?: Buffer) => {
351+
onComplete = (status: number, responseData?: Buffer) => {
351352
if (status !== 0) {
352353
const errStatus = `${STATUS_MAPPER[status]} (0x${status.toString(16).padStart(2, '0')})`;
353354
const err = new Error(`HCI Command ${this.currentCmd.cmd} failed: ${errStatus}`);
@@ -366,10 +367,10 @@ export class Hci extends EventEmitter {
366367
this.once('cmdComplete', onComplete);
367368
}
368369

370+
timeout = setTimeout(() => rejectHandler(timeoutError), this.cmdTimeout);
371+
369372
// console.log('->', 'hci', data);
370373
this.socket.write(data);
371-
372-
setTimeout(() => rejectHandler(timeoutError), this.cmdTimeout);
373374
});
374375
}
375376

@@ -579,8 +580,6 @@ export class Hci extends EventEmitter {
579580
cmd.writeUInt16LE(0x0004, 25); // min ce length
580581
cmd.writeUInt16LE(0x0006, 27); // max ce length
581582

582-
const origScope = new Error();
583-
584583
const release = await this.mutex.acquire();
585584

586585
try {
@@ -590,7 +589,32 @@ export class Hci extends EventEmitter {
590589
}
591590

592591
return new Promise<number>((resolve, reject) => {
593-
const onComplete: LeConnCompleteListener = async (status, handle, role, _addressType, _address) => {
592+
const origScope = new Error();
593+
let timeout: NodeJS.Timeout;
594+
let onComplete: LeConnCompleteListener;
595+
596+
const cleanup = () => {
597+
this.off('leConnComplete', onComplete);
598+
599+
if (timeout) {
600+
clearTimeout(timeout);
601+
timeout = null;
602+
}
603+
604+
release();
605+
};
606+
607+
const resolveHandler = (handle: number) => {
608+
cleanup();
609+
resolve(handle);
610+
};
611+
612+
const rejectHandler = (error?: any) => {
613+
cleanup();
614+
reject(error);
615+
};
616+
617+
onComplete = async (status, handle, role, _addressType, _address) => {
594618
if (_address !== address || _addressType !== addressType) {
595619
return;
596620
}
@@ -602,30 +626,27 @@ export class Hci extends EventEmitter {
602626
const err = new Error(`LE conn failed: ${errStatus}`);
603627
err.stack = err.stack.split('\n').slice(0, 2).join('\n') + '\n' + origScope.stack;
604628

605-
release();
606-
reject(err);
629+
rejectHandler(err);
607630
return;
608631
}
609632

610633
if (role !== 0) {
611634
const err = new Error(`Could not aquire le connection as master role`);
612635
err.stack = err.stack.split('\n').slice(0, 2).join('\n') + '\n' + origScope.stack;
613636

614-
release();
615-
reject(err);
637+
rejectHandler(err);
616638
return;
617639
}
618640

619-
release();
620-
resolve(handle);
641+
resolveHandler(handle);
621642
};
622643

623644
this.on('leConnComplete', onComplete);
624645

625-
this.sendCommand(cmd, true, true).catch((err) => {
626-
release();
627-
reject(err);
628-
});
646+
const timeoutError = new Error(`Creating connection timed out`);
647+
timeout = setTimeout(() => rejectHandler(timeoutError), this.cmdTimeout);
648+
649+
this.sendCommand(cmd, true, true).catch((err) => rejectHandler(err));
629650
});
630651
}
631652

src/models/Peripheral.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,20 @@ export abstract class Peripheral {
6969
}
7070

7171
/**
72-
* Connect to this peripheral. Does nothing if already connected.
72+
* Connect to this peripheral. Throws an error when connecting fails.
7373
*/
7474
public abstract connect(): Promise<void>;
7575
/**
76-
* Disconnect from this peripheral. Does nothing if not connected.
76+
* Disconnect from this peripheral. Does nothing if not connected. This method **never** throws an error.
77+
* When connecting to a peripheral you should always wrap your calls in try-catch and call this method at the end.
78+
* ```
79+
* try {
80+
* peripheral.connect()
81+
* } catch (err) {
82+
* ...
83+
* } finally {
84+
* peripheral.disconnect();
85+
* }```
7786
*/
7887
public abstract disconnect(): Promise<void>;
7988

tests/connect.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,7 @@ const main = async () => {
112112
} finally {
113113
console.log('Disconnecting...');
114114

115-
try {
116-
await peripheral.disconnect();
117-
} catch {}
115+
await peripheral.disconnect();
118116

119117
console.log('Disconnected');
120118
}

tests/parallel.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,7 @@ const main = async () => {
107107
} finally {
108108
console.log(targetAddress, 'Disconnecting...');
109109

110-
try {
111-
await peripheral.disconnect();
112-
} catch {}
110+
await peripheral.disconnect();
113111

114112
console.log(targetAddress, 'Disconnected');
115113
}

0 commit comments

Comments
 (0)