@onekeyfe/hd-transport-web-device 1.2.8-alpha.2 → 1.2.8-alpha.20

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/dist/webusb.d.ts CHANGED
@@ -9,6 +9,7 @@ export interface DeviceInfo extends OneKeyDeviceInfoBase {
9
9
  protocolType?: ProtocolType;
10
10
  }
11
11
  export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
12
+ private readonly identityProbe;
12
13
  messages: ReturnType<typeof transport.parseConfigure> | undefined;
13
14
  messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
14
15
  private protocolV2SchemaSource;
@@ -17,6 +18,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
17
18
  private deviceProtocolHints;
18
19
  private staleProtocolPaths;
19
20
  private acquiredPaths;
21
+ private identityProbeDevices;
22
+ private normalDevices;
23
+ private pendingHandleOperations;
24
+ private stopPromise?;
25
+ private identityProbeCleanupPaths;
20
26
  private probedDeviceObjects;
21
27
  private deviceEndpoints;
22
28
  name: string;
@@ -29,8 +35,12 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
29
35
  configurationId: number;
30
36
  endpointId: number;
31
37
  interfaceId: number;
32
- constructor();
38
+ constructor(identityProbe?: boolean);
33
39
  init(logger: any, emitter?: EventEmitter): void;
40
+ stop(): Promise<void>;
41
+ private stopResources;
42
+ private assertRunning;
43
+ private runHandleOperation;
34
44
  emitDeviceDisconnect(path: string, device?: USBDevice): void;
35
45
  markProtocolStale(path: string): void;
36
46
  configure(signedData: any): void;
@@ -47,6 +57,12 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
47
57
  connect(path: string, first: boolean): Promise<undefined>;
48
58
  private discoverEndpoints;
49
59
  connectToDevice(path: string, first: boolean): Promise<undefined>;
60
+ private assertHandleAvailable;
61
+ private claimNormalHandle;
62
+ private releaseNormalHandles;
63
+ private isHandleOwnershipError;
64
+ private claimIdentityProbeHandle;
65
+ private releaseIdentityProbeHandle;
50
66
  private closeOpenDevice;
51
67
  private clearEndpointHalt;
52
68
  private assertAcquired;
@@ -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.2",
3
+ "version": "1.2.8-alpha.20",
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.2",
25
- "@onekeyfe/hd-transport": "1.2.8-alpha.2"
24
+ "@onekeyfe/hd-shared": "1.2.8-alpha.20",
25
+ "@onekeyfe/hd-transport": "1.2.8-alpha.20"
26
26
  },
27
27
  "devDependencies": {
28
- "@onekeyfe/hd-transport-electron": "1.2.8-alpha.2",
28
+ "@onekeyfe/hd-transport-electron": "1.2.8-alpha.20",
29
29
  "@types/w3c-web-usb": "^1.0.6",
30
30
  "@types/web-bluetooth": "^0.0.17"
31
31
  },
32
- "gitHead": "74ac64464df3806ef44ad54d313378754c3eaed2"
32
+ "gitHead": "973f059fe93a58fdcf098ddb32ba63f621a2c78e"
33
33
  }
@@ -202,11 +202,14 @@ export default class ElectronBleTransport {
202
202
  if (error.code === HardwareErrorCode.BlePermissionError) {
203
203
  return ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
204
204
  }
205
+ if (error.code === HardwareErrorCode.BleLocationError) {
206
+ return ERRORS.TypedError(HardwareErrorCode.BleLocationError);
207
+ }
205
208
  }
206
209
  const errorMessage = error.message || String(error);
207
210
  const poweredOffMessage = HardwareErrorCodeMessage[HardwareErrorCode.BlePoweredOff];
208
211
  const unsupportedMessage = HardwareErrorCodeMessage[HardwareErrorCode.BleUnsupported];
209
- const permissionMessage = HardwareErrorCodeMessage[HardwareErrorCode.BlePermissionError];
212
+ const permissionMessage = HardwareErrorCodeMessage[HardwareErrorCode.BleLocationError];
210
213
 
211
214
  if (errorMessage.includes(poweredOffMessage) || errorMessage.includes('poweredOff')) {
212
215
  return ERRORS.TypedError(HardwareErrorCode.BlePoweredOff);
@@ -215,7 +218,7 @@ export default class ElectronBleTransport {
215
218
  return ERRORS.TypedError(HardwareErrorCode.BleUnsupported);
216
219
  }
217
220
  if (errorMessage.includes(permissionMessage) || errorMessage.includes('unauthorized')) {
218
- return ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
221
+ return ERRORS.TypedError(HardwareErrorCode.BleLocationError);
219
222
  }
220
223
  }
221
224
  return error;
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.
@@ -82,6 +83,12 @@ type WebUsbOperation = 'open' | 'claimInterface' | 'transferIn' | 'transferOut';
82
83
  */
83
84
  let activeWebUsbTransport: WebUsbTransport | undefined;
84
85
  let usbDisconnectListenerAttached = false;
86
+ // Only isolated discovery owns these handles. Normal calls must never close or
87
+ // open them, including while native open() is still pending.
88
+ const identityProbeHandles = new WeakSet<USBDevice>();
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>();
85
92
 
86
93
  function registerActiveWebUsbTransport(instance: WebUsbTransport, usb: USB) {
87
94
  activeWebUsbTransport = instance;
@@ -124,6 +131,16 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
124
131
  /** Paths currently acquired (between a successful acquire() and its release()). */
125
132
  private acquiredPaths: Set<string> = new Set();
126
133
 
134
+ private identityProbeDevices = new Map<string, USBDevice>();
135
+
136
+ private normalDevices = new Map<USBDevice, string>();
137
+
138
+ private pendingHandleOperations = new Set<Promise<unknown>>();
139
+
140
+ private stopPromise?: Promise<void>;
141
+
142
+ private identityProbeCleanupPaths = new Set<string>();
143
+
127
144
  /**
128
145
  * The exact USBDevice object each cached protocol was probed against. The
129
146
  * browser returns the same object identity for a device as long as it stays
@@ -160,7 +177,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
160
177
 
161
178
  interfaceId = INTERFACE_ID;
162
179
 
163
- constructor() {
180
+ constructor(private readonly identityProbe = false) {
164
181
  super({
165
182
  router: PROTOCOL_V2_CHANNEL_USB,
166
183
  maxFrameBytes: PROTOCOL_V2_FRAME_MAX_BYTES,
@@ -183,7 +200,97 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
183
200
  );
184
201
  }
185
202
  this.usb = usb;
186
- registerActiveWebUsbTransport(this, usb);
203
+ if (!this.identityProbe) registerActiveWebUsbTransport(this, usb);
204
+ }
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
+ }
187
294
  }
188
295
 
189
296
  /**
@@ -267,6 +374,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
267
374
  * It does NOT prompt the user to select a device
268
375
  */
269
376
  async enumerate() {
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.
383
+ }
384
+ }
270
385
  await this.getConnectedDevices();
271
386
  return this.deviceList;
272
387
  }
@@ -275,9 +390,21 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
275
390
  * Get list of connected devices
276
391
  */
277
392
  async getConnectedDevices() {
393
+ this.assertRunning();
278
394
  if (!this.usb) return [];
279
395
 
280
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();
281
408
  const onekeyDevices = devices.filter(dev => {
282
409
  const isOneKey = ONEKEY_WEBUSB_FILTER.some(
283
410
  (desc: { vendorId: number; productId: number }) =>
@@ -310,7 +437,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
310
437
  * Acquire device control
311
438
  */
312
439
  async acquire(input: AcquireInput) {
440
+ this.assertRunning();
313
441
  if (!input.path) return;
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
+ }
314
448
  try {
315
449
  await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
316
450
  await this.closeOpenDevice(input.path);
@@ -383,18 +517,25 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
383
517
  input.expectedProtocol,
384
518
  protocolHint
385
519
  );
520
+ this.assertRunning();
386
521
  this.confirmedDeviceProtocols.set(input.path, detectedProtocol);
387
522
  const probedDevice = this.deviceList.find(d => d.path === input.path)?.device;
388
523
  if (probedDevice) {
389
524
  this.probedDeviceObjects.set(input.path, probedDevice);
390
525
  }
391
526
  }
527
+ this.assertRunning();
392
528
  this.acquiredPaths.add(input.path);
393
529
  return await Promise.resolve(input.path);
394
530
  } catch (e) {
395
531
  this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
396
532
  this.acquiredPaths.delete(input.path);
397
- await this.closeOpenDevice(input.path);
533
+ try {
534
+ await this.closeOpenDevice(input.path);
535
+ } finally {
536
+ this.releaseIdentityProbeHandle(input.path);
537
+ this.releaseNormalHandles(input.path);
538
+ }
398
539
  throw e;
399
540
  }
400
541
  }
@@ -516,9 +657,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
516
657
  async connect(path: string, first: boolean) {
517
658
  const maxRetries = 5;
518
659
  for (let i = 0; i < maxRetries; i++) {
660
+ this.assertRunning();
519
661
  try {
520
662
  return await this.connectToDevice(path, first);
521
663
  } catch (e) {
664
+ this.assertRunning();
522
665
  if (e instanceof HardwareError && e.errorCode === HardwareErrorCode.BridgeNeedsPermission) {
523
666
  throw e;
524
667
  }
@@ -568,30 +711,52 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
568
711
  * Discovers interface/endpoint numbers from USB descriptors on first connection.
569
712
  */
570
713
  async connectToDevice(path: string, first: boolean) {
714
+ this.assertRunning();
571
715
  let device: USBDevice = await this.findDevice(path);
716
+ if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
717
+ await this.closeOpenDevice(path);
718
+ this.releaseIdentityProbeHandle(path);
719
+ this.claimIdentityProbeHandle(path, device);
720
+ }
721
+ if (!this.identityProbe) this.claimNormalHandle(path, device);
722
+ this.assertHandleAvailable(device);
572
723
  if (!device.opened) {
724
+ openingHandles.add(device);
573
725
  try {
574
- await device.open();
726
+ await this.runHandleOperation(() => device.open());
575
727
  } catch (error) {
576
- return this.throwWebUsbIoError(path, 'open', error);
728
+ return await this.throwWebUsbIoError(path, 'open', error);
729
+ } finally {
730
+ openingHandles.delete(device);
577
731
  }
578
732
  }
579
733
  // A V1 packet retry continues an in-flight exchange. Preserve its endpoint
580
734
  // state as in the legacy transport; probing and V2 recovery still reset.
581
735
  if (first || this.deviceProtocol.get(path) !== 'V1') {
582
736
  try {
583
- await device.reset();
737
+ await this.runHandleOperation(() => device.reset());
584
738
  } catch (error) {
739
+ this.assertRunning();
585
740
  this.Log?.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
586
741
  }
587
742
  }
588
743
  await this.getConnectedDevices();
589
744
  device = await this.findDevice(path);
745
+ if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
746
+ await this.closeOpenDevice(path);
747
+ this.releaseIdentityProbeHandle(path);
748
+ this.claimIdentityProbeHandle(path, device);
749
+ }
750
+ if (!this.identityProbe) this.claimNormalHandle(path, device);
751
+ this.assertHandleAvailable(device);
590
752
  if (!device.opened) {
753
+ openingHandles.add(device);
591
754
  try {
592
- await device.open();
755
+ await this.runHandleOperation(() => device.open());
593
756
  } catch (error) {
594
- return this.throwWebUsbIoError(path, 'open', error);
757
+ return await this.throwWebUsbIoError(path, 'open', error);
758
+ } finally {
759
+ openingHandles.delete(device);
595
760
  }
596
761
  }
597
762
 
@@ -600,14 +765,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
600
765
  !device.configuration ||
601
766
  device.configuration.configurationValue !== this.configurationId
602
767
  ) {
603
- await device.selectConfiguration(this.configurationId);
768
+ await this.runHandleOperation(() => device.selectConfiguration(this.configurationId));
604
769
  }
605
770
 
606
771
  // Discover endpoints from USB descriptors; descriptors are not used for protocol selection.
607
772
  const endpoints = this.discoverEndpoints(device);
608
773
  this.deviceEndpoints.set(path, endpoints);
609
774
  try {
610
- await device.claimInterface(endpoints.interfaceNumber);
775
+ await this.runHandleOperation(() => device.claimInterface(endpoints.interfaceNumber));
611
776
  } catch (error) {
612
777
  return this.throwWebUsbIoError(path, 'claimInterface', error);
613
778
  }
@@ -615,21 +780,90 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
615
780
  await this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
616
781
  }
617
782
 
783
+ private assertHandleAvailable(device: USBDevice) {
784
+ this.assertRunning();
785
+ const normalOwner = normalHandleOwners.get(device);
786
+ if (
787
+ (normalOwner && normalOwner !== this) ||
788
+ (identityProbeHandles.has(device) &&
789
+ ![...this.identityProbeDevices.values()].includes(device))
790
+ ) {
791
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, USB_DEVICE_IN_USE);
792
+ }
793
+ }
794
+
795
+ private claimNormalHandle(path: string, device: USBDevice) {
796
+ this.assertHandleAvailable(device);
797
+ normalHandleOwners.set(device, this);
798
+ this.normalDevices.set(device, path);
799
+ }
800
+
801
+ private releaseNormalHandles(path: string) {
802
+ if (this.stopped) return;
803
+ for (const [device, ownedPath] of this.normalDevices) {
804
+ if (ownedPath === path) {
805
+ normalHandleOwners.delete(device);
806
+ this.normalDevices.delete(device);
807
+ }
808
+ }
809
+ }
810
+
811
+ private isHandleOwnershipError(error: unknown): error is HardwareError {
812
+ return (
813
+ error instanceof HardwareError &&
814
+ error.errorCode === HardwareErrorCode.RuntimeError &&
815
+ this.getErrorMessage(error).includes(USB_DEVICE_IN_USE)
816
+ );
817
+ }
818
+
819
+ private claimIdentityProbeHandle(path: string, device: USBDevice) {
820
+ this.assertRunning();
821
+ if (
822
+ device.opened ||
823
+ openingHandles.has(device) ||
824
+ identityProbeHandles.has(device) ||
825
+ normalHandleOwners.has(device)
826
+ ) {
827
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, USB_DEVICE_IN_USE);
828
+ }
829
+ identityProbeHandles.add(device);
830
+ this.identityProbeDevices.set(path, device);
831
+ }
832
+
833
+ private releaseIdentityProbeHandle(path: string) {
834
+ if (this.stopped) return;
835
+ const device = this.identityProbeDevices.get(path);
836
+ // A failed native close must not make an open probe handle available to other callers.
837
+ if (device?.opened) {
838
+ this.identityProbeCleanupPaths.add(path);
839
+ return;
840
+ }
841
+ if (device) identityProbeHandles.delete(device);
842
+ this.identityProbeDevices.delete(path);
843
+ this.identityProbeCleanupPaths.delete(path);
844
+ }
845
+
618
846
  private async closeOpenDevice(path: string) {
619
- const current = this.deviceList.find(device => device.path === path)?.device;
847
+ if (this.stopped) return;
848
+ const current = this.identityProbe
849
+ ? this.identityProbeDevices.get(path)
850
+ : this.deviceList.find(device => device.path === path)?.device;
620
851
  if (!current?.opened) return;
852
+ this.assertHandleAvailable(current);
621
853
 
622
854
  const endpoints = this.deviceEndpoints.get(path);
623
855
  const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
624
856
  try {
625
- await current.releaseInterface(ifaceNum);
857
+ await this.runHandleOperation(() => current.releaseInterface(ifaceNum));
626
858
  } catch (error) {
627
859
  this.Log?.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
628
860
  }
861
+ this.assertHandleAvailable(current);
629
862
  try {
630
- await current.close();
863
+ await this.runHandleOperation(() => current.close());
631
864
  } catch (error) {
632
865
  this.Log?.debug('[WebUsbTransport] close before reconnect failed:', error);
866
+ if (this.identityProbe) throw error;
633
867
  }
634
868
  }
635
869
 
@@ -638,9 +872,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
638
872
  direction: USBDirection,
639
873
  endpointNumber: number
640
874
  ) {
875
+ this.assertHandleAvailable(device);
641
876
  try {
642
- await device.clearHalt(direction, endpointNumber);
877
+ await this.runHandleOperation(() => device.clearHalt(direction, endpointNumber));
643
878
  } catch (error) {
879
+ this.assertRunning();
644
880
  this.Log?.debug(
645
881
  `[WebUsbTransport] clearHalt ${direction} endpoint ${endpointNumber} failed, continuing:`,
646
882
  error
@@ -656,6 +892,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
656
892
  * protocol entry produced the same fail-fast).
657
893
  */
658
894
  private assertAcquired(path: string) {
895
+ this.assertRunning();
659
896
  if (!this.acquiredPaths.has(path)) {
660
897
  throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, `Device is not acquired for ${path}`);
661
898
  }
@@ -713,7 +950,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
713
950
  operation: WebUsbOperation,
714
951
  error: unknown
715
952
  ): Promise<never> {
716
- if (this.isWebUsbIoHardwareError(error)) throw error;
953
+ this.assertRunning();
954
+ if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error)) throw error;
717
955
  if (error instanceof ProtocolV2LinkError && error.code !== 'io') throw error;
718
956
 
719
957
  const nativeError = error instanceof ProtocolV2LinkError ? error.cause ?? error : error;
@@ -753,7 +991,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
753
991
  }
754
992
 
755
993
  private isRetryablePacketIoError(error: unknown) {
756
- if (this.isWebUsbIoHardwareError(error)) return false;
994
+ if (this.stopped) return false;
995
+ if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error)) return false;
757
996
  const message = this.getErrorMessage(error).toLowerCase();
758
997
  if (this.isWebUsbPermissionDeniedMessage(message)) return false;
759
998
  return (
@@ -784,20 +1023,25 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
784
1023
  // USB disconnect event itself was missed.
785
1024
  this.staleProtocolPaths.add(path);
786
1025
  await wait(attempt * PACKET_IO_RETRY_DELAY);
1026
+ this.assertRunning();
787
1027
 
788
1028
  try {
789
1029
  const currentDevice = await this.findDevice(path);
1030
+ this.assertHandleAvailable(currentDevice);
790
1031
  if (currentDevice.opened) {
791
1032
  const endpoints = this.deviceEndpoints.get(path);
792
1033
  const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
793
1034
  try {
794
- await currentDevice.releaseInterface(ifaceNum);
1035
+ await this.runHandleOperation(() => currentDevice.releaseInterface(ifaceNum));
795
1036
  } catch (releaseError) {
796
1037
  this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
797
1038
  }
798
- await currentDevice.close();
1039
+ this.assertHandleAvailable(currentDevice);
1040
+ await this.runHandleOperation(() => currentDevice.close());
799
1041
  }
800
1042
  } catch (closeError) {
1043
+ this.assertRunning();
1044
+ if (this.isHandleOwnershipError(closeError)) throw closeError;
801
1045
  this.Log.debug('[WebUsbTransport] close device before retry error:', closeError);
802
1046
  }
803
1047
 
@@ -839,6 +1083,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
839
1083
  try {
840
1084
  await this.reconnectForPacketIoRetry(path, 'out', attempt, error);
841
1085
  } catch (reconnectError) {
1086
+ this.assertRunning();
1087
+ if (this.isHandleOwnershipError(reconnectError)) throw reconnectError;
842
1088
  lastError = reconnectError;
843
1089
  this.Log.debug(
844
1090
  `[WebUsbTransport] transferout reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
@@ -861,7 +1107,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
861
1107
  const transferBuffer = this.toArrayBuffer(
862
1108
  packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
863
1109
  );
1110
+ this.assertHandleAvailable(device);
864
1111
  const result = await device.transferOut(endpointOut, transferBuffer);
1112
+ this.assertHandleAvailable(device);
865
1113
  if (result.status !== 'ok') {
866
1114
  return this.throwWebUsbIoError(
867
1115
  path,
@@ -888,7 +1136,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
888
1136
  }
889
1137
  const endpoints = this.deviceEndpoints.get(path);
890
1138
  const endpointIn = endpoints?.endpointIn ?? this.endpointId;
1139
+ this.assertHandleAvailable(device);
891
1140
  const result = await device.transferIn(endpointIn, length);
1141
+ this.assertHandleAvailable(device);
892
1142
  return this.getTransferInData(result);
893
1143
  } catch (error) {
894
1144
  lastError = error;
@@ -902,6 +1152,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
902
1152
  try {
903
1153
  await this.reconnectForPacketIoRetry(path, 'in', attempt, error);
904
1154
  } catch (reconnectError) {
1155
+ this.assertRunning();
1156
+ if (this.isHandleOwnershipError(reconnectError)) throw reconnectError;
905
1157
  lastError = reconnectError;
906
1158
  this.Log.debug(
907
1159
  `[WebUsbTransport] transferin reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
@@ -921,7 +1173,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
921
1173
  }
922
1174
  const endpoints = this.deviceEndpoints.get(path);
923
1175
  const endpointIn = endpoints?.endpointIn ?? this.endpointId;
1176
+ this.assertHandleAvailable(device);
924
1177
  const result = await device.transferIn(endpointIn, length);
1178
+ this.assertHandleAvailable(device);
925
1179
  return this.getTransferInData(result);
926
1180
  }
927
1181
 
@@ -984,15 +1238,15 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
984
1238
  }
985
1239
 
986
1240
  try {
987
- await this.callProtocolV1(
1241
+ const response = await this.callProtocolV1(
988
1242
  path,
989
- 'Initialize',
1243
+ this.identityProbe ? 'GetFeatures' : 'Initialize',
990
1244
  {},
991
1245
  {
992
1246
  timeoutMs: PROTOCOL_V1_PROBE_TIMEOUT,
993
1247
  }
994
1248
  );
995
- return true;
1249
+ return !this.identityProbe || response.type === 'Features';
996
1250
  } catch (error) {
997
1251
  if (this.isWebUsbIoHardwareError(error)) throw error;
998
1252
  return false;
@@ -1149,8 +1403,13 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1149
1403
  */
1150
1404
  async release(path: string) {
1151
1405
  this.acquiredPaths.delete(path);
1152
- await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
1153
- await this.closeOpenDevice(path);
1406
+ try {
1407
+ await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
1408
+ await this.closeOpenDevice(path);
1409
+ } finally {
1410
+ this.releaseIdentityProbeHandle(path);
1411
+ this.releaseNormalHandles(path);
1412
+ }
1154
1413
  // Keep deviceProtocol/deviceProtocolHints across release: the probe result is a
1155
1414
  // physical-device property, so the next acquire can skip the wire-level probe.
1156
1415
  // V2 entries are still dropped by onProtocolV2UsbLinkInvalidated via the link