@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.
@@ -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
- registerActiveWebUsbTransport(this, usb);
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
- yield this.closeOpenDevice(input.path);
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 = (_a = this.deviceList.find(device => device.path === path)) === null || _a === void 0 ? void 0 : _a.device;
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 true;
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
- yield this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
873
- yield this.closeOpenDevice(path);
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;
@@ -1 +1 @@
1
- {"version":3,"file":"webusb.d.ts","sourceRoot":"","sources":["../src/webusb.ts"],"names":[],"mappings":"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"}
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;AA6CD,MAAM,CAAC,OAAO,OAAO,eAAgB,SAAQ,0BAA0B,CAAC,MAAM,CAAC;IA+DjE,OAAO,CAAC,QAAQ,CAAC,aAAa;IA9D1C,QAAQ,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAGlE,UAAU,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAEpE,OAAO,CAAC,sBAAsB,CAAqB;IAGnD,OAAO,CAAC,cAAc,CAAwC;IAG9D,OAAO,CAAC,wBAAwB,CAAwC;IAExE,OAAO,CAAC,mBAAmB,CAAwC;IAMnE,OAAO,CAAC,kBAAkB,CAA0B;IAGpD,OAAO,CAAC,aAAa,CAA0B;IAE/C,OAAO,CAAC,oBAAoB,CAAgC;IAS5D,OAAO,CAAC,mBAAmB,CAAqC;IAGhE,OAAO,CAAC,eAAe,CAA2C;IAElE,IAAI,SAAqB;IAEzB,OAAO,UAAS;IAEhB,UAAU,UAAS;IAEnB,GAAG,CAAC,EAAE,GAAG,CAAC;IAEV,GAAG,CAAC,EAAE,GAAG,CAAC;IAEV,OAAO,CAAC,EAAE,YAAY,CAAC;IAMvB,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,CAAM;IAEnC,eAAe,SAAoB;IAEnC,UAAU,SAAe;IAEzB,WAAW,SAAgB;gBAEE,aAAa,UAAQ;IAWlD,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,YAAY;IAmBxC,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS;IAkBrD,iBAAiB,CAAC,IAAI,EAAE,MAAM;IAQ9B,SAAS,CAAC,UAAU,EAAE,GAAG;IASzB,mBAAmB,CAAC,UAAU,EAAE,GAAG;IAsB7B,kBAAkB;IAmBlB,SAAS;IAQT,mBAAmB;IAmCnB,OAAO,CAAC,KAAK,EAAE,YAAY;IAuGjC,OAAO,CAAC,2BAA2B;IAOnC,OAAO,CAAC,+BAA+B;IAOvC,OAAO,CAAC,4BAA4B;YAItB,cAAc;IAmEtB,UAAU,CAAC,IAAI,EAAE,MAAM;IAwBvB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;IAqB1C,OAAO,CAAC,iBAAiB;IAiCnB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;IAkElD,OAAO,CAAC,qBAAqB;IAS7B,OAAO,CAAC,wBAAwB;IAQhC,OAAO,CAAC,0BAA0B;YAMpB,eAAe;YAsBf,iBAAiB;IAsB/B,OAAO,CAAC,cAAc;IAMhB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IASvE,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,uBAAuB;YASjB,uBAAuB;YAWvB,kBAAkB;IAwChC,OAAO,CAAC,+BAA+B;IAIvC,OAAO,CAAC,wBAAwB;YAelB,yBAAyB;IAsCvC,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,aAAa;YASP,oBAAoB;YA2BpB,eAAe;YAoBf,mBAAmB;YA2CnB,cAAc;YAWd,yBAAyB;YAKzB,yBAAyB;YAMzB,uBAAuB;YA0CvB,eAAe;YAqBf,eAAe;IAiBvB,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,oBAAoB;YA2BlB,cAAc;YAkCd,cAAc;IAkBtB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM;IAgD5C,OAAO,CAAC,IAAI,EAAE,MAAM;IAgB1B,SAAS,CAAC,uBAAuB,IAAI,iBAAiB;IAUtD,SAAS,CAAC,sBAAsB;cAIhB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,qBAAqB,GAC9B,OAAO,CAAC,IAAI,CAAC;cAIA,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,qBAAqB,GAC9B,OAAO,CAAC,UAAU,CAAC;cASN,4BAA4B,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1F,SAAS,CAAC,8BAA8B,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAKrE,SAAS,CAAC,+BAA+B,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,KAAK;IAWjF,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;CAGxD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-web-device",
3
- "version": "1.2.8-alpha.13",
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.13",
25
- "@onekeyfe/hd-transport": "1.2.8-alpha.13"
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.13",
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": "ed22eddce37ce0562b329c42a6985b50949ca412"
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
- await this.closeOpenDevice(input.path);
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.deviceList.find(device => device.path === path)?.device;
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 true;
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
- await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
1153
- await this.closeOpenDevice(path);
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