@onekeyfe/hd-transport-web-device 1.2.8-alpha.15 → 1.2.8-alpha.17
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/__tests__/webusb-identity-probe.test.ts +570 -3
- package/dist/index.d.ts +11 -0
- package/dist/index.js +215 -18
- package/dist/webusb.d.ts +11 -0
- package/dist/webusb.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/webusb.ts +212 -18
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,14 @@ 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
|
+
|
|
142
|
+
private identityProbeCleanupPaths = new Set<string>();
|
|
143
|
+
|
|
133
144
|
/**
|
|
134
145
|
* The exact USBDevice object each cached protocol was probed against. The
|
|
135
146
|
* browser returns the same object identity for a device as long as it stays
|
|
@@ -192,6 +203,96 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
192
203
|
if (!this.identityProbe) registerActiveWebUsbTransport(this, usb);
|
|
193
204
|
}
|
|
194
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
|
+
|
|
195
296
|
/**
|
|
196
297
|
* Announce that a USB device left. Called from the module-scoped disconnect
|
|
197
298
|
* listener, which is why it is public rather than inlined.
|
|
@@ -273,6 +374,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
273
374
|
* It does NOT prompt the user to select a device
|
|
274
375
|
*/
|
|
275
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
|
+
}
|
|
276
385
|
await this.getConnectedDevices();
|
|
277
386
|
return this.deviceList;
|
|
278
387
|
}
|
|
@@ -281,9 +390,21 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
281
390
|
* Get list of connected devices
|
|
282
391
|
*/
|
|
283
392
|
async getConnectedDevices() {
|
|
393
|
+
this.assertRunning();
|
|
284
394
|
if (!this.usb) return [];
|
|
285
395
|
|
|
286
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();
|
|
287
408
|
const onekeyDevices = devices.filter(dev => {
|
|
288
409
|
const isOneKey = ONEKEY_WEBUSB_FILTER.some(
|
|
289
410
|
(desc: { vendorId: number; productId: number }) =>
|
|
@@ -316,10 +437,13 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
316
437
|
* Acquire device control
|
|
317
438
|
*/
|
|
318
439
|
async acquire(input: AcquireInput) {
|
|
440
|
+
this.assertRunning();
|
|
319
441
|
if (!input.path) return;
|
|
320
442
|
if (this.identityProbe) {
|
|
321
443
|
await this.getConnectedDevices();
|
|
322
444
|
this.claimIdentityProbeHandle(input.path, await this.findDevice(input.path));
|
|
445
|
+
} else {
|
|
446
|
+
this.claimNormalHandle(input.path, await this.findDevice(input.path));
|
|
323
447
|
}
|
|
324
448
|
try {
|
|
325
449
|
await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
|
|
@@ -393,12 +517,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
393
517
|
input.expectedProtocol,
|
|
394
518
|
protocolHint
|
|
395
519
|
);
|
|
520
|
+
this.assertRunning();
|
|
396
521
|
this.confirmedDeviceProtocols.set(input.path, detectedProtocol);
|
|
397
522
|
const probedDevice = this.deviceList.find(d => d.path === input.path)?.device;
|
|
398
523
|
if (probedDevice) {
|
|
399
524
|
this.probedDeviceObjects.set(input.path, probedDevice);
|
|
400
525
|
}
|
|
401
526
|
}
|
|
527
|
+
this.assertRunning();
|
|
402
528
|
this.acquiredPaths.add(input.path);
|
|
403
529
|
return await Promise.resolve(input.path);
|
|
404
530
|
} catch (e) {
|
|
@@ -408,6 +534,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
408
534
|
await this.closeOpenDevice(input.path);
|
|
409
535
|
} finally {
|
|
410
536
|
this.releaseIdentityProbeHandle(input.path);
|
|
537
|
+
this.releaseNormalHandles(input.path);
|
|
411
538
|
}
|
|
412
539
|
throw e;
|
|
413
540
|
}
|
|
@@ -530,9 +657,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
530
657
|
async connect(path: string, first: boolean) {
|
|
531
658
|
const maxRetries = 5;
|
|
532
659
|
for (let i = 0; i < maxRetries; i++) {
|
|
660
|
+
this.assertRunning();
|
|
533
661
|
try {
|
|
534
662
|
return await this.connectToDevice(path, first);
|
|
535
663
|
} catch (e) {
|
|
664
|
+
this.assertRunning();
|
|
536
665
|
if (e instanceof HardwareError && e.errorCode === HardwareErrorCode.BridgeNeedsPermission) {
|
|
537
666
|
throw e;
|
|
538
667
|
}
|
|
@@ -582,17 +711,19 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
582
711
|
* Discovers interface/endpoint numbers from USB descriptors on first connection.
|
|
583
712
|
*/
|
|
584
713
|
async connectToDevice(path: string, first: boolean) {
|
|
714
|
+
this.assertRunning();
|
|
585
715
|
let device: USBDevice = await this.findDevice(path);
|
|
586
716
|
if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
|
|
587
717
|
await this.closeOpenDevice(path);
|
|
588
718
|
this.releaseIdentityProbeHandle(path);
|
|
589
719
|
this.claimIdentityProbeHandle(path, device);
|
|
590
720
|
}
|
|
721
|
+
if (!this.identityProbe) this.claimNormalHandle(path, device);
|
|
591
722
|
this.assertHandleAvailable(device);
|
|
592
723
|
if (!device.opened) {
|
|
593
724
|
openingHandles.add(device);
|
|
594
725
|
try {
|
|
595
|
-
await device.open();
|
|
726
|
+
await this.runHandleOperation(() => device.open());
|
|
596
727
|
} catch (error) {
|
|
597
728
|
return await this.throwWebUsbIoError(path, 'open', error);
|
|
598
729
|
} finally {
|
|
@@ -603,8 +734,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
603
734
|
// state as in the legacy transport; probing and V2 recovery still reset.
|
|
604
735
|
if (first || this.deviceProtocol.get(path) !== 'V1') {
|
|
605
736
|
try {
|
|
606
|
-
await device.reset();
|
|
737
|
+
await this.runHandleOperation(() => device.reset());
|
|
607
738
|
} catch (error) {
|
|
739
|
+
this.assertRunning();
|
|
608
740
|
this.Log?.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
|
|
609
741
|
}
|
|
610
742
|
}
|
|
@@ -615,11 +747,12 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
615
747
|
this.releaseIdentityProbeHandle(path);
|
|
616
748
|
this.claimIdentityProbeHandle(path, device);
|
|
617
749
|
}
|
|
750
|
+
if (!this.identityProbe) this.claimNormalHandle(path, device);
|
|
618
751
|
this.assertHandleAvailable(device);
|
|
619
752
|
if (!device.opened) {
|
|
620
753
|
openingHandles.add(device);
|
|
621
754
|
try {
|
|
622
|
-
await device.open();
|
|
755
|
+
await this.runHandleOperation(() => device.open());
|
|
623
756
|
} catch (error) {
|
|
624
757
|
return await this.throwWebUsbIoError(path, 'open', error);
|
|
625
758
|
} finally {
|
|
@@ -632,14 +765,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
632
765
|
!device.configuration ||
|
|
633
766
|
device.configuration.configurationValue !== this.configurationId
|
|
634
767
|
) {
|
|
635
|
-
await device.selectConfiguration(this.configurationId);
|
|
768
|
+
await this.runHandleOperation(() => device.selectConfiguration(this.configurationId));
|
|
636
769
|
}
|
|
637
770
|
|
|
638
771
|
// Discover endpoints from USB descriptors; descriptors are not used for protocol selection.
|
|
639
772
|
const endpoints = this.discoverEndpoints(device);
|
|
640
773
|
this.deviceEndpoints.set(path, endpoints);
|
|
641
774
|
try {
|
|
642
|
-
await device.claimInterface(endpoints.interfaceNumber);
|
|
775
|
+
await this.runHandleOperation(() => device.claimInterface(endpoints.interfaceNumber));
|
|
643
776
|
} catch (error) {
|
|
644
777
|
return this.throwWebUsbIoError(path, 'claimInterface', error);
|
|
645
778
|
}
|
|
@@ -648,31 +781,70 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
648
781
|
}
|
|
649
782
|
|
|
650
783
|
private assertHandleAvailable(device: USBDevice) {
|
|
784
|
+
this.assertRunning();
|
|
785
|
+
const normalOwner = normalHandleOwners.get(device);
|
|
651
786
|
if (
|
|
652
|
-
|
|
653
|
-
|
|
787
|
+
(normalOwner && normalOwner !== this) ||
|
|
788
|
+
(identityProbeHandles.has(device) &&
|
|
789
|
+
![...this.identityProbeDevices.values()].includes(device))
|
|
654
790
|
) {
|
|
655
|
-
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError,
|
|
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
|
+
}
|
|
656
808
|
}
|
|
657
809
|
}
|
|
658
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
|
+
|
|
659
819
|
private claimIdentityProbeHandle(path: string, device: USBDevice) {
|
|
660
|
-
|
|
661
|
-
|
|
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);
|
|
662
828
|
}
|
|
663
829
|
identityProbeHandles.add(device);
|
|
664
830
|
this.identityProbeDevices.set(path, device);
|
|
665
831
|
}
|
|
666
832
|
|
|
667
833
|
private releaseIdentityProbeHandle(path: string) {
|
|
834
|
+
if (this.stopped) return;
|
|
668
835
|
const device = this.identityProbeDevices.get(path);
|
|
669
836
|
// A failed native close must not make an open probe handle available to other callers.
|
|
670
|
-
if (device?.opened)
|
|
837
|
+
if (device?.opened) {
|
|
838
|
+
this.identityProbeCleanupPaths.add(path);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
671
841
|
if (device) identityProbeHandles.delete(device);
|
|
672
842
|
this.identityProbeDevices.delete(path);
|
|
843
|
+
this.identityProbeCleanupPaths.delete(path);
|
|
673
844
|
}
|
|
674
845
|
|
|
675
846
|
private async closeOpenDevice(path: string) {
|
|
847
|
+
if (this.stopped) return;
|
|
676
848
|
const current = this.identityProbe
|
|
677
849
|
? this.identityProbeDevices.get(path)
|
|
678
850
|
: this.deviceList.find(device => device.path === path)?.device;
|
|
@@ -682,12 +854,13 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
682
854
|
const endpoints = this.deviceEndpoints.get(path);
|
|
683
855
|
const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
|
|
684
856
|
try {
|
|
685
|
-
await current.releaseInterface(ifaceNum);
|
|
857
|
+
await this.runHandleOperation(() => current.releaseInterface(ifaceNum));
|
|
686
858
|
} catch (error) {
|
|
687
859
|
this.Log?.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
|
|
688
860
|
}
|
|
861
|
+
this.assertHandleAvailable(current);
|
|
689
862
|
try {
|
|
690
|
-
await current.close();
|
|
863
|
+
await this.runHandleOperation(() => current.close());
|
|
691
864
|
} catch (error) {
|
|
692
865
|
this.Log?.debug('[WebUsbTransport] close before reconnect failed:', error);
|
|
693
866
|
if (this.identityProbe) throw error;
|
|
@@ -699,9 +872,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
699
872
|
direction: USBDirection,
|
|
700
873
|
endpointNumber: number
|
|
701
874
|
) {
|
|
875
|
+
this.assertHandleAvailable(device);
|
|
702
876
|
try {
|
|
703
|
-
await device.clearHalt(direction, endpointNumber);
|
|
877
|
+
await this.runHandleOperation(() => device.clearHalt(direction, endpointNumber));
|
|
704
878
|
} catch (error) {
|
|
879
|
+
this.assertRunning();
|
|
705
880
|
this.Log?.debug(
|
|
706
881
|
`[WebUsbTransport] clearHalt ${direction} endpoint ${endpointNumber} failed, continuing:`,
|
|
707
882
|
error
|
|
@@ -717,6 +892,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
717
892
|
* protocol entry produced the same fail-fast).
|
|
718
893
|
*/
|
|
719
894
|
private assertAcquired(path: string) {
|
|
895
|
+
this.assertRunning();
|
|
720
896
|
if (!this.acquiredPaths.has(path)) {
|
|
721
897
|
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, `Device is not acquired for ${path}`);
|
|
722
898
|
}
|
|
@@ -774,7 +950,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
774
950
|
operation: WebUsbOperation,
|
|
775
951
|
error: unknown
|
|
776
952
|
): Promise<never> {
|
|
777
|
-
|
|
953
|
+
this.assertRunning();
|
|
954
|
+
if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error)) throw error;
|
|
778
955
|
if (error instanceof ProtocolV2LinkError && error.code !== 'io') throw error;
|
|
779
956
|
|
|
780
957
|
const nativeError = error instanceof ProtocolV2LinkError ? error.cause ?? error : error;
|
|
@@ -814,7 +991,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
814
991
|
}
|
|
815
992
|
|
|
816
993
|
private isRetryablePacketIoError(error: unknown) {
|
|
817
|
-
if (this.
|
|
994
|
+
if (this.stopped) return false;
|
|
995
|
+
if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error)) return false;
|
|
818
996
|
const message = this.getErrorMessage(error).toLowerCase();
|
|
819
997
|
if (this.isWebUsbPermissionDeniedMessage(message)) return false;
|
|
820
998
|
return (
|
|
@@ -845,20 +1023,25 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
845
1023
|
// USB disconnect event itself was missed.
|
|
846
1024
|
this.staleProtocolPaths.add(path);
|
|
847
1025
|
await wait(attempt * PACKET_IO_RETRY_DELAY);
|
|
1026
|
+
this.assertRunning();
|
|
848
1027
|
|
|
849
1028
|
try {
|
|
850
1029
|
const currentDevice = await this.findDevice(path);
|
|
1030
|
+
this.assertHandleAvailable(currentDevice);
|
|
851
1031
|
if (currentDevice.opened) {
|
|
852
1032
|
const endpoints = this.deviceEndpoints.get(path);
|
|
853
1033
|
const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
|
|
854
1034
|
try {
|
|
855
|
-
await currentDevice.releaseInterface(ifaceNum);
|
|
1035
|
+
await this.runHandleOperation(() => currentDevice.releaseInterface(ifaceNum));
|
|
856
1036
|
} catch (releaseError) {
|
|
857
1037
|
this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
|
|
858
1038
|
}
|
|
859
|
-
|
|
1039
|
+
this.assertHandleAvailable(currentDevice);
|
|
1040
|
+
await this.runHandleOperation(() => currentDevice.close());
|
|
860
1041
|
}
|
|
861
1042
|
} catch (closeError) {
|
|
1043
|
+
this.assertRunning();
|
|
1044
|
+
if (this.isHandleOwnershipError(closeError)) throw closeError;
|
|
862
1045
|
this.Log.debug('[WebUsbTransport] close device before retry error:', closeError);
|
|
863
1046
|
}
|
|
864
1047
|
|
|
@@ -900,6 +1083,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
900
1083
|
try {
|
|
901
1084
|
await this.reconnectForPacketIoRetry(path, 'out', attempt, error);
|
|
902
1085
|
} catch (reconnectError) {
|
|
1086
|
+
this.assertRunning();
|
|
1087
|
+
if (this.isHandleOwnershipError(reconnectError)) throw reconnectError;
|
|
903
1088
|
lastError = reconnectError;
|
|
904
1089
|
this.Log.debug(
|
|
905
1090
|
`[WebUsbTransport] transferout reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
|
|
@@ -922,7 +1107,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
922
1107
|
const transferBuffer = this.toArrayBuffer(
|
|
923
1108
|
packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
|
|
924
1109
|
);
|
|
1110
|
+
this.assertHandleAvailable(device);
|
|
925
1111
|
const result = await device.transferOut(endpointOut, transferBuffer);
|
|
1112
|
+
this.assertHandleAvailable(device);
|
|
926
1113
|
if (result.status !== 'ok') {
|
|
927
1114
|
return this.throwWebUsbIoError(
|
|
928
1115
|
path,
|
|
@@ -949,7 +1136,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
949
1136
|
}
|
|
950
1137
|
const endpoints = this.deviceEndpoints.get(path);
|
|
951
1138
|
const endpointIn = endpoints?.endpointIn ?? this.endpointId;
|
|
1139
|
+
this.assertHandleAvailable(device);
|
|
952
1140
|
const result = await device.transferIn(endpointIn, length);
|
|
1141
|
+
this.assertHandleAvailable(device);
|
|
953
1142
|
return this.getTransferInData(result);
|
|
954
1143
|
} catch (error) {
|
|
955
1144
|
lastError = error;
|
|
@@ -963,6 +1152,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
963
1152
|
try {
|
|
964
1153
|
await this.reconnectForPacketIoRetry(path, 'in', attempt, error);
|
|
965
1154
|
} catch (reconnectError) {
|
|
1155
|
+
this.assertRunning();
|
|
1156
|
+
if (this.isHandleOwnershipError(reconnectError)) throw reconnectError;
|
|
966
1157
|
lastError = reconnectError;
|
|
967
1158
|
this.Log.debug(
|
|
968
1159
|
`[WebUsbTransport] transferin reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
|
|
@@ -982,7 +1173,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
982
1173
|
}
|
|
983
1174
|
const endpoints = this.deviceEndpoints.get(path);
|
|
984
1175
|
const endpointIn = endpoints?.endpointIn ?? this.endpointId;
|
|
1176
|
+
this.assertHandleAvailable(device);
|
|
985
1177
|
const result = await device.transferIn(endpointIn, length);
|
|
1178
|
+
this.assertHandleAvailable(device);
|
|
986
1179
|
return this.getTransferInData(result);
|
|
987
1180
|
}
|
|
988
1181
|
|
|
@@ -1215,6 +1408,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
1215
1408
|
await this.closeOpenDevice(path);
|
|
1216
1409
|
} finally {
|
|
1217
1410
|
this.releaseIdentityProbeHandle(path);
|
|
1411
|
+
this.releaseNormalHandles(path);
|
|
1218
1412
|
}
|
|
1219
1413
|
// Keep deviceProtocol/deviceProtocolHints across release: the probe result is a
|
|
1220
1414
|
// physical-device property, so the next acquire can skip the wire-level probe.
|