@onekeyfe/hd-transport-web-device 1.2.8-alpha.103 → 1.2.8-alpha.104
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 +0 -47
- package/__tests__/webusb-protocol-cache.test.ts +1 -219
- package/dist/index.d.ts +2 -20
- package/dist/index.js +65 -373
- package/dist/webusb.d.ts +2 -21
- package/dist/webusb.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/webusb.ts +57 -494
package/src/webusb.ts
CHANGED
|
@@ -45,8 +45,6 @@ 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
47
|
const USB_DEVICE_IN_USE = 'USB device is already in use';
|
|
48
|
-
const WEBUSB_LIFECYCLE_LOG_PREFIX = '[WebUsbTransport][lifecycle]';
|
|
49
|
-
const WEBUSB_ERROR_MESSAGE_LIMIT = 240;
|
|
50
48
|
// Legacy devices can take longer to answer Initialize after a WebUSB reset.
|
|
51
49
|
// Keep this aligned with Node USB so a slow Protocol V1 response is not
|
|
52
50
|
// misreported as a protocol mismatch during acquire.
|
|
@@ -76,23 +74,6 @@ interface TransferCancelToken {
|
|
|
76
74
|
|
|
77
75
|
type WebUsbOperation = 'open' | 'claimInterface' | 'transferIn' | 'transferOut';
|
|
78
76
|
|
|
79
|
-
type WebUsbHandleOperation =
|
|
80
|
-
| 'open'
|
|
81
|
-
| 'reset'
|
|
82
|
-
| 'selectConfiguration'
|
|
83
|
-
| 'claimInterface'
|
|
84
|
-
| 'clearHaltIn'
|
|
85
|
-
| 'clearHaltOut'
|
|
86
|
-
| 'releaseInterface'
|
|
87
|
-
| 'close';
|
|
88
|
-
|
|
89
|
-
type WebUsbLifecyclePhase = 'start' | 'success' | 'error' | 'skipped';
|
|
90
|
-
|
|
91
|
-
interface WebUsbConnectOptions {
|
|
92
|
-
expectedProtocol?: ProtocolType;
|
|
93
|
-
forceProtocolDetection?: boolean;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
77
|
/**
|
|
97
78
|
* The navigator.usb disconnect listener is module-scoped and attached at most
|
|
98
79
|
* once: listeners on navigator.usb are global and never garbage collected, so a
|
|
@@ -102,7 +83,6 @@ interface WebUsbConnectOptions {
|
|
|
102
83
|
*/
|
|
103
84
|
let activeWebUsbTransport: WebUsbTransport | undefined;
|
|
104
85
|
let usbDisconnectListenerAttached = false;
|
|
105
|
-
let nextWebUsbTransportInstanceId = 0;
|
|
106
86
|
// Only isolated discovery owns these handles. Normal calls must never close or
|
|
107
87
|
// open them, including while native open() is still pending.
|
|
108
88
|
const identityProbeHandles = new WeakSet<USBDevice>();
|
|
@@ -127,14 +107,6 @@ function registerActiveWebUsbTransport(instance: WebUsbTransport, usb: USB) {
|
|
|
127
107
|
}
|
|
128
108
|
|
|
129
109
|
export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
130
|
-
private readonly transportInstanceId = ++nextWebUsbTransportInstanceId;
|
|
131
|
-
|
|
132
|
-
private handleOperationSequence = 0;
|
|
133
|
-
|
|
134
|
-
private deviceLogSequence = 0;
|
|
135
|
-
|
|
136
|
-
private deviceLogIds = new WeakMap<USBDevice, number>();
|
|
137
|
-
|
|
138
110
|
messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
139
111
|
|
|
140
112
|
/** Protobuf schema for Protocol V2 transports. */
|
|
@@ -232,30 +204,15 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
232
204
|
}
|
|
233
205
|
|
|
234
206
|
stop(): Promise<void> {
|
|
235
|
-
const startedAt = Date.now();
|
|
236
207
|
this.stopped = true;
|
|
237
208
|
this.acquiredPaths.clear();
|
|
238
209
|
if (activeWebUsbTransport === this) activeWebUsbTransport = undefined;
|
|
239
210
|
if (!this.stopPromise) {
|
|
240
|
-
this.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
durationMs: Date.now() - startedAt,
|
|
246
|
-
});
|
|
247
|
-
},
|
|
248
|
-
error => {
|
|
249
|
-
this.logLifecycle('stop', {
|
|
250
|
-
phase: 'error',
|
|
251
|
-
durationMs: Date.now() - startedAt,
|
|
252
|
-
...this.getDiagnosticError(error),
|
|
253
|
-
});
|
|
254
|
-
// A later discovery can retry closure without dropping the handle guard.
|
|
255
|
-
this.stopPromise = undefined;
|
|
256
|
-
throw error;
|
|
257
|
-
}
|
|
258
|
-
);
|
|
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
|
+
});
|
|
259
216
|
}
|
|
260
217
|
return this.stopPromise;
|
|
261
218
|
}
|
|
@@ -280,34 +237,12 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
280
237
|
try {
|
|
281
238
|
if (device.opened) {
|
|
282
239
|
const iface = this.deviceEndpoints.get(path)?.interfaceNumber ?? this.interfaceId;
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
'interface-not-claimed',
|
|
288
|
-
iface
|
|
289
|
-
);
|
|
290
|
-
} else {
|
|
291
|
-
try {
|
|
292
|
-
await this.runLoggedHandleOperation({
|
|
293
|
-
operation: 'releaseInterface',
|
|
294
|
-
device,
|
|
295
|
-
path,
|
|
296
|
-
interfaceNumber: iface,
|
|
297
|
-
allowStopped: true,
|
|
298
|
-
task: () => device.releaseInterface(iface),
|
|
299
|
-
});
|
|
300
|
-
} catch (error) {
|
|
301
|
-
this.Log?.debug('[WebUsbTransport] stop releaseInterface failed:', error);
|
|
302
|
-
}
|
|
240
|
+
try {
|
|
241
|
+
await device.releaseInterface(iface);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
this.Log?.debug('[WebUsbTransport] stop releaseInterface failed:', error);
|
|
303
244
|
}
|
|
304
|
-
await
|
|
305
|
-
operation: 'close',
|
|
306
|
-
device,
|
|
307
|
-
path,
|
|
308
|
-
allowStopped: true,
|
|
309
|
-
task: () => device.close(),
|
|
310
|
-
});
|
|
245
|
+
await device.close();
|
|
311
246
|
if (device.opened) throw new Error('WebUSB handle remains open after stop');
|
|
312
247
|
}
|
|
313
248
|
if (this.identityProbe) {
|
|
@@ -358,122 +293,6 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
358
293
|
}
|
|
359
294
|
}
|
|
360
295
|
|
|
361
|
-
private getInterfaceClaimed(device: USBDevice, interfaceNumber: number) {
|
|
362
|
-
return device.configuration?.interfaces?.find(
|
|
363
|
-
usbInterface => usbInterface.interfaceNumber === interfaceNumber
|
|
364
|
-
)?.claimed;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
private getDeviceLogId(device: USBDevice) {
|
|
368
|
-
const existing = this.deviceLogIds.get(device);
|
|
369
|
-
if (existing) return existing;
|
|
370
|
-
const next = ++this.deviceLogSequence;
|
|
371
|
-
this.deviceLogIds.set(device, next);
|
|
372
|
-
return next;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
private getHandleLogState(device: USBDevice, interfaceNumber?: number) {
|
|
376
|
-
const interfaceClaimed =
|
|
377
|
-
interfaceNumber === undefined ? undefined : this.getInterfaceClaimed(device, interfaceNumber);
|
|
378
|
-
return {
|
|
379
|
-
deviceRefId: this.getDeviceLogId(device),
|
|
380
|
-
opened: device.opened,
|
|
381
|
-
configurationValue: device.configuration?.configurationValue ?? null,
|
|
382
|
-
interfaceNumber: interfaceNumber ?? null,
|
|
383
|
-
interfaceClaimed: interfaceClaimed ?? null,
|
|
384
|
-
identityProbe: this.identityProbe,
|
|
385
|
-
stopped: this.stopped,
|
|
386
|
-
acquiredPathCount: this.acquiredPaths.size,
|
|
387
|
-
normalHandleCount: this.normalDevices.size,
|
|
388
|
-
identityProbeHandleCount: this.identityProbeDevices.size,
|
|
389
|
-
pendingHandleOperationCount: this.pendingHandleOperations.size,
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
private getDiagnosticError(error: unknown, path?: string) {
|
|
394
|
-
const errorMessage = this.getErrorMessage(error);
|
|
395
|
-
const redactedMessage = path ? errorMessage.split(path).join('<device>') : errorMessage;
|
|
396
|
-
return {
|
|
397
|
-
errorName: this.getErrorName(error) || 'Error',
|
|
398
|
-
errorMessage: redactedMessage.slice(0, WEBUSB_ERROR_MESSAGE_LIMIT),
|
|
399
|
-
errorCode: error instanceof HardwareError ? error.errorCode : undefined,
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
private logLifecycle(event: string, payload: Record<string, unknown> = {}) {
|
|
404
|
-
this.Log?.debug(WEBUSB_LIFECYCLE_LOG_PREFIX, {
|
|
405
|
-
event,
|
|
406
|
-
transportInstanceId: this.transportInstanceId,
|
|
407
|
-
...payload,
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
private logSkippedHandleOperation(
|
|
412
|
-
operation: WebUsbHandleOperation,
|
|
413
|
-
device: USBDevice,
|
|
414
|
-
reason: string,
|
|
415
|
-
interfaceNumber?: number
|
|
416
|
-
) {
|
|
417
|
-
const operationId = ++this.handleOperationSequence;
|
|
418
|
-
this.logLifecycle('handle-operation', {
|
|
419
|
-
operationId,
|
|
420
|
-
operation,
|
|
421
|
-
phase: 'skipped' satisfies WebUsbLifecyclePhase,
|
|
422
|
-
reason,
|
|
423
|
-
...this.getHandleLogState(device, interfaceNumber),
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
private async runLoggedHandleOperation<T>({
|
|
428
|
-
operation,
|
|
429
|
-
device,
|
|
430
|
-
task,
|
|
431
|
-
path,
|
|
432
|
-
interfaceNumber,
|
|
433
|
-
endpointNumber,
|
|
434
|
-
allowStopped = false,
|
|
435
|
-
}: {
|
|
436
|
-
operation: WebUsbHandleOperation;
|
|
437
|
-
device: USBDevice;
|
|
438
|
-
task: () => Promise<T>;
|
|
439
|
-
path?: string;
|
|
440
|
-
interfaceNumber?: number;
|
|
441
|
-
endpointNumber?: number;
|
|
442
|
-
allowStopped?: boolean;
|
|
443
|
-
}): Promise<T> {
|
|
444
|
-
const operationId = ++this.handleOperationSequence;
|
|
445
|
-
const startedAt = Date.now();
|
|
446
|
-
const details = {
|
|
447
|
-
operationId,
|
|
448
|
-
operation,
|
|
449
|
-
...(endpointNumber === undefined ? {} : { endpointNumber }),
|
|
450
|
-
};
|
|
451
|
-
this.logLifecycle('handle-operation', {
|
|
452
|
-
...details,
|
|
453
|
-
phase: 'start' satisfies WebUsbLifecyclePhase,
|
|
454
|
-
...this.getHandleLogState(device, interfaceNumber),
|
|
455
|
-
});
|
|
456
|
-
try {
|
|
457
|
-
const result = allowStopped ? await task() : await this.runHandleOperation(task);
|
|
458
|
-
this.logLifecycle('handle-operation', {
|
|
459
|
-
...details,
|
|
460
|
-
phase: 'success' satisfies WebUsbLifecyclePhase,
|
|
461
|
-
durationMs: Date.now() - startedAt,
|
|
462
|
-
...this.getHandleLogState(device, interfaceNumber),
|
|
463
|
-
});
|
|
464
|
-
return result;
|
|
465
|
-
} catch (error) {
|
|
466
|
-
this.logLifecycle('handle-operation', {
|
|
467
|
-
...details,
|
|
468
|
-
phase: 'error' satisfies WebUsbLifecyclePhase,
|
|
469
|
-
durationMs: Date.now() - startedAt,
|
|
470
|
-
...this.getHandleLogState(device, interfaceNumber),
|
|
471
|
-
...this.getDiagnosticError(error, path),
|
|
472
|
-
});
|
|
473
|
-
throw error;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
296
|
/**
|
|
478
297
|
* Announce that a USB device left. Called from the module-scoped disconnect
|
|
479
298
|
* listener, which is why it is public rather than inlined.
|
|
@@ -537,22 +356,10 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
537
356
|
*/
|
|
538
357
|
async promptDeviceAccess() {
|
|
539
358
|
if (!this.usb) return null;
|
|
540
|
-
const startedAt = Date.now();
|
|
541
|
-
this.logLifecycle('prompt-device-access', { phase: 'start' });
|
|
542
359
|
try {
|
|
543
360
|
const device = await this.usb.requestDevice({ filters: ONEKEY_WEBUSB_FILTER });
|
|
544
|
-
this.logLifecycle('prompt-device-access', {
|
|
545
|
-
phase: 'success',
|
|
546
|
-
durationMs: Date.now() - startedAt,
|
|
547
|
-
selected: Boolean(device),
|
|
548
|
-
});
|
|
549
361
|
return device;
|
|
550
362
|
} catch (e) {
|
|
551
|
-
this.logLifecycle('prompt-device-access', {
|
|
552
|
-
phase: 'error',
|
|
553
|
-
durationMs: Date.now() - startedAt,
|
|
554
|
-
...this.getDiagnosticError(e),
|
|
555
|
-
});
|
|
556
363
|
this.Log.debug(
|
|
557
364
|
'requestDevice error: ',
|
|
558
365
|
e instanceof Error ? `${e.name}: ${e.message}` : String(e)
|
|
@@ -567,35 +374,16 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
567
374
|
* It does NOT prompt the user to select a device
|
|
568
375
|
*/
|
|
569
376
|
async enumerate() {
|
|
570
|
-
|
|
571
|
-
this.
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
// Retry only our failed cleanup before Core skips already-open USB handles.
|
|
577
|
-
for (const path of this.identityProbeCleanupPaths) {
|
|
578
|
-
try {
|
|
579
|
-
await this.release(path);
|
|
580
|
-
} catch {
|
|
581
|
-
// Keep ownership while native close still fails; retry on the next probe.
|
|
582
|
-
}
|
|
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.
|
|
583
383
|
}
|
|
584
|
-
await this.getConnectedDevices();
|
|
585
|
-
this.logLifecycle('enumerate', {
|
|
586
|
-
phase: 'success',
|
|
587
|
-
durationMs: Date.now() - startedAt,
|
|
588
|
-
deviceCount: this.deviceList.length,
|
|
589
|
-
});
|
|
590
|
-
return this.deviceList;
|
|
591
|
-
} catch (error) {
|
|
592
|
-
this.logLifecycle('enumerate', {
|
|
593
|
-
phase: 'error',
|
|
594
|
-
durationMs: Date.now() - startedAt,
|
|
595
|
-
...this.getDiagnosticError(error),
|
|
596
|
-
});
|
|
597
|
-
throw error;
|
|
598
384
|
}
|
|
385
|
+
await this.getConnectedDevices();
|
|
386
|
+
return this.deviceList;
|
|
599
387
|
}
|
|
600
388
|
|
|
601
389
|
/**
|
|
@@ -605,24 +393,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
605
393
|
this.assertRunning();
|
|
606
394
|
if (!this.usb) return [];
|
|
607
395
|
|
|
608
|
-
const
|
|
609
|
-
this.logLifecycle('get-devices', { phase: 'start' });
|
|
610
|
-
let devices: USBDevice[];
|
|
611
|
-
try {
|
|
612
|
-
devices = await this.usb.getDevices();
|
|
613
|
-
this.logLifecycle('get-devices', {
|
|
614
|
-
phase: 'success',
|
|
615
|
-
durationMs: Date.now() - startedAt,
|
|
616
|
-
authorizedDeviceCount: devices.length,
|
|
617
|
-
});
|
|
618
|
-
} catch (error) {
|
|
619
|
-
this.logLifecycle('get-devices', {
|
|
620
|
-
phase: 'error',
|
|
621
|
-
durationMs: Date.now() - startedAt,
|
|
622
|
-
...this.getDiagnosticError(error),
|
|
623
|
-
});
|
|
624
|
-
throw error;
|
|
625
|
-
}
|
|
396
|
+
const devices = await this.usb.getDevices();
|
|
626
397
|
this.assertRunning();
|
|
627
398
|
for (const owner of new Set(devices.map(device => normalHandleOwners.get(device)))) {
|
|
628
399
|
if (owner?.stopped) {
|
|
@@ -668,28 +439,16 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
668
439
|
async acquire(input: AcquireInput) {
|
|
669
440
|
this.assertRunning();
|
|
670
441
|
if (!input.path) return;
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
skipProtocolProbe: Boolean(input.skipProtocolProbe),
|
|
678
|
-
hasCachedProtocol: this.deviceProtocol.has(input.path),
|
|
679
|
-
});
|
|
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
|
+
}
|
|
680
448
|
try {
|
|
681
|
-
if (this.identityProbe) {
|
|
682
|
-
await this.getConnectedDevices();
|
|
683
|
-
this.claimIdentityProbeHandle(input.path, await this.findDevice(input.path));
|
|
684
|
-
} else {
|
|
685
|
-
this.claimNormalHandle(input.path, await this.findDevice(input.path));
|
|
686
|
-
}
|
|
687
449
|
await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
|
|
688
450
|
await this.closeOpenDevice(input.path);
|
|
689
|
-
await this.connect(input.path ?? '', true
|
|
690
|
-
expectedProtocol: input.expectedProtocol,
|
|
691
|
-
forceProtocolDetection: input.forceProtocolDetection,
|
|
692
|
-
});
|
|
451
|
+
await this.connect(input.path ?? '', true);
|
|
693
452
|
if (input.skipProtocolProbe) {
|
|
694
453
|
if (!input.expectedProtocol) {
|
|
695
454
|
throw ERRORS.TypedError(
|
|
@@ -767,18 +526,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
767
526
|
}
|
|
768
527
|
this.assertRunning();
|
|
769
528
|
this.acquiredPaths.add(input.path);
|
|
770
|
-
this.logLifecycle('acquire', {
|
|
771
|
-
phase: 'success',
|
|
772
|
-
durationMs: Date.now() - startedAt,
|
|
773
|
-
protocol: this.deviceProtocol.get(input.path) ?? null,
|
|
774
|
-
});
|
|
775
529
|
return await Promise.resolve(input.path);
|
|
776
530
|
} catch (e) {
|
|
777
|
-
this.logLifecycle('acquire', {
|
|
778
|
-
phase: 'error',
|
|
779
|
-
durationMs: Date.now() - startedAt,
|
|
780
|
-
...this.getDiagnosticError(e, input.path),
|
|
781
|
-
});
|
|
782
531
|
this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
|
|
783
532
|
this.acquiredPaths.delete(input.path);
|
|
784
533
|
try {
|
|
@@ -904,55 +653,18 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
904
653
|
/**
|
|
905
654
|
* Connect to device with retry mechanism
|
|
906
655
|
*/
|
|
907
|
-
async connect(path: string, first: boolean
|
|
656
|
+
async connect(path: string, first: boolean) {
|
|
908
657
|
const maxRetries = 5;
|
|
909
658
|
for (let i = 0; i < maxRetries; i++) {
|
|
910
659
|
this.assertRunning();
|
|
911
|
-
const attempt = i + 1;
|
|
912
|
-
const startedAt = Date.now();
|
|
913
|
-
const connectionStrategy = this.shouldUseLegacyV1Connection(path, options)
|
|
914
|
-
? 'protocol-v1-legacy-1.1.31'
|
|
915
|
-
: 'current';
|
|
916
|
-
this.logLifecycle('connect-attempt', {
|
|
917
|
-
phase: 'start',
|
|
918
|
-
attempt,
|
|
919
|
-
maxAttempts: maxRetries,
|
|
920
|
-
first,
|
|
921
|
-
connectionStrategy,
|
|
922
|
-
});
|
|
923
660
|
try {
|
|
924
|
-
|
|
925
|
-
this.logLifecycle('connect-attempt', {
|
|
926
|
-
phase: 'success',
|
|
927
|
-
attempt,
|
|
928
|
-
maxAttempts: maxRetries,
|
|
929
|
-
first,
|
|
930
|
-
connectionStrategy,
|
|
931
|
-
durationMs: Date.now() - startedAt,
|
|
932
|
-
});
|
|
933
|
-
return result;
|
|
661
|
+
return await this.connectToDevice(path, first);
|
|
934
662
|
} catch (e) {
|
|
935
663
|
this.assertRunning();
|
|
936
|
-
|
|
937
|
-
this.isHandleOwnershipError(e) ||
|
|
938
|
-
(e instanceof HardwareError &&
|
|
939
|
-
(e.errorCode === HardwareErrorCode.BridgeNeedsPermission ||
|
|
940
|
-
e.errorCode === HardwareErrorCode.WebUsbDeviceAccessError));
|
|
941
|
-
const willRetry = !isTerminalError && attempt < maxRetries;
|
|
942
|
-
this.logLifecycle('connect-attempt', {
|
|
943
|
-
phase: 'error',
|
|
944
|
-
attempt,
|
|
945
|
-
maxAttempts: maxRetries,
|
|
946
|
-
first,
|
|
947
|
-
connectionStrategy,
|
|
948
|
-
durationMs: Date.now() - startedAt,
|
|
949
|
-
willRetry,
|
|
950
|
-
...this.getDiagnosticError(e, path),
|
|
951
|
-
});
|
|
952
|
-
if (isTerminalError) {
|
|
664
|
+
if (e instanceof HardwareError && e.errorCode === HardwareErrorCode.BridgeNeedsPermission) {
|
|
953
665
|
throw e;
|
|
954
666
|
}
|
|
955
|
-
if (
|
|
667
|
+
if (i === maxRetries - 1) {
|
|
956
668
|
throw e;
|
|
957
669
|
}
|
|
958
670
|
await wait(i * 200);
|
|
@@ -960,11 +672,6 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
960
672
|
}
|
|
961
673
|
}
|
|
962
674
|
|
|
963
|
-
private shouldUseLegacyV1Connection(path: string, options: WebUsbConnectOptions) {
|
|
964
|
-
if (options.expectedProtocol) return options.expectedProtocol === 'V1';
|
|
965
|
-
return !options.forceProtocolDetection && this.deviceProtocol.get(path) === 'V1';
|
|
966
|
-
}
|
|
967
|
-
|
|
968
675
|
/**
|
|
969
676
|
* Discover vendor-class (0xFF) interface and its IN/OUT endpoint numbers from USB descriptors.
|
|
970
677
|
* Falls back to legacy hardcoded values if no vendor interface is found.
|
|
@@ -1002,7 +709,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
1002
709
|
* Connect to specific device.
|
|
1003
710
|
* Discovers interface/endpoint numbers from USB descriptors on first connection.
|
|
1004
711
|
*/
|
|
1005
|
-
async connectToDevice(path: string, first: boolean
|
|
712
|
+
async connectToDevice(path: string, first: boolean) {
|
|
1006
713
|
this.assertRunning();
|
|
1007
714
|
let device: USBDevice = await this.findDevice(path);
|
|
1008
715
|
if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
|
|
@@ -1015,63 +722,21 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
1015
722
|
if (!device.opened) {
|
|
1016
723
|
openingHandles.add(device);
|
|
1017
724
|
try {
|
|
1018
|
-
await this.
|
|
1019
|
-
operation: 'open',
|
|
1020
|
-
device,
|
|
1021
|
-
path,
|
|
1022
|
-
task: () => device.open(),
|
|
1023
|
-
});
|
|
725
|
+
await this.runHandleOperation(() => device.open());
|
|
1024
726
|
} catch (error) {
|
|
1025
727
|
return await this.throwWebUsbIoError(path, 'open', error);
|
|
1026
728
|
} finally {
|
|
1027
729
|
openingHandles.delete(device);
|
|
1028
730
|
}
|
|
1029
731
|
}
|
|
1030
|
-
|
|
1031
|
-
if (this.shouldUseLegacyV1Connection(path, options)) {
|
|
1032
|
-
// Match the last V1-only WebUSB lifecycle from SDK 1.1.31. Legacy devices
|
|
1033
|
-
// configure before reset and keep the same USBDevice object afterwards.
|
|
1034
|
-
if (first) {
|
|
1035
|
-
await this.runLoggedHandleOperation({
|
|
1036
|
-
operation: 'selectConfiguration',
|
|
1037
|
-
device,
|
|
1038
|
-
path,
|
|
1039
|
-
task: () => device.selectConfiguration(this.configurationId),
|
|
1040
|
-
});
|
|
1041
|
-
try {
|
|
1042
|
-
await this.runLoggedHandleOperation({
|
|
1043
|
-
operation: 'reset',
|
|
1044
|
-
device,
|
|
1045
|
-
path,
|
|
1046
|
-
task: () => device.reset(),
|
|
1047
|
-
});
|
|
1048
|
-
} catch {
|
|
1049
|
-
this.assertRunning();
|
|
1050
|
-
// Reset is best-effort in 1.1.31; runLoggedHandleOperation already recorded the error.
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
// Descriptor lookup is local-only and does not alter the 1.1.31 USB
|
|
1055
|
-
// operation order. Defaults remain available for legacy descriptors.
|
|
1056
|
-
const legacyEndpoints = this.discoverEndpoints(device);
|
|
1057
|
-
this.deviceEndpoints.set(path, legacyEndpoints);
|
|
1058
|
-
await this.claimDeviceInterface(path, device, legacyEndpoints);
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
732
|
// A V1 packet retry continues an in-flight exchange. Preserve its endpoint
|
|
1063
733
|
// state as in the legacy transport; probing and V2 recovery still reset.
|
|
1064
734
|
if (first || this.deviceProtocol.get(path) !== 'V1') {
|
|
1065
735
|
try {
|
|
1066
|
-
await this.
|
|
1067
|
-
|
|
1068
|
-
device,
|
|
1069
|
-
path,
|
|
1070
|
-
task: () => device.reset(),
|
|
1071
|
-
});
|
|
1072
|
-
} catch {
|
|
736
|
+
await this.runHandleOperation(() => device.reset());
|
|
737
|
+
} catch (error) {
|
|
1073
738
|
this.assertRunning();
|
|
1074
|
-
|
|
739
|
+
this.Log?.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
|
|
1075
740
|
}
|
|
1076
741
|
}
|
|
1077
742
|
await this.getConnectedDevices();
|
|
@@ -1086,12 +751,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
1086
751
|
if (!device.opened) {
|
|
1087
752
|
openingHandles.add(device);
|
|
1088
753
|
try {
|
|
1089
|
-
await this.
|
|
1090
|
-
operation: 'open',
|
|
1091
|
-
device,
|
|
1092
|
-
path,
|
|
1093
|
-
task: () => device.open(),
|
|
1094
|
-
});
|
|
754
|
+
await this.runHandleOperation(() => device.open());
|
|
1095
755
|
} catch (error) {
|
|
1096
756
|
return await this.throwWebUsbIoError(path, 'open', error);
|
|
1097
757
|
} finally {
|
|
@@ -1104,34 +764,19 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
1104
764
|
!device.configuration ||
|
|
1105
765
|
device.configuration.configurationValue !== this.configurationId
|
|
1106
766
|
) {
|
|
1107
|
-
await this.
|
|
1108
|
-
operation: 'selectConfiguration',
|
|
1109
|
-
device,
|
|
1110
|
-
path,
|
|
1111
|
-
task: () => device.selectConfiguration(this.configurationId),
|
|
1112
|
-
});
|
|
767
|
+
await this.runHandleOperation(() => device.selectConfiguration(this.configurationId));
|
|
1113
768
|
}
|
|
1114
769
|
|
|
1115
770
|
// Discover endpoints from USB descriptors; descriptors are not used for protocol selection.
|
|
1116
771
|
const endpoints = this.discoverEndpoints(device);
|
|
1117
772
|
this.deviceEndpoints.set(path, endpoints);
|
|
1118
|
-
await this.claimDeviceInterface(path, device, endpoints);
|
|
1119
|
-
await this.clearEndpointHalt(path, device, 'in', endpoints.endpointIn);
|
|
1120
|
-
await this.clearEndpointHalt(path, device, 'out', endpoints.endpointOut);
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
private async claimDeviceInterface(path: string, device: USBDevice, endpoints: DeviceEndpoints) {
|
|
1124
773
|
try {
|
|
1125
|
-
await this.
|
|
1126
|
-
operation: 'claimInterface',
|
|
1127
|
-
device,
|
|
1128
|
-
path,
|
|
1129
|
-
interfaceNumber: endpoints.interfaceNumber,
|
|
1130
|
-
task: () => device.claimInterface(endpoints.interfaceNumber),
|
|
1131
|
-
});
|
|
774
|
+
await this.runHandleOperation(() => device.claimInterface(endpoints.interfaceNumber));
|
|
1132
775
|
} catch (error) {
|
|
1133
776
|
return this.throwWebUsbIoError(path, 'claimInterface', error);
|
|
1134
777
|
}
|
|
778
|
+
await this.clearEndpointHalt(device, 'in', endpoints.endpointIn);
|
|
779
|
+
await this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
|
|
1135
780
|
}
|
|
1136
781
|
|
|
1137
782
|
private assertHandleAvailable(device: USBDevice) {
|
|
@@ -1155,9 +800,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
1155
800
|
private releaseNormalHandles(path: string) {
|
|
1156
801
|
if (this.stopped) return;
|
|
1157
802
|
for (const [device, ownedPath] of this.normalDevices) {
|
|
1158
|
-
if (ownedPath === path
|
|
1159
|
-
// Do not expose a native handle to another transport while close failed.
|
|
1160
|
-
// A later release/stop can retry cleanup through normalDevices.
|
|
803
|
+
if (ownedPath === path) {
|
|
1161
804
|
normalHandleOwners.delete(device);
|
|
1162
805
|
this.normalDevices.delete(device);
|
|
1163
806
|
}
|
|
@@ -1201,83 +844,36 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
1201
844
|
|
|
1202
845
|
private async closeOpenDevice(path: string) {
|
|
1203
846
|
if (this.stopped) return;
|
|
1204
|
-
const
|
|
1205
|
-
?
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
: [...this.normalDevices]
|
|
1209
|
-
.filter(([, ownedPath]) => ownedPath === path)
|
|
1210
|
-
.map(([device]) => device);
|
|
1211
|
-
let firstCloseError: unknown;
|
|
1212
|
-
for (const device of devices) {
|
|
1213
|
-
try {
|
|
1214
|
-
await this.closeDeviceHandle(path, device);
|
|
1215
|
-
} catch (error) {
|
|
1216
|
-
firstCloseError ??= error;
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
if (firstCloseError) {
|
|
1220
|
-
throw firstCloseError instanceof Error
|
|
1221
|
-
? firstCloseError
|
|
1222
|
-
: new Error(this.getErrorMessage(firstCloseError));
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
private async closeDeviceHandle(path: string, current: USBDevice) {
|
|
1227
|
-
if (!current.opened) return;
|
|
847
|
+
const current = this.identityProbe
|
|
848
|
+
? this.identityProbeDevices.get(path)
|
|
849
|
+
: this.deviceList.find(device => device.path === path)?.device;
|
|
850
|
+
if (!current?.opened) return;
|
|
1228
851
|
this.assertHandleAvailable(current);
|
|
1229
852
|
|
|
1230
853
|
const endpoints = this.deviceEndpoints.get(path);
|
|
1231
854
|
const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
|
|
1232
|
-
|
|
1233
|
-
this.
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
'interface-not-claimed',
|
|
1237
|
-
ifaceNum
|
|
1238
|
-
);
|
|
1239
|
-
} else {
|
|
1240
|
-
try {
|
|
1241
|
-
await this.runLoggedHandleOperation({
|
|
1242
|
-
operation: 'releaseInterface',
|
|
1243
|
-
device: current,
|
|
1244
|
-
path,
|
|
1245
|
-
interfaceNumber: ifaceNum,
|
|
1246
|
-
task: () => current.releaseInterface(ifaceNum),
|
|
1247
|
-
});
|
|
1248
|
-
} catch (error) {
|
|
1249
|
-
this.Log?.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
|
|
1250
|
-
}
|
|
855
|
+
try {
|
|
856
|
+
await this.runHandleOperation(() => current.releaseInterface(ifaceNum));
|
|
857
|
+
} catch (error) {
|
|
858
|
+
this.Log?.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
|
|
1251
859
|
}
|
|
1252
860
|
this.assertHandleAvailable(current);
|
|
1253
861
|
try {
|
|
1254
|
-
await this.
|
|
1255
|
-
operation: 'close',
|
|
1256
|
-
device: current,
|
|
1257
|
-
path,
|
|
1258
|
-
task: () => current.close(),
|
|
1259
|
-
});
|
|
862
|
+
await this.runHandleOperation(() => current.close());
|
|
1260
863
|
} catch (error) {
|
|
1261
864
|
this.Log?.debug('[WebUsbTransport] close before reconnect failed:', error);
|
|
1262
|
-
throw error;
|
|
865
|
+
if (this.identityProbe) throw error;
|
|
1263
866
|
}
|
|
1264
867
|
}
|
|
1265
868
|
|
|
1266
869
|
private async clearEndpointHalt(
|
|
1267
|
-
path: string,
|
|
1268
870
|
device: USBDevice,
|
|
1269
871
|
direction: USBDirection,
|
|
1270
872
|
endpointNumber: number
|
|
1271
873
|
) {
|
|
1272
874
|
this.assertHandleAvailable(device);
|
|
1273
875
|
try {
|
|
1274
|
-
await this.
|
|
1275
|
-
operation: direction === 'in' ? 'clearHaltIn' : 'clearHaltOut',
|
|
1276
|
-
device,
|
|
1277
|
-
path,
|
|
1278
|
-
endpointNumber,
|
|
1279
|
-
task: () => device.clearHalt(direction, endpointNumber),
|
|
1280
|
-
});
|
|
876
|
+
await this.runHandleOperation(() => device.clearHalt(direction, endpointNumber));
|
|
1281
877
|
} catch (error) {
|
|
1282
878
|
this.assertRunning();
|
|
1283
879
|
this.Log?.debug(
|
|
@@ -1434,33 +1030,13 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
1434
1030
|
if (currentDevice.opened) {
|
|
1435
1031
|
const endpoints = this.deviceEndpoints.get(path);
|
|
1436
1032
|
const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
|
|
1437
|
-
|
|
1438
|
-
this.
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
'interface-not-claimed',
|
|
1442
|
-
ifaceNum
|
|
1443
|
-
);
|
|
1444
|
-
} else {
|
|
1445
|
-
try {
|
|
1446
|
-
await this.runLoggedHandleOperation({
|
|
1447
|
-
operation: 'releaseInterface',
|
|
1448
|
-
device: currentDevice,
|
|
1449
|
-
path,
|
|
1450
|
-
interfaceNumber: ifaceNum,
|
|
1451
|
-
task: () => currentDevice.releaseInterface(ifaceNum),
|
|
1452
|
-
});
|
|
1453
|
-
} catch (releaseError) {
|
|
1454
|
-
this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
|
|
1455
|
-
}
|
|
1033
|
+
try {
|
|
1034
|
+
await this.runHandleOperation(() => currentDevice.releaseInterface(ifaceNum));
|
|
1035
|
+
} catch (releaseError) {
|
|
1036
|
+
this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
|
|
1456
1037
|
}
|
|
1457
1038
|
this.assertHandleAvailable(currentDevice);
|
|
1458
|
-
await this.
|
|
1459
|
-
operation: 'close',
|
|
1460
|
-
device: currentDevice,
|
|
1461
|
-
path,
|
|
1462
|
-
task: () => currentDevice.close(),
|
|
1463
|
-
});
|
|
1039
|
+
await this.runHandleOperation(() => currentDevice.close());
|
|
1464
1040
|
}
|
|
1465
1041
|
} catch (closeError) {
|
|
1466
1042
|
this.assertRunning();
|
|
@@ -1825,23 +1401,10 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
1825
1401
|
* Release device
|
|
1826
1402
|
*/
|
|
1827
1403
|
async release(path: string) {
|
|
1828
|
-
const startedAt = Date.now();
|
|
1829
|
-
this.logLifecycle('release', { phase: 'start' });
|
|
1830
1404
|
this.acquiredPaths.delete(path);
|
|
1831
1405
|
try {
|
|
1832
1406
|
await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
|
|
1833
1407
|
await this.closeOpenDevice(path);
|
|
1834
|
-
this.logLifecycle('release', {
|
|
1835
|
-
phase: 'success',
|
|
1836
|
-
durationMs: Date.now() - startedAt,
|
|
1837
|
-
});
|
|
1838
|
-
} catch (error) {
|
|
1839
|
-
this.logLifecycle('release', {
|
|
1840
|
-
phase: 'error',
|
|
1841
|
-
durationMs: Date.now() - startedAt,
|
|
1842
|
-
...this.getDiagnosticError(error, path),
|
|
1843
|
-
});
|
|
1844
|
-
throw error;
|
|
1845
1408
|
} finally {
|
|
1846
1409
|
this.releaseIdentityProbeHandle(path);
|
|
1847
1410
|
this.releaseNormalHandles(path);
|