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