@onekeyfe/hd-transport-web-device 1.2.8-alpha.103 → 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,63 +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
- // Descriptor lookup is local-only and does not alter the 1.1.31 USB
1055
- // operation order. Defaults remain available for legacy descriptors.
1056
- const legacyEndpoints = this.discoverEndpoints(device);
1057
- this.deviceEndpoints.set(path, legacyEndpoints);
1058
- await this.claimDeviceInterface(path, device, legacyEndpoints);
1059
- return;
1060
- }
1061
-
1062
732
  // A V1 packet retry continues an in-flight exchange. Preserve its endpoint
1063
733
  // state as in the legacy transport; probing and V2 recovery still reset.
1064
734
  if (first || this.deviceProtocol.get(path) !== 'V1') {
1065
735
  try {
1066
- await this.runLoggedHandleOperation({
1067
- operation: 'reset',
1068
- device,
1069
- path,
1070
- task: () => device.reset(),
1071
- });
1072
- } catch {
736
+ await this.runHandleOperation(() => device.reset());
737
+ } catch (error) {
1073
738
  this.assertRunning();
1074
- // Reset remains best-effort; runLoggedHandleOperation already recorded the error.
739
+ this.Log?.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
1075
740
  }
1076
741
  }
1077
742
  await this.getConnectedDevices();
@@ -1086,12 +751,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1086
751
  if (!device.opened) {
1087
752
  openingHandles.add(device);
1088
753
  try {
1089
- await this.runLoggedHandleOperation({
1090
- operation: 'open',
1091
- device,
1092
- path,
1093
- task: () => device.open(),
1094
- });
754
+ await this.runHandleOperation(() => device.open());
1095
755
  } catch (error) {
1096
756
  return await this.throwWebUsbIoError(path, 'open', error);
1097
757
  } finally {
@@ -1104,34 +764,19 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1104
764
  !device.configuration ||
1105
765
  device.configuration.configurationValue !== this.configurationId
1106
766
  ) {
1107
- await this.runLoggedHandleOperation({
1108
- operation: 'selectConfiguration',
1109
- device,
1110
- path,
1111
- task: () => device.selectConfiguration(this.configurationId),
1112
- });
767
+ await this.runHandleOperation(() => device.selectConfiguration(this.configurationId));
1113
768
  }
1114
769
 
1115
770
  // Discover endpoints from USB descriptors; descriptors are not used for protocol selection.
1116
771
  const endpoints = this.discoverEndpoints(device);
1117
772
  this.deviceEndpoints.set(path, endpoints);
1118
- await this.claimDeviceInterface(path, device, endpoints);
1119
- await this.clearEndpointHalt(path, device, 'in', endpoints.endpointIn);
1120
- await this.clearEndpointHalt(path, device, 'out', endpoints.endpointOut);
1121
- }
1122
-
1123
- private async claimDeviceInterface(path: string, device: USBDevice, endpoints: DeviceEndpoints) {
1124
773
  try {
1125
- await this.runLoggedHandleOperation({
1126
- operation: 'claimInterface',
1127
- device,
1128
- path,
1129
- interfaceNumber: endpoints.interfaceNumber,
1130
- task: () => device.claimInterface(endpoints.interfaceNumber),
1131
- });
774
+ await this.runHandleOperation(() => device.claimInterface(endpoints.interfaceNumber));
1132
775
  } catch (error) {
1133
776
  return this.throwWebUsbIoError(path, 'claimInterface', error);
1134
777
  }
778
+ await this.clearEndpointHalt(device, 'in', endpoints.endpointIn);
779
+ await this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
1135
780
  }
1136
781
 
1137
782
  private assertHandleAvailable(device: USBDevice) {
@@ -1155,9 +800,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1155
800
  private releaseNormalHandles(path: string) {
1156
801
  if (this.stopped) return;
1157
802
  for (const [device, ownedPath] of this.normalDevices) {
1158
- if (ownedPath === path && !device.opened) {
1159
- // Do not expose a native handle to another transport while close failed.
1160
- // A later release/stop can retry cleanup through normalDevices.
803
+ if (ownedPath === path) {
1161
804
  normalHandleOwners.delete(device);
1162
805
  this.normalDevices.delete(device);
1163
806
  }
@@ -1201,83 +844,36 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1201
844
 
1202
845
  private async closeOpenDevice(path: string) {
1203
846
  if (this.stopped) return;
1204
- const devices = this.identityProbe
1205
- ? [this.identityProbeDevices.get(path)].filter(
1206
- (device): device is USBDevice => device !== undefined
1207
- )
1208
- : [...this.normalDevices]
1209
- .filter(([, ownedPath]) => ownedPath === path)
1210
- .map(([device]) => device);
1211
- let firstCloseError: unknown;
1212
- for (const device of devices) {
1213
- try {
1214
- await this.closeDeviceHandle(path, device);
1215
- } catch (error) {
1216
- firstCloseError ??= error;
1217
- }
1218
- }
1219
- if (firstCloseError) {
1220
- throw firstCloseError instanceof Error
1221
- ? firstCloseError
1222
- : new Error(this.getErrorMessage(firstCloseError));
1223
- }
1224
- }
1225
-
1226
- private async closeDeviceHandle(path: string, current: USBDevice) {
1227
- 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;
1228
851
  this.assertHandleAvailable(current);
1229
852
 
1230
853
  const endpoints = this.deviceEndpoints.get(path);
1231
854
  const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
1232
- if (this.getInterfaceClaimed(current, ifaceNum) === false) {
1233
- this.logSkippedHandleOperation(
1234
- 'releaseInterface',
1235
- current,
1236
- 'interface-not-claimed',
1237
- ifaceNum
1238
- );
1239
- } else {
1240
- try {
1241
- await this.runLoggedHandleOperation({
1242
- operation: 'releaseInterface',
1243
- device: current,
1244
- path,
1245
- interfaceNumber: ifaceNum,
1246
- task: () => current.releaseInterface(ifaceNum),
1247
- });
1248
- } catch (error) {
1249
- this.Log?.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
1250
- }
855
+ try {
856
+ await this.runHandleOperation(() => current.releaseInterface(ifaceNum));
857
+ } catch (error) {
858
+ this.Log?.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
1251
859
  }
1252
860
  this.assertHandleAvailable(current);
1253
861
  try {
1254
- await this.runLoggedHandleOperation({
1255
- operation: 'close',
1256
- device: current,
1257
- path,
1258
- task: () => current.close(),
1259
- });
862
+ await this.runHandleOperation(() => current.close());
1260
863
  } catch (error) {
1261
864
  this.Log?.debug('[WebUsbTransport] close before reconnect failed:', error);
1262
- throw error;
865
+ if (this.identityProbe) throw error;
1263
866
  }
1264
867
  }
1265
868
 
1266
869
  private async clearEndpointHalt(
1267
- path: string,
1268
870
  device: USBDevice,
1269
871
  direction: USBDirection,
1270
872
  endpointNumber: number
1271
873
  ) {
1272
874
  this.assertHandleAvailable(device);
1273
875
  try {
1274
- await this.runLoggedHandleOperation({
1275
- operation: direction === 'in' ? 'clearHaltIn' : 'clearHaltOut',
1276
- device,
1277
- path,
1278
- endpointNumber,
1279
- task: () => device.clearHalt(direction, endpointNumber),
1280
- });
876
+ await this.runHandleOperation(() => device.clearHalt(direction, endpointNumber));
1281
877
  } catch (error) {
1282
878
  this.assertRunning();
1283
879
  this.Log?.debug(
@@ -1434,33 +1030,13 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1434
1030
  if (currentDevice.opened) {
1435
1031
  const endpoints = this.deviceEndpoints.get(path);
1436
1032
  const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
1437
- if (this.getInterfaceClaimed(currentDevice, ifaceNum) === false) {
1438
- this.logSkippedHandleOperation(
1439
- 'releaseInterface',
1440
- currentDevice,
1441
- 'interface-not-claimed',
1442
- ifaceNum
1443
- );
1444
- } else {
1445
- try {
1446
- await this.runLoggedHandleOperation({
1447
- operation: 'releaseInterface',
1448
- device: currentDevice,
1449
- path,
1450
- interfaceNumber: ifaceNum,
1451
- task: () => currentDevice.releaseInterface(ifaceNum),
1452
- });
1453
- } catch (releaseError) {
1454
- this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
1455
- }
1033
+ try {
1034
+ await this.runHandleOperation(() => currentDevice.releaseInterface(ifaceNum));
1035
+ } catch (releaseError) {
1036
+ this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
1456
1037
  }
1457
1038
  this.assertHandleAvailable(currentDevice);
1458
- await this.runLoggedHandleOperation({
1459
- operation: 'close',
1460
- device: currentDevice,
1461
- path,
1462
- task: () => currentDevice.close(),
1463
- });
1039
+ await this.runHandleOperation(() => currentDevice.close());
1464
1040
  }
1465
1041
  } catch (closeError) {
1466
1042
  this.assertRunning();
@@ -1825,23 +1401,10 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1825
1401
  * Release device
1826
1402
  */
1827
1403
  async release(path: string) {
1828
- const startedAt = Date.now();
1829
- this.logLifecycle('release', { phase: 'start' });
1830
1404
  this.acquiredPaths.delete(path);
1831
1405
  try {
1832
1406
  await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
1833
1407
  await this.closeOpenDevice(path);
1834
- this.logLifecycle('release', {
1835
- phase: 'success',
1836
- durationMs: Date.now() - startedAt,
1837
- });
1838
- } catch (error) {
1839
- this.logLifecycle('release', {
1840
- phase: 'error',
1841
- durationMs: Date.now() - startedAt,
1842
- ...this.getDiagnosticError(error, path),
1843
- });
1844
- throw error;
1845
1408
  } finally {
1846
1409
  this.releaseIdentityProbeHandle(path);
1847
1410
  this.releaseNormalHandles(path);