@onekeyfe/hd-transport-web-device 1.2.8-alpha.102 → 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-protocol-cache.test.ts +1 -197
- package/dist/index.d.ts +2 -20
- package/dist/index.js +65 -377
- 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 -496
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,65 +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
|
-
const legacyEndpoints = {
|
|
1055
|
-
interfaceNumber: this.interfaceId,
|
|
1056
|
-
endpointIn: this.endpointId,
|
|
1057
|
-
endpointOut: this.endpointId,
|
|
1058
|
-
};
|
|
1059
|
-
this.deviceEndpoints.set(path, legacyEndpoints);
|
|
1060
|
-
await this.claimDeviceInterface(path, device, legacyEndpoints);
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
732
|
// A V1 packet retry continues an in-flight exchange. Preserve its endpoint
|
|
1065
733
|
// state as in the legacy transport; probing and V2 recovery still reset.
|
|
1066
734
|
if (first || this.deviceProtocol.get(path) !== 'V1') {
|
|
1067
735
|
try {
|
|
1068
|
-
await this.
|
|
1069
|
-
|
|
1070
|
-
device,
|
|
1071
|
-
path,
|
|
1072
|
-
task: () => device.reset(),
|
|
1073
|
-
});
|
|
1074
|
-
} catch {
|
|
736
|
+
await this.runHandleOperation(() => device.reset());
|
|
737
|
+
} catch (error) {
|
|
1075
738
|
this.assertRunning();
|
|
1076
|
-
|
|
739
|
+
this.Log?.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
|
|
1077
740
|
}
|
|
1078
741
|
}
|
|
1079
742
|
await this.getConnectedDevices();
|
|
@@ -1088,12 +751,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
1088
751
|
if (!device.opened) {
|
|
1089
752
|
openingHandles.add(device);
|
|
1090
753
|
try {
|
|
1091
|
-
await this.
|
|
1092
|
-
operation: 'open',
|
|
1093
|
-
device,
|
|
1094
|
-
path,
|
|
1095
|
-
task: () => device.open(),
|
|
1096
|
-
});
|
|
754
|
+
await this.runHandleOperation(() => device.open());
|
|
1097
755
|
} catch (error) {
|
|
1098
756
|
return await this.throwWebUsbIoError(path, 'open', error);
|
|
1099
757
|
} finally {
|
|
@@ -1106,34 +764,19 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
1106
764
|
!device.configuration ||
|
|
1107
765
|
device.configuration.configurationValue !== this.configurationId
|
|
1108
766
|
) {
|
|
1109
|
-
await this.
|
|
1110
|
-
operation: 'selectConfiguration',
|
|
1111
|
-
device,
|
|
1112
|
-
path,
|
|
1113
|
-
task: () => device.selectConfiguration(this.configurationId),
|
|
1114
|
-
});
|
|
767
|
+
await this.runHandleOperation(() => device.selectConfiguration(this.configurationId));
|
|
1115
768
|
}
|
|
1116
769
|
|
|
1117
770
|
// Discover endpoints from USB descriptors; descriptors are not used for protocol selection.
|
|
1118
771
|
const endpoints = this.discoverEndpoints(device);
|
|
1119
772
|
this.deviceEndpoints.set(path, endpoints);
|
|
1120
|
-
await this.claimDeviceInterface(path, device, endpoints);
|
|
1121
|
-
await this.clearEndpointHalt(path, device, 'in', endpoints.endpointIn);
|
|
1122
|
-
await this.clearEndpointHalt(path, device, 'out', endpoints.endpointOut);
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
private async claimDeviceInterface(path: string, device: USBDevice, endpoints: DeviceEndpoints) {
|
|
1126
773
|
try {
|
|
1127
|
-
await this.
|
|
1128
|
-
operation: 'claimInterface',
|
|
1129
|
-
device,
|
|
1130
|
-
path,
|
|
1131
|
-
interfaceNumber: endpoints.interfaceNumber,
|
|
1132
|
-
task: () => device.claimInterface(endpoints.interfaceNumber),
|
|
1133
|
-
});
|
|
774
|
+
await this.runHandleOperation(() => device.claimInterface(endpoints.interfaceNumber));
|
|
1134
775
|
} catch (error) {
|
|
1135
776
|
return this.throwWebUsbIoError(path, 'claimInterface', error);
|
|
1136
777
|
}
|
|
778
|
+
await this.clearEndpointHalt(device, 'in', endpoints.endpointIn);
|
|
779
|
+
await this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
|
|
1137
780
|
}
|
|
1138
781
|
|
|
1139
782
|
private assertHandleAvailable(device: USBDevice) {
|
|
@@ -1157,9 +800,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
1157
800
|
private releaseNormalHandles(path: string) {
|
|
1158
801
|
if (this.stopped) return;
|
|
1159
802
|
for (const [device, ownedPath] of this.normalDevices) {
|
|
1160
|
-
if (ownedPath === path
|
|
1161
|
-
// Do not expose a native handle to another transport while close failed.
|
|
1162
|
-
// A later release/stop can retry cleanup through normalDevices.
|
|
803
|
+
if (ownedPath === path) {
|
|
1163
804
|
normalHandleOwners.delete(device);
|
|
1164
805
|
this.normalDevices.delete(device);
|
|
1165
806
|
}
|
|
@@ -1203,83 +844,36 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
1203
844
|
|
|
1204
845
|
private async closeOpenDevice(path: string) {
|
|
1205
846
|
if (this.stopped) return;
|
|
1206
|
-
const
|
|
1207
|
-
?
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
: [...this.normalDevices]
|
|
1211
|
-
.filter(([, ownedPath]) => ownedPath === path)
|
|
1212
|
-
.map(([device]) => device);
|
|
1213
|
-
let firstCloseError: unknown;
|
|
1214
|
-
for (const device of devices) {
|
|
1215
|
-
try {
|
|
1216
|
-
await this.closeDeviceHandle(path, device);
|
|
1217
|
-
} catch (error) {
|
|
1218
|
-
firstCloseError ??= error;
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
if (firstCloseError) {
|
|
1222
|
-
throw firstCloseError instanceof Error
|
|
1223
|
-
? firstCloseError
|
|
1224
|
-
: new Error(this.getErrorMessage(firstCloseError));
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
private async closeDeviceHandle(path: string, current: USBDevice) {
|
|
1229
|
-
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;
|
|
1230
851
|
this.assertHandleAvailable(current);
|
|
1231
852
|
|
|
1232
853
|
const endpoints = this.deviceEndpoints.get(path);
|
|
1233
854
|
const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
|
|
1234
|
-
|
|
1235
|
-
this.
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
'interface-not-claimed',
|
|
1239
|
-
ifaceNum
|
|
1240
|
-
);
|
|
1241
|
-
} else {
|
|
1242
|
-
try {
|
|
1243
|
-
await this.runLoggedHandleOperation({
|
|
1244
|
-
operation: 'releaseInterface',
|
|
1245
|
-
device: current,
|
|
1246
|
-
path,
|
|
1247
|
-
interfaceNumber: ifaceNum,
|
|
1248
|
-
task: () => current.releaseInterface(ifaceNum),
|
|
1249
|
-
});
|
|
1250
|
-
} catch (error) {
|
|
1251
|
-
this.Log?.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
|
|
1252
|
-
}
|
|
855
|
+
try {
|
|
856
|
+
await this.runHandleOperation(() => current.releaseInterface(ifaceNum));
|
|
857
|
+
} catch (error) {
|
|
858
|
+
this.Log?.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
|
|
1253
859
|
}
|
|
1254
860
|
this.assertHandleAvailable(current);
|
|
1255
861
|
try {
|
|
1256
|
-
await this.
|
|
1257
|
-
operation: 'close',
|
|
1258
|
-
device: current,
|
|
1259
|
-
path,
|
|
1260
|
-
task: () => current.close(),
|
|
1261
|
-
});
|
|
862
|
+
await this.runHandleOperation(() => current.close());
|
|
1262
863
|
} catch (error) {
|
|
1263
864
|
this.Log?.debug('[WebUsbTransport] close before reconnect failed:', error);
|
|
1264
|
-
throw error;
|
|
865
|
+
if (this.identityProbe) throw error;
|
|
1265
866
|
}
|
|
1266
867
|
}
|
|
1267
868
|
|
|
1268
869
|
private async clearEndpointHalt(
|
|
1269
|
-
path: string,
|
|
1270
870
|
device: USBDevice,
|
|
1271
871
|
direction: USBDirection,
|
|
1272
872
|
endpointNumber: number
|
|
1273
873
|
) {
|
|
1274
874
|
this.assertHandleAvailable(device);
|
|
1275
875
|
try {
|
|
1276
|
-
await this.
|
|
1277
|
-
operation: direction === 'in' ? 'clearHaltIn' : 'clearHaltOut',
|
|
1278
|
-
device,
|
|
1279
|
-
path,
|
|
1280
|
-
endpointNumber,
|
|
1281
|
-
task: () => device.clearHalt(direction, endpointNumber),
|
|
1282
|
-
});
|
|
876
|
+
await this.runHandleOperation(() => device.clearHalt(direction, endpointNumber));
|
|
1283
877
|
} catch (error) {
|
|
1284
878
|
this.assertRunning();
|
|
1285
879
|
this.Log?.debug(
|
|
@@ -1436,33 +1030,13 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
1436
1030
|
if (currentDevice.opened) {
|
|
1437
1031
|
const endpoints = this.deviceEndpoints.get(path);
|
|
1438
1032
|
const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
|
|
1439
|
-
|
|
1440
|
-
this.
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
'interface-not-claimed',
|
|
1444
|
-
ifaceNum
|
|
1445
|
-
);
|
|
1446
|
-
} else {
|
|
1447
|
-
try {
|
|
1448
|
-
await this.runLoggedHandleOperation({
|
|
1449
|
-
operation: 'releaseInterface',
|
|
1450
|
-
device: currentDevice,
|
|
1451
|
-
path,
|
|
1452
|
-
interfaceNumber: ifaceNum,
|
|
1453
|
-
task: () => currentDevice.releaseInterface(ifaceNum),
|
|
1454
|
-
});
|
|
1455
|
-
} catch (releaseError) {
|
|
1456
|
-
this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
|
|
1457
|
-
}
|
|
1033
|
+
try {
|
|
1034
|
+
await this.runHandleOperation(() => currentDevice.releaseInterface(ifaceNum));
|
|
1035
|
+
} catch (releaseError) {
|
|
1036
|
+
this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
|
|
1458
1037
|
}
|
|
1459
1038
|
this.assertHandleAvailable(currentDevice);
|
|
1460
|
-
await this.
|
|
1461
|
-
operation: 'close',
|
|
1462
|
-
device: currentDevice,
|
|
1463
|
-
path,
|
|
1464
|
-
task: () => currentDevice.close(),
|
|
1465
|
-
});
|
|
1039
|
+
await this.runHandleOperation(() => currentDevice.close());
|
|
1466
1040
|
}
|
|
1467
1041
|
} catch (closeError) {
|
|
1468
1042
|
this.assertRunning();
|
|
@@ -1827,23 +1401,10 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
1827
1401
|
* Release device
|
|
1828
1402
|
*/
|
|
1829
1403
|
async release(path: string) {
|
|
1830
|
-
const startedAt = Date.now();
|
|
1831
|
-
this.logLifecycle('release', { phase: 'start' });
|
|
1832
1404
|
this.acquiredPaths.delete(path);
|
|
1833
1405
|
try {
|
|
1834
1406
|
await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
|
|
1835
1407
|
await this.closeOpenDevice(path);
|
|
1836
|
-
this.logLifecycle('release', {
|
|
1837
|
-
phase: 'success',
|
|
1838
|
-
durationMs: Date.now() - startedAt,
|
|
1839
|
-
});
|
|
1840
|
-
} catch (error) {
|
|
1841
|
-
this.logLifecycle('release', {
|
|
1842
|
-
phase: 'error',
|
|
1843
|
-
durationMs: Date.now() - startedAt,
|
|
1844
|
-
...this.getDiagnosticError(error, path),
|
|
1845
|
-
});
|
|
1846
|
-
throw error;
|
|
1847
1408
|
} finally {
|
|
1848
1409
|
this.releaseIdentityProbeHandle(path);
|
|
1849
1410
|
this.releaseNormalHandles(path);
|