@onekeyfe/hd-transport-electron 1.2.2-alpha.102 → 1.2.2-alpha.104
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{index-8e6acfc1.js → index-985e2bf3.js} +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/{noble-ble-handler-3a1d131d.js → noble-ble-handler-263d4736.js} +120 -31
- package/dist/noble-ble-handler.d.ts.map +1 -1
- package/dist/types/noble-extended.d.ts +1 -0
- package/dist/types/noble-extended.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/noble-ble-handler.test.ts +153 -9
- package/src/noble-ble-handler.ts +118 -31
- package/src/types/noble-extended.ts +1 -0
|
@@ -55,13 +55,13 @@ function invokeNobleBleIpc(request) {
|
|
|
55
55
|
|
|
56
56
|
function initNobleBleSupport(webContents) {
|
|
57
57
|
return __awaiter(this, void 0, void 0, function* () {
|
|
58
|
-
const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-
|
|
58
|
+
const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-263d4736.js'); });
|
|
59
59
|
setupNobleBleHandlers(webContents);
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
62
|
function disposeNobleBleSupport(releaseNoble) {
|
|
63
63
|
return __awaiter(this, void 0, void 0, function* () {
|
|
64
|
-
const handler = yield Promise.resolve().then(function () { return require('./noble-ble-handler-
|
|
64
|
+
const handler = yield Promise.resolve().then(function () { return require('./noble-ble-handler-263d4736.js'); });
|
|
65
65
|
yield handler.disposeNobleBleSupport(releaseNoble);
|
|
66
66
|
});
|
|
67
67
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ interface NobleModule {
|
|
|
18
18
|
startScanning(serviceUUIDs: string[], allowDuplicates: boolean, callback?: (error?: Error) => void): void;
|
|
19
19
|
stopScanning(callback?: () => void): void;
|
|
20
20
|
stop(): void;
|
|
21
|
+
cancelConnect?(id: string): void;
|
|
21
22
|
on(event: 'stateChange', listener: (state: string) => void): void;
|
|
22
23
|
on(event: 'discover', listener: (peripheral: Peripheral) => void): void;
|
|
23
24
|
removeListener(event: 'stateChange', listener: (state: string) => void): void;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var index = require('./index-
|
|
3
|
+
var index = require('./index-985e2bf3.js');
|
|
4
4
|
var hdShared = require('@onekeyfe/hd-shared');
|
|
5
5
|
var pRetry = require('p-retry');
|
|
6
6
|
|
|
@@ -101,9 +101,21 @@ const NOBLE_BLE_SUBSCRIBE_TIMEOUT_MS = 10000;
|
|
|
101
101
|
let noble = null;
|
|
102
102
|
let logger = null;
|
|
103
103
|
let disposing = false;
|
|
104
|
+
let nativeReleased = false;
|
|
104
105
|
let disposePromise;
|
|
106
|
+
let windowCleanup;
|
|
105
107
|
let removeIpcHandlers;
|
|
106
108
|
const pendingCancellations = new Set();
|
|
109
|
+
const pendingEnumerations = new Set();
|
|
110
|
+
const nativeConnections = new Set();
|
|
111
|
+
function trackNativeConnection(operation, cancel) {
|
|
112
|
+
const connection = {
|
|
113
|
+
cancel,
|
|
114
|
+
settled: operation.then(() => undefined, () => undefined),
|
|
115
|
+
};
|
|
116
|
+
nativeConnections.add(connection);
|
|
117
|
+
connection.settled = connection.settled.finally(() => nativeConnections.delete(connection));
|
|
118
|
+
}
|
|
107
119
|
function assertBleActive() {
|
|
108
120
|
if (disposing) {
|
|
109
121
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, 'Noble BLE is shutting down');
|
|
@@ -182,9 +194,24 @@ function connectPeripheralWithCancellation(peripheral, deviceId, messagePrefix =
|
|
|
182
194
|
(_a = connectingDevices
|
|
183
195
|
.get(deviceId)) === null || _a === void 0 ? void 0 : _a.cancel(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, `Device ${deviceId} connect attempt was superseded`));
|
|
184
196
|
connectingDevices.set(deviceId, pendingConnection);
|
|
185
|
-
|
|
197
|
+
let completeNative;
|
|
198
|
+
trackNativeConnection(new Promise(resolve => {
|
|
199
|
+
completeNative = resolve;
|
|
200
|
+
}), () => {
|
|
201
|
+
var _a;
|
|
202
|
+
if (typeof peripheral.cancelConnect === 'function')
|
|
203
|
+
peripheral.cancelConnect();
|
|
204
|
+
else
|
|
205
|
+
(_a = noble === null || noble === void 0 ? void 0 : noble.cancelConnect) === null || _a === void 0 ? void 0 : _a.call(noble, deviceId);
|
|
206
|
+
});
|
|
207
|
+
const onConnect = (error) => {
|
|
208
|
+
if (nativeReleased) {
|
|
209
|
+
completeNative();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
186
212
|
if (error) {
|
|
187
213
|
settle(() => reject(createNobleBleConnectionError(error, messagePrefix)));
|
|
214
|
+
completeNative();
|
|
188
215
|
return;
|
|
189
216
|
}
|
|
190
217
|
if (!settle(() => {
|
|
@@ -195,13 +222,27 @@ function connectPeripheralWithCancellation(peripheral, deviceId, messagePrefix =
|
|
|
195
222
|
if (!activeConnection || activeConnection.peripheral !== peripheral) {
|
|
196
223
|
try {
|
|
197
224
|
peripheral.removeAllListeners('disconnect');
|
|
198
|
-
peripheral.disconnect(() =>
|
|
225
|
+
runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
226
|
+
timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
|
|
227
|
+
timeoutBehavior: 'resolve',
|
|
228
|
+
})
|
|
229
|
+
.catch(() => undefined)
|
|
230
|
+
.finally(completeNative);
|
|
231
|
+
return;
|
|
199
232
|
}
|
|
200
233
|
catch (_a) {
|
|
201
234
|
}
|
|
202
235
|
}
|
|
203
236
|
}
|
|
204
|
-
|
|
237
|
+
completeNative();
|
|
238
|
+
};
|
|
239
|
+
try {
|
|
240
|
+
peripheral.connect(onConnect);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
completeNative();
|
|
244
|
+
settle(() => reject(error));
|
|
245
|
+
}
|
|
205
246
|
});
|
|
206
247
|
}
|
|
207
248
|
const pairedDevices = new Set();
|
|
@@ -833,7 +874,7 @@ function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN
|
|
|
833
874
|
});
|
|
834
875
|
});
|
|
835
876
|
}
|
|
836
|
-
function enumerateDevices() {
|
|
877
|
+
function enumerateDevices(isWindowDestroyed) {
|
|
837
878
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
838
879
|
if (!noble) {
|
|
839
880
|
yield initializeNoble();
|
|
@@ -842,6 +883,8 @@ function enumerateDevices() {
|
|
|
842
883
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
843
884
|
}
|
|
844
885
|
assertBleActive();
|
|
886
|
+
if (isWindowDestroyed())
|
|
887
|
+
return [];
|
|
845
888
|
const nobleInstance = noble;
|
|
846
889
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Starting device enumeration');
|
|
847
890
|
discoveredDevices.clear();
|
|
@@ -850,14 +893,20 @@ function enumerateDevices() {
|
|
|
850
893
|
const devices = [];
|
|
851
894
|
let settled = false;
|
|
852
895
|
let intervalId;
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
896
|
+
let cleanupPromise;
|
|
897
|
+
const cleanup = () => {
|
|
898
|
+
if (!cleanupPromise) {
|
|
899
|
+
settled = true;
|
|
900
|
+
pendingCancellations.delete(cancel);
|
|
901
|
+
clearTimeout(timeoutId);
|
|
902
|
+
if (intervalId)
|
|
903
|
+
clearInterval(intervalId);
|
|
904
|
+
cleanupPromise = waitForNobleScanStop(nobleInstance).finally(() => {
|
|
905
|
+
pendingEnumerations.delete(cancel);
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
return cleanupPromise;
|
|
909
|
+
};
|
|
861
910
|
const pushDevice = (peripheral, id) => {
|
|
862
911
|
var _a;
|
|
863
912
|
if (devices.some(d => d.id === id))
|
|
@@ -881,10 +930,9 @@ function enumerateDevices() {
|
|
|
881
930
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scan completed, found devices:', devices.length);
|
|
882
931
|
resolve(devices);
|
|
883
932
|
}), DEVICE_SCAN_TIMEOUT);
|
|
884
|
-
const cancel = () =>
|
|
885
|
-
cleanup().then(() => resolve([]), reject);
|
|
886
|
-
};
|
|
933
|
+
const cancel = () => cleanup().then(() => resolve([]), reject);
|
|
887
934
|
pendingCancellations.add(cancel);
|
|
935
|
+
pendingEnumerations.add(cancel);
|
|
888
936
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning for OneKey BLE devices');
|
|
889
937
|
nobleInstance.startScanning([], true, (error) => index.__awaiter(this, void 0, void 0, function* () {
|
|
890
938
|
if (settled)
|
|
@@ -903,7 +951,7 @@ function enumerateDevices() {
|
|
|
903
951
|
}
|
|
904
952
|
function stopScanning() {
|
|
905
953
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
906
|
-
if (!noble)
|
|
954
|
+
if (!noble || nativeReleased)
|
|
907
955
|
return;
|
|
908
956
|
const nobleInstance = noble;
|
|
909
957
|
yield waitForNobleScanStop(nobleInstance);
|
|
@@ -1168,12 +1216,28 @@ function tryDirectConnectById(deviceId) {
|
|
|
1168
1216
|
return undefined;
|
|
1169
1217
|
let timer;
|
|
1170
1218
|
let cancel;
|
|
1219
|
+
let abandoned = false;
|
|
1171
1220
|
try {
|
|
1172
1221
|
const directPromise = nobleInstance.connectAsync(deviceId);
|
|
1222
|
+
trackNativeConnection(directPromise.then((late) => index.__awaiter(this, void 0, void 0, function* () {
|
|
1223
|
+
if ((!abandoned && !disposing) || nativeReleased)
|
|
1224
|
+
return;
|
|
1225
|
+
const peripheral = late !== null && late !== void 0 ? late : discoveredDevices.get(deviceId);
|
|
1226
|
+
if ((peripheral === null || peripheral === void 0 ? void 0 : peripheral.state) === 'connected' && !connectedDevices.has(deviceId)) {
|
|
1227
|
+
peripheral.removeAllListeners('disconnect');
|
|
1228
|
+
yield runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1229
|
+
timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
|
|
1230
|
+
timeoutBehavior: 'resolve',
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
})), () => { var _a; return (_a = nobleInstance.cancelConnect) === null || _a === void 0 ? void 0 : _a.call(nobleInstance, deviceId); });
|
|
1173
1234
|
const raced = yield Promise.race([
|
|
1174
1235
|
directPromise,
|
|
1175
1236
|
new Promise(resolve => {
|
|
1176
|
-
cancel = () =>
|
|
1237
|
+
cancel = () => {
|
|
1238
|
+
abandoned = true;
|
|
1239
|
+
resolve('timeout');
|
|
1240
|
+
};
|
|
1177
1241
|
pendingCancellations.add(cancel);
|
|
1178
1242
|
timer = setTimeout(cancel, DIRECT_CONNECT_TIMEOUT_MS);
|
|
1179
1243
|
}),
|
|
@@ -1183,17 +1247,6 @@ function tryDirectConnectById(deviceId) {
|
|
|
1183
1247
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct connect-by-id timed out, falling back to scan', {
|
|
1184
1248
|
deviceId,
|
|
1185
1249
|
});
|
|
1186
|
-
directPromise
|
|
1187
|
-
.then(late => {
|
|
1188
|
-
const latePeripheral = late !== null && late !== void 0 ? late : discoveredDevices.get(deviceId);
|
|
1189
|
-
if (latePeripheral &&
|
|
1190
|
-
latePeripheral.state === 'connected' &&
|
|
1191
|
-
!connectedDevices.has(deviceId)) {
|
|
1192
|
-
latePeripheral.removeAllListeners('disconnect');
|
|
1193
|
-
latePeripheral.disconnect(() => undefined);
|
|
1194
|
-
}
|
|
1195
|
-
})
|
|
1196
|
-
.catch(() => undefined);
|
|
1197
1250
|
return undefined;
|
|
1198
1251
|
}
|
|
1199
1252
|
const peripheral = raced !== null && raced !== void 0 ? raced : discoveredDevices.get(deviceId);
|
|
@@ -1333,6 +1386,8 @@ function connectDevice(deviceId, webContents) {
|
|
|
1333
1386
|
function disconnectDevice(deviceId) {
|
|
1334
1387
|
var _a;
|
|
1335
1388
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
1389
|
+
if (nativeReleased)
|
|
1390
|
+
return;
|
|
1336
1391
|
const pendingConnection = connectingDevices.get(deviceId);
|
|
1337
1392
|
const peripheral = (_a = connectedDevices.get(deviceId)) !== null && _a !== void 0 ? _a : pendingConnection === null || pendingConnection === void 0 ? void 0 : pendingConnection.peripheral;
|
|
1338
1393
|
if (!peripheral) {
|
|
@@ -1362,6 +1417,8 @@ function disconnectDevice(deviceId) {
|
|
|
1362
1417
|
}
|
|
1363
1418
|
function unsubscribeNotifications(deviceId) {
|
|
1364
1419
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
1420
|
+
if (nativeReleased)
|
|
1421
|
+
return;
|
|
1365
1422
|
const peripheral = connectedDevices.get(deviceId);
|
|
1366
1423
|
const characteristics = deviceCharacteristics.get(deviceId);
|
|
1367
1424
|
if (!peripheral || !characteristics) {
|
|
@@ -1469,6 +1526,7 @@ function subscribeNotifications(deviceId, callback) {
|
|
|
1469
1526
|
function setupNobleBleHandlers(webContents) {
|
|
1470
1527
|
if (disposing)
|
|
1471
1528
|
return;
|
|
1529
|
+
let windowDestroyed = false;
|
|
1472
1530
|
try {
|
|
1473
1531
|
logger = require('electron-log');
|
|
1474
1532
|
const { ipcMain } = require('electron');
|
|
@@ -1479,7 +1537,12 @@ function setupNobleBleHandlers(webContents) {
|
|
|
1479
1537
|
ipcMain.removeHandler(channel);
|
|
1480
1538
|
ipcMain.handle(channel, (...args) => index.__awaiter(this, void 0, void 0, function* () {
|
|
1481
1539
|
try {
|
|
1540
|
+
if (windowCleanup)
|
|
1541
|
+
yield windowCleanup;
|
|
1482
1542
|
assertBleActive();
|
|
1543
|
+
if (windowDestroyed) {
|
|
1544
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, 'BLE window destroyed');
|
|
1545
|
+
}
|
|
1483
1546
|
return yield Promise.resolve(listener(...args));
|
|
1484
1547
|
}
|
|
1485
1548
|
catch (error) {
|
|
@@ -1490,7 +1553,7 @@ function setupNobleBleHandlers(webContents) {
|
|
|
1490
1553
|
safeLog(logger, 'info', 'Setting up Noble BLE IPC handlers');
|
|
1491
1554
|
handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE, () => index.__awaiter(this, void 0, void 0, function* () {
|
|
1492
1555
|
try {
|
|
1493
|
-
const devices = yield enumerateDevices();
|
|
1556
|
+
const devices = yield enumerateDevices(() => windowDestroyed);
|
|
1494
1557
|
safeLog(logger, 'debug', 'Enumeration completed', {
|
|
1495
1558
|
count: devices.length,
|
|
1496
1559
|
devices: devices.map(device => ({ id: device.id, name: device.name })),
|
|
@@ -1582,15 +1645,29 @@ function setupNobleBleHandlers(webContents) {
|
|
|
1582
1645
|
}
|
|
1583
1646
|
}));
|
|
1584
1647
|
webContents.on('destroyed', () => {
|
|
1648
|
+
windowDestroyed = true;
|
|
1585
1649
|
if (disposing)
|
|
1586
1650
|
return;
|
|
1587
1651
|
safeLog(logger, 'info', 'Cleaning up Noble BLE handlers');
|
|
1588
|
-
|
|
1652
|
+
const previousCleanup = windowCleanup;
|
|
1653
|
+
const enumerations = Array.from(pendingEnumerations, cancel => cancel());
|
|
1654
|
+
windowCleanup = (() => index.__awaiter(this, void 0, void 0, function* () {
|
|
1655
|
+
if (previousCleanup)
|
|
1656
|
+
yield previousCleanup;
|
|
1657
|
+
yield Promise.allSettled(enumerations);
|
|
1658
|
+
if (disposing)
|
|
1659
|
+
return;
|
|
1589
1660
|
const deviceIds = Array.from(connectedDevices.keys());
|
|
1590
1661
|
for (const deviceId of deviceIds) {
|
|
1662
|
+
if (disposing)
|
|
1663
|
+
return;
|
|
1591
1664
|
yield unsubscribeNotifications(deviceId).catch(() => undefined);
|
|
1665
|
+
if (disposing)
|
|
1666
|
+
return;
|
|
1592
1667
|
yield disconnectDevice(deviceId).catch(() => undefined);
|
|
1593
1668
|
}
|
|
1669
|
+
if (disposing)
|
|
1670
|
+
return;
|
|
1594
1671
|
yield stopScanning().catch(() => undefined);
|
|
1595
1672
|
cleanupNobleListeners();
|
|
1596
1673
|
discoveredDevices.clear();
|
|
@@ -1621,11 +1698,22 @@ function disposeNobleBleSupport(releaseNoble = instance => instance.stop()) {
|
|
|
1621
1698
|
for (const pending of connectingDevices.values()) {
|
|
1622
1699
|
pending.cancel(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, 'Noble BLE is shutting down'));
|
|
1623
1700
|
}
|
|
1701
|
+
const connections = Array.from(nativeConnections);
|
|
1702
|
+
for (const connection of connections) {
|
|
1703
|
+
try {
|
|
1704
|
+
connection.cancel();
|
|
1705
|
+
}
|
|
1706
|
+
catch (error) {
|
|
1707
|
+
logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Native connect cancellation failed', error);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1624
1710
|
let timeout;
|
|
1625
1711
|
disposePromise = (() => index.__awaiter(this, void 0, void 0, function* () {
|
|
1626
1712
|
try {
|
|
1627
1713
|
yield Promise.race([
|
|
1628
1714
|
Promise.allSettled([
|
|
1715
|
+
windowCleanup,
|
|
1716
|
+
...connections.map(connection => connection.settled),
|
|
1629
1717
|
stopScanning(),
|
|
1630
1718
|
...pendingPeripherals.map(peripheral => runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1631
1719
|
timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
|
|
@@ -1646,6 +1734,7 @@ function disposeNobleBleSupport(releaseNoble = instance => instance.stop()) {
|
|
|
1646
1734
|
}
|
|
1647
1735
|
finally {
|
|
1648
1736
|
clearTimeout(timeout);
|
|
1737
|
+
nativeReleased = true;
|
|
1649
1738
|
cleanupNobleListeners();
|
|
1650
1739
|
if (instance && persistentStateListener) {
|
|
1651
1740
|
instance.removeListener('stateChange', persistentStateListener);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAA+B,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,KAAK,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAA+B,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,KAAK,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAiC1F,KAAK,mBAAmB,GAAG,KAAK,GAAG;IACjC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,mBAAmB,EAAE,aAAa,SAAK,+CAiB3F;AAED,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,OAAO,GAAG,wBAAwB,CAgCvF;AA+KD,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE,oBAAoB,UAI7E;AAkyDD,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAkOpE;AAOD,wBAAgB,sBAAsB,CACpC,YAAY,GAAE,CAAC,QAAQ,EAAE;IAAE,IAAI,IAAI,IAAI,CAAA;CAAE,KAAK,IAAkC,GAC/E,OAAO,CAAC,IAAI,CAAC,CAiEf"}
|
|
@@ -15,6 +15,7 @@ export interface NobleModule {
|
|
|
15
15
|
startScanning(serviceUUIDs: string[], allowDuplicates: boolean, callback?: (error?: Error) => void): void;
|
|
16
16
|
stopScanning(callback?: () => void): void;
|
|
17
17
|
stop(): void;
|
|
18
|
+
cancelConnect?(id: string): void;
|
|
18
19
|
on(event: 'stateChange', listener: (state: string) => void): void;
|
|
19
20
|
on(event: 'discover', listener: (peripheral: Peripheral) => void): void;
|
|
20
21
|
removeListener(event: 'stateChange', listener: (state: string) => void): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"noble-extended.d.ts","sourceRoot":"","sources":["../../src/types/noble-extended.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAGnE,MAAM,WAAW,UAAW,SAAQ,oBAAoB;IACtD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAGD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,cAAc,CAAC;CACxB;AAGD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CACX,YAAY,EAAE,MAAM,EAAE,EACtB,eAAe,EAAE,OAAO,EACxB,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,GACjC,IAAI,CAAC;IACR,YAAY,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC1C,IAAI,IAAI,IAAI,CAAC;IACb,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAClE,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;IACxE,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAC9E,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;CACrF;AAGD,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC7C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;CAC9C;AAGD,wBAAgB,OAAO,CACrB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,EACjC,OAAO,EAAE,MAAM,EACf,GAAG,IAAI,EAAE,GAAG,EAAE,GACb,IAAI,CAMN"}
|
|
1
|
+
{"version":3,"file":"noble-extended.d.ts","sourceRoot":"","sources":["../../src/types/noble-extended.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAGnE,MAAM,WAAW,UAAW,SAAQ,oBAAoB;IACtD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAGD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,cAAc,CAAC;CACxB;AAGD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CACX,YAAY,EAAE,MAAM,EAAE,EACtB,eAAe,EAAE,OAAO,EACxB,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,GACjC,IAAI,CAAC;IACR,YAAY,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC1C,IAAI,IAAI,IAAI,CAAC;IACb,aAAa,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAClE,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;IACxE,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAC9E,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;CACrF;AAGD,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC7C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;CAC9C;AAGD,wBAAgB,OAAO,CACrB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,EACjC,OAAO,EAAE,MAAM,EACf,GAAG,IAAI,EAAE,GAAG,EAAE,GACb,IAAI,CAMN"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport-electron",
|
|
3
|
-
"version": "1.2.2-alpha.
|
|
3
|
+
"version": "1.2.2-alpha.104",
|
|
4
4
|
"author": "OneKey",
|
|
5
5
|
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"electron-log": ">=4.0.0"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@onekeyfe/hd-core": "1.2.2-alpha.
|
|
29
|
-
"@onekeyfe/hd-shared": "1.2.2-alpha.
|
|
30
|
-
"@onekeyfe/hd-transport": "1.2.2-alpha.
|
|
28
|
+
"@onekeyfe/hd-core": "1.2.2-alpha.104",
|
|
29
|
+
"@onekeyfe/hd-shared": "1.2.2-alpha.104",
|
|
30
|
+
"@onekeyfe/hd-transport": "1.2.2-alpha.104",
|
|
31
31
|
"@stoprocent/noble": "2.3.16",
|
|
32
32
|
"p-retry": "^4.6.2"
|
|
33
33
|
},
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
"electron": "^25.0.0",
|
|
37
37
|
"typescript": "^5.3.3"
|
|
38
38
|
},
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "67835939d9f779ffd50cd8b482d8c5b53bd580bd"
|
|
40
40
|
}
|
|
@@ -750,6 +750,10 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
750
750
|
});
|
|
751
751
|
|
|
752
752
|
describe('Noble BLE process shutdown', () => {
|
|
753
|
+
const flushCallbacks = () =>
|
|
754
|
+
new Promise<void>(resolve => {
|
|
755
|
+
setImmediate(resolve);
|
|
756
|
+
});
|
|
753
757
|
afterEach(() => {
|
|
754
758
|
jest.useRealTimers();
|
|
755
759
|
jest.resetModules();
|
|
@@ -825,16 +829,20 @@ describe('Noble BLE process shutdown', () => {
|
|
|
825
829
|
expect(jest.getTimerCount()).toBe(0);
|
|
826
830
|
});
|
|
827
831
|
|
|
828
|
-
test('
|
|
832
|
+
test('cancels native connects and waits for disconnect before release', async () => {
|
|
829
833
|
const { sdk, native, handlers } = await setup();
|
|
830
834
|
await handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK)?.({});
|
|
831
|
-
let connectCallback: (() => void) | undefined;
|
|
835
|
+
let connectCallback: ((error?: Error) => void) | undefined;
|
|
836
|
+
let disconnectCallback: (() => void) | undefined;
|
|
832
837
|
const peripheral = Object.assign(new EventEmitter(), {
|
|
833
838
|
...createPeripheral('pending-device', 'OneKey Pro'),
|
|
834
839
|
connect: jest.fn(callback => {
|
|
835
840
|
connectCallback = callback;
|
|
836
841
|
}),
|
|
837
|
-
|
|
842
|
+
cancelConnect: jest.fn(() => connectCallback?.(new Error('connection canceled'))),
|
|
843
|
+
disconnect: jest.fn(callback => {
|
|
844
|
+
disconnectCallback = callback;
|
|
845
|
+
}),
|
|
838
846
|
discoverServices: jest.fn(),
|
|
839
847
|
});
|
|
840
848
|
native.emit('discover', peripheral);
|
|
@@ -842,14 +850,70 @@ describe('Noble BLE process shutdown', () => {
|
|
|
842
850
|
await Promise.resolve();
|
|
843
851
|
await Promise.resolve();
|
|
844
852
|
expect(peripheral.connect).toHaveBeenCalledTimes(1);
|
|
845
|
-
|
|
853
|
+
const disposing = sdk.disposeNobleBleSupport();
|
|
854
|
+
await flushCallbacks();
|
|
855
|
+
expect(peripheral.cancelConnect).toHaveBeenCalledTimes(1);
|
|
846
856
|
expect(peripheral.disconnect).toHaveBeenCalled();
|
|
857
|
+
expect(native.stop).not.toHaveBeenCalled();
|
|
858
|
+
disconnectCallback?.();
|
|
859
|
+
await disposing;
|
|
847
860
|
expect(native.stop).toHaveBeenCalledTimes(1);
|
|
848
|
-
connectCallback?.();
|
|
849
861
|
expect(await connecting).toMatchObject({ success: false });
|
|
850
862
|
expect(peripheral.discoverServices).not.toHaveBeenCalled();
|
|
851
863
|
});
|
|
852
864
|
|
|
865
|
+
test.each(['peripheral', 'direct'])(
|
|
866
|
+
'keeps late %s connects inert after the disposal timeout',
|
|
867
|
+
async route => {
|
|
868
|
+
jest.useFakeTimers({ doNotFake: ['performance', 'setImmediate'] });
|
|
869
|
+
const { sdk, native, handlers } = await setup();
|
|
870
|
+
await handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK)?.({});
|
|
871
|
+
let finishConnect: () => void = () => undefined;
|
|
872
|
+
const peripheral = Object.assign(new EventEmitter(), {
|
|
873
|
+
...createPeripheral('late-device', 'OneKey Pro'),
|
|
874
|
+
connect: jest.fn(callback => {
|
|
875
|
+
finishConnect = () => callback();
|
|
876
|
+
}),
|
|
877
|
+
cancelConnect: jest.fn(),
|
|
878
|
+
disconnect: jest.fn(callback => callback?.()),
|
|
879
|
+
discoverServices: jest.fn(),
|
|
880
|
+
});
|
|
881
|
+
const connectAsync = jest.fn(
|
|
882
|
+
() =>
|
|
883
|
+
new Promise(resolve => {
|
|
884
|
+
finishConnect = () => resolve(peripheral);
|
|
885
|
+
})
|
|
886
|
+
);
|
|
887
|
+
const cancelConnect = jest.fn();
|
|
888
|
+
if (route === 'direct') Object.assign(native, { connectAsync, cancelConnect });
|
|
889
|
+
else native.emit('discover', peripheral);
|
|
890
|
+
const connecting = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT)?.({}, peripheral.id);
|
|
891
|
+
await flushCallbacks();
|
|
892
|
+
if (route === 'direct') {
|
|
893
|
+
jest.advanceTimersByTime(1500);
|
|
894
|
+
await flushCallbacks();
|
|
895
|
+
}
|
|
896
|
+
expect(route === 'direct' ? connectAsync : peripheral.connect).toHaveBeenCalledTimes(1);
|
|
897
|
+
const disposing = sdk.disposeNobleBleSupport();
|
|
898
|
+
await flushCallbacks();
|
|
899
|
+
expect(route === 'direct' ? cancelConnect : peripheral.cancelConnect).toHaveBeenCalledTimes(
|
|
900
|
+
1
|
|
901
|
+
);
|
|
902
|
+
expect(native.stop).not.toHaveBeenCalled();
|
|
903
|
+
jest.advanceTimersByTime(3500);
|
|
904
|
+
await disposing;
|
|
905
|
+
const disconnects = peripheral.disconnect.mock.calls.length;
|
|
906
|
+
peripheral.state = 'connected';
|
|
907
|
+
finishConnect();
|
|
908
|
+
await flushCallbacks();
|
|
909
|
+
expect(peripheral.disconnect).toHaveBeenCalledTimes(disconnects);
|
|
910
|
+
expect(peripheral.discoverServices).not.toHaveBeenCalled();
|
|
911
|
+
expect(native.stop).toHaveBeenCalledTimes(1);
|
|
912
|
+
expect(await connecting).toMatchObject({ success: false });
|
|
913
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
914
|
+
}
|
|
915
|
+
);
|
|
916
|
+
|
|
853
917
|
test('allows a host to defer shared native release until both transports are idle', async () => {
|
|
854
918
|
const { sdk, native, handlers } = await setup();
|
|
855
919
|
await handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK)?.({});
|
|
@@ -859,16 +923,96 @@ describe('Noble BLE process shutdown', () => {
|
|
|
859
923
|
expect(native.stop).not.toHaveBeenCalled();
|
|
860
924
|
});
|
|
861
925
|
|
|
862
|
-
test('window
|
|
926
|
+
test('replacement window scanning waits for old cleanup and preserves native', async () => {
|
|
927
|
+
jest.useFakeTimers({ doNotFake: ['performance', 'setImmediate'] });
|
|
863
928
|
const { sdk, native, handlers, window } = await setup();
|
|
864
929
|
await handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK)?.({});
|
|
930
|
+
let finishOldCleanup: (() => void) | undefined;
|
|
931
|
+
native.stopScanning.mockImplementationOnce(callback => {
|
|
932
|
+
finishOldCleanup = callback;
|
|
933
|
+
});
|
|
865
934
|
window.emit('destroyed');
|
|
866
|
-
await Promise.resolve();
|
|
867
|
-
await Promise.resolve();
|
|
868
935
|
sdk.setupNobleBleHandlers(new EventEmitter() as unknown as WebContents);
|
|
869
|
-
|
|
936
|
+
const scan = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE)?.({});
|
|
937
|
+
await flushCallbacks();
|
|
938
|
+
expect(native.startScanning).not.toHaveBeenCalled();
|
|
939
|
+
finishOldCleanup?.();
|
|
940
|
+
await flushCallbacks();
|
|
941
|
+
expect(native.startScanning).toHaveBeenCalledTimes(1);
|
|
942
|
+
expect(native.listenerCount('discover')).toBe(1);
|
|
943
|
+
native.emit('discover', createPeripheral('replacement-device', 'OneKey Pro'));
|
|
944
|
+
jest.advanceTimersByTime(5000);
|
|
945
|
+
expect(await scan).toEqual([expect.objectContaining({ id: 'replacement-device' })]);
|
|
870
946
|
expect(native.stop).not.toHaveBeenCalled();
|
|
871
947
|
await sdk.disposeNobleBleSupport();
|
|
872
948
|
expect(native.stop).toHaveBeenCalledTimes(1);
|
|
873
949
|
});
|
|
950
|
+
|
|
951
|
+
test.each(['started', 'late-start-callback'])(
|
|
952
|
+
'retires an old enumeration before a shared native scan resumes: %s',
|
|
953
|
+
async scanState => {
|
|
954
|
+
jest.useFakeTimers({ doNotFake: ['performance', 'setImmediate'] });
|
|
955
|
+
const { sdk, native, handlers, window } = await setup();
|
|
956
|
+
let lateStart: (() => void) | undefined;
|
|
957
|
+
if (scanState === 'late-start-callback') {
|
|
958
|
+
native.startScanning.mockImplementationOnce((_uuids, _duplicates, callback) => {
|
|
959
|
+
lateStart = callback;
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
let oldScanSettled = false;
|
|
963
|
+
const oldScan = Promise.resolve(
|
|
964
|
+
handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE)?.({})
|
|
965
|
+
).then(result => {
|
|
966
|
+
oldScanSettled = true;
|
|
967
|
+
return result;
|
|
968
|
+
});
|
|
969
|
+
await flushCallbacks();
|
|
970
|
+
jest.advanceTimersByTime(1000);
|
|
971
|
+
|
|
972
|
+
let finishOldScan: (() => void) | undefined;
|
|
973
|
+
native.stopScanning.mockImplementationOnce(callback => {
|
|
974
|
+
finishOldScan = callback;
|
|
975
|
+
});
|
|
976
|
+
window.emit('destroyed');
|
|
977
|
+
sdk.setupNobleBleHandlers(new EventEmitter() as unknown as WebContents);
|
|
978
|
+
const replacementReady = handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK)?.({});
|
|
979
|
+
let replacementSettled = false;
|
|
980
|
+
Promise.resolve(replacementReady).then(() => {
|
|
981
|
+
replacementSettled = true;
|
|
982
|
+
});
|
|
983
|
+
await flushCallbacks();
|
|
984
|
+
expect(replacementSettled).toBe(false);
|
|
985
|
+
finishOldScan?.();
|
|
986
|
+
await replacementReady;
|
|
987
|
+
await flushCallbacks();
|
|
988
|
+
expect(oldScanSettled).toBe(true);
|
|
989
|
+
expect(await oldScan).toEqual([]);
|
|
990
|
+
|
|
991
|
+
// Another transport can now scan on the same process-wide Noble instance.
|
|
992
|
+
native.startScanning([], true, () => undefined);
|
|
993
|
+
const stopCount = native.stopScanning.mock.calls.length;
|
|
994
|
+
lateStart?.();
|
|
995
|
+
jest.advanceTimersByTime(5000);
|
|
996
|
+
await flushCallbacks();
|
|
997
|
+
expect(native.stopScanning).toHaveBeenCalledTimes(stopCount);
|
|
998
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
999
|
+
expect(native.stop).not.toHaveBeenCalled();
|
|
1000
|
+
await sdk.disposeNobleBleSupport();
|
|
1001
|
+
}
|
|
1002
|
+
);
|
|
1003
|
+
|
|
1004
|
+
test('does not start an old window enumeration after Bluetooth becomes ready', async () => {
|
|
1005
|
+
const { sdk, native, handlers, window } = await setup('unknown');
|
|
1006
|
+
const oldScan = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE)?.({});
|
|
1007
|
+
await flushCallbacks();
|
|
1008
|
+
window.emit('destroyed');
|
|
1009
|
+
sdk.setupNobleBleHandlers(new EventEmitter() as unknown as WebContents);
|
|
1010
|
+
await handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK)?.({});
|
|
1011
|
+
|
|
1012
|
+
native.state = 'poweredOn';
|
|
1013
|
+
native.emit('stateChange', 'poweredOn');
|
|
1014
|
+
expect(await oldScan).toEqual([]);
|
|
1015
|
+
expect(native.startScanning).not.toHaveBeenCalled();
|
|
1016
|
+
await sdk.disposeNobleBleSupport();
|
|
1017
|
+
});
|
|
874
1018
|
});
|
package/src/noble-ble-handler.ts
CHANGED
|
@@ -41,9 +41,25 @@ import type { CharacteristicPair, DeviceInfo, Logger, NobleModule } from './type
|
|
|
41
41
|
let noble: NobleModule | null = null;
|
|
42
42
|
let logger: Logger | null = null;
|
|
43
43
|
let disposing = false;
|
|
44
|
+
let nativeReleased = false;
|
|
44
45
|
let disposePromise: Promise<void> | undefined;
|
|
46
|
+
let windowCleanup: Promise<void> | undefined;
|
|
45
47
|
let removeIpcHandlers: (() => void) | undefined;
|
|
46
48
|
const pendingCancellations = new Set<() => void>();
|
|
49
|
+
const pendingEnumerations = new Set<() => Promise<void>>();
|
|
50
|
+
const nativeConnections = new Set<{ cancel: () => void; settled: Promise<void> }>();
|
|
51
|
+
|
|
52
|
+
function trackNativeConnection(operation: Promise<unknown>, cancel: () => void): void {
|
|
53
|
+
const connection = {
|
|
54
|
+
cancel,
|
|
55
|
+
settled: operation.then(
|
|
56
|
+
() => undefined,
|
|
57
|
+
() => undefined
|
|
58
|
+
),
|
|
59
|
+
};
|
|
60
|
+
nativeConnections.add(connection);
|
|
61
|
+
connection.settled = connection.settled.finally(() => nativeConnections.delete(connection));
|
|
62
|
+
}
|
|
47
63
|
|
|
48
64
|
function assertBleActive(): void {
|
|
49
65
|
if (disposing) {
|
|
@@ -170,9 +186,24 @@ function connectPeripheralWithCancellation(
|
|
|
170
186
|
);
|
|
171
187
|
connectingDevices.set(deviceId, pendingConnection);
|
|
172
188
|
|
|
173
|
-
|
|
189
|
+
let completeNative!: () => void;
|
|
190
|
+
trackNativeConnection(
|
|
191
|
+
new Promise<void>(resolve => {
|
|
192
|
+
completeNative = resolve;
|
|
193
|
+
}),
|
|
194
|
+
() => {
|
|
195
|
+
if (typeof peripheral.cancelConnect === 'function') peripheral.cancelConnect();
|
|
196
|
+
else noble?.cancelConnect?.(deviceId);
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
const onConnect = (error?: Error | null) => {
|
|
200
|
+
if (nativeReleased) {
|
|
201
|
+
completeNative();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
174
204
|
if (error) {
|
|
175
205
|
settle(() => reject(createNobleBleConnectionError(error, messagePrefix)));
|
|
206
|
+
completeNative();
|
|
176
207
|
return;
|
|
177
208
|
}
|
|
178
209
|
|
|
@@ -186,13 +217,26 @@ function connectPeripheralWithCancellation(
|
|
|
186
217
|
if (!activeConnection || activeConnection.peripheral !== peripheral) {
|
|
187
218
|
try {
|
|
188
219
|
peripheral.removeAllListeners('disconnect');
|
|
189
|
-
peripheral.disconnect(() =>
|
|
220
|
+
runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
221
|
+
timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
|
|
222
|
+
timeoutBehavior: 'resolve',
|
|
223
|
+
})
|
|
224
|
+
.catch(() => undefined)
|
|
225
|
+
.finally(completeNative);
|
|
226
|
+
return;
|
|
190
227
|
} catch {
|
|
191
228
|
// Best-effort cleanup for a stale native callback.
|
|
192
229
|
}
|
|
193
230
|
}
|
|
194
231
|
}
|
|
195
|
-
|
|
232
|
+
completeNative();
|
|
233
|
+
};
|
|
234
|
+
try {
|
|
235
|
+
peripheral.connect(onConnect);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
completeNative();
|
|
238
|
+
settle(() => reject(error));
|
|
239
|
+
}
|
|
196
240
|
});
|
|
197
241
|
}
|
|
198
242
|
const pairedDevices = new Set<string>(); // Windows BLE device pairing status tracking
|
|
@@ -1107,7 +1151,7 @@ async function performTargetedScan(
|
|
|
1107
1151
|
}
|
|
1108
1152
|
|
|
1109
1153
|
// Enumerate devices
|
|
1110
|
-
async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
1154
|
+
async function enumerateDevices(isWindowDestroyed: () => boolean): Promise<DeviceInfo[]> {
|
|
1111
1155
|
if (!noble) {
|
|
1112
1156
|
await initializeNoble();
|
|
1113
1157
|
}
|
|
@@ -1117,6 +1161,7 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
1117
1161
|
}
|
|
1118
1162
|
|
|
1119
1163
|
assertBleActive();
|
|
1164
|
+
if (isWindowDestroyed()) return [];
|
|
1120
1165
|
// Capture noble reference for use in closures (TypeScript narrowing)
|
|
1121
1166
|
const nobleInstance = noble;
|
|
1122
1167
|
|
|
@@ -1133,16 +1178,22 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
1133
1178
|
const devices: DeviceInfo[] = [];
|
|
1134
1179
|
let settled = false;
|
|
1135
1180
|
let intervalId: ReturnType<typeof setInterval> | undefined;
|
|
1181
|
+
let cleanupPromise: Promise<void> | undefined;
|
|
1136
1182
|
|
|
1137
1183
|
// Cleanup function: clears timers and waits until Noble confirms scanning
|
|
1138
1184
|
// has stopped. Resolving enumerate before this callback creates a race with
|
|
1139
1185
|
// an immediately-following connection attempt.
|
|
1140
|
-
const cleanup =
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1186
|
+
const cleanup = () => {
|
|
1187
|
+
if (!cleanupPromise) {
|
|
1188
|
+
settled = true;
|
|
1189
|
+
pendingCancellations.delete(cancel);
|
|
1190
|
+
clearTimeout(timeoutId);
|
|
1191
|
+
if (intervalId) clearInterval(intervalId);
|
|
1192
|
+
cleanupPromise = waitForNobleScanStop(nobleInstance).finally(() => {
|
|
1193
|
+
pendingEnumerations.delete(cancel);
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
return cleanupPromise;
|
|
1146
1197
|
};
|
|
1147
1198
|
|
|
1148
1199
|
// Collect discovered devices into the devices array
|
|
@@ -1171,10 +1222,9 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
1171
1222
|
resolve(devices);
|
|
1172
1223
|
}, DEVICE_SCAN_TIMEOUT);
|
|
1173
1224
|
|
|
1174
|
-
const cancel = () =>
|
|
1175
|
-
cleanup().then(() => resolve([]), reject);
|
|
1176
|
-
};
|
|
1225
|
+
const cancel = () => cleanup().then(() => resolve([]), reject);
|
|
1177
1226
|
pendingCancellations.add(cancel);
|
|
1227
|
+
pendingEnumerations.add(cancel);
|
|
1178
1228
|
|
|
1179
1229
|
// Start scanning without a service UUID filter so Pro2 advertisements with
|
|
1180
1230
|
// short vendor UUIDs can be found. Repeated advertisements are required when
|
|
@@ -1199,7 +1249,7 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
1199
1249
|
|
|
1200
1250
|
// Stop scanning
|
|
1201
1251
|
async function stopScanning(): Promise<void> {
|
|
1202
|
-
if (!noble) return;
|
|
1252
|
+
if (!noble || nativeReleased) return;
|
|
1203
1253
|
const nobleInstance = noble;
|
|
1204
1254
|
await waitForNobleScanStop(nobleInstance);
|
|
1205
1255
|
logger?.info('[NobleBLE] Scanning stopped');
|
|
@@ -1651,13 +1701,31 @@ async function tryDirectConnectById(deviceId: string): Promise<Peripheral | unde
|
|
|
1651
1701
|
|
|
1652
1702
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1653
1703
|
let cancel: (() => void) | undefined;
|
|
1704
|
+
let abandoned = false;
|
|
1654
1705
|
try {
|
|
1655
1706
|
// The late-orphan guard must attach to THIS pending connect.
|
|
1656
1707
|
const directPromise = nobleInstance.connectAsync(deviceId);
|
|
1708
|
+
trackNativeConnection(
|
|
1709
|
+
directPromise.then(async late => {
|
|
1710
|
+
if ((!abandoned && !disposing) || nativeReleased) return;
|
|
1711
|
+
const peripheral = late ?? discoveredDevices.get(deviceId);
|
|
1712
|
+
if (peripheral?.state === 'connected' && !connectedDevices.has(deviceId)) {
|
|
1713
|
+
peripheral.removeAllListeners('disconnect');
|
|
1714
|
+
await runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1715
|
+
timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
|
|
1716
|
+
timeoutBehavior: 'resolve',
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
}),
|
|
1720
|
+
() => nobleInstance.cancelConnect?.(deviceId)
|
|
1721
|
+
);
|
|
1657
1722
|
const raced = await Promise.race([
|
|
1658
1723
|
directPromise,
|
|
1659
1724
|
new Promise<'timeout'>(resolve => {
|
|
1660
|
-
cancel = () =>
|
|
1725
|
+
cancel = () => {
|
|
1726
|
+
abandoned = true;
|
|
1727
|
+
resolve('timeout');
|
|
1728
|
+
};
|
|
1661
1729
|
pendingCancellations.add(cancel);
|
|
1662
1730
|
timer = setTimeout(cancel, DIRECT_CONNECT_TIMEOUT_MS);
|
|
1663
1731
|
}),
|
|
@@ -1667,20 +1735,6 @@ async function tryDirectConnectById(deviceId: string): Promise<Peripheral | unde
|
|
|
1667
1735
|
logger?.info('[NobleBLE] Direct connect-by-id timed out, falling back to scan', {
|
|
1668
1736
|
deviceId,
|
|
1669
1737
|
});
|
|
1670
|
-
// Promise.race times out the caller only; a late success orphans the link.
|
|
1671
|
-
directPromise
|
|
1672
|
-
.then(late => {
|
|
1673
|
-
const latePeripheral = late ?? discoveredDevices.get(deviceId);
|
|
1674
|
-
if (
|
|
1675
|
-
latePeripheral &&
|
|
1676
|
-
latePeripheral.state === 'connected' &&
|
|
1677
|
-
!connectedDevices.has(deviceId)
|
|
1678
|
-
) {
|
|
1679
|
-
latePeripheral.removeAllListeners('disconnect');
|
|
1680
|
-
latePeripheral.disconnect(() => undefined);
|
|
1681
|
-
}
|
|
1682
|
-
})
|
|
1683
|
-
.catch(() => undefined);
|
|
1684
1738
|
return undefined;
|
|
1685
1739
|
}
|
|
1686
1740
|
// Backends emit `discover` as a side effect, so the cache may hold it.
|
|
@@ -1877,6 +1931,7 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1877
1931
|
|
|
1878
1932
|
// Disconnect device
|
|
1879
1933
|
async function disconnectDevice(deviceId: string): Promise<void> {
|
|
1934
|
+
if (nativeReleased) return;
|
|
1880
1935
|
const pendingConnection = connectingDevices.get(deviceId);
|
|
1881
1936
|
const peripheral = connectedDevices.get(deviceId) ?? pendingConnection?.peripheral;
|
|
1882
1937
|
if (!peripheral) {
|
|
@@ -1917,6 +1972,7 @@ async function disconnectDevice(deviceId: string): Promise<void> {
|
|
|
1917
1972
|
|
|
1918
1973
|
// Unsubscribe from notifications
|
|
1919
1974
|
async function unsubscribeNotifications(deviceId: string): Promise<void> {
|
|
1975
|
+
if (nativeReleased) return;
|
|
1920
1976
|
const peripheral = connectedDevices.get(deviceId);
|
|
1921
1977
|
const characteristics = deviceCharacteristics.get(deviceId);
|
|
1922
1978
|
|
|
@@ -2074,6 +2130,7 @@ async function subscribeNotifications(
|
|
|
2074
2130
|
// Setup IPC handlers
|
|
2075
2131
|
export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
2076
2132
|
if (disposing) return;
|
|
2133
|
+
let windowDestroyed = false;
|
|
2077
2134
|
try {
|
|
2078
2135
|
// @ts-ignore – electron-log is only available at runtime
|
|
2079
2136
|
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
|
@@ -2092,7 +2149,15 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
2092
2149
|
ipcMain.removeHandler(channel);
|
|
2093
2150
|
ipcMain.handle(channel, async (...args) => {
|
|
2094
2151
|
try {
|
|
2152
|
+
// A replacement renderer must not race the old renderer's shared-state cleanup.
|
|
2153
|
+
if (windowCleanup) await windowCleanup;
|
|
2095
2154
|
assertBleActive();
|
|
2155
|
+
if (windowDestroyed) {
|
|
2156
|
+
throw ERRORS.TypedError(
|
|
2157
|
+
HardwareErrorCode.BleDeviceDisconnected,
|
|
2158
|
+
'BLE window destroyed'
|
|
2159
|
+
);
|
|
2160
|
+
}
|
|
2096
2161
|
return await Promise.resolve(listener(...args));
|
|
2097
2162
|
} catch (error) {
|
|
2098
2163
|
return createNobleBleIpcErrorResponse(error);
|
|
@@ -2105,7 +2170,7 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
2105
2170
|
// Handle enumerate request
|
|
2106
2171
|
handle(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE, async () => {
|
|
2107
2172
|
try {
|
|
2108
|
-
const devices = await enumerateDevices();
|
|
2173
|
+
const devices = await enumerateDevices(() => windowDestroyed);
|
|
2109
2174
|
safeLog(logger, 'debug', 'Enumeration completed', {
|
|
2110
2175
|
count: devices.length,
|
|
2111
2176
|
devices: devices.map(device => ({ id: device.id, name: device.name })),
|
|
@@ -2250,15 +2315,25 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
2250
2315
|
|
|
2251
2316
|
// Window cleanup also runs during renderer soft restart; native release belongs to app quit.
|
|
2252
2317
|
webContents.on('destroyed', () => {
|
|
2318
|
+
windowDestroyed = true;
|
|
2253
2319
|
if (disposing) return;
|
|
2254
2320
|
safeLog(logger, 'info', 'Cleaning up Noble BLE handlers');
|
|
2255
|
-
|
|
2321
|
+
const previousCleanup = windowCleanup;
|
|
2322
|
+
// Cancel before yielding so old timers cannot stop a replacement renderer's scan.
|
|
2323
|
+
const enumerations = Array.from(pendingEnumerations, cancel => cancel());
|
|
2324
|
+
windowCleanup = (async () => {
|
|
2325
|
+
if (previousCleanup) await previousCleanup;
|
|
2326
|
+
await Promise.allSettled(enumerations);
|
|
2327
|
+
if (disposing) return;
|
|
2256
2328
|
const deviceIds = Array.from(connectedDevices.keys());
|
|
2257
2329
|
for (const deviceId of deviceIds) {
|
|
2330
|
+
if (disposing) return;
|
|
2258
2331
|
await unsubscribeNotifications(deviceId).catch(() => undefined);
|
|
2332
|
+
if (disposing) return;
|
|
2259
2333
|
await disconnectDevice(deviceId).catch(() => undefined);
|
|
2260
2334
|
}
|
|
2261
2335
|
|
|
2336
|
+
if (disposing) return;
|
|
2262
2337
|
await stopScanning().catch(() => undefined);
|
|
2263
2338
|
// persistentStateListener is process-lifetime, NOT per-window: this
|
|
2264
2339
|
// destroy handler also fires on a renderer soft restart, and removing
|
|
@@ -2302,11 +2377,21 @@ export function disposeNobleBleSupport(
|
|
|
2302
2377
|
ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected, 'Noble BLE is shutting down')
|
|
2303
2378
|
);
|
|
2304
2379
|
}
|
|
2380
|
+
const connections = Array.from(nativeConnections);
|
|
2381
|
+
for (const connection of connections) {
|
|
2382
|
+
try {
|
|
2383
|
+
connection.cancel();
|
|
2384
|
+
} catch (error) {
|
|
2385
|
+
logger?.warn('[NobleBLE] Native connect cancellation failed', error);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2305
2388
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
2306
2389
|
disposePromise = (async () => {
|
|
2307
2390
|
try {
|
|
2308
2391
|
await Promise.race([
|
|
2309
2392
|
Promise.allSettled([
|
|
2393
|
+
windowCleanup,
|
|
2394
|
+
...connections.map(connection => connection.settled),
|
|
2310
2395
|
stopScanning(),
|
|
2311
2396
|
...pendingPeripherals.map(peripheral =>
|
|
2312
2397
|
runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
@@ -2328,6 +2413,8 @@ export function disposeNobleBleSupport(
|
|
|
2328
2413
|
]);
|
|
2329
2414
|
} finally {
|
|
2330
2415
|
clearTimeout(timeout);
|
|
2416
|
+
// Bounded cleanup may finish before a broken native callback; it must stay inert afterwards.
|
|
2417
|
+
nativeReleased = true;
|
|
2331
2418
|
cleanupNobleListeners();
|
|
2332
2419
|
if (instance && persistentStateListener) {
|
|
2333
2420
|
instance.removeListener('stateChange', persistentStateListener);
|
|
@@ -30,6 +30,7 @@ export interface NobleModule {
|
|
|
30
30
|
): void;
|
|
31
31
|
stopScanning(callback?: () => void): void;
|
|
32
32
|
stop(): void;
|
|
33
|
+
cancelConnect?(id: string): void;
|
|
33
34
|
on(event: 'stateChange', listener: (state: string) => void): void;
|
|
34
35
|
on(event: 'discover', listener: (peripheral: Peripheral) => void): void;
|
|
35
36
|
removeListener(event: 'stateChange', listener: (state: string) => void): void;
|