@onekeyfe/hd-transport-web-device 1.2.0-alpha.99 → 1.2.0
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/__tests__/electron-ble-transport.test.ts +289 -32
- package/__tests__/webusb-protocol-cache.test.ts +276 -0
- package/__tests__/webusb-protocol-v2-timeout.test.ts +22 -8
- package/dist/electron-ble-transport.d.ts +6 -1
- package/dist/electron-ble-transport.d.ts.map +1 -1
- package/dist/index.d.ts +64 -3
- package/dist/index.js +232 -110
- package/dist/webusb.d.ts +11 -1
- package/dist/webusb.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/electron-ble-transport.ts +172 -85
- package/src/webusb.ts +201 -33
package/src/webusb.ts
CHANGED
|
@@ -8,19 +8,21 @@ import transport, {
|
|
|
8
8
|
PROTOCOL_V2_FRAME_MAX_BYTES,
|
|
9
9
|
ProtocolV2LinkError,
|
|
10
10
|
ProtocolV2UsbTransportBase,
|
|
11
|
+
TRANSPORT_EVENT,
|
|
11
12
|
probeProtocolV2 as probeProtocolV2Helper,
|
|
12
13
|
} from '@onekeyfe/hd-transport';
|
|
13
14
|
import {
|
|
14
15
|
ERRORS,
|
|
15
16
|
HardwareErrorCode,
|
|
16
17
|
ONEKEY_WEBUSB_FILTER,
|
|
18
|
+
inferProtocolHintFromUsbId,
|
|
17
19
|
isKnownTrezorWebUsbDevice,
|
|
20
|
+
resolveOneKeyUsbDevicePath,
|
|
18
21
|
wait,
|
|
19
22
|
} from '@onekeyfe/hd-shared';
|
|
20
23
|
import ByteBuffer from 'bytebuffer';
|
|
21
24
|
|
|
22
|
-
import
|
|
23
|
-
|
|
25
|
+
import type EventEmitter from 'events';
|
|
24
26
|
import type {
|
|
25
27
|
AcquireInput,
|
|
26
28
|
OneKeyDeviceInfoBase,
|
|
@@ -47,9 +49,6 @@ const PACKET_IO_RETRY_DELAY = 300;
|
|
|
47
49
|
const PROTOCOL_V1_PROBE_TIMEOUT = 5000;
|
|
48
50
|
const PROTOCOL_V2_PROBE_TIMEOUT = 1000;
|
|
49
51
|
const EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS = 2;
|
|
50
|
-
function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
|
|
51
|
-
return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
|
|
52
|
-
}
|
|
53
52
|
|
|
54
53
|
/**
|
|
55
54
|
* Device information with path and WebUSB device instance
|
|
@@ -71,6 +70,32 @@ interface TransferCancelToken {
|
|
|
71
70
|
cancelled: boolean;
|
|
72
71
|
}
|
|
73
72
|
|
|
73
|
+
/**
|
|
74
|
+
* The navigator.usb disconnect listener is module-scoped and attached at most
|
|
75
|
+
* once: listeners on navigator.usb are global and never garbage collected, so a
|
|
76
|
+
* per-instance listener would retain every transport instance created across
|
|
77
|
+
* SDK re-initializations. Events are routed to the most recently initialized
|
|
78
|
+
* transport instance instead.
|
|
79
|
+
*/
|
|
80
|
+
let activeWebUsbTransport: WebUsbTransport | undefined;
|
|
81
|
+
let usbDisconnectListenerAttached = false;
|
|
82
|
+
|
|
83
|
+
function registerActiveWebUsbTransport(instance: WebUsbTransport, usb: USB) {
|
|
84
|
+
activeWebUsbTransport = instance;
|
|
85
|
+
if (usbDisconnectListenerAttached) return;
|
|
86
|
+
usbDisconnectListenerAttached = true;
|
|
87
|
+
usb.addEventListener('disconnect', event => {
|
|
88
|
+
if (!event.device) return;
|
|
89
|
+
const path = resolveOneKeyUsbDevicePath(event.device);
|
|
90
|
+
if (!path) return;
|
|
91
|
+
activeWebUsbTransport?.markProtocolStale(path);
|
|
92
|
+
// WebUSB has no device-list poller behind it, so this event is the only
|
|
93
|
+
// signal that the device is gone. Without it consumers never see a
|
|
94
|
+
// DEVICE.DISCONNECT for USB (OK-60486).
|
|
95
|
+
activeWebUsbTransport?.emitDeviceDisconnect(path, event.device);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
74
99
|
export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
75
100
|
messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
76
101
|
|
|
@@ -82,8 +107,29 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
82
107
|
/** Per-path protocol type detected by active wire-level probe. */
|
|
83
108
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
84
109
|
|
|
110
|
+
/** Protocols previously confirmed by an active response for this transport instance. */
|
|
111
|
+
private confirmedDeviceProtocols: Map<string, ProtocolType> = new Map();
|
|
112
|
+
|
|
85
113
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
86
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Paths whose cached protocol must be re-probed on the next acquire (a USB
|
|
117
|
+
* disconnect was seen, or a transfer-level reconnect happened mid-call).
|
|
118
|
+
*/
|
|
119
|
+
private staleProtocolPaths: Set<string> = new Set();
|
|
120
|
+
|
|
121
|
+
/** Paths currently acquired (between a successful acquire() and its release()). */
|
|
122
|
+
private acquiredPaths: Set<string> = new Set();
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The exact USBDevice object each cached protocol was probed against. The
|
|
126
|
+
* browser returns the same object identity for a device as long as it stays
|
|
127
|
+
* connected, and a replug/reboot always yields a new object — so an identity
|
|
128
|
+
* mismatch proves the device was re-enumerated since the probe, even when the
|
|
129
|
+
* disconnect event itself was delayed or missed.
|
|
130
|
+
*/
|
|
131
|
+
private probedDeviceObjects: Map<string, USBDevice> = new Map();
|
|
132
|
+
|
|
87
133
|
/** Per-path USB endpoint / interface numbers (discovered from USB descriptors) */
|
|
88
134
|
private deviceEndpoints: Map<string, DeviceEndpoints> = new Map();
|
|
89
135
|
|
|
@@ -97,6 +143,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
97
143
|
|
|
98
144
|
usb?: USB;
|
|
99
145
|
|
|
146
|
+
emitter?: EventEmitter;
|
|
147
|
+
|
|
100
148
|
/**
|
|
101
149
|
* Cached list of connected devices
|
|
102
150
|
* This is essential for maintaining device references between operations
|
|
@@ -120,8 +168,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
120
168
|
/**
|
|
121
169
|
* Initialize WebUSB transport
|
|
122
170
|
*/
|
|
123
|
-
init(logger: any) {
|
|
171
|
+
init(logger: any, emitter?: EventEmitter) {
|
|
124
172
|
this.Log = logger;
|
|
173
|
+
this.emitter = emitter;
|
|
125
174
|
|
|
126
175
|
const { usb } = navigator;
|
|
127
176
|
if (!usb) {
|
|
@@ -131,6 +180,34 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
131
180
|
);
|
|
132
181
|
}
|
|
133
182
|
this.usb = usb;
|
|
183
|
+
registerActiveWebUsbTransport(this, usb);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Announce that a USB device left. Called from the module-scoped disconnect
|
|
188
|
+
* listener, which is why it is public rather than inlined.
|
|
189
|
+
*/
|
|
190
|
+
emitDeviceDisconnect(path: string, device?: USBDevice) {
|
|
191
|
+
this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
|
|
192
|
+
name: device?.productName ?? '',
|
|
193
|
+
id: path,
|
|
194
|
+
connectId: path,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Protocol type is a property of the physical device keyed by USB serial number.
|
|
200
|
+
* It can only change across a device reboot (e.g. normal ↔ bootloader mode), and
|
|
201
|
+
* a reboot always surfaces as a USB disconnect. Disconnects (and transfer-level
|
|
202
|
+
* reconnects, which cover a missed disconnect event) only MARK the cached probe
|
|
203
|
+
* result stale instead of deleting it: an in-flight session keeps using the old
|
|
204
|
+
* value so the transfer-level reconnect retries can absorb a transient
|
|
205
|
+
* re-enumeration exactly as they did before the cache existed, while the next
|
|
206
|
+
* acquire re-probes from scratch.
|
|
207
|
+
*/
|
|
208
|
+
markProtocolStale(path: string) {
|
|
209
|
+
this.staleProtocolPaths.add(path);
|
|
210
|
+
this.probedDeviceObjects.delete(path);
|
|
134
211
|
}
|
|
135
212
|
|
|
136
213
|
/**
|
|
@@ -203,22 +280,24 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
203
280
|
(desc: { vendorId: number; productId: number }) =>
|
|
204
281
|
dev.vendorId === desc.vendorId && dev.productId === desc.productId
|
|
205
282
|
);
|
|
206
|
-
|
|
207
|
-
return isOneKey && hasSerialNumber && !isKnownTrezorWebUsbDevice(dev);
|
|
283
|
+
return isOneKey && !isKnownTrezorWebUsbDevice(dev);
|
|
208
284
|
});
|
|
209
285
|
|
|
210
|
-
this.deviceList = onekeyDevices.
|
|
211
|
-
const path = device
|
|
212
|
-
|
|
286
|
+
this.deviceList = onekeyDevices.flatMap(device => {
|
|
287
|
+
const path = resolveOneKeyUsbDevicePath(device);
|
|
288
|
+
if (!path) return [];
|
|
289
|
+
const protocolHint = inferProtocolHintFromUsbId(device.vendorId, device.productId);
|
|
213
290
|
if (protocolHint) {
|
|
214
291
|
this.deviceProtocolHints.set(path, protocolHint);
|
|
215
292
|
}
|
|
216
293
|
|
|
217
|
-
return
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
294
|
+
return [
|
|
295
|
+
{
|
|
296
|
+
path,
|
|
297
|
+
device,
|
|
298
|
+
commType: 'webusb',
|
|
299
|
+
},
|
|
300
|
+
];
|
|
222
301
|
});
|
|
223
302
|
|
|
224
303
|
return this.deviceList;
|
|
@@ -233,20 +312,85 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
233
312
|
await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
|
|
234
313
|
await this.closeOpenDevice(input.path);
|
|
235
314
|
await this.connect(input.path ?? '', true);
|
|
236
|
-
|
|
237
|
-
.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
315
|
+
if (input.skipProtocolProbe) {
|
|
316
|
+
if (!input.expectedProtocol) {
|
|
317
|
+
throw ERRORS.TypedError(
|
|
318
|
+
HardwareErrorCode.RuntimeError,
|
|
319
|
+
'skipProtocolProbe requires an expected protocol'
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
if (this.confirmedDeviceProtocols.get(input.path) !== input.expectedProtocol) {
|
|
323
|
+
throw ERRORS.TypedError(
|
|
324
|
+
HardwareErrorCode.RuntimeError,
|
|
325
|
+
'skipProtocolProbe requires a previously confirmed protocol for this WebUSB endpoint'
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
this.staleProtocolPaths.delete(input.path);
|
|
329
|
+
this.deviceProtocol.set(input.path, input.expectedProtocol);
|
|
330
|
+
const currentDevice = this.deviceList.find(d => d.path === input.path)?.device;
|
|
331
|
+
if (currentDevice) {
|
|
332
|
+
this.probedDeviceObjects.set(input.path, currentDevice);
|
|
333
|
+
}
|
|
334
|
+
} else if (input.forceProtocolDetection) {
|
|
335
|
+
// Explicit recovery/discovery (e.g. detectDeviceConnectProtocol) must
|
|
336
|
+
// probe on the wire regardless of any cached result — the cached
|
|
337
|
+
// binding may be exactly what the caller is trying to recover from.
|
|
338
|
+
this.staleProtocolPaths.delete(input.path);
|
|
339
|
+
this.deviceProtocol.delete(input.path);
|
|
340
|
+
this.deviceProtocolHints.delete(input.path);
|
|
341
|
+
}
|
|
342
|
+
if (!input.skipProtocolProbe && this.staleProtocolPaths.has(input.path)) {
|
|
343
|
+
// The device disconnected (possibly rebooting into another mode) since
|
|
344
|
+
// the protocol was probed — drop the cache so it is re-probed below.
|
|
345
|
+
this.staleProtocolPaths.delete(input.path);
|
|
346
|
+
this.deviceProtocol.delete(input.path);
|
|
347
|
+
}
|
|
348
|
+
if (!input.skipProtocolProbe && this.deviceProtocol.has(input.path)) {
|
|
349
|
+
const currentDevice = this.deviceList.find(d => d.path === input.path)?.device;
|
|
350
|
+
if (!currentDevice || currentDevice !== this.probedDeviceObjects.get(input.path)) {
|
|
351
|
+
// The OS re-enumerated the device since the probe (a replug/reboot we
|
|
352
|
+
// may not have seen a disconnect event for) — the cached protocol can
|
|
353
|
+
// no longer be trusted; re-probe on the wire below.
|
|
354
|
+
this.deviceProtocol.delete(input.path);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const cachedProtocol = this.deviceProtocol.get(input.path);
|
|
358
|
+
if (
|
|
359
|
+
!input.skipProtocolProbe &&
|
|
360
|
+
cachedProtocol &&
|
|
361
|
+
input.expectedProtocol &&
|
|
362
|
+
cachedProtocol !== input.expectedProtocol
|
|
363
|
+
) {
|
|
364
|
+
// The caller expects a different protocol than the cached probe result;
|
|
365
|
+
// the cache is stale — drop it and re-probe on the wire below.
|
|
366
|
+
this.deviceProtocol.delete(input.path);
|
|
367
|
+
}
|
|
368
|
+
if (!this.deviceProtocol.has(input.path)) {
|
|
369
|
+
const usbDevice = this.deviceList.find(device => device.path === input.path)?.device;
|
|
370
|
+
const protocolHint = input.expectedProtocol
|
|
371
|
+
? undefined
|
|
372
|
+
: input.protocolHint ??
|
|
373
|
+
this.deviceProtocolHints.get(input.path) ??
|
|
374
|
+
inferProtocolHintFromUsbId(usbDevice?.vendorId, usbDevice?.productId);
|
|
375
|
+
if (protocolHint) {
|
|
376
|
+
this.deviceProtocolHints.set(input.path, protocolHint);
|
|
377
|
+
}
|
|
378
|
+
const detectedProtocol = await this.detectProtocol(
|
|
379
|
+
input.path,
|
|
380
|
+
input.expectedProtocol,
|
|
381
|
+
protocolHint
|
|
382
|
+
);
|
|
383
|
+
this.confirmedDeviceProtocols.set(input.path, detectedProtocol);
|
|
384
|
+
const probedDevice = this.deviceList.find(d => d.path === input.path)?.device;
|
|
385
|
+
if (probedDevice) {
|
|
386
|
+
this.probedDeviceObjects.set(input.path, probedDevice);
|
|
387
|
+
}
|
|
245
388
|
}
|
|
246
|
-
|
|
389
|
+
this.acquiredPaths.add(input.path);
|
|
247
390
|
return await Promise.resolve(input.path);
|
|
248
391
|
} catch (e) {
|
|
249
392
|
this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
|
|
393
|
+
this.acquiredPaths.delete(input.path);
|
|
250
394
|
await this.closeOpenDevice(input.path);
|
|
251
395
|
throw e;
|
|
252
396
|
}
|
|
@@ -266,7 +410,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
266
410
|
|
|
267
411
|
private createProtocolProbeTimeoutError(expected: ProtocolType, attempts: number) {
|
|
268
412
|
return ERRORS.TypedError(
|
|
269
|
-
HardwareErrorCode.
|
|
413
|
+
HardwareErrorCode.DeviceInitializeFailed,
|
|
270
414
|
`Protocol ${expected} probe timeout after ${attempts} attempts`
|
|
271
415
|
);
|
|
272
416
|
}
|
|
@@ -482,7 +626,25 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
482
626
|
}
|
|
483
627
|
}
|
|
484
628
|
|
|
629
|
+
/**
|
|
630
|
+
* With the protocol cache surviving release(), deviceProtocol presence no
|
|
631
|
+
* longer implies an active session. Guard call()/post() explicitly so a
|
|
632
|
+
* post-release straggler fails fast instead of silently reopening the device
|
|
633
|
+
* and driving it outside any session (pre-cache behavior: the deleted
|
|
634
|
+
* protocol entry produced the same fail-fast).
|
|
635
|
+
*/
|
|
636
|
+
private assertAcquired(path: string) {
|
|
637
|
+
if (!this.acquiredPaths.has(path)) {
|
|
638
|
+
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, `Device is not acquired for ${path}`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
485
642
|
async post(session: string, name: string, data: Record<string, unknown>) {
|
|
643
|
+
this.assertAcquired(session);
|
|
644
|
+
if (this.deviceProtocol.get(session) === 'V2') {
|
|
645
|
+
await this.sendProtocolV2UsbFlowControl(session, name, data);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
486
648
|
await this.call(session, name, data);
|
|
487
649
|
}
|
|
488
650
|
|
|
@@ -520,6 +682,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
520
682
|
error
|
|
521
683
|
)}`
|
|
522
684
|
);
|
|
685
|
+
// The device dropped off the bus mid-call and may have rebooted into a
|
|
686
|
+
// different mode. The in-flight retry keeps the known protocol, but the
|
|
687
|
+
// next acquire must re-probe — this also self-heals a stale cache when the
|
|
688
|
+
// USB disconnect event itself was missed.
|
|
689
|
+
this.staleProtocolPaths.add(path);
|
|
523
690
|
await wait(attempt * PACKET_IO_RETRY_DELAY);
|
|
524
691
|
|
|
525
692
|
try {
|
|
@@ -753,6 +920,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
753
920
|
if (this.messages == null) {
|
|
754
921
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
755
922
|
}
|
|
923
|
+
this.assertAcquired(path);
|
|
756
924
|
|
|
757
925
|
const device = await this.findDevice(path);
|
|
758
926
|
if (!device) {
|
|
@@ -767,10 +935,6 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
767
935
|
);
|
|
768
936
|
}
|
|
769
937
|
|
|
770
|
-
if (!shouldSuppressHighVolumeCallLog(name)) {
|
|
771
|
-
this.Log.debug('transport call', createTransportCallLog(name, protocol, data));
|
|
772
|
-
}
|
|
773
|
-
|
|
774
938
|
if (protocol === 'V2') {
|
|
775
939
|
return this.callProtocolV2(path, name, data, options);
|
|
776
940
|
}
|
|
@@ -873,10 +1037,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
873
1037
|
* Release device
|
|
874
1038
|
*/
|
|
875
1039
|
async release(path: string) {
|
|
1040
|
+
this.acquiredPaths.delete(path);
|
|
876
1041
|
await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
|
|
877
1042
|
await this.closeOpenDevice(path);
|
|
878
|
-
|
|
879
|
-
|
|
1043
|
+
// Keep deviceProtocol/deviceProtocolHints across release: the probe result is a
|
|
1044
|
+
// physical-device property, so the next acquire can skip the wire-level probe.
|
|
1045
|
+
// V2 entries are still dropped by onProtocolV2UsbLinkInvalidated via the link
|
|
1046
|
+
// invalidation above, and any entry is re-probed after a USB disconnect or a
|
|
1047
|
+
// transfer-level reconnect (see markProtocolStale).
|
|
880
1048
|
this.deviceEndpoints.delete(path);
|
|
881
1049
|
}
|
|
882
1050
|
|