@onekeyfe/hd-transport-web-device 1.2.8-alpha.52 → 1.2.8-alpha.54
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 +887 -0
- 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 +17 -1
- package/dist/index.js +296 -27
- package/dist/webusb.d.ts +17 -1
- 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 +290 -32
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.54",
|
|
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.54",
|
|
25
|
+
"@onekeyfe/hd-transport": "1.2.8-alpha.54"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@onekeyfe/hd-transport-electron": "1.2.8-alpha.
|
|
28
|
+
"@onekeyfe/hd-transport-electron": "1.2.8-alpha.54",
|
|
29
29
|
"@types/w3c-web-usb": "^1.0.6",
|
|
30
30
|
"@types/web-bluetooth": "^0.0.17"
|
|
31
31
|
},
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "167d8127232ef23d345ccc0c12fd0c21fcaf6552"
|
|
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.
|
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -405,21 +546,20 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
405
546
|
* fall back to a Protocol V2 Ping probe.
|
|
406
547
|
*/
|
|
407
548
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
408
|
-
return ERRORS.TypedError(
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
);
|
|
549
|
+
return ERRORS.TypedError(HardwareErrorCode.TransportProtocolProbeFailed, undefined, {
|
|
550
|
+
expectedProtocol: expected,
|
|
551
|
+
});
|
|
412
552
|
}
|
|
413
553
|
|
|
414
554
|
private createProtocolProbeTimeoutError(expected: ProtocolType, attempts: number) {
|
|
415
|
-
return ERRORS.TypedError(
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
);
|
|
555
|
+
return ERRORS.TypedError(HardwareErrorCode.TransportProtocolProbeFailed, undefined, {
|
|
556
|
+
expectedProtocol: expected,
|
|
557
|
+
attempts,
|
|
558
|
+
});
|
|
419
559
|
}
|
|
420
560
|
|
|
421
561
|
private createProtocolDetectionError() {
|
|
422
|
-
return ERRORS.TypedError(HardwareErrorCode.
|
|
562
|
+
return ERRORS.TypedError(HardwareErrorCode.TransportProtocolProbeFailed);
|
|
423
563
|
}
|
|
424
564
|
|
|
425
565
|
private async detectProtocol(
|
|
@@ -516,9 +656,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
516
656
|
async connect(path: string, first: boolean) {
|
|
517
657
|
const maxRetries = 5;
|
|
518
658
|
for (let i = 0; i < maxRetries; i++) {
|
|
659
|
+
this.assertRunning();
|
|
519
660
|
try {
|
|
520
661
|
return await this.connectToDevice(path, first);
|
|
521
662
|
} catch (e) {
|
|
663
|
+
this.assertRunning();
|
|
522
664
|
if (e instanceof HardwareError && e.errorCode === HardwareErrorCode.BridgeNeedsPermission) {
|
|
523
665
|
throw e;
|
|
524
666
|
}
|
|
@@ -568,30 +710,52 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
568
710
|
* Discovers interface/endpoint numbers from USB descriptors on first connection.
|
|
569
711
|
*/
|
|
570
712
|
async connectToDevice(path: string, first: boolean) {
|
|
713
|
+
this.assertRunning();
|
|
571
714
|
let device: USBDevice = await this.findDevice(path);
|
|
715
|
+
if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
|
|
716
|
+
await this.closeOpenDevice(path);
|
|
717
|
+
this.releaseIdentityProbeHandle(path);
|
|
718
|
+
this.claimIdentityProbeHandle(path, device);
|
|
719
|
+
}
|
|
720
|
+
if (!this.identityProbe) this.claimNormalHandle(path, device);
|
|
721
|
+
this.assertHandleAvailable(device);
|
|
572
722
|
if (!device.opened) {
|
|
723
|
+
openingHandles.add(device);
|
|
573
724
|
try {
|
|
574
|
-
await device.open();
|
|
725
|
+
await this.runHandleOperation(() => device.open());
|
|
575
726
|
} catch (error) {
|
|
576
|
-
return this.throwWebUsbIoError(path, 'open', error);
|
|
727
|
+
return await this.throwWebUsbIoError(path, 'open', error);
|
|
728
|
+
} finally {
|
|
729
|
+
openingHandles.delete(device);
|
|
577
730
|
}
|
|
578
731
|
}
|
|
579
732
|
// A V1 packet retry continues an in-flight exchange. Preserve its endpoint
|
|
580
733
|
// state as in the legacy transport; probing and V2 recovery still reset.
|
|
581
734
|
if (first || this.deviceProtocol.get(path) !== 'V1') {
|
|
582
735
|
try {
|
|
583
|
-
await device.reset();
|
|
736
|
+
await this.runHandleOperation(() => device.reset());
|
|
584
737
|
} catch (error) {
|
|
738
|
+
this.assertRunning();
|
|
585
739
|
this.Log?.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
|
|
586
740
|
}
|
|
587
741
|
}
|
|
588
742
|
await this.getConnectedDevices();
|
|
589
743
|
device = await this.findDevice(path);
|
|
744
|
+
if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
|
|
745
|
+
await this.closeOpenDevice(path);
|
|
746
|
+
this.releaseIdentityProbeHandle(path);
|
|
747
|
+
this.claimIdentityProbeHandle(path, device);
|
|
748
|
+
}
|
|
749
|
+
if (!this.identityProbe) this.claimNormalHandle(path, device);
|
|
750
|
+
this.assertHandleAvailable(device);
|
|
590
751
|
if (!device.opened) {
|
|
752
|
+
openingHandles.add(device);
|
|
591
753
|
try {
|
|
592
|
-
await device.open();
|
|
754
|
+
await this.runHandleOperation(() => device.open());
|
|
593
755
|
} catch (error) {
|
|
594
|
-
return this.throwWebUsbIoError(path, 'open', error);
|
|
756
|
+
return await this.throwWebUsbIoError(path, 'open', error);
|
|
757
|
+
} finally {
|
|
758
|
+
openingHandles.delete(device);
|
|
595
759
|
}
|
|
596
760
|
}
|
|
597
761
|
|
|
@@ -600,14 +764,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
600
764
|
!device.configuration ||
|
|
601
765
|
device.configuration.configurationValue !== this.configurationId
|
|
602
766
|
) {
|
|
603
|
-
await device.selectConfiguration(this.configurationId);
|
|
767
|
+
await this.runHandleOperation(() => device.selectConfiguration(this.configurationId));
|
|
604
768
|
}
|
|
605
769
|
|
|
606
770
|
// Discover endpoints from USB descriptors; descriptors are not used for protocol selection.
|
|
607
771
|
const endpoints = this.discoverEndpoints(device);
|
|
608
772
|
this.deviceEndpoints.set(path, endpoints);
|
|
609
773
|
try {
|
|
610
|
-
await device.claimInterface(endpoints.interfaceNumber);
|
|
774
|
+
await this.runHandleOperation(() => device.claimInterface(endpoints.interfaceNumber));
|
|
611
775
|
} catch (error) {
|
|
612
776
|
return this.throwWebUsbIoError(path, 'claimInterface', error);
|
|
613
777
|
}
|
|
@@ -615,21 +779,90 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
615
779
|
await this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
|
|
616
780
|
}
|
|
617
781
|
|
|
782
|
+
private assertHandleAvailable(device: USBDevice) {
|
|
783
|
+
this.assertRunning();
|
|
784
|
+
const normalOwner = normalHandleOwners.get(device);
|
|
785
|
+
if (
|
|
786
|
+
(normalOwner && normalOwner !== this) ||
|
|
787
|
+
(identityProbeHandles.has(device) &&
|
|
788
|
+
![...this.identityProbeDevices.values()].includes(device))
|
|
789
|
+
) {
|
|
790
|
+
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, USB_DEVICE_IN_USE);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
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
|
+
|
|
818
|
+
private claimIdentityProbeHandle(path: string, device: USBDevice) {
|
|
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);
|
|
827
|
+
}
|
|
828
|
+
identityProbeHandles.add(device);
|
|
829
|
+
this.identityProbeDevices.set(path, device);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
private releaseIdentityProbeHandle(path: string) {
|
|
833
|
+
if (this.stopped) return;
|
|
834
|
+
const device = this.identityProbeDevices.get(path);
|
|
835
|
+
// A failed native close must not make an open probe handle available to other callers.
|
|
836
|
+
if (device?.opened) {
|
|
837
|
+
this.identityProbeCleanupPaths.add(path);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (device) identityProbeHandles.delete(device);
|
|
841
|
+
this.identityProbeDevices.delete(path);
|
|
842
|
+
this.identityProbeCleanupPaths.delete(path);
|
|
843
|
+
}
|
|
844
|
+
|
|
618
845
|
private async closeOpenDevice(path: string) {
|
|
619
|
-
|
|
846
|
+
if (this.stopped) return;
|
|
847
|
+
const current = this.identityProbe
|
|
848
|
+
? this.identityProbeDevices.get(path)
|
|
849
|
+
: this.deviceList.find(device => device.path === path)?.device;
|
|
620
850
|
if (!current?.opened) return;
|
|
851
|
+
this.assertHandleAvailable(current);
|
|
621
852
|
|
|
622
853
|
const endpoints = this.deviceEndpoints.get(path);
|
|
623
854
|
const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
|
|
624
855
|
try {
|
|
625
|
-
await current.releaseInterface(ifaceNum);
|
|
856
|
+
await this.runHandleOperation(() => current.releaseInterface(ifaceNum));
|
|
626
857
|
} catch (error) {
|
|
627
858
|
this.Log?.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
|
|
628
859
|
}
|
|
860
|
+
this.assertHandleAvailable(current);
|
|
629
861
|
try {
|
|
630
|
-
await current.close();
|
|
862
|
+
await this.runHandleOperation(() => current.close());
|
|
631
863
|
} catch (error) {
|
|
632
864
|
this.Log?.debug('[WebUsbTransport] close before reconnect failed:', error);
|
|
865
|
+
if (this.identityProbe) throw error;
|
|
633
866
|
}
|
|
634
867
|
}
|
|
635
868
|
|
|
@@ -638,9 +871,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
638
871
|
direction: USBDirection,
|
|
639
872
|
endpointNumber: number
|
|
640
873
|
) {
|
|
874
|
+
this.assertHandleAvailable(device);
|
|
641
875
|
try {
|
|
642
|
-
await device.clearHalt(direction, endpointNumber);
|
|
876
|
+
await this.runHandleOperation(() => device.clearHalt(direction, endpointNumber));
|
|
643
877
|
} catch (error) {
|
|
878
|
+
this.assertRunning();
|
|
644
879
|
this.Log?.debug(
|
|
645
880
|
`[WebUsbTransport] clearHalt ${direction} endpoint ${endpointNumber} failed, continuing:`,
|
|
646
881
|
error
|
|
@@ -656,6 +891,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
656
891
|
* protocol entry produced the same fail-fast).
|
|
657
892
|
*/
|
|
658
893
|
private assertAcquired(path: string) {
|
|
894
|
+
this.assertRunning();
|
|
659
895
|
if (!this.acquiredPaths.has(path)) {
|
|
660
896
|
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, `Device is not acquired for ${path}`);
|
|
661
897
|
}
|
|
@@ -713,7 +949,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
713
949
|
operation: WebUsbOperation,
|
|
714
950
|
error: unknown
|
|
715
951
|
): Promise<never> {
|
|
716
|
-
|
|
952
|
+
this.assertRunning();
|
|
953
|
+
if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error)) throw error;
|
|
717
954
|
if (error instanceof ProtocolV2LinkError && error.code !== 'io') throw error;
|
|
718
955
|
|
|
719
956
|
const nativeError = error instanceof ProtocolV2LinkError ? error.cause ?? error : error;
|
|
@@ -753,7 +990,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
753
990
|
}
|
|
754
991
|
|
|
755
992
|
private isRetryablePacketIoError(error: unknown) {
|
|
756
|
-
if (this.
|
|
993
|
+
if (this.stopped) return false;
|
|
994
|
+
if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error)) return false;
|
|
757
995
|
const message = this.getErrorMessage(error).toLowerCase();
|
|
758
996
|
if (this.isWebUsbPermissionDeniedMessage(message)) return false;
|
|
759
997
|
return (
|
|
@@ -784,20 +1022,25 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
784
1022
|
// USB disconnect event itself was missed.
|
|
785
1023
|
this.staleProtocolPaths.add(path);
|
|
786
1024
|
await wait(attempt * PACKET_IO_RETRY_DELAY);
|
|
1025
|
+
this.assertRunning();
|
|
787
1026
|
|
|
788
1027
|
try {
|
|
789
1028
|
const currentDevice = await this.findDevice(path);
|
|
1029
|
+
this.assertHandleAvailable(currentDevice);
|
|
790
1030
|
if (currentDevice.opened) {
|
|
791
1031
|
const endpoints = this.deviceEndpoints.get(path);
|
|
792
1032
|
const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
|
|
793
1033
|
try {
|
|
794
|
-
await currentDevice.releaseInterface(ifaceNum);
|
|
1034
|
+
await this.runHandleOperation(() => currentDevice.releaseInterface(ifaceNum));
|
|
795
1035
|
} catch (releaseError) {
|
|
796
1036
|
this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
|
|
797
1037
|
}
|
|
798
|
-
|
|
1038
|
+
this.assertHandleAvailable(currentDevice);
|
|
1039
|
+
await this.runHandleOperation(() => currentDevice.close());
|
|
799
1040
|
}
|
|
800
1041
|
} catch (closeError) {
|
|
1042
|
+
this.assertRunning();
|
|
1043
|
+
if (this.isHandleOwnershipError(closeError)) throw closeError;
|
|
801
1044
|
this.Log.debug('[WebUsbTransport] close device before retry error:', closeError);
|
|
802
1045
|
}
|
|
803
1046
|
|
|
@@ -839,6 +1082,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
839
1082
|
try {
|
|
840
1083
|
await this.reconnectForPacketIoRetry(path, 'out', attempt, error);
|
|
841
1084
|
} catch (reconnectError) {
|
|
1085
|
+
this.assertRunning();
|
|
1086
|
+
if (this.isHandleOwnershipError(reconnectError)) throw reconnectError;
|
|
842
1087
|
lastError = reconnectError;
|
|
843
1088
|
this.Log.debug(
|
|
844
1089
|
`[WebUsbTransport] transferout reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
|
|
@@ -861,7 +1106,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
861
1106
|
const transferBuffer = this.toArrayBuffer(
|
|
862
1107
|
packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
|
|
863
1108
|
);
|
|
1109
|
+
this.assertHandleAvailable(device);
|
|
864
1110
|
const result = await device.transferOut(endpointOut, transferBuffer);
|
|
1111
|
+
this.assertHandleAvailable(device);
|
|
865
1112
|
if (result.status !== 'ok') {
|
|
866
1113
|
return this.throwWebUsbIoError(
|
|
867
1114
|
path,
|
|
@@ -888,7 +1135,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
888
1135
|
}
|
|
889
1136
|
const endpoints = this.deviceEndpoints.get(path);
|
|
890
1137
|
const endpointIn = endpoints?.endpointIn ?? this.endpointId;
|
|
1138
|
+
this.assertHandleAvailable(device);
|
|
891
1139
|
const result = await device.transferIn(endpointIn, length);
|
|
1140
|
+
this.assertHandleAvailable(device);
|
|
892
1141
|
return this.getTransferInData(result);
|
|
893
1142
|
} catch (error) {
|
|
894
1143
|
lastError = error;
|
|
@@ -902,6 +1151,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
902
1151
|
try {
|
|
903
1152
|
await this.reconnectForPacketIoRetry(path, 'in', attempt, error);
|
|
904
1153
|
} catch (reconnectError) {
|
|
1154
|
+
this.assertRunning();
|
|
1155
|
+
if (this.isHandleOwnershipError(reconnectError)) throw reconnectError;
|
|
905
1156
|
lastError = reconnectError;
|
|
906
1157
|
this.Log.debug(
|
|
907
1158
|
`[WebUsbTransport] transferin reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
|
|
@@ -921,7 +1172,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
921
1172
|
}
|
|
922
1173
|
const endpoints = this.deviceEndpoints.get(path);
|
|
923
1174
|
const endpointIn = endpoints?.endpointIn ?? this.endpointId;
|
|
1175
|
+
this.assertHandleAvailable(device);
|
|
924
1176
|
const result = await device.transferIn(endpointIn, length);
|
|
1177
|
+
this.assertHandleAvailable(device);
|
|
925
1178
|
return this.getTransferInData(result);
|
|
926
1179
|
}
|
|
927
1180
|
|
|
@@ -984,15 +1237,15 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
984
1237
|
}
|
|
985
1238
|
|
|
986
1239
|
try {
|
|
987
|
-
await this.callProtocolV1(
|
|
1240
|
+
const response = await this.callProtocolV1(
|
|
988
1241
|
path,
|
|
989
|
-
'Initialize',
|
|
1242
|
+
this.identityProbe ? 'GetFeatures' : 'Initialize',
|
|
990
1243
|
{},
|
|
991
1244
|
{
|
|
992
1245
|
timeoutMs: PROTOCOL_V1_PROBE_TIMEOUT,
|
|
993
1246
|
}
|
|
994
1247
|
);
|
|
995
|
-
return
|
|
1248
|
+
return !this.identityProbe || response.type === 'Features';
|
|
996
1249
|
} catch (error) {
|
|
997
1250
|
if (this.isWebUsbIoHardwareError(error)) throw error;
|
|
998
1251
|
return false;
|
|
@@ -1149,8 +1402,13 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
1149
1402
|
*/
|
|
1150
1403
|
async release(path: string) {
|
|
1151
1404
|
this.acquiredPaths.delete(path);
|
|
1152
|
-
|
|
1153
|
-
|
|
1405
|
+
try {
|
|
1406
|
+
await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
|
|
1407
|
+
await this.closeOpenDevice(path);
|
|
1408
|
+
} finally {
|
|
1409
|
+
this.releaseIdentityProbeHandle(path);
|
|
1410
|
+
this.releaseNormalHandles(path);
|
|
1411
|
+
}
|
|
1154
1412
|
// Keep deviceProtocol/deviceProtocolHints across release: the probe result is a
|
|
1155
1413
|
// physical-device property, so the next acquire can skip the wire-level probe.
|
|
1156
1414
|
// V2 entries are still dropped by onProtocolV2UsbLinkInvalidated via the link
|