@onekeyfe/hd-transport-web-device 1.2.8-alpha.51 → 1.2.8-alpha.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-web-device",
3
- "version": "1.2.8-alpha.51",
3
+ "version": "1.2.8-alpha.53",
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.51",
25
- "@onekeyfe/hd-transport": "1.2.8-alpha.51"
24
+ "@onekeyfe/hd-shared": "1.2.8-alpha.53",
25
+ "@onekeyfe/hd-transport": "1.2.8-alpha.53"
26
26
  },
27
27
  "devDependencies": {
28
- "@onekeyfe/hd-transport-electron": "1.2.8-alpha.51",
28
+ "@onekeyfe/hd-transport-electron": "1.2.8-alpha.53",
29
29
  "@types/w3c-web-usb": "^1.0.6",
30
30
  "@types/web-bluetooth": "^0.0.17"
31
31
  },
32
- "gitHead": "0ccbc08199c62e96f6084e864e558055100b66ed"
32
+ "gitHead": "8b628967c6057e743ffe0d5fa8a8d2120e64c5e3"
33
33
  }
@@ -526,17 +526,13 @@ export default class ElectronBleTransport {
526
526
  }
527
527
 
528
528
  private createProtocolMismatchError(expected: ProtocolType) {
529
- return ERRORS.TypedError(
530
- HardwareErrorCode.RuntimeError,
531
- `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
532
- );
529
+ return ERRORS.TypedError(HardwareErrorCode.TransportProtocolProbeFailed, undefined, {
530
+ expectedProtocol: expected,
531
+ });
533
532
  }
534
533
 
535
534
  private createProtocolDetectionError() {
536
- return ERRORS.TypedError(
537
- HardwareErrorCode.BleTimeoutError,
538
- 'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping'
539
- );
535
+ return ERRORS.TypedError(HardwareErrorCode.TransportProtocolProbeFailed);
540
536
  }
541
537
 
542
538
  private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
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,12 @@ 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 pendingHandleOperations = new Set<Promise<unknown>>();
139
+
140
+ private stopPromise?: Promise<void>;
141
+
133
142
  private identityProbeCleanupPaths = new Set<string>();
134
143
 
135
144
  /**
@@ -194,6 +203,96 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
194
203
  if (!this.identityProbe) registerActiveWebUsbTransport(this, usb);
195
204
  }
196
205
 
206
+ stop(): Promise<void> {
207
+ this.stopped = true;
208
+ this.acquiredPaths.clear();
209
+ if (activeWebUsbTransport === this) activeWebUsbTransport = undefined;
210
+ if (!this.stopPromise) {
211
+ this.stopPromise = this.stopResources().catch(error => {
212
+ // A later discovery can retry closure without dropping the handle guard.
213
+ this.stopPromise = undefined;
214
+ throw error;
215
+ });
216
+ }
217
+ return this.stopPromise;
218
+ }
219
+
220
+ private async stopResources() {
221
+ let cleanupError: unknown;
222
+ try {
223
+ await this.disposeProtocolV2UsbLinks('WebUSB transport stopped');
224
+ } catch (error) {
225
+ cleanupError = error;
226
+ }
227
+ // Wait for late open/reset/claim/close completions before releasing ownership.
228
+ // Reads are aborted by closing their handle and reject late results below.
229
+ await Promise.allSettled([...this.pendingHandleOperations]);
230
+ const handles = this.identityProbe
231
+ ? [...this.identityProbeDevices].map(([path, device]) => [device, path] as const)
232
+ : [...this.normalDevices];
233
+ for (const [device, path] of handles) {
234
+ if (!this.identityProbe && normalHandleOwners.get(device) !== this) {
235
+ this.normalDevices.delete(device);
236
+ } else {
237
+ try {
238
+ if (device.opened) {
239
+ const iface = this.deviceEndpoints.get(path)?.interfaceNumber ?? this.interfaceId;
240
+ try {
241
+ await device.releaseInterface(iface);
242
+ } catch (error) {
243
+ this.Log?.debug('[WebUsbTransport] stop releaseInterface failed:', error);
244
+ }
245
+ await device.close();
246
+ if (device.opened) throw new Error('WebUSB handle remains open after stop');
247
+ }
248
+ if (this.identityProbe) {
249
+ identityProbeHandles.delete(device);
250
+ this.identityProbeDevices.delete(path);
251
+ this.identityProbeCleanupPaths.delete(path);
252
+ } else {
253
+ normalHandleOwners.delete(device);
254
+ this.normalDevices.delete(device);
255
+ }
256
+ } catch (error) {
257
+ cleanupError ??= error;
258
+ }
259
+ }
260
+ }
261
+ this.deviceList = [];
262
+ this.deviceProtocol.clear();
263
+ this.confirmedDeviceProtocols.clear();
264
+ this.deviceProtocolHints.clear();
265
+ this.staleProtocolPaths.clear();
266
+ this.probedDeviceObjects.clear();
267
+ if (!this.normalDevices.size && !this.identityProbeDevices.size) {
268
+ this.deviceEndpoints.clear();
269
+ }
270
+ if (cleanupError) {
271
+ throw cleanupError instanceof Error
272
+ ? cleanupError
273
+ : new Error(this.getErrorMessage(cleanupError));
274
+ }
275
+ }
276
+
277
+ private assertRunning() {
278
+ if (this.stopped) {
279
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'WebUSB transport is stopped');
280
+ }
281
+ }
282
+
283
+ private async runHandleOperation<T>(operation: () => Promise<T>): Promise<T> {
284
+ this.assertRunning();
285
+ const pending = Promise.resolve(operation());
286
+ this.pendingHandleOperations.add(pending);
287
+ try {
288
+ const result = await pending;
289
+ this.assertRunning();
290
+ return result;
291
+ } finally {
292
+ this.pendingHandleOperations.delete(pending);
293
+ }
294
+ }
295
+
197
296
  /**
198
297
  * Announce that a USB device left. Called from the module-scoped disconnect
199
298
  * listener, which is why it is public rather than inlined.
@@ -291,9 +390,21 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
291
390
  * Get list of connected devices
292
391
  */
293
392
  async getConnectedDevices() {
393
+ this.assertRunning();
294
394
  if (!this.usb) return [];
295
395
 
296
396
  const devices = await this.usb.getDevices();
397
+ this.assertRunning();
398
+ for (const owner of new Set(devices.map(device => normalHandleOwners.get(device)))) {
399
+ if (owner?.stopped) {
400
+ try {
401
+ await owner.stop();
402
+ } catch (error) {
403
+ this.Log?.debug('[WebUsbTransport] stopped owner cleanup failed:', error);
404
+ }
405
+ }
406
+ }
407
+ this.assertRunning();
297
408
  const onekeyDevices = devices.filter(dev => {
298
409
  const isOneKey = ONEKEY_WEBUSB_FILTER.some(
299
410
  (desc: { vendorId: number; productId: number }) =>
@@ -326,10 +437,13 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
326
437
  * Acquire device control
327
438
  */
328
439
  async acquire(input: AcquireInput) {
440
+ this.assertRunning();
329
441
  if (!input.path) return;
330
442
  if (this.identityProbe) {
331
443
  await this.getConnectedDevices();
332
444
  this.claimIdentityProbeHandle(input.path, await this.findDevice(input.path));
445
+ } else {
446
+ this.claimNormalHandle(input.path, await this.findDevice(input.path));
333
447
  }
334
448
  try {
335
449
  await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
@@ -403,12 +517,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
403
517
  input.expectedProtocol,
404
518
  protocolHint
405
519
  );
520
+ this.assertRunning();
406
521
  this.confirmedDeviceProtocols.set(input.path, detectedProtocol);
407
522
  const probedDevice = this.deviceList.find(d => d.path === input.path)?.device;
408
523
  if (probedDevice) {
409
524
  this.probedDeviceObjects.set(input.path, probedDevice);
410
525
  }
411
526
  }
527
+ this.assertRunning();
412
528
  this.acquiredPaths.add(input.path);
413
529
  return await Promise.resolve(input.path);
414
530
  } catch (e) {
@@ -418,6 +534,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
418
534
  await this.closeOpenDevice(input.path);
419
535
  } finally {
420
536
  this.releaseIdentityProbeHandle(input.path);
537
+ this.releaseNormalHandles(input.path);
421
538
  }
422
539
  throw e;
423
540
  }
@@ -429,21 +546,20 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
429
546
  * fall back to a Protocol V2 Ping probe.
430
547
  */
431
548
  private createProtocolMismatchError(expected: ProtocolType) {
432
- return ERRORS.TypedError(
433
- HardwareErrorCode.RuntimeError,
434
- `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
435
- );
549
+ return ERRORS.TypedError(HardwareErrorCode.TransportProtocolProbeFailed, undefined, {
550
+ expectedProtocol: expected,
551
+ });
436
552
  }
437
553
 
438
554
  private createProtocolProbeTimeoutError(expected: ProtocolType, attempts: number) {
439
- return ERRORS.TypedError(
440
- HardwareErrorCode.DeviceInitializeFailed,
441
- `Protocol ${expected} probe timeout after ${attempts} attempts`
442
- );
555
+ return ERRORS.TypedError(HardwareErrorCode.TransportProtocolProbeFailed, undefined, {
556
+ expectedProtocol: expected,
557
+ attempts,
558
+ });
443
559
  }
444
560
 
445
561
  private createProtocolDetectionError() {
446
- return ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
562
+ return ERRORS.TypedError(HardwareErrorCode.TransportProtocolProbeFailed);
447
563
  }
448
564
 
449
565
  private async detectProtocol(
@@ -540,9 +656,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
540
656
  async connect(path: string, first: boolean) {
541
657
  const maxRetries = 5;
542
658
  for (let i = 0; i < maxRetries; i++) {
659
+ this.assertRunning();
543
660
  try {
544
661
  return await this.connectToDevice(path, first);
545
662
  } catch (e) {
663
+ this.assertRunning();
546
664
  if (e instanceof HardwareError && e.errorCode === HardwareErrorCode.BridgeNeedsPermission) {
547
665
  throw e;
548
666
  }
@@ -592,17 +710,19 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
592
710
  * Discovers interface/endpoint numbers from USB descriptors on first connection.
593
711
  */
594
712
  async connectToDevice(path: string, first: boolean) {
713
+ this.assertRunning();
595
714
  let device: USBDevice = await this.findDevice(path);
596
715
  if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
597
716
  await this.closeOpenDevice(path);
598
717
  this.releaseIdentityProbeHandle(path);
599
718
  this.claimIdentityProbeHandle(path, device);
600
719
  }
720
+ if (!this.identityProbe) this.claimNormalHandle(path, device);
601
721
  this.assertHandleAvailable(device);
602
722
  if (!device.opened) {
603
723
  openingHandles.add(device);
604
724
  try {
605
- await device.open();
725
+ await this.runHandleOperation(() => device.open());
606
726
  } catch (error) {
607
727
  return await this.throwWebUsbIoError(path, 'open', error);
608
728
  } finally {
@@ -613,8 +733,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
613
733
  // state as in the legacy transport; probing and V2 recovery still reset.
614
734
  if (first || this.deviceProtocol.get(path) !== 'V1') {
615
735
  try {
616
- await device.reset();
736
+ await this.runHandleOperation(() => device.reset());
617
737
  } catch (error) {
738
+ this.assertRunning();
618
739
  this.Log?.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
619
740
  }
620
741
  }
@@ -625,11 +746,12 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
625
746
  this.releaseIdentityProbeHandle(path);
626
747
  this.claimIdentityProbeHandle(path, device);
627
748
  }
749
+ if (!this.identityProbe) this.claimNormalHandle(path, device);
628
750
  this.assertHandleAvailable(device);
629
751
  if (!device.opened) {
630
752
  openingHandles.add(device);
631
753
  try {
632
- await device.open();
754
+ await this.runHandleOperation(() => device.open());
633
755
  } catch (error) {
634
756
  return await this.throwWebUsbIoError(path, 'open', error);
635
757
  } finally {
@@ -642,14 +764,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
642
764
  !device.configuration ||
643
765
  device.configuration.configurationValue !== this.configurationId
644
766
  ) {
645
- await device.selectConfiguration(this.configurationId);
767
+ await this.runHandleOperation(() => device.selectConfiguration(this.configurationId));
646
768
  }
647
769
 
648
770
  // Discover endpoints from USB descriptors; descriptors are not used for protocol selection.
649
771
  const endpoints = this.discoverEndpoints(device);
650
772
  this.deviceEndpoints.set(path, endpoints);
651
773
  try {
652
- await device.claimInterface(endpoints.interfaceNumber);
774
+ await this.runHandleOperation(() => device.claimInterface(endpoints.interfaceNumber));
653
775
  } catch (error) {
654
776
  return this.throwWebUsbIoError(path, 'claimInterface', error);
655
777
  }
@@ -658,23 +780,57 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
658
780
  }
659
781
 
660
782
  private assertHandleAvailable(device: USBDevice) {
783
+ this.assertRunning();
784
+ const normalOwner = normalHandleOwners.get(device);
661
785
  if (
662
- identityProbeHandles.has(device) &&
663
- ![...this.identityProbeDevices.values()].includes(device)
786
+ (normalOwner && normalOwner !== this) ||
787
+ (identityProbeHandles.has(device) &&
788
+ ![...this.identityProbeDevices.values()].includes(device))
664
789
  ) {
665
- throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'USB device is already in use');
790
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, USB_DEVICE_IN_USE);
666
791
  }
667
792
  }
668
793
 
794
+ private claimNormalHandle(path: string, device: USBDevice) {
795
+ this.assertHandleAvailable(device);
796
+ normalHandleOwners.set(device, this);
797
+ this.normalDevices.set(device, path);
798
+ }
799
+
800
+ private releaseNormalHandles(path: string) {
801
+ if (this.stopped) return;
802
+ for (const [device, ownedPath] of this.normalDevices) {
803
+ if (ownedPath === path) {
804
+ normalHandleOwners.delete(device);
805
+ this.normalDevices.delete(device);
806
+ }
807
+ }
808
+ }
809
+
810
+ private isHandleOwnershipError(error: unknown): error is HardwareError {
811
+ return (
812
+ error instanceof HardwareError &&
813
+ error.errorCode === HardwareErrorCode.RuntimeError &&
814
+ this.getErrorMessage(error).includes(USB_DEVICE_IN_USE)
815
+ );
816
+ }
817
+
669
818
  private claimIdentityProbeHandle(path: string, device: USBDevice) {
670
- if (device.opened || openingHandles.has(device) || identityProbeHandles.has(device)) {
671
- throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'USB device is already in use');
819
+ this.assertRunning();
820
+ if (
821
+ device.opened ||
822
+ openingHandles.has(device) ||
823
+ identityProbeHandles.has(device) ||
824
+ normalHandleOwners.has(device)
825
+ ) {
826
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, USB_DEVICE_IN_USE);
672
827
  }
673
828
  identityProbeHandles.add(device);
674
829
  this.identityProbeDevices.set(path, device);
675
830
  }
676
831
 
677
832
  private releaseIdentityProbeHandle(path: string) {
833
+ if (this.stopped) return;
678
834
  const device = this.identityProbeDevices.get(path);
679
835
  // A failed native close must not make an open probe handle available to other callers.
680
836
  if (device?.opened) {
@@ -687,6 +843,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
687
843
  }
688
844
 
689
845
  private async closeOpenDevice(path: string) {
846
+ if (this.stopped) return;
690
847
  const current = this.identityProbe
691
848
  ? this.identityProbeDevices.get(path)
692
849
  : this.deviceList.find(device => device.path === path)?.device;
@@ -696,12 +853,13 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
696
853
  const endpoints = this.deviceEndpoints.get(path);
697
854
  const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
698
855
  try {
699
- await current.releaseInterface(ifaceNum);
856
+ await this.runHandleOperation(() => current.releaseInterface(ifaceNum));
700
857
  } catch (error) {
701
858
  this.Log?.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
702
859
  }
860
+ this.assertHandleAvailable(current);
703
861
  try {
704
- await current.close();
862
+ await this.runHandleOperation(() => current.close());
705
863
  } catch (error) {
706
864
  this.Log?.debug('[WebUsbTransport] close before reconnect failed:', error);
707
865
  if (this.identityProbe) throw error;
@@ -713,9 +871,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
713
871
  direction: USBDirection,
714
872
  endpointNumber: number
715
873
  ) {
874
+ this.assertHandleAvailable(device);
716
875
  try {
717
- await device.clearHalt(direction, endpointNumber);
876
+ await this.runHandleOperation(() => device.clearHalt(direction, endpointNumber));
718
877
  } catch (error) {
878
+ this.assertRunning();
719
879
  this.Log?.debug(
720
880
  `[WebUsbTransport] clearHalt ${direction} endpoint ${endpointNumber} failed, continuing:`,
721
881
  error
@@ -731,6 +891,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
731
891
  * protocol entry produced the same fail-fast).
732
892
  */
733
893
  private assertAcquired(path: string) {
894
+ this.assertRunning();
734
895
  if (!this.acquiredPaths.has(path)) {
735
896
  throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, `Device is not acquired for ${path}`);
736
897
  }
@@ -788,7 +949,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
788
949
  operation: WebUsbOperation,
789
950
  error: unknown
790
951
  ): Promise<never> {
791
- if (this.isWebUsbIoHardwareError(error)) throw error;
952
+ this.assertRunning();
953
+ if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error)) throw error;
792
954
  if (error instanceof ProtocolV2LinkError && error.code !== 'io') throw error;
793
955
 
794
956
  const nativeError = error instanceof ProtocolV2LinkError ? error.cause ?? error : error;
@@ -828,7 +990,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
828
990
  }
829
991
 
830
992
  private isRetryablePacketIoError(error: unknown) {
831
- if (this.isWebUsbIoHardwareError(error)) return false;
993
+ if (this.stopped) return false;
994
+ if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error)) return false;
832
995
  const message = this.getErrorMessage(error).toLowerCase();
833
996
  if (this.isWebUsbPermissionDeniedMessage(message)) return false;
834
997
  return (
@@ -859,20 +1022,25 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
859
1022
  // USB disconnect event itself was missed.
860
1023
  this.staleProtocolPaths.add(path);
861
1024
  await wait(attempt * PACKET_IO_RETRY_DELAY);
1025
+ this.assertRunning();
862
1026
 
863
1027
  try {
864
1028
  const currentDevice = await this.findDevice(path);
1029
+ this.assertHandleAvailable(currentDevice);
865
1030
  if (currentDevice.opened) {
866
1031
  const endpoints = this.deviceEndpoints.get(path);
867
1032
  const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
868
1033
  try {
869
- await currentDevice.releaseInterface(ifaceNum);
1034
+ await this.runHandleOperation(() => currentDevice.releaseInterface(ifaceNum));
870
1035
  } catch (releaseError) {
871
1036
  this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
872
1037
  }
873
- await currentDevice.close();
1038
+ this.assertHandleAvailable(currentDevice);
1039
+ await this.runHandleOperation(() => currentDevice.close());
874
1040
  }
875
1041
  } catch (closeError) {
1042
+ this.assertRunning();
1043
+ if (this.isHandleOwnershipError(closeError)) throw closeError;
876
1044
  this.Log.debug('[WebUsbTransport] close device before retry error:', closeError);
877
1045
  }
878
1046
 
@@ -914,6 +1082,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
914
1082
  try {
915
1083
  await this.reconnectForPacketIoRetry(path, 'out', attempt, error);
916
1084
  } catch (reconnectError) {
1085
+ this.assertRunning();
1086
+ if (this.isHandleOwnershipError(reconnectError)) throw reconnectError;
917
1087
  lastError = reconnectError;
918
1088
  this.Log.debug(
919
1089
  `[WebUsbTransport] transferout reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
@@ -936,7 +1106,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
936
1106
  const transferBuffer = this.toArrayBuffer(
937
1107
  packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
938
1108
  );
1109
+ this.assertHandleAvailable(device);
939
1110
  const result = await device.transferOut(endpointOut, transferBuffer);
1111
+ this.assertHandleAvailable(device);
940
1112
  if (result.status !== 'ok') {
941
1113
  return this.throwWebUsbIoError(
942
1114
  path,
@@ -963,7 +1135,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
963
1135
  }
964
1136
  const endpoints = this.deviceEndpoints.get(path);
965
1137
  const endpointIn = endpoints?.endpointIn ?? this.endpointId;
1138
+ this.assertHandleAvailable(device);
966
1139
  const result = await device.transferIn(endpointIn, length);
1140
+ this.assertHandleAvailable(device);
967
1141
  return this.getTransferInData(result);
968
1142
  } catch (error) {
969
1143
  lastError = error;
@@ -977,6 +1151,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
977
1151
  try {
978
1152
  await this.reconnectForPacketIoRetry(path, 'in', attempt, error);
979
1153
  } catch (reconnectError) {
1154
+ this.assertRunning();
1155
+ if (this.isHandleOwnershipError(reconnectError)) throw reconnectError;
980
1156
  lastError = reconnectError;
981
1157
  this.Log.debug(
982
1158
  `[WebUsbTransport] transferin reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
@@ -996,7 +1172,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
996
1172
  }
997
1173
  const endpoints = this.deviceEndpoints.get(path);
998
1174
  const endpointIn = endpoints?.endpointIn ?? this.endpointId;
1175
+ this.assertHandleAvailable(device);
999
1176
  const result = await device.transferIn(endpointIn, length);
1177
+ this.assertHandleAvailable(device);
1000
1178
  return this.getTransferInData(result);
1001
1179
  }
1002
1180
 
@@ -1229,6 +1407,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1229
1407
  await this.closeOpenDevice(path);
1230
1408
  } finally {
1231
1409
  this.releaseIdentityProbeHandle(path);
1410
+ this.releaseNormalHandles(path);
1232
1411
  }
1233
1412
  // Keep deviceProtocol/deviceProtocolHints across release: the probe result is a
1234
1413
  // physical-device property, so the next acquire can skip the wire-level probe.