@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/__tests__/electron-ble-transport.test.ts +14 -0
- package/__tests__/webusb-identity-probe.test.ts +887 -0
- package/dist/electron-ble-transport.d.ts.map +1 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +289 -24
- 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 +5 -2
- package/src/webusb.ts +282 -23
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;
|
package/dist/webusb.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webusb.d.ts","sourceRoot":"","sources":["../src/webusb.ts"],"names":[],"mappings":";;AACA,OAAO,SAAS,EAAE,EAQhB,0BAA0B,EAG3B,MAAM,wBAAwB,CAAC;AAahC,OAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EACpB,YAAY,EACZ,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACrB,MAAM,wBAAwB,CAAC;
|
|
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.
|
|
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.
|
|
25
|
-
"@onekeyfe/hd-transport": "1.2.8-alpha.
|
|
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.
|
|
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": "
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1153
|
-
|
|
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
|