@onekeyfe/hd-transport-web-device 1.2.8-alpha.15 → 1.2.8-alpha.16

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.
@@ -1,7 +1,37 @@
1
+ import { ERRORS, HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
2
+
1
3
  import WebUsbTransport from '../src/webusb';
4
+ import { probeUsbDeviceIdentities } from '@onekeyfe/hd-common-connect-sdk/src';
2
5
 
3
6
  import type { MessageFromOneKey, TransportCallOptions } from '@onekeyfe/hd-transport';
4
7
 
8
+ jest.mock('@onekeyfe/hd-core', () => jest.requireActual('../../core/src'));
9
+ jest.mock('../../core/src/data/config', () => ({
10
+ getSDKVersion: () => '1.0.0-test',
11
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0-test/',
12
+ }));
13
+ jest.mock('@onekeyfe/hd-transport-web-device', () => ({
14
+ WebUsbTransport: jest.requireActual('../src/webusb').default,
15
+ }));
16
+ jest.mock('@onekeyfe/hd-transport-http', () => ({ __esModule: true, default: jest.fn() }));
17
+ jest.mock('@onekeyfe/hd-transport-lowlevel', () => ({}));
18
+ jest.mock('@onekeyfe/hd-transport-emulator', () => ({}));
19
+
20
+ type PacketIoTransport = {
21
+ rotateProtocolV2UsbGeneration: () => Promise<void>;
22
+ detectProtocol: () => Promise<'V1'>;
23
+ transferInWithRetry: (path: string, length: number) => Promise<DataView>;
24
+ transferInOnce: (path: string, length: number) => Promise<DataView>;
25
+ transferOutWithRetry: (path: string, packet: Uint8Array) => Promise<void>;
26
+ transferOutOnce: (path: string, packet: Uint8Array) => Promise<void>;
27
+ reconnectForPacketIoRetry: (
28
+ path: string,
29
+ direction: 'in' | 'out',
30
+ attempt: number,
31
+ error: unknown
32
+ ) => Promise<void>;
33
+ };
34
+
5
35
  describe('WebUSB identity probe ownership', () => {
6
36
  let originalNavigator: PropertyDescriptor | undefined;
7
37
  const device = {
@@ -80,6 +110,7 @@ describe('WebUSB identity probe ownership', () => {
80
110
  expect(device.close).not.toHaveBeenCalled();
81
111
  finishOpen();
82
112
  await connecting;
113
+ await active.release('USB_PATH');
83
114
  });
84
115
 
85
116
  test('a normal connection cannot close an isolated probe handle and release closes the captured handle after re-enumeration', async () => {
@@ -105,8 +136,10 @@ describe('WebUSB identity probe ownership', () => {
105
136
  const replacement = { ...device, close: jest.fn() };
106
137
  usb.getDevices.mockResolvedValue([replacement]);
107
138
  await probe.getConnectedDevices();
108
- await probe.release('USB_PATH');
109
- expect(device.close).toHaveBeenCalledTimes(1);
139
+ device.close.mockRejectedValueOnce(new Error('native close failed'));
140
+ await expect(probe.release('USB_PATH')).rejects.toThrow('native close failed');
141
+ await probe.enumerate();
142
+ expect(device.close).toHaveBeenCalledTimes(2);
110
143
  expect(replacement.close).not.toHaveBeenCalled();
111
144
  });
112
145
 
@@ -175,6 +208,292 @@ describe('WebUSB identity probe ownership', () => {
175
208
  }
176
209
  );
177
210
 
211
+ test.each(['release', 'acquire failure'])(
212
+ 'the public probe retries failed %s cleanup on its next call',
213
+ async phase => {
214
+ const internals = WebUsbTransport.prototype as unknown as {
215
+ rotateProtocolV2UsbGeneration: () => Promise<void>;
216
+ detectProtocol: () => Promise<'V1'>;
217
+ };
218
+ jest.spyOn(internals, 'rotateProtocolV2UsbGeneration').mockResolvedValue(undefined);
219
+ jest.spyOn(internals, 'detectProtocol').mockResolvedValue('V1');
220
+ jest.spyOn(WebUsbTransport.prototype, 'getProtocolType').mockReturnValue('V1');
221
+ jest.spyOn(WebUsbTransport.prototype, 'configure').mockImplementation(() => undefined);
222
+ jest
223
+ .spyOn(WebUsbTransport.prototype, 'configureProtocolV2')
224
+ .mockImplementation(() => undefined);
225
+ const connect = jest.spyOn(WebUsbTransport.prototype, 'connect').mockImplementation(() => {
226
+ device.opened = true;
227
+ return Promise.resolve();
228
+ });
229
+ if (phase === 'acquire failure') {
230
+ connect.mockImplementationOnce(() => {
231
+ device.opened = true;
232
+ return Promise.reject(new Error('probe failed'));
233
+ });
234
+ }
235
+ jest.spyOn(WebUsbTransport.prototype, 'call').mockResolvedValue({
236
+ type: 'Features',
237
+ message: { onekey_serial_no: 'PHYSICAL_SERIAL', onekey_device_type: 'CLASSIC1S' },
238
+ });
239
+ device.close.mockRejectedValueOnce(new Error('native close failed'));
240
+ try {
241
+ await expect(probeUsbDeviceIdentities({ commType: 'webusb' })).resolves.toEqual([]);
242
+ expect(device.opened).toBe(true);
243
+ const active = new WebUsbTransport();
244
+ active.init({ debug: jest.fn() });
245
+ await expect(active.connectToDevice('USB_PATH', true)).rejects.toThrow(
246
+ 'USB device is already in use'
247
+ );
248
+ device.close.mockRejectedValueOnce(new Error('native close still failing'));
249
+ await expect(probeUsbDeviceIdentities({ commType: 'webusb' })).resolves.toEqual([]);
250
+ expect(device.opened).toBe(true);
251
+ expect(connect).toHaveBeenCalledTimes(1);
252
+ expect(device.close).toHaveBeenCalledTimes(2);
253
+ await expect(probeUsbDeviceIdentities({ commType: 'webusb' })).resolves.toMatchObject([
254
+ { path: 'USB_PATH', serialNo: 'PHYSICAL_SERIAL' },
255
+ ]);
256
+ expect(connect).toHaveBeenCalledTimes(2);
257
+ expect(device.opened).toBe(false);
258
+ } finally {
259
+ jest.restoreAllMocks();
260
+ }
261
+ }
262
+ );
263
+
264
+ test.each(['in', 'out'] as const)(
265
+ 'the public probe cannot take over a first acquire while transfer%s recovery has closed the handle',
266
+ async direction => {
267
+ const closed = createDeferred<void>();
268
+ const resume = createDeferred<void>();
269
+ Object.assign(device, {
270
+ open: jest.fn(() => {
271
+ device.opened = true;
272
+ return Promise.resolve();
273
+ }),
274
+ releaseInterface: jest.fn(),
275
+ reset: jest.fn(),
276
+ configurations: [],
277
+ configuration: { configurationValue: 1 },
278
+ selectConfiguration: jest.fn(),
279
+ claimInterface: jest.fn(),
280
+ clearHalt: jest.fn(),
281
+ transferIn: jest
282
+ .fn()
283
+ .mockResolvedValue({ status: 'ok', data: new DataView(new ArrayBuffer(64)) }),
284
+ transferOut: jest.fn().mockResolvedValue({ status: 'ok' }),
285
+ });
286
+ const native = device as typeof device & { transferIn: jest.Mock; transferOut: jest.Mock };
287
+ native[direction === 'in' ? 'transferIn' : 'transferOut'].mockRejectedValueOnce(
288
+ new Error(`transfer${direction} NetworkError`)
289
+ );
290
+ device.close.mockImplementationOnce(async () => {
291
+ device.opened = false;
292
+ closed.resolve();
293
+ await resume.promise;
294
+ });
295
+ const active = new WebUsbTransport();
296
+ active.init({ debug: jest.fn() });
297
+ const internals = active as unknown as PacketIoTransport;
298
+ jest
299
+ .spyOn(
300
+ WebUsbTransport.prototype as unknown as PacketIoTransport,
301
+ 'rotateProtocolV2UsbGeneration'
302
+ )
303
+ .mockResolvedValue(undefined);
304
+ jest
305
+ .spyOn(WebUsbTransport.prototype as unknown as PacketIoTransport, 'detectProtocol')
306
+ .mockImplementation(async function (this: WebUsbTransport) {
307
+ if (this !== active) return 'V1';
308
+ if (direction === 'in') await internals.transferInWithRetry('USB_PATH', 64);
309
+ else await internals.transferOutWithRetry('USB_PATH', new Uint8Array([1]));
310
+ return 'V1';
311
+ });
312
+ jest.spyOn(WebUsbTransport.prototype, 'getProtocolType').mockReturnValue('V1');
313
+ jest.spyOn(WebUsbTransport.prototype, 'configure').mockImplementation(() => undefined);
314
+ jest
315
+ .spyOn(WebUsbTransport.prototype, 'configureProtocolV2')
316
+ .mockImplementation(() => undefined);
317
+ jest
318
+ .spyOn(WebUsbTransport.prototype, 'connect')
319
+ .mockImplementation(async function (this: WebUsbTransport, path, first) {
320
+ if (this === active && !first) return this.connectToDevice(path, first);
321
+ device.opened = true;
322
+ });
323
+ jest.spyOn(WebUsbTransport.prototype, 'call').mockResolvedValue({
324
+ type: 'Features',
325
+ message: { onekey_serial_no: 'PHYSICAL_SERIAL', onekey_device_type: 'CLASSIC1S' },
326
+ });
327
+ try {
328
+ const acquiring = active.acquire({ path: 'USB_PATH' });
329
+ try {
330
+ await closed.promise;
331
+ await expect(probeUsbDeviceIdentities({ commType: 'webusb' })).resolves.toEqual([]);
332
+ resume.resolve();
333
+ await expect(acquiring).resolves.toBe('USB_PATH');
334
+ } finally {
335
+ resume.resolve();
336
+ await acquiring.catch(() => undefined);
337
+ await active.release('USB_PATH');
338
+ }
339
+ await expect(probeUsbDeviceIdentities({ commType: 'webusb' })).resolves.toMatchObject([
340
+ { serialNo: 'PHYSICAL_SERIAL' },
341
+ ]);
342
+ } finally {
343
+ jest.restoreAllMocks();
344
+ }
345
+ }
346
+ );
347
+
348
+ test('releases ownership after a failed normal acquire', async () => {
349
+ const active = new WebUsbTransport();
350
+ const probe = new WebUsbTransport(true);
351
+ active.init({ debug: jest.fn() });
352
+ probe.init({ debug: jest.fn() });
353
+ const state = active as unknown as PacketIoTransport;
354
+ jest.spyOn(state, 'rotateProtocolV2UsbGeneration').mockResolvedValue(undefined);
355
+ jest.spyOn(active, 'connect').mockImplementation(() => {
356
+ device.opened = true;
357
+ return Promise.resolve();
358
+ });
359
+ jest.spyOn(state, 'detectProtocol').mockRejectedValue(new Error('protocol failed'));
360
+ await expect(active.acquire({ path: 'USB_PATH' })).rejects.toThrow('protocol failed');
361
+ expect(device.opened).toBe(false);
362
+ const probeState = probe as unknown as PacketIoTransport;
363
+ jest.spyOn(probeState, 'rotateProtocolV2UsbGeneration').mockResolvedValue(undefined);
364
+ jest.spyOn(probeState, 'detectProtocol').mockResolvedValue('V1');
365
+ jest.spyOn(probe, 'connect').mockImplementation(() => {
366
+ device.opened = true;
367
+ return Promise.resolve();
368
+ });
369
+ try {
370
+ await expect(probe.acquire({ path: 'USB_PATH' })).resolves.toBe('USB_PATH');
371
+ } finally {
372
+ await probe.release('USB_PATH');
373
+ }
374
+ });
375
+
376
+ test('releasing one path preserves another closed handle owned by the same transport', async () => {
377
+ const other = {
378
+ ...device,
379
+ serialNumber: 'OTHER_PATH',
380
+ opened: false,
381
+ close: jest.fn(() => {
382
+ other.opened = false;
383
+ return Promise.resolve();
384
+ }),
385
+ };
386
+ usb.getDevices.mockResolvedValue([device, other]);
387
+ const active = new WebUsbTransport();
388
+ active.init({ debug: jest.fn() });
389
+ const state = active as unknown as PacketIoTransport;
390
+ jest.spyOn(state, 'rotateProtocolV2UsbGeneration').mockResolvedValue(undefined);
391
+ jest.spyOn(state, 'detectProtocol').mockResolvedValue('V1');
392
+ jest.spyOn(active, 'connect').mockImplementation(path => {
393
+ const target = path === 'USB_PATH' ? device : other;
394
+ target.opened = true;
395
+ return Promise.resolve();
396
+ });
397
+ await active.acquire({ path: 'USB_PATH' });
398
+ await active.acquire({ path: 'OTHER_PATH' });
399
+ await active.release('USB_PATH');
400
+ other.opened = false;
401
+ const probe = new WebUsbTransport(true);
402
+ probe.init({ debug: jest.fn() });
403
+ const probeState = probe as unknown as PacketIoTransport;
404
+ jest.spyOn(probeState, 'rotateProtocolV2UsbGeneration').mockResolvedValue(undefined);
405
+ jest.spyOn(probeState, 'detectProtocol').mockResolvedValue('V1');
406
+ jest.spyOn(probe, 'connect').mockImplementation(path => {
407
+ const target = path === 'USB_PATH' ? device : other;
408
+ target.opened = true;
409
+ return Promise.resolve();
410
+ });
411
+ try {
412
+ await expect(probe.acquire({ path: 'USB_PATH' })).resolves.toBe('USB_PATH');
413
+ await expect(probe.acquire({ path: 'OTHER_PATH' })).rejects.toThrow(
414
+ 'USB device is already in use'
415
+ );
416
+ } finally {
417
+ await active.release('OTHER_PATH');
418
+ await probe.release('USB_PATH');
419
+ }
420
+ });
421
+
422
+ test.each(['read', 'read retry', 'write', 'write retry', 'retry cleanup'])(
423
+ 'blocks native %s while an isolated probe owns the handle',
424
+ async operation => {
425
+ const probe = new WebUsbTransport(true);
426
+ probe.init({ debug: jest.fn() });
427
+ const probeState = probe as unknown as PacketIoTransport;
428
+ jest.spyOn(probeState, 'rotateProtocolV2UsbGeneration').mockResolvedValue(undefined);
429
+ jest.spyOn(probeState, 'detectProtocol').mockResolvedValue('V1');
430
+ jest.spyOn(probe, 'connect').mockImplementation(() => {
431
+ device.opened = true;
432
+ return Promise.resolve();
433
+ });
434
+ const native = Object.assign(device, {
435
+ releaseInterface: jest.fn(),
436
+ transferIn: jest
437
+ .fn()
438
+ .mockResolvedValue({ status: 'ok', data: new DataView(new ArrayBuffer(64)) }),
439
+ transferOut: jest.fn().mockResolvedValue({ status: 'ok' }),
440
+ });
441
+ await probe.acquire({ path: 'USB_PATH' });
442
+ const active = new WebUsbTransport();
443
+ active.init({ debug: jest.fn() });
444
+ const state = active as unknown as PacketIoTransport;
445
+ try {
446
+ let call: Promise<unknown>;
447
+ if (operation === 'read') call = state.transferInOnce('USB_PATH', 64);
448
+ else if (operation === 'read retry') call = state.transferInWithRetry('USB_PATH', 64);
449
+ else if (operation === 'write')
450
+ call = state.transferOutOnce('USB_PATH', new Uint8Array([1]));
451
+ else if (operation === 'write retry')
452
+ call = state.transferOutWithRetry('USB_PATH', new Uint8Array([1]));
453
+ else
454
+ call = state.reconnectForPacketIoRetry(
455
+ 'USB_PATH',
456
+ 'in',
457
+ 0,
458
+ new Error('transferIn failed')
459
+ );
460
+ await expect(call).rejects.toThrow('USB device is already in use');
461
+ expect(native.transferIn).not.toHaveBeenCalled();
462
+ expect(native.transferOut).not.toHaveBeenCalled();
463
+ expect(native.releaseInterface).not.toHaveBeenCalled();
464
+ expect(device.close).not.toHaveBeenCalled();
465
+ } finally {
466
+ await probe.release('USB_PATH');
467
+ }
468
+ }
469
+ );
470
+
471
+ test.each(['in', 'out'] as const)(
472
+ 'ends transfer%s immediately if reconnect reports an ownership conflict',
473
+ async direction => {
474
+ const active = new WebUsbTransport();
475
+ active.init({ debug: jest.fn() });
476
+ device.opened = true;
477
+ const native = Object.assign(device, {
478
+ transferIn: jest.fn().mockRejectedValue(new Error('transferIn NetworkError')),
479
+ transferOut: jest.fn().mockRejectedValue(new Error('transferOut NetworkError')),
480
+ });
481
+ const state = active as unknown as PacketIoTransport;
482
+ const conflict = ERRORS.TypedError(
483
+ HardwareErrorCode.RuntimeError,
484
+ 'USB device is already in use'
485
+ );
486
+ const reconnect = jest.spyOn(state, 'reconnectForPacketIoRetry').mockRejectedValue(conflict);
487
+ const call =
488
+ direction === 'in'
489
+ ? state.transferInWithRetry('USB_PATH', 64)
490
+ : state.transferOutWithRetry('USB_PATH', new Uint8Array([1]));
491
+ await expect(call).rejects.toBe(conflict);
492
+ expect(reconnect).toHaveBeenCalledTimes(1);
493
+ expect(native[direction === 'in' ? 'transferIn' : 'transferOut']).toHaveBeenCalledTimes(1);
494
+ }
495
+ );
496
+
178
497
  test.each([false, true])(
179
498
  'uses a read-only V1 probe only for isolated discovery (%s)',
180
499
  async identityProbe => {
package/dist/index.d.ts CHANGED
@@ -31,6 +31,8 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
31
31
  /** Paths currently acquired (between a successful acquire() and its release()). */
32
32
  private acquiredPaths;
33
33
  private identityProbeDevices;
34
+ private normalDevices;
35
+ private identityProbeCleanupPaths;
34
36
  /**
35
37
  * The exact USBDevice object each cached protocol was probed against. The
36
38
  * browser returns the same object identity for a device as long as it stays
@@ -132,6 +134,9 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
132
134
  */
133
135
  connectToDevice(path: string, first: boolean): Promise<undefined>;
134
136
  private assertHandleAvailable;
137
+ private claimNormalHandle;
138
+ private releaseNormalHandles;
139
+ private isHandleOwnershipError;
135
140
  private claimIdentityProbeHandle;
136
141
  private releaseIdentityProbeHandle;
137
142
  private closeOpenDevice;
package/dist/index.js CHANGED
@@ -51,6 +51,7 @@ const REPORT_ID = transport.PROTOCOL_V1_REPORT_ID;
51
51
  const HEADER_LENGTH = transport.PROTOCOL_V1_MESSAGE_HEADER_SIZE;
52
52
  const PACKET_IO_MAX_RETRIES = 3;
53
53
  const PACKET_IO_RETRY_DELAY = 300;
54
+ const USB_DEVICE_IN_USE = 'USB device is already in use';
54
55
  const PROTOCOL_V1_PROBE_TIMEOUT = 5000;
55
56
  const PROTOCOL_V2_PROBE_TIMEOUT = 1000;
56
57
  const EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS = 2;
@@ -58,6 +59,7 @@ let activeWebUsbTransport;
58
59
  let usbDisconnectListenerAttached = false;
59
60
  const identityProbeHandles = new WeakSet();
60
61
  const openingHandles = new WeakSet();
62
+ const normalHandleOwners = new WeakMap();
61
63
  function registerActiveWebUsbTransport(instance, usb) {
62
64
  activeWebUsbTransport = instance;
63
65
  if (usbDisconnectListenerAttached)
@@ -87,6 +89,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
87
89
  this.staleProtocolPaths = new Set();
88
90
  this.acquiredPaths = new Set();
89
91
  this.identityProbeDevices = new Map();
92
+ this.normalDevices = new Map();
93
+ this.identityProbeCleanupPaths = new Set();
90
94
  this.probedDeviceObjects = new Map();
91
95
  this.deviceEndpoints = new Map();
92
96
  this.name = 'WebUsbTransport';
@@ -155,6 +159,13 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
155
159
  }
156
160
  enumerate() {
157
161
  return __awaiter(this, void 0, void 0, function* () {
162
+ for (const path of this.identityProbeCleanupPaths) {
163
+ try {
164
+ yield this.release(path);
165
+ }
166
+ catch (_a) {
167
+ }
168
+ }
158
169
  yield this.getConnectedDevices();
159
170
  return this.deviceList;
160
171
  });
@@ -196,6 +207,9 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
196
207
  yield this.getConnectedDevices();
197
208
  this.claimIdentityProbeHandle(input.path, yield this.findDevice(input.path));
198
209
  }
210
+ else {
211
+ this.claimNormalHandle(input.path, yield this.findDevice(input.path));
212
+ }
199
213
  try {
200
214
  yield this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
201
215
  yield this.closeOpenDevice(input.path);
@@ -262,6 +276,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
262
276
  }
263
277
  finally {
264
278
  this.releaseIdentityProbeHandle(input.path);
279
+ this.releaseNormalHandles(input.path);
265
280
  }
266
281
  throw e;
267
282
  }
@@ -394,6 +409,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
394
409
  this.releaseIdentityProbeHandle(path);
395
410
  this.claimIdentityProbeHandle(path, device);
396
411
  }
412
+ if (!this.identityProbe)
413
+ this.claimNormalHandle(path, device);
397
414
  this.assertHandleAvailable(device);
398
415
  if (!device.opened) {
399
416
  openingHandles.add(device);
@@ -422,6 +439,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
422
439
  this.releaseIdentityProbeHandle(path);
423
440
  this.claimIdentityProbeHandle(path, device);
424
441
  }
442
+ if (!this.identityProbe)
443
+ this.claimNormalHandle(path, device);
425
444
  this.assertHandleAvailable(device);
426
445
  if (!device.opened) {
427
446
  openingHandles.add(device);
@@ -453,25 +472,51 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
453
472
  });
454
473
  }
455
474
  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');
475
+ const normalOwner = normalHandleOwners.get(device);
476
+ if ((normalOwner && normalOwner !== this) ||
477
+ (identityProbeHandles.has(device) &&
478
+ ![...this.identityProbeDevices.values()].includes(device))) {
479
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, USB_DEVICE_IN_USE);
459
480
  }
460
481
  }
482
+ claimNormalHandle(path, device) {
483
+ this.assertHandleAvailable(device);
484
+ normalHandleOwners.set(device, this);
485
+ this.normalDevices.set(device, path);
486
+ }
487
+ releaseNormalHandles(path) {
488
+ for (const [device, ownedPath] of this.normalDevices) {
489
+ if (ownedPath === path) {
490
+ normalHandleOwners.delete(device);
491
+ this.normalDevices.delete(device);
492
+ }
493
+ }
494
+ }
495
+ isHandleOwnershipError(error) {
496
+ return (error instanceof hdShared.HardwareError &&
497
+ error.errorCode === hdShared.HardwareErrorCode.RuntimeError &&
498
+ this.getErrorMessage(error).includes(USB_DEVICE_IN_USE));
499
+ }
461
500
  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');
501
+ if (device.opened ||
502
+ openingHandles.has(device) ||
503
+ identityProbeHandles.has(device) ||
504
+ normalHandleOwners.has(device)) {
505
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, USB_DEVICE_IN_USE);
464
506
  }
465
507
  identityProbeHandles.add(device);
466
508
  this.identityProbeDevices.set(path, device);
467
509
  }
468
510
  releaseIdentityProbeHandle(path) {
469
511
  const device = this.identityProbeDevices.get(path);
470
- if (device === null || device === void 0 ? void 0 : device.opened)
512
+ if (device === null || device === void 0 ? void 0 : device.opened) {
513
+ this.identityProbeCleanupPaths.add(path);
471
514
  return;
515
+ }
472
516
  if (device)
473
517
  identityProbeHandles.delete(device);
474
518
  this.identityProbeDevices.delete(path);
519
+ this.identityProbeCleanupPaths.delete(path);
475
520
  }
476
521
  closeOpenDevice(path) {
477
522
  var _a, _b, _c, _d;
@@ -490,6 +535,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
490
535
  catch (error) {
491
536
  (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
492
537
  }
538
+ this.assertHandleAvailable(current);
493
539
  try {
494
540
  yield current.close();
495
541
  }
@@ -503,6 +549,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
503
549
  clearEndpointHalt(device, direction, endpointNumber) {
504
550
  var _a;
505
551
  return __awaiter(this, void 0, void 0, function* () {
552
+ this.assertHandleAvailable(device);
506
553
  try {
507
554
  yield device.clearHalt(direction, endpointNumber);
508
555
  }
@@ -568,7 +615,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
568
615
  throwWebUsbIoError(path, operation, error) {
569
616
  var _a;
570
617
  return __awaiter(this, void 0, void 0, function* () {
571
- if (this.isWebUsbIoHardwareError(error))
618
+ if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error))
572
619
  throw error;
573
620
  if (error instanceof transport.ProtocolV2LinkError && error.code !== 'io')
574
621
  throw error;
@@ -602,7 +649,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
602
649
  return /LIBUSB_ERROR_ACCESS|\b(?:access|permission) denied\b/i.test(message);
603
650
  }
604
651
  isRetryablePacketIoError(error) {
605
- if (this.isWebUsbIoHardwareError(error))
652
+ if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error))
606
653
  return false;
607
654
  const message = this.getErrorMessage(error).toLowerCase();
608
655
  if (this.isWebUsbPermissionDeniedMessage(message))
@@ -623,6 +670,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
623
670
  yield hdShared.wait(attempt * PACKET_IO_RETRY_DELAY);
624
671
  try {
625
672
  const currentDevice = yield this.findDevice(path);
673
+ this.assertHandleAvailable(currentDevice);
626
674
  if (currentDevice.opened) {
627
675
  const endpoints = this.deviceEndpoints.get(path);
628
676
  const ifaceNum = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.interfaceNumber) !== null && _a !== void 0 ? _a : this.interfaceId;
@@ -632,10 +680,13 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
632
680
  catch (releaseError) {
633
681
  this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
634
682
  }
683
+ this.assertHandleAvailable(currentDevice);
635
684
  yield currentDevice.close();
636
685
  }
637
686
  }
638
687
  catch (closeError) {
688
+ if (this.isHandleOwnershipError(closeError))
689
+ throw closeError;
639
690
  this.Log.debug('[WebUsbTransport] close device before retry error:', closeError);
640
691
  }
641
692
  yield this.getConnectedDevices();
@@ -677,6 +728,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
677
728
  yield this.reconnectForPacketIoRetry(path, 'out', attempt, error);
678
729
  }
679
730
  catch (reconnectError) {
731
+ if (this.isHandleOwnershipError(reconnectError))
732
+ throw reconnectError;
680
733
  lastError = reconnectError;
681
734
  this.Log.debug(`[WebUsbTransport] transferout reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(reconnectError)}`);
682
735
  }
@@ -695,6 +748,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
695
748
  const endpoints = this.deviceEndpoints.get(path);
696
749
  const endpointOut = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.endpointOut) !== null && _a !== void 0 ? _a : this.endpointId;
697
750
  const transferBuffer = this.toArrayBuffer(packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength));
751
+ this.assertHandleAvailable(device);
698
752
  const result = yield device.transferOut(endpointOut, transferBuffer);
699
753
  if (result.status !== 'ok') {
700
754
  return this.throwWebUsbIoError(path, 'transferOut', new Error(`transferOut status: ${String(result.status)}`));
@@ -716,6 +770,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
716
770
  }
717
771
  const endpoints = this.deviceEndpoints.get(path);
718
772
  const endpointIn = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.endpointIn) !== null && _a !== void 0 ? _a : this.endpointId;
773
+ this.assertHandleAvailable(device);
719
774
  const result = yield device.transferIn(endpointIn, length);
720
775
  return this.getTransferInData(result);
721
776
  }
@@ -732,6 +787,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
732
787
  yield this.reconnectForPacketIoRetry(path, 'in', attempt, error);
733
788
  }
734
789
  catch (reconnectError) {
790
+ if (this.isHandleOwnershipError(reconnectError))
791
+ throw reconnectError;
735
792
  lastError = reconnectError;
736
793
  this.Log.debug(`[WebUsbTransport] transferin reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(reconnectError)}`);
737
794
  }
@@ -749,6 +806,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
749
806
  }
750
807
  const endpoints = this.deviceEndpoints.get(path);
751
808
  const endpointIn = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.endpointIn) !== null && _a !== void 0 ? _a : this.endpointId;
809
+ this.assertHandleAvailable(device);
752
810
  const result = yield device.transferIn(endpointIn, length);
753
811
  return this.getTransferInData(result);
754
812
  });
@@ -935,6 +993,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
935
993
  }
936
994
  finally {
937
995
  this.releaseIdentityProbeHandle(path);
996
+ this.releaseNormalHandles(path);
938
997
  }
939
998
  this.deviceEndpoints.delete(path);
940
999
  });
package/dist/webusb.d.ts CHANGED
@@ -19,6 +19,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
19
19
  private staleProtocolPaths;
20
20
  private acquiredPaths;
21
21
  private identityProbeDevices;
22
+ private normalDevices;
23
+ private identityProbeCleanupPaths;
22
24
  private probedDeviceObjects;
23
25
  private deviceEndpoints;
24
26
  name: string;
@@ -50,6 +52,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
50
52
  private discoverEndpoints;
51
53
  connectToDevice(path: string, first: boolean): Promise<undefined>;
52
54
  private assertHandleAvailable;
55
+ private claimNormalHandle;
56
+ private releaseNormalHandles;
57
+ private isHandleOwnershipError;
53
58
  private claimIdentityProbeHandle;
54
59
  private releaseIdentityProbeHandle;
55
60
  private closeOpenDevice;
@@ -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":"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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.15",
3
+ "version": "1.2.8-alpha.16",
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.15",
25
- "@onekeyfe/hd-transport": "1.2.8-alpha.15"
24
+ "@onekeyfe/hd-shared": "1.2.8-alpha.16",
25
+ "@onekeyfe/hd-transport": "1.2.8-alpha.16"
26
26
  },
27
27
  "devDependencies": {
28
- "@onekeyfe/hd-transport-electron": "1.2.8-alpha.15",
28
+ "@onekeyfe/hd-transport-electron": "1.2.8-alpha.16",
29
29
  "@types/w3c-web-usb": "^1.0.6",
30
30
  "@types/web-bluetooth": "^0.0.17"
31
31
  },
32
- "gitHead": "e84d88f3866ddfb99e18eed9188a583f56a30b17"
32
+ "gitHead": "7ccc3f507e2b536e6b5832e1cbb24a5fc1970ebe"
33
33
  }
package/src/webusb.ts CHANGED
@@ -44,6 +44,7 @@ const REPORT_ID = PROTOCOL_V1_REPORT_ID;
44
44
  const HEADER_LENGTH = PROTOCOL_V1_MESSAGE_HEADER_SIZE;
45
45
  const PACKET_IO_MAX_RETRIES = 3;
46
46
  const PACKET_IO_RETRY_DELAY = 300;
47
+ const USB_DEVICE_IN_USE = 'USB device is already in use';
47
48
  // Legacy devices can take longer to answer Initialize after a WebUSB reset.
48
49
  // Keep this aligned with Node USB so a slow Protocol V1 response is not
49
50
  // misreported as a protocol mismatch during acquire.
@@ -86,6 +87,8 @@ let usbDisconnectListenerAttached = false;
86
87
  // open them, including while native open() is still pending.
87
88
  const identityProbeHandles = new WeakSet<USBDevice>();
88
89
  const openingHandles = new WeakSet<USBDevice>();
90
+ // A normal acquire keeps ownership even while recovery temporarily closes the handle.
91
+ const normalHandleOwners = new WeakMap<USBDevice, WebUsbTransport>();
89
92
 
90
93
  function registerActiveWebUsbTransport(instance: WebUsbTransport, usb: USB) {
91
94
  activeWebUsbTransport = instance;
@@ -130,6 +133,10 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
130
133
 
131
134
  private identityProbeDevices = new Map<string, USBDevice>();
132
135
 
136
+ private normalDevices = new Map<USBDevice, string>();
137
+
138
+ private identityProbeCleanupPaths = new Set<string>();
139
+
133
140
  /**
134
141
  * The exact USBDevice object each cached protocol was probed against. The
135
142
  * browser returns the same object identity for a device as long as it stays
@@ -273,6 +280,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
273
280
  * It does NOT prompt the user to select a device
274
281
  */
275
282
  async enumerate() {
283
+ // Retry only our failed cleanup before Core skips already-open USB handles.
284
+ for (const path of this.identityProbeCleanupPaths) {
285
+ try {
286
+ await this.release(path);
287
+ } catch {
288
+ // Keep ownership while native close still fails; retry on the next probe.
289
+ }
290
+ }
276
291
  await this.getConnectedDevices();
277
292
  return this.deviceList;
278
293
  }
@@ -320,6 +335,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
320
335
  if (this.identityProbe) {
321
336
  await this.getConnectedDevices();
322
337
  this.claimIdentityProbeHandle(input.path, await this.findDevice(input.path));
338
+ } else {
339
+ this.claimNormalHandle(input.path, await this.findDevice(input.path));
323
340
  }
324
341
  try {
325
342
  await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
@@ -408,6 +425,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
408
425
  await this.closeOpenDevice(input.path);
409
426
  } finally {
410
427
  this.releaseIdentityProbeHandle(input.path);
428
+ this.releaseNormalHandles(input.path);
411
429
  }
412
430
  throw e;
413
431
  }
@@ -588,6 +606,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
588
606
  this.releaseIdentityProbeHandle(path);
589
607
  this.claimIdentityProbeHandle(path, device);
590
608
  }
609
+ if (!this.identityProbe) this.claimNormalHandle(path, device);
591
610
  this.assertHandleAvailable(device);
592
611
  if (!device.opened) {
593
612
  openingHandles.add(device);
@@ -615,6 +634,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
615
634
  this.releaseIdentityProbeHandle(path);
616
635
  this.claimIdentityProbeHandle(path, device);
617
636
  }
637
+ if (!this.identityProbe) this.claimNormalHandle(path, device);
618
638
  this.assertHandleAvailable(device);
619
639
  if (!device.opened) {
620
640
  openingHandles.add(device);
@@ -648,17 +668,47 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
648
668
  }
649
669
 
650
670
  private assertHandleAvailable(device: USBDevice) {
671
+ const normalOwner = normalHandleOwners.get(device);
651
672
  if (
652
- identityProbeHandles.has(device) &&
653
- ![...this.identityProbeDevices.values()].includes(device)
673
+ (normalOwner && normalOwner !== this) ||
674
+ (identityProbeHandles.has(device) &&
675
+ ![...this.identityProbeDevices.values()].includes(device))
654
676
  ) {
655
- throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'USB device is already in use');
677
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, USB_DEVICE_IN_USE);
656
678
  }
657
679
  }
658
680
 
681
+ private claimNormalHandle(path: string, device: USBDevice) {
682
+ this.assertHandleAvailable(device);
683
+ normalHandleOwners.set(device, this);
684
+ this.normalDevices.set(device, path);
685
+ }
686
+
687
+ private releaseNormalHandles(path: string) {
688
+ for (const [device, ownedPath] of this.normalDevices) {
689
+ if (ownedPath === path) {
690
+ normalHandleOwners.delete(device);
691
+ this.normalDevices.delete(device);
692
+ }
693
+ }
694
+ }
695
+
696
+ private isHandleOwnershipError(error: unknown): error is HardwareError {
697
+ return (
698
+ error instanceof HardwareError &&
699
+ error.errorCode === HardwareErrorCode.RuntimeError &&
700
+ this.getErrorMessage(error).includes(USB_DEVICE_IN_USE)
701
+ );
702
+ }
703
+
659
704
  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');
705
+ if (
706
+ device.opened ||
707
+ openingHandles.has(device) ||
708
+ identityProbeHandles.has(device) ||
709
+ normalHandleOwners.has(device)
710
+ ) {
711
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, USB_DEVICE_IN_USE);
662
712
  }
663
713
  identityProbeHandles.add(device);
664
714
  this.identityProbeDevices.set(path, device);
@@ -667,9 +717,13 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
667
717
  private releaseIdentityProbeHandle(path: string) {
668
718
  const device = this.identityProbeDevices.get(path);
669
719
  // A failed native close must not make an open probe handle available to other callers.
670
- if (device?.opened) return;
720
+ if (device?.opened) {
721
+ this.identityProbeCleanupPaths.add(path);
722
+ return;
723
+ }
671
724
  if (device) identityProbeHandles.delete(device);
672
725
  this.identityProbeDevices.delete(path);
726
+ this.identityProbeCleanupPaths.delete(path);
673
727
  }
674
728
 
675
729
  private async closeOpenDevice(path: string) {
@@ -686,6 +740,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
686
740
  } catch (error) {
687
741
  this.Log?.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
688
742
  }
743
+ this.assertHandleAvailable(current);
689
744
  try {
690
745
  await current.close();
691
746
  } catch (error) {
@@ -699,6 +754,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
699
754
  direction: USBDirection,
700
755
  endpointNumber: number
701
756
  ) {
757
+ this.assertHandleAvailable(device);
702
758
  try {
703
759
  await device.clearHalt(direction, endpointNumber);
704
760
  } catch (error) {
@@ -774,7 +830,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
774
830
  operation: WebUsbOperation,
775
831
  error: unknown
776
832
  ): Promise<never> {
777
- if (this.isWebUsbIoHardwareError(error)) throw error;
833
+ if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error)) throw error;
778
834
  if (error instanceof ProtocolV2LinkError && error.code !== 'io') throw error;
779
835
 
780
836
  const nativeError = error instanceof ProtocolV2LinkError ? error.cause ?? error : error;
@@ -814,7 +870,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
814
870
  }
815
871
 
816
872
  private isRetryablePacketIoError(error: unknown) {
817
- if (this.isWebUsbIoHardwareError(error)) return false;
873
+ if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error)) return false;
818
874
  const message = this.getErrorMessage(error).toLowerCase();
819
875
  if (this.isWebUsbPermissionDeniedMessage(message)) return false;
820
876
  return (
@@ -848,6 +904,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
848
904
 
849
905
  try {
850
906
  const currentDevice = await this.findDevice(path);
907
+ this.assertHandleAvailable(currentDevice);
851
908
  if (currentDevice.opened) {
852
909
  const endpoints = this.deviceEndpoints.get(path);
853
910
  const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
@@ -856,9 +913,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
856
913
  } catch (releaseError) {
857
914
  this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
858
915
  }
916
+ this.assertHandleAvailable(currentDevice);
859
917
  await currentDevice.close();
860
918
  }
861
919
  } catch (closeError) {
920
+ if (this.isHandleOwnershipError(closeError)) throw closeError;
862
921
  this.Log.debug('[WebUsbTransport] close device before retry error:', closeError);
863
922
  }
864
923
 
@@ -900,6 +959,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
900
959
  try {
901
960
  await this.reconnectForPacketIoRetry(path, 'out', attempt, error);
902
961
  } catch (reconnectError) {
962
+ if (this.isHandleOwnershipError(reconnectError)) throw reconnectError;
903
963
  lastError = reconnectError;
904
964
  this.Log.debug(
905
965
  `[WebUsbTransport] transferout reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
@@ -922,6 +982,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
922
982
  const transferBuffer = this.toArrayBuffer(
923
983
  packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
924
984
  );
985
+ this.assertHandleAvailable(device);
925
986
  const result = await device.transferOut(endpointOut, transferBuffer);
926
987
  if (result.status !== 'ok') {
927
988
  return this.throwWebUsbIoError(
@@ -949,6 +1010,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
949
1010
  }
950
1011
  const endpoints = this.deviceEndpoints.get(path);
951
1012
  const endpointIn = endpoints?.endpointIn ?? this.endpointId;
1013
+ this.assertHandleAvailable(device);
952
1014
  const result = await device.transferIn(endpointIn, length);
953
1015
  return this.getTransferInData(result);
954
1016
  } catch (error) {
@@ -963,6 +1025,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
963
1025
  try {
964
1026
  await this.reconnectForPacketIoRetry(path, 'in', attempt, error);
965
1027
  } catch (reconnectError) {
1028
+ if (this.isHandleOwnershipError(reconnectError)) throw reconnectError;
966
1029
  lastError = reconnectError;
967
1030
  this.Log.debug(
968
1031
  `[WebUsbTransport] transferin reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
@@ -982,6 +1045,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
982
1045
  }
983
1046
  const endpoints = this.deviceEndpoints.get(path);
984
1047
  const endpointIn = endpoints?.endpointIn ?? this.endpointId;
1048
+ this.assertHandleAvailable(device);
985
1049
  const result = await device.transferIn(endpointIn, length);
986
1050
  return this.getTransferInData(result);
987
1051
  }
@@ -1215,6 +1279,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1215
1279
  await this.closeOpenDevice(path);
1216
1280
  } finally {
1217
1281
  this.releaseIdentityProbeHandle(path);
1282
+ this.releaseNormalHandles(path);
1218
1283
  }
1219
1284
  // Keep deviceProtocol/deviceProtocolHints across release: the probe result is a
1220
1285
  // physical-device property, so the next acquire can skip the wire-level probe.