@onekeyfe/hd-transport-web-device 1.2.8-alpha.13 → 1.2.8-alpha.14
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__/webusb-identity-probe.test.ts +144 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +73 -10
- package/dist/webusb.d.ts +6 -1
- package/dist/webusb.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/webusb.ts +74 -11
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import WebUsbTransport from '../src/webusb';
|
|
2
|
+
|
|
3
|
+
import type { MessageFromOneKey, TransportCallOptions } from '@onekeyfe/hd-transport';
|
|
4
|
+
|
|
5
|
+
describe('WebUSB identity probe ownership', () => {
|
|
6
|
+
let originalNavigator: PropertyDescriptor | undefined;
|
|
7
|
+
const device = {
|
|
8
|
+
vendorId: 0x1209,
|
|
9
|
+
productId: 0x53c1,
|
|
10
|
+
serialNumber: 'USB_PATH',
|
|
11
|
+
opened: false,
|
|
12
|
+
close: jest.fn(),
|
|
13
|
+
};
|
|
14
|
+
let disconnectListener: (event: { device: typeof device }) => void;
|
|
15
|
+
const usb = {
|
|
16
|
+
getDevices: jest.fn(),
|
|
17
|
+
addEventListener: jest.fn((_type, callback) => {
|
|
18
|
+
disconnectListener = callback;
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
|
|
23
|
+
device.opened = false;
|
|
24
|
+
device.close.mockClear();
|
|
25
|
+
usb.getDevices.mockResolvedValue([device]);
|
|
26
|
+
usb.addEventListener.mockClear();
|
|
27
|
+
Object.defineProperty(globalThis, 'navigator', { configurable: true, value: { usb } });
|
|
28
|
+
});
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
if (originalNavigator) Object.defineProperty(globalThis, 'navigator', originalNavigator);
|
|
31
|
+
else delete (globalThis as { navigator?: Navigator }).navigator;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('does not close a handle already opened by another operation', async () => {
|
|
35
|
+
const probe = new WebUsbTransport(true);
|
|
36
|
+
probe.init({ debug: jest.fn() });
|
|
37
|
+
device.opened = true;
|
|
38
|
+
await expect(probe.acquire({ path: 'USB_PATH' })).rejects.toThrow(
|
|
39
|
+
'USB device is already in use'
|
|
40
|
+
);
|
|
41
|
+
expect(device.close).not.toHaveBeenCalled();
|
|
42
|
+
expect(usb.addEventListener).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('does not borrow a handle while a normal native open is pending', async () => {
|
|
46
|
+
const active = new WebUsbTransport();
|
|
47
|
+
const probe = new WebUsbTransport(true);
|
|
48
|
+
active.init({ debug: jest.fn() });
|
|
49
|
+
probe.init({ debug: jest.fn() });
|
|
50
|
+
let finishOpen!: () => void;
|
|
51
|
+
let started!: () => void;
|
|
52
|
+
const opening = new Promise<void>(resolve => {
|
|
53
|
+
finishOpen = resolve;
|
|
54
|
+
});
|
|
55
|
+
const entered = new Promise<void>(resolve => {
|
|
56
|
+
started = resolve;
|
|
57
|
+
});
|
|
58
|
+
const native = Object.assign(device, {
|
|
59
|
+
open: jest.fn(async () => {
|
|
60
|
+
started();
|
|
61
|
+
await opening;
|
|
62
|
+
device.opened = true;
|
|
63
|
+
}),
|
|
64
|
+
reset: jest.fn(),
|
|
65
|
+
configurations: [],
|
|
66
|
+
configuration: { configurationValue: 1 },
|
|
67
|
+
selectConfiguration: jest.fn(),
|
|
68
|
+
claimInterface: jest.fn(),
|
|
69
|
+
clearHalt: jest.fn(),
|
|
70
|
+
});
|
|
71
|
+
jest.spyOn(active, 'findDevice').mockResolvedValue(native as unknown as USBDevice);
|
|
72
|
+
const connecting = active.connectToDevice('USB_PATH', true);
|
|
73
|
+
await entered;
|
|
74
|
+
await expect(probe.acquire({ path: 'USB_PATH' })).rejects.toThrow(
|
|
75
|
+
'USB device is already in use'
|
|
76
|
+
);
|
|
77
|
+
expect(device.close).not.toHaveBeenCalled();
|
|
78
|
+
finishOpen();
|
|
79
|
+
await connecting;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('a normal connection cannot close an isolated probe handle and release closes the captured handle after re-enumeration', async () => {
|
|
83
|
+
const probe = new WebUsbTransport(true);
|
|
84
|
+
probe.init({ debug: jest.fn() });
|
|
85
|
+
const internals = probe as unknown as {
|
|
86
|
+
rotateProtocolV2UsbGeneration: () => Promise<void>;
|
|
87
|
+
detectProtocol: () => Promise<'V1'>;
|
|
88
|
+
};
|
|
89
|
+
jest.spyOn(internals, 'rotateProtocolV2UsbGeneration').mockResolvedValue(undefined);
|
|
90
|
+
jest.spyOn(internals, 'detectProtocol').mockResolvedValue('V1');
|
|
91
|
+
jest.spyOn(probe, 'connect').mockImplementation(() => {
|
|
92
|
+
device.opened = true;
|
|
93
|
+
return Promise.resolve();
|
|
94
|
+
});
|
|
95
|
+
await probe.acquire({ path: 'USB_PATH' });
|
|
96
|
+
const active = new WebUsbTransport();
|
|
97
|
+
active.init({ debug: jest.fn() });
|
|
98
|
+
await expect(active.connectToDevice('USB_PATH', true)).rejects.toThrow(
|
|
99
|
+
'USB device is already in use'
|
|
100
|
+
);
|
|
101
|
+
expect(device.close).not.toHaveBeenCalled();
|
|
102
|
+
const replacement = { ...device, close: jest.fn() };
|
|
103
|
+
usb.getDevices.mockResolvedValue([replacement]);
|
|
104
|
+
await probe.getConnectedDevices();
|
|
105
|
+
await probe.release('USB_PATH');
|
|
106
|
+
expect(device.close).toHaveBeenCalledTimes(1);
|
|
107
|
+
expect(replacement.close).not.toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('does not replace the active SDK disconnect observer', () => {
|
|
111
|
+
const active = new WebUsbTransport();
|
|
112
|
+
active.init({ debug: jest.fn() });
|
|
113
|
+
const probe = new WebUsbTransport(true);
|
|
114
|
+
probe.init({ debug: jest.fn() });
|
|
115
|
+
const activeDisconnect = jest.spyOn(active, 'markProtocolStale');
|
|
116
|
+
const probeDisconnect = jest.spyOn(probe, 'markProtocolStale');
|
|
117
|
+
disconnectListener({ device });
|
|
118
|
+
expect(activeDisconnect).toHaveBeenCalledWith('USB_PATH');
|
|
119
|
+
expect(probeDisconnect).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test.each([false, true])(
|
|
123
|
+
'uses a read-only V1 probe only for isolated discovery (%s)',
|
|
124
|
+
async identityProbe => {
|
|
125
|
+
const probe = new WebUsbTransport(identityProbe);
|
|
126
|
+
const internals = probe as unknown as {
|
|
127
|
+
messages: object;
|
|
128
|
+
callProtocolV1: (
|
|
129
|
+
path: string,
|
|
130
|
+
name: string,
|
|
131
|
+
data: object,
|
|
132
|
+
options: TransportCallOptions
|
|
133
|
+
) => Promise<MessageFromOneKey>;
|
|
134
|
+
probeProtocolV1: (path: string) => Promise<boolean>;
|
|
135
|
+
};
|
|
136
|
+
internals.messages = {};
|
|
137
|
+
const call = jest
|
|
138
|
+
.spyOn(internals, 'callProtocolV1')
|
|
139
|
+
.mockResolvedValue({ type: 'Features', message: {} });
|
|
140
|
+
await expect(internals.probeProtocolV1('USB_PATH')).resolves.toBe(true);
|
|
141
|
+
expect(call.mock.calls[0][1]).toBe(identityProbe ? 'GetFeatures' : 'Initialize');
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ interface DeviceInfo extends OneKeyDeviceInfoBase {
|
|
|
13
13
|
protocolType?: ProtocolType;
|
|
14
14
|
}
|
|
15
15
|
declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
16
|
+
private readonly identityProbe;
|
|
16
17
|
messages: ReturnType<typeof _onekeyfe_hd_transport__default.parseConfigure> | undefined;
|
|
17
18
|
/** Protobuf schema for Protocol V2 transports. */
|
|
18
19
|
messagesV2: ReturnType<typeof _onekeyfe_hd_transport__default.parseConfigure> | undefined;
|
|
@@ -29,6 +30,7 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
|
29
30
|
private staleProtocolPaths;
|
|
30
31
|
/** Paths currently acquired (between a successful acquire() and its release()). */
|
|
31
32
|
private acquiredPaths;
|
|
33
|
+
private identityProbeDevices;
|
|
32
34
|
/**
|
|
33
35
|
* The exact USBDevice object each cached protocol was probed against. The
|
|
34
36
|
* browser returns the same object identity for a device as long as it stays
|
|
@@ -53,7 +55,7 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
|
53
55
|
configurationId: number;
|
|
54
56
|
endpointId: number;
|
|
55
57
|
interfaceId: number;
|
|
56
|
-
constructor();
|
|
58
|
+
constructor(identityProbe?: boolean);
|
|
57
59
|
/**
|
|
58
60
|
* Initialize WebUSB transport
|
|
59
61
|
*/
|
|
@@ -129,6 +131,9 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
|
129
131
|
* Discovers interface/endpoint numbers from USB descriptors on first connection.
|
|
130
132
|
*/
|
|
131
133
|
connectToDevice(path: string, first: boolean): Promise<undefined>;
|
|
134
|
+
private assertHandleAvailable;
|
|
135
|
+
private claimIdentityProbeHandle;
|
|
136
|
+
private releaseIdentityProbeHandle;
|
|
132
137
|
private closeOpenDevice;
|
|
133
138
|
private clearEndpointHalt;
|
|
134
139
|
/**
|
package/dist/index.js
CHANGED
|
@@ -56,6 +56,8 @@ const PROTOCOL_V2_PROBE_TIMEOUT = 1000;
|
|
|
56
56
|
const EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS = 2;
|
|
57
57
|
let activeWebUsbTransport;
|
|
58
58
|
let usbDisconnectListenerAttached = false;
|
|
59
|
+
const identityProbeHandles = new WeakSet();
|
|
60
|
+
const openingHandles = new WeakSet();
|
|
59
61
|
function registerActiveWebUsbTransport(instance, usb) {
|
|
60
62
|
activeWebUsbTransport = instance;
|
|
61
63
|
if (usbDisconnectListenerAttached)
|
|
@@ -72,17 +74,19 @@ function registerActiveWebUsbTransport(instance, usb) {
|
|
|
72
74
|
});
|
|
73
75
|
}
|
|
74
76
|
class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
75
|
-
constructor() {
|
|
77
|
+
constructor(identityProbe = false) {
|
|
76
78
|
super({
|
|
77
79
|
router: transport.PROTOCOL_V2_CHANNEL_USB,
|
|
78
80
|
maxFrameBytes: transport.PROTOCOL_V2_FRAME_MAX_BYTES,
|
|
79
81
|
logPrefix: 'ProtocolV2 WebUSB',
|
|
80
82
|
});
|
|
83
|
+
this.identityProbe = identityProbe;
|
|
81
84
|
this.deviceProtocol = new Map();
|
|
82
85
|
this.confirmedDeviceProtocols = new Map();
|
|
83
86
|
this.deviceProtocolHints = new Map();
|
|
84
87
|
this.staleProtocolPaths = new Set();
|
|
85
88
|
this.acquiredPaths = new Set();
|
|
89
|
+
this.identityProbeDevices = new Map();
|
|
86
90
|
this.probedDeviceObjects = new Map();
|
|
87
91
|
this.deviceEndpoints = new Map();
|
|
88
92
|
this.name = 'WebUsbTransport';
|
|
@@ -101,7 +105,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
101
105
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'WebUSB is not supported by current browsers');
|
|
102
106
|
}
|
|
103
107
|
this.usb = usb;
|
|
104
|
-
|
|
108
|
+
if (!this.identityProbe)
|
|
109
|
+
registerActiveWebUsbTransport(this, usb);
|
|
105
110
|
}
|
|
106
111
|
emitDeviceDisconnect(path, device) {
|
|
107
112
|
var _a, _b;
|
|
@@ -187,6 +192,10 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
187
192
|
return __awaiter(this, void 0, void 0, function* () {
|
|
188
193
|
if (!input.path)
|
|
189
194
|
return;
|
|
195
|
+
if (this.identityProbe) {
|
|
196
|
+
yield this.getConnectedDevices();
|
|
197
|
+
this.claimIdentityProbeHandle(input.path, yield this.findDevice(input.path));
|
|
198
|
+
}
|
|
190
199
|
try {
|
|
191
200
|
yield this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
|
|
192
201
|
yield this.closeOpenDevice(input.path);
|
|
@@ -248,7 +257,12 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
248
257
|
catch (e) {
|
|
249
258
|
this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
|
|
250
259
|
this.acquiredPaths.delete(input.path);
|
|
251
|
-
|
|
260
|
+
try {
|
|
261
|
+
yield this.closeOpenDevice(input.path);
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
this.releaseIdentityProbeHandle(input.path);
|
|
265
|
+
}
|
|
252
266
|
throw e;
|
|
253
267
|
}
|
|
254
268
|
});
|
|
@@ -375,12 +389,22 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
375
389
|
var _a;
|
|
376
390
|
return __awaiter(this, void 0, void 0, function* () {
|
|
377
391
|
let device = yield this.findDevice(path);
|
|
392
|
+
if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
|
|
393
|
+
yield this.closeOpenDevice(path);
|
|
394
|
+
this.releaseIdentityProbeHandle(path);
|
|
395
|
+
this.claimIdentityProbeHandle(path, device);
|
|
396
|
+
}
|
|
397
|
+
this.assertHandleAvailable(device);
|
|
378
398
|
if (!device.opened) {
|
|
399
|
+
openingHandles.add(device);
|
|
379
400
|
try {
|
|
380
401
|
yield device.open();
|
|
381
402
|
}
|
|
382
403
|
catch (error) {
|
|
383
|
-
return this.throwWebUsbIoError(path, 'open', error);
|
|
404
|
+
return yield this.throwWebUsbIoError(path, 'open', error);
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
openingHandles.delete(device);
|
|
384
408
|
}
|
|
385
409
|
}
|
|
386
410
|
if (first || this.deviceProtocol.get(path) !== 'V1') {
|
|
@@ -393,12 +417,22 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
393
417
|
}
|
|
394
418
|
yield this.getConnectedDevices();
|
|
395
419
|
device = yield this.findDevice(path);
|
|
420
|
+
if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
|
|
421
|
+
yield this.closeOpenDevice(path);
|
|
422
|
+
this.releaseIdentityProbeHandle(path);
|
|
423
|
+
this.claimIdentityProbeHandle(path, device);
|
|
424
|
+
}
|
|
425
|
+
this.assertHandleAvailable(device);
|
|
396
426
|
if (!device.opened) {
|
|
427
|
+
openingHandles.add(device);
|
|
397
428
|
try {
|
|
398
429
|
yield device.open();
|
|
399
430
|
}
|
|
400
431
|
catch (error) {
|
|
401
|
-
return this.throwWebUsbIoError(path, 'open', error);
|
|
432
|
+
return yield this.throwWebUsbIoError(path, 'open', error);
|
|
433
|
+
}
|
|
434
|
+
finally {
|
|
435
|
+
openingHandles.delete(device);
|
|
402
436
|
}
|
|
403
437
|
}
|
|
404
438
|
if (first ||
|
|
@@ -418,12 +452,34 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
418
452
|
yield this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
|
|
419
453
|
});
|
|
420
454
|
}
|
|
455
|
+
assertHandleAvailable(device) {
|
|
456
|
+
if (identityProbeHandles.has(device) &&
|
|
457
|
+
![...this.identityProbeDevices.values()].includes(device)) {
|
|
458
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'USB device is already in use');
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
claimIdentityProbeHandle(path, device) {
|
|
462
|
+
if (device.opened || openingHandles.has(device) || identityProbeHandles.has(device)) {
|
|
463
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'USB device is already in use');
|
|
464
|
+
}
|
|
465
|
+
identityProbeHandles.add(device);
|
|
466
|
+
this.identityProbeDevices.set(path, device);
|
|
467
|
+
}
|
|
468
|
+
releaseIdentityProbeHandle(path) {
|
|
469
|
+
const device = this.identityProbeDevices.get(path);
|
|
470
|
+
if (device)
|
|
471
|
+
identityProbeHandles.delete(device);
|
|
472
|
+
this.identityProbeDevices.delete(path);
|
|
473
|
+
}
|
|
421
474
|
closeOpenDevice(path) {
|
|
422
475
|
var _a, _b, _c, _d;
|
|
423
476
|
return __awaiter(this, void 0, void 0, function* () {
|
|
424
|
-
const current =
|
|
477
|
+
const current = this.identityProbe
|
|
478
|
+
? this.identityProbeDevices.get(path)
|
|
479
|
+
: (_a = this.deviceList.find(device => device.path === path)) === null || _a === void 0 ? void 0 : _a.device;
|
|
425
480
|
if (!(current === null || current === void 0 ? void 0 : current.opened))
|
|
426
481
|
return;
|
|
482
|
+
this.assertHandleAvailable(current);
|
|
427
483
|
const endpoints = this.deviceEndpoints.get(path);
|
|
428
484
|
const ifaceNum = (_b = endpoints === null || endpoints === void 0 ? void 0 : endpoints.interfaceNumber) !== null && _b !== void 0 ? _b : this.interfaceId;
|
|
429
485
|
try {
|
|
@@ -437,6 +493,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
437
493
|
}
|
|
438
494
|
catch (error) {
|
|
439
495
|
(_d = this.Log) === null || _d === void 0 ? void 0 : _d.debug('[WebUsbTransport] close before reconnect failed:', error);
|
|
496
|
+
if (this.identityProbe)
|
|
497
|
+
throw error;
|
|
440
498
|
}
|
|
441
499
|
});
|
|
442
500
|
}
|
|
@@ -749,10 +807,10 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
749
807
|
return false;
|
|
750
808
|
}
|
|
751
809
|
try {
|
|
752
|
-
yield this.callProtocolV1(path, 'Initialize', {}, {
|
|
810
|
+
const response = yield this.callProtocolV1(path, this.identityProbe ? 'GetFeatures' : 'Initialize', {}, {
|
|
753
811
|
timeoutMs: PROTOCOL_V1_PROBE_TIMEOUT,
|
|
754
812
|
});
|
|
755
|
-
return
|
|
813
|
+
return !this.identityProbe || response.type === 'Features';
|
|
756
814
|
}
|
|
757
815
|
catch (error) {
|
|
758
816
|
if (this.isWebUsbIoHardwareError(error))
|
|
@@ -869,8 +927,13 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
869
927
|
release(path) {
|
|
870
928
|
return __awaiter(this, void 0, void 0, function* () {
|
|
871
929
|
this.acquiredPaths.delete(path);
|
|
872
|
-
|
|
873
|
-
|
|
930
|
+
try {
|
|
931
|
+
yield this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
|
|
932
|
+
yield this.closeOpenDevice(path);
|
|
933
|
+
}
|
|
934
|
+
finally {
|
|
935
|
+
this.releaseIdentityProbeHandle(path);
|
|
936
|
+
}
|
|
874
937
|
this.deviceEndpoints.delete(path);
|
|
875
938
|
});
|
|
876
939
|
}
|
package/dist/webusb.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface DeviceInfo extends OneKeyDeviceInfoBase {
|
|
|
9
9
|
protocolType?: ProtocolType;
|
|
10
10
|
}
|
|
11
11
|
export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
12
|
+
private readonly identityProbe;
|
|
12
13
|
messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
13
14
|
messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
14
15
|
private protocolV2SchemaSource;
|
|
@@ -17,6 +18,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
17
18
|
private deviceProtocolHints;
|
|
18
19
|
private staleProtocolPaths;
|
|
19
20
|
private acquiredPaths;
|
|
21
|
+
private identityProbeDevices;
|
|
20
22
|
private probedDeviceObjects;
|
|
21
23
|
private deviceEndpoints;
|
|
22
24
|
name: string;
|
|
@@ -29,7 +31,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
29
31
|
configurationId: number;
|
|
30
32
|
endpointId: number;
|
|
31
33
|
interfaceId: number;
|
|
32
|
-
constructor();
|
|
34
|
+
constructor(identityProbe?: boolean);
|
|
33
35
|
init(logger: any, emitter?: EventEmitter): void;
|
|
34
36
|
emitDeviceDisconnect(path: string, device?: USBDevice): void;
|
|
35
37
|
markProtocolStale(path: string): void;
|
|
@@ -47,6 +49,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
47
49
|
connect(path: string, first: boolean): Promise<undefined>;
|
|
48
50
|
private discoverEndpoints;
|
|
49
51
|
connectToDevice(path: string, first: boolean): Promise<undefined>;
|
|
52
|
+
private assertHandleAvailable;
|
|
53
|
+
private claimIdentityProbeHandle;
|
|
54
|
+
private releaseIdentityProbeHandle;
|
|
50
55
|
private closeOpenDevice;
|
|
51
56
|
private clearEndpointHalt;
|
|
52
57
|
private assertAcquired;
|
package/dist/webusb.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webusb.d.ts","sourceRoot":"","sources":["../src/webusb.ts"],"names":[],"mappings":";;AACA,OAAO,SAAS,EAAE,EAQhB,0BAA0B,EAG3B,MAAM,wBAAwB,CAAC;AAahC,OAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EACpB,YAAY,EACZ,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACrB,MAAM,wBAAwB,CAAC;AAuBhC,MAAM,WAAW,UAAW,SAAQ,oBAAoB;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,CAAC;IAClB,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;
|
|
1
|
+
{"version":3,"file":"webusb.d.ts","sourceRoot":"","sources":["../src/webusb.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport-web-device",
|
|
3
|
-
"version": "1.2.8-alpha.
|
|
3
|
+
"version": "1.2.8-alpha.14",
|
|
4
4
|
"author": "OneKey",
|
|
5
5
|
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,13 +21,13 @@
|
|
|
21
21
|
"lint:fix": "eslint . --fix"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@onekeyfe/hd-shared": "1.2.8-alpha.
|
|
25
|
-
"@onekeyfe/hd-transport": "1.2.8-alpha.
|
|
24
|
+
"@onekeyfe/hd-shared": "1.2.8-alpha.14",
|
|
25
|
+
"@onekeyfe/hd-transport": "1.2.8-alpha.14"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@onekeyfe/hd-transport-electron": "1.2.8-alpha.
|
|
28
|
+
"@onekeyfe/hd-transport-electron": "1.2.8-alpha.14",
|
|
29
29
|
"@types/w3c-web-usb": "^1.0.6",
|
|
30
30
|
"@types/web-bluetooth": "^0.0.17"
|
|
31
31
|
},
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "f2784ca5eb21e4a3ebf167965f689e2ff2cb6249"
|
|
33
33
|
}
|
package/src/webusb.ts
CHANGED
|
@@ -82,6 +82,10 @@ type WebUsbOperation = 'open' | 'claimInterface' | 'transferIn' | 'transferOut';
|
|
|
82
82
|
*/
|
|
83
83
|
let activeWebUsbTransport: WebUsbTransport | undefined;
|
|
84
84
|
let usbDisconnectListenerAttached = false;
|
|
85
|
+
// Only isolated discovery owns these handles. Normal calls must never close or
|
|
86
|
+
// open them, including while native open() is still pending.
|
|
87
|
+
const identityProbeHandles = new WeakSet<USBDevice>();
|
|
88
|
+
const openingHandles = new WeakSet<USBDevice>();
|
|
85
89
|
|
|
86
90
|
function registerActiveWebUsbTransport(instance: WebUsbTransport, usb: USB) {
|
|
87
91
|
activeWebUsbTransport = instance;
|
|
@@ -124,6 +128,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
124
128
|
/** Paths currently acquired (between a successful acquire() and its release()). */
|
|
125
129
|
private acquiredPaths: Set<string> = new Set();
|
|
126
130
|
|
|
131
|
+
private identityProbeDevices = new Map<string, USBDevice>();
|
|
132
|
+
|
|
127
133
|
/**
|
|
128
134
|
* The exact USBDevice object each cached protocol was probed against. The
|
|
129
135
|
* browser returns the same object identity for a device as long as it stays
|
|
@@ -160,7 +166,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
160
166
|
|
|
161
167
|
interfaceId = INTERFACE_ID;
|
|
162
168
|
|
|
163
|
-
constructor() {
|
|
169
|
+
constructor(private readonly identityProbe = false) {
|
|
164
170
|
super({
|
|
165
171
|
router: PROTOCOL_V2_CHANNEL_USB,
|
|
166
172
|
maxFrameBytes: PROTOCOL_V2_FRAME_MAX_BYTES,
|
|
@@ -183,7 +189,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
183
189
|
);
|
|
184
190
|
}
|
|
185
191
|
this.usb = usb;
|
|
186
|
-
registerActiveWebUsbTransport(this, usb);
|
|
192
|
+
if (!this.identityProbe) registerActiveWebUsbTransport(this, usb);
|
|
187
193
|
}
|
|
188
194
|
|
|
189
195
|
/**
|
|
@@ -311,6 +317,10 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
311
317
|
*/
|
|
312
318
|
async acquire(input: AcquireInput) {
|
|
313
319
|
if (!input.path) return;
|
|
320
|
+
if (this.identityProbe) {
|
|
321
|
+
await this.getConnectedDevices();
|
|
322
|
+
this.claimIdentityProbeHandle(input.path, await this.findDevice(input.path));
|
|
323
|
+
}
|
|
314
324
|
try {
|
|
315
325
|
await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
|
|
316
326
|
await this.closeOpenDevice(input.path);
|
|
@@ -394,7 +404,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
394
404
|
} catch (e) {
|
|
395
405
|
this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
|
|
396
406
|
this.acquiredPaths.delete(input.path);
|
|
397
|
-
|
|
407
|
+
try {
|
|
408
|
+
await this.closeOpenDevice(input.path);
|
|
409
|
+
} finally {
|
|
410
|
+
this.releaseIdentityProbeHandle(input.path);
|
|
411
|
+
}
|
|
398
412
|
throw e;
|
|
399
413
|
}
|
|
400
414
|
}
|
|
@@ -569,11 +583,20 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
569
583
|
*/
|
|
570
584
|
async connectToDevice(path: string, first: boolean) {
|
|
571
585
|
let device: USBDevice = await this.findDevice(path);
|
|
586
|
+
if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
|
|
587
|
+
await this.closeOpenDevice(path);
|
|
588
|
+
this.releaseIdentityProbeHandle(path);
|
|
589
|
+
this.claimIdentityProbeHandle(path, device);
|
|
590
|
+
}
|
|
591
|
+
this.assertHandleAvailable(device);
|
|
572
592
|
if (!device.opened) {
|
|
593
|
+
openingHandles.add(device);
|
|
573
594
|
try {
|
|
574
595
|
await device.open();
|
|
575
596
|
} catch (error) {
|
|
576
|
-
return this.throwWebUsbIoError(path, 'open', error);
|
|
597
|
+
return await this.throwWebUsbIoError(path, 'open', error);
|
|
598
|
+
} finally {
|
|
599
|
+
openingHandles.delete(device);
|
|
577
600
|
}
|
|
578
601
|
}
|
|
579
602
|
// A V1 packet retry continues an in-flight exchange. Preserve its endpoint
|
|
@@ -587,11 +610,20 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
587
610
|
}
|
|
588
611
|
await this.getConnectedDevices();
|
|
589
612
|
device = await this.findDevice(path);
|
|
613
|
+
if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
|
|
614
|
+
await this.closeOpenDevice(path);
|
|
615
|
+
this.releaseIdentityProbeHandle(path);
|
|
616
|
+
this.claimIdentityProbeHandle(path, device);
|
|
617
|
+
}
|
|
618
|
+
this.assertHandleAvailable(device);
|
|
590
619
|
if (!device.opened) {
|
|
620
|
+
openingHandles.add(device);
|
|
591
621
|
try {
|
|
592
622
|
await device.open();
|
|
593
623
|
} catch (error) {
|
|
594
|
-
return this.throwWebUsbIoError(path, 'open', error);
|
|
624
|
+
return await this.throwWebUsbIoError(path, 'open', error);
|
|
625
|
+
} finally {
|
|
626
|
+
openingHandles.delete(device);
|
|
595
627
|
}
|
|
596
628
|
}
|
|
597
629
|
|
|
@@ -615,9 +647,35 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
615
647
|
await this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
|
|
616
648
|
}
|
|
617
649
|
|
|
650
|
+
private assertHandleAvailable(device: USBDevice) {
|
|
651
|
+
if (
|
|
652
|
+
identityProbeHandles.has(device) &&
|
|
653
|
+
![...this.identityProbeDevices.values()].includes(device)
|
|
654
|
+
) {
|
|
655
|
+
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'USB device is already in use');
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
private claimIdentityProbeHandle(path: string, device: USBDevice) {
|
|
660
|
+
if (device.opened || openingHandles.has(device) || identityProbeHandles.has(device)) {
|
|
661
|
+
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'USB device is already in use');
|
|
662
|
+
}
|
|
663
|
+
identityProbeHandles.add(device);
|
|
664
|
+
this.identityProbeDevices.set(path, device);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
private releaseIdentityProbeHandle(path: string) {
|
|
668
|
+
const device = this.identityProbeDevices.get(path);
|
|
669
|
+
if (device) identityProbeHandles.delete(device);
|
|
670
|
+
this.identityProbeDevices.delete(path);
|
|
671
|
+
}
|
|
672
|
+
|
|
618
673
|
private async closeOpenDevice(path: string) {
|
|
619
|
-
const current = this.
|
|
674
|
+
const current = this.identityProbe
|
|
675
|
+
? this.identityProbeDevices.get(path)
|
|
676
|
+
: this.deviceList.find(device => device.path === path)?.device;
|
|
620
677
|
if (!current?.opened) return;
|
|
678
|
+
this.assertHandleAvailable(current);
|
|
621
679
|
|
|
622
680
|
const endpoints = this.deviceEndpoints.get(path);
|
|
623
681
|
const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
|
|
@@ -630,6 +688,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
630
688
|
await current.close();
|
|
631
689
|
} catch (error) {
|
|
632
690
|
this.Log?.debug('[WebUsbTransport] close before reconnect failed:', error);
|
|
691
|
+
if (this.identityProbe) throw error;
|
|
633
692
|
}
|
|
634
693
|
}
|
|
635
694
|
|
|
@@ -984,15 +1043,15 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
984
1043
|
}
|
|
985
1044
|
|
|
986
1045
|
try {
|
|
987
|
-
await this.callProtocolV1(
|
|
1046
|
+
const response = await this.callProtocolV1(
|
|
988
1047
|
path,
|
|
989
|
-
'Initialize',
|
|
1048
|
+
this.identityProbe ? 'GetFeatures' : 'Initialize',
|
|
990
1049
|
{},
|
|
991
1050
|
{
|
|
992
1051
|
timeoutMs: PROTOCOL_V1_PROBE_TIMEOUT,
|
|
993
1052
|
}
|
|
994
1053
|
);
|
|
995
|
-
return
|
|
1054
|
+
return !this.identityProbe || response.type === 'Features';
|
|
996
1055
|
} catch (error) {
|
|
997
1056
|
if (this.isWebUsbIoHardwareError(error)) throw error;
|
|
998
1057
|
return false;
|
|
@@ -1149,8 +1208,12 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
1149
1208
|
*/
|
|
1150
1209
|
async release(path: string) {
|
|
1151
1210
|
this.acquiredPaths.delete(path);
|
|
1152
|
-
|
|
1153
|
-
|
|
1211
|
+
try {
|
|
1212
|
+
await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
|
|
1213
|
+
await this.closeOpenDevice(path);
|
|
1214
|
+
} finally {
|
|
1215
|
+
this.releaseIdentityProbeHandle(path);
|
|
1216
|
+
}
|
|
1154
1217
|
// Keep deviceProtocol/deviceProtocolHints across release: the probe result is a
|
|
1155
1218
|
// physical-device property, so the next acquire can skip the wire-level probe.
|
|
1156
1219
|
// V2 entries are still dropped by onProtocolV2UsbLinkInvalidated via the link
|