@onekeyfe/hd-transport-web-device 1.2.8-alpha.14 → 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 = {
@@ -21,7 +51,10 @@ describe('WebUSB identity probe ownership', () => {
21
51
  beforeEach(() => {
22
52
  originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
23
53
  device.opened = false;
24
- device.close.mockClear();
54
+ device.close.mockReset().mockImplementation(() => {
55
+ device.opened = false;
56
+ return Promise.resolve();
57
+ });
25
58
  usb.getDevices.mockResolvedValue([device]);
26
59
  usb.addEventListener.mockClear();
27
60
  Object.defineProperty(globalThis, 'navigator', { configurable: true, value: { usb } });
@@ -77,6 +110,7 @@ describe('WebUSB identity probe ownership', () => {
77
110
  expect(device.close).not.toHaveBeenCalled();
78
111
  finishOpen();
79
112
  await connecting;
113
+ await active.release('USB_PATH');
80
114
  });
81
115
 
82
116
  test('a normal connection cannot close an isolated probe handle and release closes the captured handle after re-enumeration', async () => {
@@ -102,8 +136,10 @@ describe('WebUSB identity probe ownership', () => {
102
136
  const replacement = { ...device, close: jest.fn() };
103
137
  usb.getDevices.mockResolvedValue([replacement]);
104
138
  await probe.getConnectedDevices();
105
- await probe.release('USB_PATH');
106
- 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);
107
143
  expect(replacement.close).not.toHaveBeenCalled();
108
144
  });
109
145
 
@@ -119,6 +155,345 @@ describe('WebUSB identity probe ownership', () => {
119
155
  expect(probeDisconnect).not.toHaveBeenCalled();
120
156
  });
121
157
 
158
+ test.each(['release', 'acquire failure'])(
159
+ 'keeps a probe handle protected after close fails during %s and recovers after cleanup',
160
+ async phase => {
161
+ Object.assign(device, {
162
+ open: jest.fn(() => {
163
+ device.opened = true;
164
+ return Promise.resolve();
165
+ }),
166
+ releaseInterface: jest.fn().mockResolvedValue(undefined),
167
+ reset: jest.fn(),
168
+ configurations: [],
169
+ configuration: { configurationValue: 1 },
170
+ selectConfiguration: jest.fn(),
171
+ claimInterface: jest.fn(),
172
+ clearHalt: jest.fn(),
173
+ });
174
+ const probe = new WebUsbTransport(true);
175
+ probe.init({ debug: jest.fn() });
176
+ const internals = probe as unknown as {
177
+ rotateProtocolV2UsbGeneration: () => Promise<void>;
178
+ detectProtocol: () => Promise<'V1'>;
179
+ };
180
+ jest.spyOn(internals, 'rotateProtocolV2UsbGeneration').mockResolvedValue(undefined);
181
+ jest.spyOn(internals, 'detectProtocol').mockResolvedValue('V1');
182
+ jest.spyOn(probe, 'connect').mockImplementation(() => {
183
+ device.opened = true;
184
+ if (phase === 'acquire failure') return Promise.reject(new Error('probe failed'));
185
+ return Promise.resolve();
186
+ });
187
+ const closeFailure = new Error('native close failed');
188
+ device.close.mockRejectedValueOnce(closeFailure);
189
+ if (phase === 'release') {
190
+ await probe.acquire({ path: 'USB_PATH' });
191
+ await expect(probe.release('USB_PATH')).rejects.toBe(closeFailure);
192
+ } else {
193
+ await expect(probe.acquire({ path: 'USB_PATH' })).rejects.toBe(closeFailure);
194
+ }
195
+ expect(device.opened).toBe(true);
196
+ const active = new WebUsbTransport();
197
+ active.init({ debug: jest.fn() });
198
+ await expect(active.connectToDevice('USB_PATH', true)).rejects.toThrow(
199
+ 'USB device is already in use'
200
+ );
201
+ expect(device.close).toHaveBeenCalledTimes(1);
202
+
203
+ await probe.release('USB_PATH');
204
+ expect(device.opened).toBe(false);
205
+ await expect(active.connectToDevice('USB_PATH', true)).resolves.toBeUndefined();
206
+ expect(device.opened).toBe(true);
207
+ await active.release('USB_PATH');
208
+ }
209
+ );
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
+
122
497
  test.each([false, true])(
123
498
  'uses a read-only V1 probe only for isolated discovery (%s)',
124
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,23 +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);
480
+ }
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
+ }
459
493
  }
460
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);
512
+ if (device === null || device === void 0 ? void 0 : device.opened) {
513
+ this.identityProbeCleanupPaths.add(path);
514
+ return;
515
+ }
470
516
  if (device)
471
517
  identityProbeHandles.delete(device);
472
518
  this.identityProbeDevices.delete(path);
519
+ this.identityProbeCleanupPaths.delete(path);
473
520
  }
474
521
  closeOpenDevice(path) {
475
522
  var _a, _b, _c, _d;
@@ -488,6 +535,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
488
535
  catch (error) {
489
536
  (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
490
537
  }
538
+ this.assertHandleAvailable(current);
491
539
  try {
492
540
  yield current.close();
493
541
  }
@@ -501,6 +549,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
501
549
  clearEndpointHalt(device, direction, endpointNumber) {
502
550
  var _a;
503
551
  return __awaiter(this, void 0, void 0, function* () {
552
+ this.assertHandleAvailable(device);
504
553
  try {
505
554
  yield device.clearHalt(direction, endpointNumber);
506
555
  }
@@ -566,7 +615,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
566
615
  throwWebUsbIoError(path, operation, error) {
567
616
  var _a;
568
617
  return __awaiter(this, void 0, void 0, function* () {
569
- if (this.isWebUsbIoHardwareError(error))
618
+ if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error))
570
619
  throw error;
571
620
  if (error instanceof transport.ProtocolV2LinkError && error.code !== 'io')
572
621
  throw error;
@@ -600,7 +649,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
600
649
  return /LIBUSB_ERROR_ACCESS|\b(?:access|permission) denied\b/i.test(message);
601
650
  }
602
651
  isRetryablePacketIoError(error) {
603
- if (this.isWebUsbIoHardwareError(error))
652
+ if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error))
604
653
  return false;
605
654
  const message = this.getErrorMessage(error).toLowerCase();
606
655
  if (this.isWebUsbPermissionDeniedMessage(message))
@@ -621,6 +670,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
621
670
  yield hdShared.wait(attempt * PACKET_IO_RETRY_DELAY);
622
671
  try {
623
672
  const currentDevice = yield this.findDevice(path);
673
+ this.assertHandleAvailable(currentDevice);
624
674
  if (currentDevice.opened) {
625
675
  const endpoints = this.deviceEndpoints.get(path);
626
676
  const ifaceNum = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.interfaceNumber) !== null && _a !== void 0 ? _a : this.interfaceId;
@@ -630,10 +680,13 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
630
680
  catch (releaseError) {
631
681
  this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
632
682
  }
683
+ this.assertHandleAvailable(currentDevice);
633
684
  yield currentDevice.close();
634
685
  }
635
686
  }
636
687
  catch (closeError) {
688
+ if (this.isHandleOwnershipError(closeError))
689
+ throw closeError;
637
690
  this.Log.debug('[WebUsbTransport] close device before retry error:', closeError);
638
691
  }
639
692
  yield this.getConnectedDevices();
@@ -675,6 +728,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
675
728
  yield this.reconnectForPacketIoRetry(path, 'out', attempt, error);
676
729
  }
677
730
  catch (reconnectError) {
731
+ if (this.isHandleOwnershipError(reconnectError))
732
+ throw reconnectError;
678
733
  lastError = reconnectError;
679
734
  this.Log.debug(`[WebUsbTransport] transferout reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(reconnectError)}`);
680
735
  }
@@ -693,6 +748,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
693
748
  const endpoints = this.deviceEndpoints.get(path);
694
749
  const endpointOut = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.endpointOut) !== null && _a !== void 0 ? _a : this.endpointId;
695
750
  const transferBuffer = this.toArrayBuffer(packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength));
751
+ this.assertHandleAvailable(device);
696
752
  const result = yield device.transferOut(endpointOut, transferBuffer);
697
753
  if (result.status !== 'ok') {
698
754
  return this.throwWebUsbIoError(path, 'transferOut', new Error(`transferOut status: ${String(result.status)}`));
@@ -714,6 +770,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
714
770
  }
715
771
  const endpoints = this.deviceEndpoints.get(path);
716
772
  const endpointIn = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.endpointIn) !== null && _a !== void 0 ? _a : this.endpointId;
773
+ this.assertHandleAvailable(device);
717
774
  const result = yield device.transferIn(endpointIn, length);
718
775
  return this.getTransferInData(result);
719
776
  }
@@ -730,6 +787,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
730
787
  yield this.reconnectForPacketIoRetry(path, 'in', attempt, error);
731
788
  }
732
789
  catch (reconnectError) {
790
+ if (this.isHandleOwnershipError(reconnectError))
791
+ throw reconnectError;
733
792
  lastError = reconnectError;
734
793
  this.Log.debug(`[WebUsbTransport] transferin reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(reconnectError)}`);
735
794
  }
@@ -747,6 +806,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
747
806
  }
748
807
  const endpoints = this.deviceEndpoints.get(path);
749
808
  const endpointIn = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.endpointIn) !== null && _a !== void 0 ? _a : this.endpointId;
809
+ this.assertHandleAvailable(device);
750
810
  const result = yield device.transferIn(endpointIn, length);
751
811
  return this.getTransferInData(result);
752
812
  });
@@ -933,6 +993,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
933
993
  }
934
994
  finally {
935
995
  this.releaseIdentityProbeHandle(path);
996
+ this.releaseNormalHandles(path);
936
997
  }
937
998
  this.deviceEndpoints.delete(path);
938
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.14",
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.14",
25
- "@onekeyfe/hd-transport": "1.2.8-alpha.14"
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.14",
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": "f2784ca5eb21e4a3ebf167965f689e2ff2cb6249"
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);
@@ -666,8 +716,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
666
716
 
667
717
  private releaseIdentityProbeHandle(path: string) {
668
718
  const device = this.identityProbeDevices.get(path);
719
+ // A failed native close must not make an open probe handle available to other callers.
720
+ if (device?.opened) {
721
+ this.identityProbeCleanupPaths.add(path);
722
+ return;
723
+ }
669
724
  if (device) identityProbeHandles.delete(device);
670
725
  this.identityProbeDevices.delete(path);
726
+ this.identityProbeCleanupPaths.delete(path);
671
727
  }
672
728
 
673
729
  private async closeOpenDevice(path: string) {
@@ -684,6 +740,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
684
740
  } catch (error) {
685
741
  this.Log?.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
686
742
  }
743
+ this.assertHandleAvailable(current);
687
744
  try {
688
745
  await current.close();
689
746
  } catch (error) {
@@ -697,6 +754,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
697
754
  direction: USBDirection,
698
755
  endpointNumber: number
699
756
  ) {
757
+ this.assertHandleAvailable(device);
700
758
  try {
701
759
  await device.clearHalt(direction, endpointNumber);
702
760
  } catch (error) {
@@ -772,7 +830,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
772
830
  operation: WebUsbOperation,
773
831
  error: unknown
774
832
  ): Promise<never> {
775
- if (this.isWebUsbIoHardwareError(error)) throw error;
833
+ if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error)) throw error;
776
834
  if (error instanceof ProtocolV2LinkError && error.code !== 'io') throw error;
777
835
 
778
836
  const nativeError = error instanceof ProtocolV2LinkError ? error.cause ?? error : error;
@@ -812,7 +870,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
812
870
  }
813
871
 
814
872
  private isRetryablePacketIoError(error: unknown) {
815
- if (this.isWebUsbIoHardwareError(error)) return false;
873
+ if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error)) return false;
816
874
  const message = this.getErrorMessage(error).toLowerCase();
817
875
  if (this.isWebUsbPermissionDeniedMessage(message)) return false;
818
876
  return (
@@ -846,6 +904,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
846
904
 
847
905
  try {
848
906
  const currentDevice = await this.findDevice(path);
907
+ this.assertHandleAvailable(currentDevice);
849
908
  if (currentDevice.opened) {
850
909
  const endpoints = this.deviceEndpoints.get(path);
851
910
  const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
@@ -854,9 +913,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
854
913
  } catch (releaseError) {
855
914
  this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
856
915
  }
916
+ this.assertHandleAvailable(currentDevice);
857
917
  await currentDevice.close();
858
918
  }
859
919
  } catch (closeError) {
920
+ if (this.isHandleOwnershipError(closeError)) throw closeError;
860
921
  this.Log.debug('[WebUsbTransport] close device before retry error:', closeError);
861
922
  }
862
923
 
@@ -898,6 +959,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
898
959
  try {
899
960
  await this.reconnectForPacketIoRetry(path, 'out', attempt, error);
900
961
  } catch (reconnectError) {
962
+ if (this.isHandleOwnershipError(reconnectError)) throw reconnectError;
901
963
  lastError = reconnectError;
902
964
  this.Log.debug(
903
965
  `[WebUsbTransport] transferout reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
@@ -920,6 +982,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
920
982
  const transferBuffer = this.toArrayBuffer(
921
983
  packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
922
984
  );
985
+ this.assertHandleAvailable(device);
923
986
  const result = await device.transferOut(endpointOut, transferBuffer);
924
987
  if (result.status !== 'ok') {
925
988
  return this.throwWebUsbIoError(
@@ -947,6 +1010,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
947
1010
  }
948
1011
  const endpoints = this.deviceEndpoints.get(path);
949
1012
  const endpointIn = endpoints?.endpointIn ?? this.endpointId;
1013
+ this.assertHandleAvailable(device);
950
1014
  const result = await device.transferIn(endpointIn, length);
951
1015
  return this.getTransferInData(result);
952
1016
  } catch (error) {
@@ -961,6 +1025,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
961
1025
  try {
962
1026
  await this.reconnectForPacketIoRetry(path, 'in', attempt, error);
963
1027
  } catch (reconnectError) {
1028
+ if (this.isHandleOwnershipError(reconnectError)) throw reconnectError;
964
1029
  lastError = reconnectError;
965
1030
  this.Log.debug(
966
1031
  `[WebUsbTransport] transferin reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
@@ -980,6 +1045,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
980
1045
  }
981
1046
  const endpoints = this.deviceEndpoints.get(path);
982
1047
  const endpointIn = endpoints?.endpointIn ?? this.endpointId;
1048
+ this.assertHandleAvailable(device);
983
1049
  const result = await device.transferIn(endpointIn, length);
984
1050
  return this.getTransferInData(result);
985
1051
  }
@@ -1213,6 +1279,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1213
1279
  await this.closeOpenDevice(path);
1214
1280
  } finally {
1215
1281
  this.releaseIdentityProbeHandle(path);
1282
+ this.releaseNormalHandles(path);
1216
1283
  }
1217
1284
  // Keep deviceProtocol/deviceProtocolHints across release: the probe result is a
1218
1285
  // physical-device property, so the next acquire can skip the wire-level probe.