@onekeyfe/hd-transport-web-device 1.2.8-alpha.102 → 1.2.8-alpha.104

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/src/webusb.ts CHANGED
@@ -45,8 +45,6 @@ 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
47
  const USB_DEVICE_IN_USE = 'USB device is already in use';
48
- const WEBUSB_LIFECYCLE_LOG_PREFIX = '[WebUsbTransport][lifecycle]';
49
- const WEBUSB_ERROR_MESSAGE_LIMIT = 240;
50
48
  // Legacy devices can take longer to answer Initialize after a WebUSB reset.
51
49
  // Keep this aligned with Node USB so a slow Protocol V1 response is not
52
50
  // misreported as a protocol mismatch during acquire.
@@ -76,23 +74,6 @@ interface TransferCancelToken {
76
74
 
77
75
  type WebUsbOperation = 'open' | 'claimInterface' | 'transferIn' | 'transferOut';
78
76
 
79
- type WebUsbHandleOperation =
80
- | 'open'
81
- | 'reset'
82
- | 'selectConfiguration'
83
- | 'claimInterface'
84
- | 'clearHaltIn'
85
- | 'clearHaltOut'
86
- | 'releaseInterface'
87
- | 'close';
88
-
89
- type WebUsbLifecyclePhase = 'start' | 'success' | 'error' | 'skipped';
90
-
91
- interface WebUsbConnectOptions {
92
- expectedProtocol?: ProtocolType;
93
- forceProtocolDetection?: boolean;
94
- }
95
-
96
77
  /**
97
78
  * The navigator.usb disconnect listener is module-scoped and attached at most
98
79
  * once: listeners on navigator.usb are global and never garbage collected, so a
@@ -102,7 +83,6 @@ interface WebUsbConnectOptions {
102
83
  */
103
84
  let activeWebUsbTransport: WebUsbTransport | undefined;
104
85
  let usbDisconnectListenerAttached = false;
105
- let nextWebUsbTransportInstanceId = 0;
106
86
  // Only isolated discovery owns these handles. Normal calls must never close or
107
87
  // open them, including while native open() is still pending.
108
88
  const identityProbeHandles = new WeakSet<USBDevice>();
@@ -127,14 +107,6 @@ function registerActiveWebUsbTransport(instance: WebUsbTransport, usb: USB) {
127
107
  }
128
108
 
129
109
  export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
130
- private readonly transportInstanceId = ++nextWebUsbTransportInstanceId;
131
-
132
- private handleOperationSequence = 0;
133
-
134
- private deviceLogSequence = 0;
135
-
136
- private deviceLogIds = new WeakMap<USBDevice, number>();
137
-
138
110
  messages: ReturnType<typeof transport.parseConfigure> | undefined;
139
111
 
140
112
  /** Protobuf schema for Protocol V2 transports. */
@@ -232,30 +204,15 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
232
204
  }
233
205
 
234
206
  stop(): Promise<void> {
235
- const startedAt = Date.now();
236
207
  this.stopped = true;
237
208
  this.acquiredPaths.clear();
238
209
  if (activeWebUsbTransport === this) activeWebUsbTransport = undefined;
239
210
  if (!this.stopPromise) {
240
- this.logLifecycle('stop', { phase: 'start' });
241
- this.stopPromise = this.stopResources().then(
242
- () => {
243
- this.logLifecycle('stop', {
244
- phase: 'success',
245
- durationMs: Date.now() - startedAt,
246
- });
247
- },
248
- error => {
249
- this.logLifecycle('stop', {
250
- phase: 'error',
251
- durationMs: Date.now() - startedAt,
252
- ...this.getDiagnosticError(error),
253
- });
254
- // A later discovery can retry closure without dropping the handle guard.
255
- this.stopPromise = undefined;
256
- throw error;
257
- }
258
- );
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
+ });
259
216
  }
260
217
  return this.stopPromise;
261
218
  }
@@ -280,34 +237,12 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
280
237
  try {
281
238
  if (device.opened) {
282
239
  const iface = this.deviceEndpoints.get(path)?.interfaceNumber ?? this.interfaceId;
283
- if (this.getInterfaceClaimed(device, iface) === false) {
284
- this.logSkippedHandleOperation(
285
- 'releaseInterface',
286
- device,
287
- 'interface-not-claimed',
288
- iface
289
- );
290
- } else {
291
- try {
292
- await this.runLoggedHandleOperation({
293
- operation: 'releaseInterface',
294
- device,
295
- path,
296
- interfaceNumber: iface,
297
- allowStopped: true,
298
- task: () => device.releaseInterface(iface),
299
- });
300
- } catch (error) {
301
- this.Log?.debug('[WebUsbTransport] stop releaseInterface failed:', error);
302
- }
240
+ try {
241
+ await device.releaseInterface(iface);
242
+ } catch (error) {
243
+ this.Log?.debug('[WebUsbTransport] stop releaseInterface failed:', error);
303
244
  }
304
- await this.runLoggedHandleOperation({
305
- operation: 'close',
306
- device,
307
- path,
308
- allowStopped: true,
309
- task: () => device.close(),
310
- });
245
+ await device.close();
311
246
  if (device.opened) throw new Error('WebUSB handle remains open after stop');
312
247
  }
313
248
  if (this.identityProbe) {
@@ -358,122 +293,6 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
358
293
  }
359
294
  }
360
295
 
361
- private getInterfaceClaimed(device: USBDevice, interfaceNumber: number) {
362
- return device.configuration?.interfaces?.find(
363
- usbInterface => usbInterface.interfaceNumber === interfaceNumber
364
- )?.claimed;
365
- }
366
-
367
- private getDeviceLogId(device: USBDevice) {
368
- const existing = this.deviceLogIds.get(device);
369
- if (existing) return existing;
370
- const next = ++this.deviceLogSequence;
371
- this.deviceLogIds.set(device, next);
372
- return next;
373
- }
374
-
375
- private getHandleLogState(device: USBDevice, interfaceNumber?: number) {
376
- const interfaceClaimed =
377
- interfaceNumber === undefined ? undefined : this.getInterfaceClaimed(device, interfaceNumber);
378
- return {
379
- deviceRefId: this.getDeviceLogId(device),
380
- opened: device.opened,
381
- configurationValue: device.configuration?.configurationValue ?? null,
382
- interfaceNumber: interfaceNumber ?? null,
383
- interfaceClaimed: interfaceClaimed ?? null,
384
- identityProbe: this.identityProbe,
385
- stopped: this.stopped,
386
- acquiredPathCount: this.acquiredPaths.size,
387
- normalHandleCount: this.normalDevices.size,
388
- identityProbeHandleCount: this.identityProbeDevices.size,
389
- pendingHandleOperationCount: this.pendingHandleOperations.size,
390
- };
391
- }
392
-
393
- private getDiagnosticError(error: unknown, path?: string) {
394
- const errorMessage = this.getErrorMessage(error);
395
- const redactedMessage = path ? errorMessage.split(path).join('<device>') : errorMessage;
396
- return {
397
- errorName: this.getErrorName(error) || 'Error',
398
- errorMessage: redactedMessage.slice(0, WEBUSB_ERROR_MESSAGE_LIMIT),
399
- errorCode: error instanceof HardwareError ? error.errorCode : undefined,
400
- };
401
- }
402
-
403
- private logLifecycle(event: string, payload: Record<string, unknown> = {}) {
404
- this.Log?.debug(WEBUSB_LIFECYCLE_LOG_PREFIX, {
405
- event,
406
- transportInstanceId: this.transportInstanceId,
407
- ...payload,
408
- });
409
- }
410
-
411
- private logSkippedHandleOperation(
412
- operation: WebUsbHandleOperation,
413
- device: USBDevice,
414
- reason: string,
415
- interfaceNumber?: number
416
- ) {
417
- const operationId = ++this.handleOperationSequence;
418
- this.logLifecycle('handle-operation', {
419
- operationId,
420
- operation,
421
- phase: 'skipped' satisfies WebUsbLifecyclePhase,
422
- reason,
423
- ...this.getHandleLogState(device, interfaceNumber),
424
- });
425
- }
426
-
427
- private async runLoggedHandleOperation<T>({
428
- operation,
429
- device,
430
- task,
431
- path,
432
- interfaceNumber,
433
- endpointNumber,
434
- allowStopped = false,
435
- }: {
436
- operation: WebUsbHandleOperation;
437
- device: USBDevice;
438
- task: () => Promise<T>;
439
- path?: string;
440
- interfaceNumber?: number;
441
- endpointNumber?: number;
442
- allowStopped?: boolean;
443
- }): Promise<T> {
444
- const operationId = ++this.handleOperationSequence;
445
- const startedAt = Date.now();
446
- const details = {
447
- operationId,
448
- operation,
449
- ...(endpointNumber === undefined ? {} : { endpointNumber }),
450
- };
451
- this.logLifecycle('handle-operation', {
452
- ...details,
453
- phase: 'start' satisfies WebUsbLifecyclePhase,
454
- ...this.getHandleLogState(device, interfaceNumber),
455
- });
456
- try {
457
- const result = allowStopped ? await task() : await this.runHandleOperation(task);
458
- this.logLifecycle('handle-operation', {
459
- ...details,
460
- phase: 'success' satisfies WebUsbLifecyclePhase,
461
- durationMs: Date.now() - startedAt,
462
- ...this.getHandleLogState(device, interfaceNumber),
463
- });
464
- return result;
465
- } catch (error) {
466
- this.logLifecycle('handle-operation', {
467
- ...details,
468
- phase: 'error' satisfies WebUsbLifecyclePhase,
469
- durationMs: Date.now() - startedAt,
470
- ...this.getHandleLogState(device, interfaceNumber),
471
- ...this.getDiagnosticError(error, path),
472
- });
473
- throw error;
474
- }
475
- }
476
-
477
296
  /**
478
297
  * Announce that a USB device left. Called from the module-scoped disconnect
479
298
  * listener, which is why it is public rather than inlined.
@@ -537,22 +356,10 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
537
356
  */
538
357
  async promptDeviceAccess() {
539
358
  if (!this.usb) return null;
540
- const startedAt = Date.now();
541
- this.logLifecycle('prompt-device-access', { phase: 'start' });
542
359
  try {
543
360
  const device = await this.usb.requestDevice({ filters: ONEKEY_WEBUSB_FILTER });
544
- this.logLifecycle('prompt-device-access', {
545
- phase: 'success',
546
- durationMs: Date.now() - startedAt,
547
- selected: Boolean(device),
548
- });
549
361
  return device;
550
362
  } catch (e) {
551
- this.logLifecycle('prompt-device-access', {
552
- phase: 'error',
553
- durationMs: Date.now() - startedAt,
554
- ...this.getDiagnosticError(e),
555
- });
556
363
  this.Log.debug(
557
364
  'requestDevice error: ',
558
365
  e instanceof Error ? `${e.name}: ${e.message}` : String(e)
@@ -567,35 +374,16 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
567
374
  * It does NOT prompt the user to select a device
568
375
  */
569
376
  async enumerate() {
570
- const startedAt = Date.now();
571
- this.logLifecycle('enumerate', {
572
- phase: 'start',
573
- pendingProbeCleanupCount: this.identityProbeCleanupPaths.size,
574
- });
575
- try {
576
- // Retry only our failed cleanup before Core skips already-open USB handles.
577
- for (const path of this.identityProbeCleanupPaths) {
578
- try {
579
- await this.release(path);
580
- } catch {
581
- // Keep ownership while native close still fails; retry on the next probe.
582
- }
377
+ // Retry only our failed cleanup before Core skips already-open USB handles.
378
+ for (const path of this.identityProbeCleanupPaths) {
379
+ try {
380
+ await this.release(path);
381
+ } catch {
382
+ // Keep ownership while native close still fails; retry on the next probe.
583
383
  }
584
- await this.getConnectedDevices();
585
- this.logLifecycle('enumerate', {
586
- phase: 'success',
587
- durationMs: Date.now() - startedAt,
588
- deviceCount: this.deviceList.length,
589
- });
590
- return this.deviceList;
591
- } catch (error) {
592
- this.logLifecycle('enumerate', {
593
- phase: 'error',
594
- durationMs: Date.now() - startedAt,
595
- ...this.getDiagnosticError(error),
596
- });
597
- throw error;
598
384
  }
385
+ await this.getConnectedDevices();
386
+ return this.deviceList;
599
387
  }
600
388
 
601
389
  /**
@@ -605,24 +393,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
605
393
  this.assertRunning();
606
394
  if (!this.usb) return [];
607
395
 
608
- const startedAt = Date.now();
609
- this.logLifecycle('get-devices', { phase: 'start' });
610
- let devices: USBDevice[];
611
- try {
612
- devices = await this.usb.getDevices();
613
- this.logLifecycle('get-devices', {
614
- phase: 'success',
615
- durationMs: Date.now() - startedAt,
616
- authorizedDeviceCount: devices.length,
617
- });
618
- } catch (error) {
619
- this.logLifecycle('get-devices', {
620
- phase: 'error',
621
- durationMs: Date.now() - startedAt,
622
- ...this.getDiagnosticError(error),
623
- });
624
- throw error;
625
- }
396
+ const devices = await this.usb.getDevices();
626
397
  this.assertRunning();
627
398
  for (const owner of new Set(devices.map(device => normalHandleOwners.get(device)))) {
628
399
  if (owner?.stopped) {
@@ -668,28 +439,16 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
668
439
  async acquire(input: AcquireInput) {
669
440
  this.assertRunning();
670
441
  if (!input.path) return;
671
- const startedAt = Date.now();
672
- this.logLifecycle('acquire', {
673
- phase: 'start',
674
- expectedProtocol: input.expectedProtocol ?? null,
675
- protocolHint: input.protocolHint ?? null,
676
- forceProtocolDetection: Boolean(input.forceProtocolDetection),
677
- skipProtocolProbe: Boolean(input.skipProtocolProbe),
678
- hasCachedProtocol: this.deviceProtocol.has(input.path),
679
- });
442
+ if (this.identityProbe) {
443
+ await this.getConnectedDevices();
444
+ this.claimIdentityProbeHandle(input.path, await this.findDevice(input.path));
445
+ } else {
446
+ this.claimNormalHandle(input.path, await this.findDevice(input.path));
447
+ }
680
448
  try {
681
- if (this.identityProbe) {
682
- await this.getConnectedDevices();
683
- this.claimIdentityProbeHandle(input.path, await this.findDevice(input.path));
684
- } else {
685
- this.claimNormalHandle(input.path, await this.findDevice(input.path));
686
- }
687
449
  await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
688
450
  await this.closeOpenDevice(input.path);
689
- await this.connect(input.path ?? '', true, {
690
- expectedProtocol: input.expectedProtocol,
691
- forceProtocolDetection: input.forceProtocolDetection,
692
- });
451
+ await this.connect(input.path ?? '', true);
693
452
  if (input.skipProtocolProbe) {
694
453
  if (!input.expectedProtocol) {
695
454
  throw ERRORS.TypedError(
@@ -767,18 +526,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
767
526
  }
768
527
  this.assertRunning();
769
528
  this.acquiredPaths.add(input.path);
770
- this.logLifecycle('acquire', {
771
- phase: 'success',
772
- durationMs: Date.now() - startedAt,
773
- protocol: this.deviceProtocol.get(input.path) ?? null,
774
- });
775
529
  return await Promise.resolve(input.path);
776
530
  } catch (e) {
777
- this.logLifecycle('acquire', {
778
- phase: 'error',
779
- durationMs: Date.now() - startedAt,
780
- ...this.getDiagnosticError(e, input.path),
781
- });
782
531
  this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
783
532
  this.acquiredPaths.delete(input.path);
784
533
  try {
@@ -904,55 +653,18 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
904
653
  /**
905
654
  * Connect to device with retry mechanism
906
655
  */
907
- async connect(path: string, first: boolean, options: WebUsbConnectOptions = {}) {
656
+ async connect(path: string, first: boolean) {
908
657
  const maxRetries = 5;
909
658
  for (let i = 0; i < maxRetries; i++) {
910
659
  this.assertRunning();
911
- const attempt = i + 1;
912
- const startedAt = Date.now();
913
- const connectionStrategy = this.shouldUseLegacyV1Connection(path, options)
914
- ? 'protocol-v1-legacy-1.1.31'
915
- : 'current';
916
- this.logLifecycle('connect-attempt', {
917
- phase: 'start',
918
- attempt,
919
- maxAttempts: maxRetries,
920
- first,
921
- connectionStrategy,
922
- });
923
660
  try {
924
- const result = await this.connectToDevice(path, first, options);
925
- this.logLifecycle('connect-attempt', {
926
- phase: 'success',
927
- attempt,
928
- maxAttempts: maxRetries,
929
- first,
930
- connectionStrategy,
931
- durationMs: Date.now() - startedAt,
932
- });
933
- return result;
661
+ return await this.connectToDevice(path, first);
934
662
  } catch (e) {
935
663
  this.assertRunning();
936
- const isTerminalError =
937
- this.isHandleOwnershipError(e) ||
938
- (e instanceof HardwareError &&
939
- (e.errorCode === HardwareErrorCode.BridgeNeedsPermission ||
940
- e.errorCode === HardwareErrorCode.WebUsbDeviceAccessError));
941
- const willRetry = !isTerminalError && attempt < maxRetries;
942
- this.logLifecycle('connect-attempt', {
943
- phase: 'error',
944
- attempt,
945
- maxAttempts: maxRetries,
946
- first,
947
- connectionStrategy,
948
- durationMs: Date.now() - startedAt,
949
- willRetry,
950
- ...this.getDiagnosticError(e, path),
951
- });
952
- if (isTerminalError) {
664
+ if (e instanceof HardwareError && e.errorCode === HardwareErrorCode.BridgeNeedsPermission) {
953
665
  throw e;
954
666
  }
955
- if (!willRetry) {
667
+ if (i === maxRetries - 1) {
956
668
  throw e;
957
669
  }
958
670
  await wait(i * 200);
@@ -960,11 +672,6 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
960
672
  }
961
673
  }
962
674
 
963
- private shouldUseLegacyV1Connection(path: string, options: WebUsbConnectOptions) {
964
- if (options.expectedProtocol) return options.expectedProtocol === 'V1';
965
- return !options.forceProtocolDetection && this.deviceProtocol.get(path) === 'V1';
966
- }
967
-
968
675
  /**
969
676
  * Discover vendor-class (0xFF) interface and its IN/OUT endpoint numbers from USB descriptors.
970
677
  * Falls back to legacy hardcoded values if no vendor interface is found.
@@ -1002,7 +709,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1002
709
  * Connect to specific device.
1003
710
  * Discovers interface/endpoint numbers from USB descriptors on first connection.
1004
711
  */
1005
- async connectToDevice(path: string, first: boolean, options: WebUsbConnectOptions = {}) {
712
+ async connectToDevice(path: string, first: boolean) {
1006
713
  this.assertRunning();
1007
714
  let device: USBDevice = await this.findDevice(path);
1008
715
  if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
@@ -1015,65 +722,21 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1015
722
  if (!device.opened) {
1016
723
  openingHandles.add(device);
1017
724
  try {
1018
- await this.runLoggedHandleOperation({
1019
- operation: 'open',
1020
- device,
1021
- path,
1022
- task: () => device.open(),
1023
- });
725
+ await this.runHandleOperation(() => device.open());
1024
726
  } catch (error) {
1025
727
  return await this.throwWebUsbIoError(path, 'open', error);
1026
728
  } finally {
1027
729
  openingHandles.delete(device);
1028
730
  }
1029
731
  }
1030
-
1031
- if (this.shouldUseLegacyV1Connection(path, options)) {
1032
- // Match the last V1-only WebUSB lifecycle from SDK 1.1.31. Legacy devices
1033
- // configure before reset and keep the same USBDevice object afterwards.
1034
- if (first) {
1035
- await this.runLoggedHandleOperation({
1036
- operation: 'selectConfiguration',
1037
- device,
1038
- path,
1039
- task: () => device.selectConfiguration(this.configurationId),
1040
- });
1041
- try {
1042
- await this.runLoggedHandleOperation({
1043
- operation: 'reset',
1044
- device,
1045
- path,
1046
- task: () => device.reset(),
1047
- });
1048
- } catch {
1049
- this.assertRunning();
1050
- // Reset is best-effort in 1.1.31; runLoggedHandleOperation already recorded the error.
1051
- }
1052
- }
1053
-
1054
- const legacyEndpoints = {
1055
- interfaceNumber: this.interfaceId,
1056
- endpointIn: this.endpointId,
1057
- endpointOut: this.endpointId,
1058
- };
1059
- this.deviceEndpoints.set(path, legacyEndpoints);
1060
- await this.claimDeviceInterface(path, device, legacyEndpoints);
1061
- return;
1062
- }
1063
-
1064
732
  // A V1 packet retry continues an in-flight exchange. Preserve its endpoint
1065
733
  // state as in the legacy transport; probing and V2 recovery still reset.
1066
734
  if (first || this.deviceProtocol.get(path) !== 'V1') {
1067
735
  try {
1068
- await this.runLoggedHandleOperation({
1069
- operation: 'reset',
1070
- device,
1071
- path,
1072
- task: () => device.reset(),
1073
- });
1074
- } catch {
736
+ await this.runHandleOperation(() => device.reset());
737
+ } catch (error) {
1075
738
  this.assertRunning();
1076
- // Reset remains best-effort; runLoggedHandleOperation already recorded the error.
739
+ this.Log?.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
1077
740
  }
1078
741
  }
1079
742
  await this.getConnectedDevices();
@@ -1088,12 +751,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1088
751
  if (!device.opened) {
1089
752
  openingHandles.add(device);
1090
753
  try {
1091
- await this.runLoggedHandleOperation({
1092
- operation: 'open',
1093
- device,
1094
- path,
1095
- task: () => device.open(),
1096
- });
754
+ await this.runHandleOperation(() => device.open());
1097
755
  } catch (error) {
1098
756
  return await this.throwWebUsbIoError(path, 'open', error);
1099
757
  } finally {
@@ -1106,34 +764,19 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1106
764
  !device.configuration ||
1107
765
  device.configuration.configurationValue !== this.configurationId
1108
766
  ) {
1109
- await this.runLoggedHandleOperation({
1110
- operation: 'selectConfiguration',
1111
- device,
1112
- path,
1113
- task: () => device.selectConfiguration(this.configurationId),
1114
- });
767
+ await this.runHandleOperation(() => device.selectConfiguration(this.configurationId));
1115
768
  }
1116
769
 
1117
770
  // Discover endpoints from USB descriptors; descriptors are not used for protocol selection.
1118
771
  const endpoints = this.discoverEndpoints(device);
1119
772
  this.deviceEndpoints.set(path, endpoints);
1120
- await this.claimDeviceInterface(path, device, endpoints);
1121
- await this.clearEndpointHalt(path, device, 'in', endpoints.endpointIn);
1122
- await this.clearEndpointHalt(path, device, 'out', endpoints.endpointOut);
1123
- }
1124
-
1125
- private async claimDeviceInterface(path: string, device: USBDevice, endpoints: DeviceEndpoints) {
1126
773
  try {
1127
- await this.runLoggedHandleOperation({
1128
- operation: 'claimInterface',
1129
- device,
1130
- path,
1131
- interfaceNumber: endpoints.interfaceNumber,
1132
- task: () => device.claimInterface(endpoints.interfaceNumber),
1133
- });
774
+ await this.runHandleOperation(() => device.claimInterface(endpoints.interfaceNumber));
1134
775
  } catch (error) {
1135
776
  return this.throwWebUsbIoError(path, 'claimInterface', error);
1136
777
  }
778
+ await this.clearEndpointHalt(device, 'in', endpoints.endpointIn);
779
+ await this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
1137
780
  }
1138
781
 
1139
782
  private assertHandleAvailable(device: USBDevice) {
@@ -1157,9 +800,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1157
800
  private releaseNormalHandles(path: string) {
1158
801
  if (this.stopped) return;
1159
802
  for (const [device, ownedPath] of this.normalDevices) {
1160
- if (ownedPath === path && !device.opened) {
1161
- // Do not expose a native handle to another transport while close failed.
1162
- // A later release/stop can retry cleanup through normalDevices.
803
+ if (ownedPath === path) {
1163
804
  normalHandleOwners.delete(device);
1164
805
  this.normalDevices.delete(device);
1165
806
  }
@@ -1203,83 +844,36 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1203
844
 
1204
845
  private async closeOpenDevice(path: string) {
1205
846
  if (this.stopped) return;
1206
- const devices = this.identityProbe
1207
- ? [this.identityProbeDevices.get(path)].filter(
1208
- (device): device is USBDevice => device !== undefined
1209
- )
1210
- : [...this.normalDevices]
1211
- .filter(([, ownedPath]) => ownedPath === path)
1212
- .map(([device]) => device);
1213
- let firstCloseError: unknown;
1214
- for (const device of devices) {
1215
- try {
1216
- await this.closeDeviceHandle(path, device);
1217
- } catch (error) {
1218
- firstCloseError ??= error;
1219
- }
1220
- }
1221
- if (firstCloseError) {
1222
- throw firstCloseError instanceof Error
1223
- ? firstCloseError
1224
- : new Error(this.getErrorMessage(firstCloseError));
1225
- }
1226
- }
1227
-
1228
- private async closeDeviceHandle(path: string, current: USBDevice) {
1229
- if (!current.opened) return;
847
+ const current = this.identityProbe
848
+ ? this.identityProbeDevices.get(path)
849
+ : this.deviceList.find(device => device.path === path)?.device;
850
+ if (!current?.opened) return;
1230
851
  this.assertHandleAvailable(current);
1231
852
 
1232
853
  const endpoints = this.deviceEndpoints.get(path);
1233
854
  const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
1234
- if (this.getInterfaceClaimed(current, ifaceNum) === false) {
1235
- this.logSkippedHandleOperation(
1236
- 'releaseInterface',
1237
- current,
1238
- 'interface-not-claimed',
1239
- ifaceNum
1240
- );
1241
- } else {
1242
- try {
1243
- await this.runLoggedHandleOperation({
1244
- operation: 'releaseInterface',
1245
- device: current,
1246
- path,
1247
- interfaceNumber: ifaceNum,
1248
- task: () => current.releaseInterface(ifaceNum),
1249
- });
1250
- } catch (error) {
1251
- this.Log?.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
1252
- }
855
+ try {
856
+ await this.runHandleOperation(() => current.releaseInterface(ifaceNum));
857
+ } catch (error) {
858
+ this.Log?.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
1253
859
  }
1254
860
  this.assertHandleAvailable(current);
1255
861
  try {
1256
- await this.runLoggedHandleOperation({
1257
- operation: 'close',
1258
- device: current,
1259
- path,
1260
- task: () => current.close(),
1261
- });
862
+ await this.runHandleOperation(() => current.close());
1262
863
  } catch (error) {
1263
864
  this.Log?.debug('[WebUsbTransport] close before reconnect failed:', error);
1264
- throw error;
865
+ if (this.identityProbe) throw error;
1265
866
  }
1266
867
  }
1267
868
 
1268
869
  private async clearEndpointHalt(
1269
- path: string,
1270
870
  device: USBDevice,
1271
871
  direction: USBDirection,
1272
872
  endpointNumber: number
1273
873
  ) {
1274
874
  this.assertHandleAvailable(device);
1275
875
  try {
1276
- await this.runLoggedHandleOperation({
1277
- operation: direction === 'in' ? 'clearHaltIn' : 'clearHaltOut',
1278
- device,
1279
- path,
1280
- endpointNumber,
1281
- task: () => device.clearHalt(direction, endpointNumber),
1282
- });
876
+ await this.runHandleOperation(() => device.clearHalt(direction, endpointNumber));
1283
877
  } catch (error) {
1284
878
  this.assertRunning();
1285
879
  this.Log?.debug(
@@ -1436,33 +1030,13 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1436
1030
  if (currentDevice.opened) {
1437
1031
  const endpoints = this.deviceEndpoints.get(path);
1438
1032
  const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
1439
- if (this.getInterfaceClaimed(currentDevice, ifaceNum) === false) {
1440
- this.logSkippedHandleOperation(
1441
- 'releaseInterface',
1442
- currentDevice,
1443
- 'interface-not-claimed',
1444
- ifaceNum
1445
- );
1446
- } else {
1447
- try {
1448
- await this.runLoggedHandleOperation({
1449
- operation: 'releaseInterface',
1450
- device: currentDevice,
1451
- path,
1452
- interfaceNumber: ifaceNum,
1453
- task: () => currentDevice.releaseInterface(ifaceNum),
1454
- });
1455
- } catch (releaseError) {
1456
- this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
1457
- }
1033
+ try {
1034
+ await this.runHandleOperation(() => currentDevice.releaseInterface(ifaceNum));
1035
+ } catch (releaseError) {
1036
+ this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
1458
1037
  }
1459
1038
  this.assertHandleAvailable(currentDevice);
1460
- await this.runLoggedHandleOperation({
1461
- operation: 'close',
1462
- device: currentDevice,
1463
- path,
1464
- task: () => currentDevice.close(),
1465
- });
1039
+ await this.runHandleOperation(() => currentDevice.close());
1466
1040
  }
1467
1041
  } catch (closeError) {
1468
1042
  this.assertRunning();
@@ -1827,23 +1401,10 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1827
1401
  * Release device
1828
1402
  */
1829
1403
  async release(path: string) {
1830
- const startedAt = Date.now();
1831
- this.logLifecycle('release', { phase: 'start' });
1832
1404
  this.acquiredPaths.delete(path);
1833
1405
  try {
1834
1406
  await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
1835
1407
  await this.closeOpenDevice(path);
1836
- this.logLifecycle('release', {
1837
- phase: 'success',
1838
- durationMs: Date.now() - startedAt,
1839
- });
1840
- } catch (error) {
1841
- this.logLifecycle('release', {
1842
- phase: 'error',
1843
- durationMs: Date.now() - startedAt,
1844
- ...this.getDiagnosticError(error, path),
1845
- });
1846
- throw error;
1847
1408
  } finally {
1848
1409
  this.releaseIdentityProbeHandle(path);
1849
1410
  this.releaseNormalHandles(path);