@onekeyfe/hd-transport-web-device 1.2.8-alpha.15 → 1.2.8-alpha.16
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 +321 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +67 -8
- package/dist/webusb.d.ts +5 -0
- package/dist/webusb.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/webusb.ts +73 -8
|
@@ -1,7 +1,37 @@
|
|
|
1
|
+
import { ERRORS, HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
1
3
|
import WebUsbTransport from '../src/webusb';
|
|
4
|
+
import { probeUsbDeviceIdentities } from '@onekeyfe/hd-common-connect-sdk/src';
|
|
2
5
|
|
|
3
6
|
import type { MessageFromOneKey, TransportCallOptions } from '@onekeyfe/hd-transport';
|
|
4
7
|
|
|
8
|
+
jest.mock('@onekeyfe/hd-core', () => jest.requireActual('../../core/src'));
|
|
9
|
+
jest.mock('../../core/src/data/config', () => ({
|
|
10
|
+
getSDKVersion: () => '1.0.0-test',
|
|
11
|
+
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0-test/',
|
|
12
|
+
}));
|
|
13
|
+
jest.mock('@onekeyfe/hd-transport-web-device', () => ({
|
|
14
|
+
WebUsbTransport: jest.requireActual('../src/webusb').default,
|
|
15
|
+
}));
|
|
16
|
+
jest.mock('@onekeyfe/hd-transport-http', () => ({ __esModule: true, default: jest.fn() }));
|
|
17
|
+
jest.mock('@onekeyfe/hd-transport-lowlevel', () => ({}));
|
|
18
|
+
jest.mock('@onekeyfe/hd-transport-emulator', () => ({}));
|
|
19
|
+
|
|
20
|
+
type PacketIoTransport = {
|
|
21
|
+
rotateProtocolV2UsbGeneration: () => Promise<void>;
|
|
22
|
+
detectProtocol: () => Promise<'V1'>;
|
|
23
|
+
transferInWithRetry: (path: string, length: number) => Promise<DataView>;
|
|
24
|
+
transferInOnce: (path: string, length: number) => Promise<DataView>;
|
|
25
|
+
transferOutWithRetry: (path: string, packet: Uint8Array) => Promise<void>;
|
|
26
|
+
transferOutOnce: (path: string, packet: Uint8Array) => Promise<void>;
|
|
27
|
+
reconnectForPacketIoRetry: (
|
|
28
|
+
path: string,
|
|
29
|
+
direction: 'in' | 'out',
|
|
30
|
+
attempt: number,
|
|
31
|
+
error: unknown
|
|
32
|
+
) => Promise<void>;
|
|
33
|
+
};
|
|
34
|
+
|
|
5
35
|
describe('WebUSB identity probe ownership', () => {
|
|
6
36
|
let originalNavigator: PropertyDescriptor | undefined;
|
|
7
37
|
const device = {
|
|
@@ -80,6 +110,7 @@ describe('WebUSB identity probe ownership', () => {
|
|
|
80
110
|
expect(device.close).not.toHaveBeenCalled();
|
|
81
111
|
finishOpen();
|
|
82
112
|
await connecting;
|
|
113
|
+
await active.release('USB_PATH');
|
|
83
114
|
});
|
|
84
115
|
|
|
85
116
|
test('a normal connection cannot close an isolated probe handle and release closes the captured handle after re-enumeration', async () => {
|
|
@@ -105,8 +136,10 @@ describe('WebUSB identity probe ownership', () => {
|
|
|
105
136
|
const replacement = { ...device, close: jest.fn() };
|
|
106
137
|
usb.getDevices.mockResolvedValue([replacement]);
|
|
107
138
|
await probe.getConnectedDevices();
|
|
108
|
-
|
|
109
|
-
expect(
|
|
139
|
+
device.close.mockRejectedValueOnce(new Error('native close failed'));
|
|
140
|
+
await expect(probe.release('USB_PATH')).rejects.toThrow('native close failed');
|
|
141
|
+
await probe.enumerate();
|
|
142
|
+
expect(device.close).toHaveBeenCalledTimes(2);
|
|
110
143
|
expect(replacement.close).not.toHaveBeenCalled();
|
|
111
144
|
});
|
|
112
145
|
|
|
@@ -175,6 +208,292 @@ describe('WebUSB identity probe ownership', () => {
|
|
|
175
208
|
}
|
|
176
209
|
);
|
|
177
210
|
|
|
211
|
+
test.each(['release', 'acquire failure'])(
|
|
212
|
+
'the public probe retries failed %s cleanup on its next call',
|
|
213
|
+
async phase => {
|
|
214
|
+
const internals = WebUsbTransport.prototype as unknown as {
|
|
215
|
+
rotateProtocolV2UsbGeneration: () => Promise<void>;
|
|
216
|
+
detectProtocol: () => Promise<'V1'>;
|
|
217
|
+
};
|
|
218
|
+
jest.spyOn(internals, 'rotateProtocolV2UsbGeneration').mockResolvedValue(undefined);
|
|
219
|
+
jest.spyOn(internals, 'detectProtocol').mockResolvedValue('V1');
|
|
220
|
+
jest.spyOn(WebUsbTransport.prototype, 'getProtocolType').mockReturnValue('V1');
|
|
221
|
+
jest.spyOn(WebUsbTransport.prototype, 'configure').mockImplementation(() => undefined);
|
|
222
|
+
jest
|
|
223
|
+
.spyOn(WebUsbTransport.prototype, 'configureProtocolV2')
|
|
224
|
+
.mockImplementation(() => undefined);
|
|
225
|
+
const connect = jest.spyOn(WebUsbTransport.prototype, 'connect').mockImplementation(() => {
|
|
226
|
+
device.opened = true;
|
|
227
|
+
return Promise.resolve();
|
|
228
|
+
});
|
|
229
|
+
if (phase === 'acquire failure') {
|
|
230
|
+
connect.mockImplementationOnce(() => {
|
|
231
|
+
device.opened = true;
|
|
232
|
+
return Promise.reject(new Error('probe failed'));
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
jest.spyOn(WebUsbTransport.prototype, 'call').mockResolvedValue({
|
|
236
|
+
type: 'Features',
|
|
237
|
+
message: { onekey_serial_no: 'PHYSICAL_SERIAL', onekey_device_type: 'CLASSIC1S' },
|
|
238
|
+
});
|
|
239
|
+
device.close.mockRejectedValueOnce(new Error('native close failed'));
|
|
240
|
+
try {
|
|
241
|
+
await expect(probeUsbDeviceIdentities({ commType: 'webusb' })).resolves.toEqual([]);
|
|
242
|
+
expect(device.opened).toBe(true);
|
|
243
|
+
const active = new WebUsbTransport();
|
|
244
|
+
active.init({ debug: jest.fn() });
|
|
245
|
+
await expect(active.connectToDevice('USB_PATH', true)).rejects.toThrow(
|
|
246
|
+
'USB device is already in use'
|
|
247
|
+
);
|
|
248
|
+
device.close.mockRejectedValueOnce(new Error('native close still failing'));
|
|
249
|
+
await expect(probeUsbDeviceIdentities({ commType: 'webusb' })).resolves.toEqual([]);
|
|
250
|
+
expect(device.opened).toBe(true);
|
|
251
|
+
expect(connect).toHaveBeenCalledTimes(1);
|
|
252
|
+
expect(device.close).toHaveBeenCalledTimes(2);
|
|
253
|
+
await expect(probeUsbDeviceIdentities({ commType: 'webusb' })).resolves.toMatchObject([
|
|
254
|
+
{ path: 'USB_PATH', serialNo: 'PHYSICAL_SERIAL' },
|
|
255
|
+
]);
|
|
256
|
+
expect(connect).toHaveBeenCalledTimes(2);
|
|
257
|
+
expect(device.opened).toBe(false);
|
|
258
|
+
} finally {
|
|
259
|
+
jest.restoreAllMocks();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
test.each(['in', 'out'] as const)(
|
|
265
|
+
'the public probe cannot take over a first acquire while transfer%s recovery has closed the handle',
|
|
266
|
+
async direction => {
|
|
267
|
+
const closed = createDeferred<void>();
|
|
268
|
+
const resume = createDeferred<void>();
|
|
269
|
+
Object.assign(device, {
|
|
270
|
+
open: jest.fn(() => {
|
|
271
|
+
device.opened = true;
|
|
272
|
+
return Promise.resolve();
|
|
273
|
+
}),
|
|
274
|
+
releaseInterface: jest.fn(),
|
|
275
|
+
reset: jest.fn(),
|
|
276
|
+
configurations: [],
|
|
277
|
+
configuration: { configurationValue: 1 },
|
|
278
|
+
selectConfiguration: jest.fn(),
|
|
279
|
+
claimInterface: jest.fn(),
|
|
280
|
+
clearHalt: jest.fn(),
|
|
281
|
+
transferIn: jest
|
|
282
|
+
.fn()
|
|
283
|
+
.mockResolvedValue({ status: 'ok', data: new DataView(new ArrayBuffer(64)) }),
|
|
284
|
+
transferOut: jest.fn().mockResolvedValue({ status: 'ok' }),
|
|
285
|
+
});
|
|
286
|
+
const native = device as typeof device & { transferIn: jest.Mock; transferOut: jest.Mock };
|
|
287
|
+
native[direction === 'in' ? 'transferIn' : 'transferOut'].mockRejectedValueOnce(
|
|
288
|
+
new Error(`transfer${direction} NetworkError`)
|
|
289
|
+
);
|
|
290
|
+
device.close.mockImplementationOnce(async () => {
|
|
291
|
+
device.opened = false;
|
|
292
|
+
closed.resolve();
|
|
293
|
+
await resume.promise;
|
|
294
|
+
});
|
|
295
|
+
const active = new WebUsbTransport();
|
|
296
|
+
active.init({ debug: jest.fn() });
|
|
297
|
+
const internals = active as unknown as PacketIoTransport;
|
|
298
|
+
jest
|
|
299
|
+
.spyOn(
|
|
300
|
+
WebUsbTransport.prototype as unknown as PacketIoTransport,
|
|
301
|
+
'rotateProtocolV2UsbGeneration'
|
|
302
|
+
)
|
|
303
|
+
.mockResolvedValue(undefined);
|
|
304
|
+
jest
|
|
305
|
+
.spyOn(WebUsbTransport.prototype as unknown as PacketIoTransport, 'detectProtocol')
|
|
306
|
+
.mockImplementation(async function (this: WebUsbTransport) {
|
|
307
|
+
if (this !== active) return 'V1';
|
|
308
|
+
if (direction === 'in') await internals.transferInWithRetry('USB_PATH', 64);
|
|
309
|
+
else await internals.transferOutWithRetry('USB_PATH', new Uint8Array([1]));
|
|
310
|
+
return 'V1';
|
|
311
|
+
});
|
|
312
|
+
jest.spyOn(WebUsbTransport.prototype, 'getProtocolType').mockReturnValue('V1');
|
|
313
|
+
jest.spyOn(WebUsbTransport.prototype, 'configure').mockImplementation(() => undefined);
|
|
314
|
+
jest
|
|
315
|
+
.spyOn(WebUsbTransport.prototype, 'configureProtocolV2')
|
|
316
|
+
.mockImplementation(() => undefined);
|
|
317
|
+
jest
|
|
318
|
+
.spyOn(WebUsbTransport.prototype, 'connect')
|
|
319
|
+
.mockImplementation(async function (this: WebUsbTransport, path, first) {
|
|
320
|
+
if (this === active && !first) return this.connectToDevice(path, first);
|
|
321
|
+
device.opened = true;
|
|
322
|
+
});
|
|
323
|
+
jest.spyOn(WebUsbTransport.prototype, 'call').mockResolvedValue({
|
|
324
|
+
type: 'Features',
|
|
325
|
+
message: { onekey_serial_no: 'PHYSICAL_SERIAL', onekey_device_type: 'CLASSIC1S' },
|
|
326
|
+
});
|
|
327
|
+
try {
|
|
328
|
+
const acquiring = active.acquire({ path: 'USB_PATH' });
|
|
329
|
+
try {
|
|
330
|
+
await closed.promise;
|
|
331
|
+
await expect(probeUsbDeviceIdentities({ commType: 'webusb' })).resolves.toEqual([]);
|
|
332
|
+
resume.resolve();
|
|
333
|
+
await expect(acquiring).resolves.toBe('USB_PATH');
|
|
334
|
+
} finally {
|
|
335
|
+
resume.resolve();
|
|
336
|
+
await acquiring.catch(() => undefined);
|
|
337
|
+
await active.release('USB_PATH');
|
|
338
|
+
}
|
|
339
|
+
await expect(probeUsbDeviceIdentities({ commType: 'webusb' })).resolves.toMatchObject([
|
|
340
|
+
{ serialNo: 'PHYSICAL_SERIAL' },
|
|
341
|
+
]);
|
|
342
|
+
} finally {
|
|
343
|
+
jest.restoreAllMocks();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
test('releases ownership after a failed normal acquire', async () => {
|
|
349
|
+
const active = new WebUsbTransport();
|
|
350
|
+
const probe = new WebUsbTransport(true);
|
|
351
|
+
active.init({ debug: jest.fn() });
|
|
352
|
+
probe.init({ debug: jest.fn() });
|
|
353
|
+
const state = active as unknown as PacketIoTransport;
|
|
354
|
+
jest.spyOn(state, 'rotateProtocolV2UsbGeneration').mockResolvedValue(undefined);
|
|
355
|
+
jest.spyOn(active, 'connect').mockImplementation(() => {
|
|
356
|
+
device.opened = true;
|
|
357
|
+
return Promise.resolve();
|
|
358
|
+
});
|
|
359
|
+
jest.spyOn(state, 'detectProtocol').mockRejectedValue(new Error('protocol failed'));
|
|
360
|
+
await expect(active.acquire({ path: 'USB_PATH' })).rejects.toThrow('protocol failed');
|
|
361
|
+
expect(device.opened).toBe(false);
|
|
362
|
+
const probeState = probe as unknown as PacketIoTransport;
|
|
363
|
+
jest.spyOn(probeState, 'rotateProtocolV2UsbGeneration').mockResolvedValue(undefined);
|
|
364
|
+
jest.spyOn(probeState, 'detectProtocol').mockResolvedValue('V1');
|
|
365
|
+
jest.spyOn(probe, 'connect').mockImplementation(() => {
|
|
366
|
+
device.opened = true;
|
|
367
|
+
return Promise.resolve();
|
|
368
|
+
});
|
|
369
|
+
try {
|
|
370
|
+
await expect(probe.acquire({ path: 'USB_PATH' })).resolves.toBe('USB_PATH');
|
|
371
|
+
} finally {
|
|
372
|
+
await probe.release('USB_PATH');
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test('releasing one path preserves another closed handle owned by the same transport', async () => {
|
|
377
|
+
const other = {
|
|
378
|
+
...device,
|
|
379
|
+
serialNumber: 'OTHER_PATH',
|
|
380
|
+
opened: false,
|
|
381
|
+
close: jest.fn(() => {
|
|
382
|
+
other.opened = false;
|
|
383
|
+
return Promise.resolve();
|
|
384
|
+
}),
|
|
385
|
+
};
|
|
386
|
+
usb.getDevices.mockResolvedValue([device, other]);
|
|
387
|
+
const active = new WebUsbTransport();
|
|
388
|
+
active.init({ debug: jest.fn() });
|
|
389
|
+
const state = active as unknown as PacketIoTransport;
|
|
390
|
+
jest.spyOn(state, 'rotateProtocolV2UsbGeneration').mockResolvedValue(undefined);
|
|
391
|
+
jest.spyOn(state, 'detectProtocol').mockResolvedValue('V1');
|
|
392
|
+
jest.spyOn(active, 'connect').mockImplementation(path => {
|
|
393
|
+
const target = path === 'USB_PATH' ? device : other;
|
|
394
|
+
target.opened = true;
|
|
395
|
+
return Promise.resolve();
|
|
396
|
+
});
|
|
397
|
+
await active.acquire({ path: 'USB_PATH' });
|
|
398
|
+
await active.acquire({ path: 'OTHER_PATH' });
|
|
399
|
+
await active.release('USB_PATH');
|
|
400
|
+
other.opened = false;
|
|
401
|
+
const probe = new WebUsbTransport(true);
|
|
402
|
+
probe.init({ debug: jest.fn() });
|
|
403
|
+
const probeState = probe as unknown as PacketIoTransport;
|
|
404
|
+
jest.spyOn(probeState, 'rotateProtocolV2UsbGeneration').mockResolvedValue(undefined);
|
|
405
|
+
jest.spyOn(probeState, 'detectProtocol').mockResolvedValue('V1');
|
|
406
|
+
jest.spyOn(probe, 'connect').mockImplementation(path => {
|
|
407
|
+
const target = path === 'USB_PATH' ? device : other;
|
|
408
|
+
target.opened = true;
|
|
409
|
+
return Promise.resolve();
|
|
410
|
+
});
|
|
411
|
+
try {
|
|
412
|
+
await expect(probe.acquire({ path: 'USB_PATH' })).resolves.toBe('USB_PATH');
|
|
413
|
+
await expect(probe.acquire({ path: 'OTHER_PATH' })).rejects.toThrow(
|
|
414
|
+
'USB device is already in use'
|
|
415
|
+
);
|
|
416
|
+
} finally {
|
|
417
|
+
await active.release('OTHER_PATH');
|
|
418
|
+
await probe.release('USB_PATH');
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test.each(['read', 'read retry', 'write', 'write retry', 'retry cleanup'])(
|
|
423
|
+
'blocks native %s while an isolated probe owns the handle',
|
|
424
|
+
async operation => {
|
|
425
|
+
const probe = new WebUsbTransport(true);
|
|
426
|
+
probe.init({ debug: jest.fn() });
|
|
427
|
+
const probeState = probe as unknown as PacketIoTransport;
|
|
428
|
+
jest.spyOn(probeState, 'rotateProtocolV2UsbGeneration').mockResolvedValue(undefined);
|
|
429
|
+
jest.spyOn(probeState, 'detectProtocol').mockResolvedValue('V1');
|
|
430
|
+
jest.spyOn(probe, 'connect').mockImplementation(() => {
|
|
431
|
+
device.opened = true;
|
|
432
|
+
return Promise.resolve();
|
|
433
|
+
});
|
|
434
|
+
const native = Object.assign(device, {
|
|
435
|
+
releaseInterface: jest.fn(),
|
|
436
|
+
transferIn: jest
|
|
437
|
+
.fn()
|
|
438
|
+
.mockResolvedValue({ status: 'ok', data: new DataView(new ArrayBuffer(64)) }),
|
|
439
|
+
transferOut: jest.fn().mockResolvedValue({ status: 'ok' }),
|
|
440
|
+
});
|
|
441
|
+
await probe.acquire({ path: 'USB_PATH' });
|
|
442
|
+
const active = new WebUsbTransport();
|
|
443
|
+
active.init({ debug: jest.fn() });
|
|
444
|
+
const state = active as unknown as PacketIoTransport;
|
|
445
|
+
try {
|
|
446
|
+
let call: Promise<unknown>;
|
|
447
|
+
if (operation === 'read') call = state.transferInOnce('USB_PATH', 64);
|
|
448
|
+
else if (operation === 'read retry') call = state.transferInWithRetry('USB_PATH', 64);
|
|
449
|
+
else if (operation === 'write')
|
|
450
|
+
call = state.transferOutOnce('USB_PATH', new Uint8Array([1]));
|
|
451
|
+
else if (operation === 'write retry')
|
|
452
|
+
call = state.transferOutWithRetry('USB_PATH', new Uint8Array([1]));
|
|
453
|
+
else
|
|
454
|
+
call = state.reconnectForPacketIoRetry(
|
|
455
|
+
'USB_PATH',
|
|
456
|
+
'in',
|
|
457
|
+
0,
|
|
458
|
+
new Error('transferIn failed')
|
|
459
|
+
);
|
|
460
|
+
await expect(call).rejects.toThrow('USB device is already in use');
|
|
461
|
+
expect(native.transferIn).not.toHaveBeenCalled();
|
|
462
|
+
expect(native.transferOut).not.toHaveBeenCalled();
|
|
463
|
+
expect(native.releaseInterface).not.toHaveBeenCalled();
|
|
464
|
+
expect(device.close).not.toHaveBeenCalled();
|
|
465
|
+
} finally {
|
|
466
|
+
await probe.release('USB_PATH');
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
test.each(['in', 'out'] as const)(
|
|
472
|
+
'ends transfer%s immediately if reconnect reports an ownership conflict',
|
|
473
|
+
async direction => {
|
|
474
|
+
const active = new WebUsbTransport();
|
|
475
|
+
active.init({ debug: jest.fn() });
|
|
476
|
+
device.opened = true;
|
|
477
|
+
const native = Object.assign(device, {
|
|
478
|
+
transferIn: jest.fn().mockRejectedValue(new Error('transferIn NetworkError')),
|
|
479
|
+
transferOut: jest.fn().mockRejectedValue(new Error('transferOut NetworkError')),
|
|
480
|
+
});
|
|
481
|
+
const state = active as unknown as PacketIoTransport;
|
|
482
|
+
const conflict = ERRORS.TypedError(
|
|
483
|
+
HardwareErrorCode.RuntimeError,
|
|
484
|
+
'USB device is already in use'
|
|
485
|
+
);
|
|
486
|
+
const reconnect = jest.spyOn(state, 'reconnectForPacketIoRetry').mockRejectedValue(conflict);
|
|
487
|
+
const call =
|
|
488
|
+
direction === 'in'
|
|
489
|
+
? state.transferInWithRetry('USB_PATH', 64)
|
|
490
|
+
: state.transferOutWithRetry('USB_PATH', new Uint8Array([1]));
|
|
491
|
+
await expect(call).rejects.toBe(conflict);
|
|
492
|
+
expect(reconnect).toHaveBeenCalledTimes(1);
|
|
493
|
+
expect(native[direction === 'in' ? 'transferIn' : 'transferOut']).toHaveBeenCalledTimes(1);
|
|
494
|
+
}
|
|
495
|
+
);
|
|
496
|
+
|
|
178
497
|
test.each([false, true])(
|
|
179
498
|
'uses a read-only V1 probe only for isolated discovery (%s)',
|
|
180
499
|
async identityProbe => {
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
|
31
31
|
/** Paths currently acquired (between a successful acquire() and its release()). */
|
|
32
32
|
private acquiredPaths;
|
|
33
33
|
private identityProbeDevices;
|
|
34
|
+
private normalDevices;
|
|
35
|
+
private identityProbeCleanupPaths;
|
|
34
36
|
/**
|
|
35
37
|
* The exact USBDevice object each cached protocol was probed against. The
|
|
36
38
|
* browser returns the same object identity for a device as long as it stays
|
|
@@ -132,6 +134,9 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
|
132
134
|
*/
|
|
133
135
|
connectToDevice(path: string, first: boolean): Promise<undefined>;
|
|
134
136
|
private assertHandleAvailable;
|
|
137
|
+
private claimNormalHandle;
|
|
138
|
+
private releaseNormalHandles;
|
|
139
|
+
private isHandleOwnershipError;
|
|
135
140
|
private claimIdentityProbeHandle;
|
|
136
141
|
private releaseIdentityProbeHandle;
|
|
137
142
|
private closeOpenDevice;
|
package/dist/index.js
CHANGED
|
@@ -51,6 +51,7 @@ const REPORT_ID = transport.PROTOCOL_V1_REPORT_ID;
|
|
|
51
51
|
const HEADER_LENGTH = transport.PROTOCOL_V1_MESSAGE_HEADER_SIZE;
|
|
52
52
|
const PACKET_IO_MAX_RETRIES = 3;
|
|
53
53
|
const PACKET_IO_RETRY_DELAY = 300;
|
|
54
|
+
const USB_DEVICE_IN_USE = 'USB device is already in use';
|
|
54
55
|
const PROTOCOL_V1_PROBE_TIMEOUT = 5000;
|
|
55
56
|
const PROTOCOL_V2_PROBE_TIMEOUT = 1000;
|
|
56
57
|
const EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS = 2;
|
|
@@ -58,6 +59,7 @@ let activeWebUsbTransport;
|
|
|
58
59
|
let usbDisconnectListenerAttached = false;
|
|
59
60
|
const identityProbeHandles = new WeakSet();
|
|
60
61
|
const openingHandles = new WeakSet();
|
|
62
|
+
const normalHandleOwners = new WeakMap();
|
|
61
63
|
function registerActiveWebUsbTransport(instance, usb) {
|
|
62
64
|
activeWebUsbTransport = instance;
|
|
63
65
|
if (usbDisconnectListenerAttached)
|
|
@@ -87,6 +89,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
87
89
|
this.staleProtocolPaths = new Set();
|
|
88
90
|
this.acquiredPaths = new Set();
|
|
89
91
|
this.identityProbeDevices = new Map();
|
|
92
|
+
this.normalDevices = new Map();
|
|
93
|
+
this.identityProbeCleanupPaths = new Set();
|
|
90
94
|
this.probedDeviceObjects = new Map();
|
|
91
95
|
this.deviceEndpoints = new Map();
|
|
92
96
|
this.name = 'WebUsbTransport';
|
|
@@ -155,6 +159,13 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
155
159
|
}
|
|
156
160
|
enumerate() {
|
|
157
161
|
return __awaiter(this, void 0, void 0, function* () {
|
|
162
|
+
for (const path of this.identityProbeCleanupPaths) {
|
|
163
|
+
try {
|
|
164
|
+
yield this.release(path);
|
|
165
|
+
}
|
|
166
|
+
catch (_a) {
|
|
167
|
+
}
|
|
168
|
+
}
|
|
158
169
|
yield this.getConnectedDevices();
|
|
159
170
|
return this.deviceList;
|
|
160
171
|
});
|
|
@@ -196,6 +207,9 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
196
207
|
yield this.getConnectedDevices();
|
|
197
208
|
this.claimIdentityProbeHandle(input.path, yield this.findDevice(input.path));
|
|
198
209
|
}
|
|
210
|
+
else {
|
|
211
|
+
this.claimNormalHandle(input.path, yield this.findDevice(input.path));
|
|
212
|
+
}
|
|
199
213
|
try {
|
|
200
214
|
yield this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
|
|
201
215
|
yield this.closeOpenDevice(input.path);
|
|
@@ -262,6 +276,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
262
276
|
}
|
|
263
277
|
finally {
|
|
264
278
|
this.releaseIdentityProbeHandle(input.path);
|
|
279
|
+
this.releaseNormalHandles(input.path);
|
|
265
280
|
}
|
|
266
281
|
throw e;
|
|
267
282
|
}
|
|
@@ -394,6 +409,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
394
409
|
this.releaseIdentityProbeHandle(path);
|
|
395
410
|
this.claimIdentityProbeHandle(path, device);
|
|
396
411
|
}
|
|
412
|
+
if (!this.identityProbe)
|
|
413
|
+
this.claimNormalHandle(path, device);
|
|
397
414
|
this.assertHandleAvailable(device);
|
|
398
415
|
if (!device.opened) {
|
|
399
416
|
openingHandles.add(device);
|
|
@@ -422,6 +439,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
422
439
|
this.releaseIdentityProbeHandle(path);
|
|
423
440
|
this.claimIdentityProbeHandle(path, device);
|
|
424
441
|
}
|
|
442
|
+
if (!this.identityProbe)
|
|
443
|
+
this.claimNormalHandle(path, device);
|
|
425
444
|
this.assertHandleAvailable(device);
|
|
426
445
|
if (!device.opened) {
|
|
427
446
|
openingHandles.add(device);
|
|
@@ -453,25 +472,51 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
453
472
|
});
|
|
454
473
|
}
|
|
455
474
|
assertHandleAvailable(device) {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
475
|
+
const normalOwner = normalHandleOwners.get(device);
|
|
476
|
+
if ((normalOwner && normalOwner !== this) ||
|
|
477
|
+
(identityProbeHandles.has(device) &&
|
|
478
|
+
![...this.identityProbeDevices.values()].includes(device))) {
|
|
479
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, USB_DEVICE_IN_USE);
|
|
459
480
|
}
|
|
460
481
|
}
|
|
482
|
+
claimNormalHandle(path, device) {
|
|
483
|
+
this.assertHandleAvailable(device);
|
|
484
|
+
normalHandleOwners.set(device, this);
|
|
485
|
+
this.normalDevices.set(device, path);
|
|
486
|
+
}
|
|
487
|
+
releaseNormalHandles(path) {
|
|
488
|
+
for (const [device, ownedPath] of this.normalDevices) {
|
|
489
|
+
if (ownedPath === path) {
|
|
490
|
+
normalHandleOwners.delete(device);
|
|
491
|
+
this.normalDevices.delete(device);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
isHandleOwnershipError(error) {
|
|
496
|
+
return (error instanceof hdShared.HardwareError &&
|
|
497
|
+
error.errorCode === hdShared.HardwareErrorCode.RuntimeError &&
|
|
498
|
+
this.getErrorMessage(error).includes(USB_DEVICE_IN_USE));
|
|
499
|
+
}
|
|
461
500
|
claimIdentityProbeHandle(path, device) {
|
|
462
|
-
if (device.opened ||
|
|
463
|
-
|
|
501
|
+
if (device.opened ||
|
|
502
|
+
openingHandles.has(device) ||
|
|
503
|
+
identityProbeHandles.has(device) ||
|
|
504
|
+
normalHandleOwners.has(device)) {
|
|
505
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, USB_DEVICE_IN_USE);
|
|
464
506
|
}
|
|
465
507
|
identityProbeHandles.add(device);
|
|
466
508
|
this.identityProbeDevices.set(path, device);
|
|
467
509
|
}
|
|
468
510
|
releaseIdentityProbeHandle(path) {
|
|
469
511
|
const device = this.identityProbeDevices.get(path);
|
|
470
|
-
if (device === null || device === void 0 ? void 0 : device.opened)
|
|
512
|
+
if (device === null || device === void 0 ? void 0 : device.opened) {
|
|
513
|
+
this.identityProbeCleanupPaths.add(path);
|
|
471
514
|
return;
|
|
515
|
+
}
|
|
472
516
|
if (device)
|
|
473
517
|
identityProbeHandles.delete(device);
|
|
474
518
|
this.identityProbeDevices.delete(path);
|
|
519
|
+
this.identityProbeCleanupPaths.delete(path);
|
|
475
520
|
}
|
|
476
521
|
closeOpenDevice(path) {
|
|
477
522
|
var _a, _b, _c, _d;
|
|
@@ -490,6 +535,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
490
535
|
catch (error) {
|
|
491
536
|
(_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
|
|
492
537
|
}
|
|
538
|
+
this.assertHandleAvailable(current);
|
|
493
539
|
try {
|
|
494
540
|
yield current.close();
|
|
495
541
|
}
|
|
@@ -503,6 +549,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
503
549
|
clearEndpointHalt(device, direction, endpointNumber) {
|
|
504
550
|
var _a;
|
|
505
551
|
return __awaiter(this, void 0, void 0, function* () {
|
|
552
|
+
this.assertHandleAvailable(device);
|
|
506
553
|
try {
|
|
507
554
|
yield device.clearHalt(direction, endpointNumber);
|
|
508
555
|
}
|
|
@@ -568,7 +615,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
568
615
|
throwWebUsbIoError(path, operation, error) {
|
|
569
616
|
var _a;
|
|
570
617
|
return __awaiter(this, void 0, void 0, function* () {
|
|
571
|
-
if (this.isWebUsbIoHardwareError(error))
|
|
618
|
+
if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error))
|
|
572
619
|
throw error;
|
|
573
620
|
if (error instanceof transport.ProtocolV2LinkError && error.code !== 'io')
|
|
574
621
|
throw error;
|
|
@@ -602,7 +649,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
602
649
|
return /LIBUSB_ERROR_ACCESS|\b(?:access|permission) denied\b/i.test(message);
|
|
603
650
|
}
|
|
604
651
|
isRetryablePacketIoError(error) {
|
|
605
|
-
if (this.isWebUsbIoHardwareError(error))
|
|
652
|
+
if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error))
|
|
606
653
|
return false;
|
|
607
654
|
const message = this.getErrorMessage(error).toLowerCase();
|
|
608
655
|
if (this.isWebUsbPermissionDeniedMessage(message))
|
|
@@ -623,6 +670,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
623
670
|
yield hdShared.wait(attempt * PACKET_IO_RETRY_DELAY);
|
|
624
671
|
try {
|
|
625
672
|
const currentDevice = yield this.findDevice(path);
|
|
673
|
+
this.assertHandleAvailable(currentDevice);
|
|
626
674
|
if (currentDevice.opened) {
|
|
627
675
|
const endpoints = this.deviceEndpoints.get(path);
|
|
628
676
|
const ifaceNum = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.interfaceNumber) !== null && _a !== void 0 ? _a : this.interfaceId;
|
|
@@ -632,10 +680,13 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
632
680
|
catch (releaseError) {
|
|
633
681
|
this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
|
|
634
682
|
}
|
|
683
|
+
this.assertHandleAvailable(currentDevice);
|
|
635
684
|
yield currentDevice.close();
|
|
636
685
|
}
|
|
637
686
|
}
|
|
638
687
|
catch (closeError) {
|
|
688
|
+
if (this.isHandleOwnershipError(closeError))
|
|
689
|
+
throw closeError;
|
|
639
690
|
this.Log.debug('[WebUsbTransport] close device before retry error:', closeError);
|
|
640
691
|
}
|
|
641
692
|
yield this.getConnectedDevices();
|
|
@@ -677,6 +728,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
677
728
|
yield this.reconnectForPacketIoRetry(path, 'out', attempt, error);
|
|
678
729
|
}
|
|
679
730
|
catch (reconnectError) {
|
|
731
|
+
if (this.isHandleOwnershipError(reconnectError))
|
|
732
|
+
throw reconnectError;
|
|
680
733
|
lastError = reconnectError;
|
|
681
734
|
this.Log.debug(`[WebUsbTransport] transferout reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(reconnectError)}`);
|
|
682
735
|
}
|
|
@@ -695,6 +748,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
695
748
|
const endpoints = this.deviceEndpoints.get(path);
|
|
696
749
|
const endpointOut = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.endpointOut) !== null && _a !== void 0 ? _a : this.endpointId;
|
|
697
750
|
const transferBuffer = this.toArrayBuffer(packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength));
|
|
751
|
+
this.assertHandleAvailable(device);
|
|
698
752
|
const result = yield device.transferOut(endpointOut, transferBuffer);
|
|
699
753
|
if (result.status !== 'ok') {
|
|
700
754
|
return this.throwWebUsbIoError(path, 'transferOut', new Error(`transferOut status: ${String(result.status)}`));
|
|
@@ -716,6 +770,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
716
770
|
}
|
|
717
771
|
const endpoints = this.deviceEndpoints.get(path);
|
|
718
772
|
const endpointIn = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.endpointIn) !== null && _a !== void 0 ? _a : this.endpointId;
|
|
773
|
+
this.assertHandleAvailable(device);
|
|
719
774
|
const result = yield device.transferIn(endpointIn, length);
|
|
720
775
|
return this.getTransferInData(result);
|
|
721
776
|
}
|
|
@@ -732,6 +787,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
732
787
|
yield this.reconnectForPacketIoRetry(path, 'in', attempt, error);
|
|
733
788
|
}
|
|
734
789
|
catch (reconnectError) {
|
|
790
|
+
if (this.isHandleOwnershipError(reconnectError))
|
|
791
|
+
throw reconnectError;
|
|
735
792
|
lastError = reconnectError;
|
|
736
793
|
this.Log.debug(`[WebUsbTransport] transferin reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(reconnectError)}`);
|
|
737
794
|
}
|
|
@@ -749,6 +806,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
749
806
|
}
|
|
750
807
|
const endpoints = this.deviceEndpoints.get(path);
|
|
751
808
|
const endpointIn = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.endpointIn) !== null && _a !== void 0 ? _a : this.endpointId;
|
|
809
|
+
this.assertHandleAvailable(device);
|
|
752
810
|
const result = yield device.transferIn(endpointIn, length);
|
|
753
811
|
return this.getTransferInData(result);
|
|
754
812
|
});
|
|
@@ -935,6 +993,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
935
993
|
}
|
|
936
994
|
finally {
|
|
937
995
|
this.releaseIdentityProbeHandle(path);
|
|
996
|
+
this.releaseNormalHandles(path);
|
|
938
997
|
}
|
|
939
998
|
this.deviceEndpoints.delete(path);
|
|
940
999
|
});
|
package/dist/webusb.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
19
19
|
private staleProtocolPaths;
|
|
20
20
|
private acquiredPaths;
|
|
21
21
|
private identityProbeDevices;
|
|
22
|
+
private normalDevices;
|
|
23
|
+
private identityProbeCleanupPaths;
|
|
22
24
|
private probedDeviceObjects;
|
|
23
25
|
private deviceEndpoints;
|
|
24
26
|
name: string;
|
|
@@ -50,6 +52,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
50
52
|
private discoverEndpoints;
|
|
51
53
|
connectToDevice(path: string, first: boolean): Promise<undefined>;
|
|
52
54
|
private assertHandleAvailable;
|
|
55
|
+
private claimNormalHandle;
|
|
56
|
+
private releaseNormalHandles;
|
|
57
|
+
private isHandleOwnershipError;
|
|
53
58
|
private claimIdentityProbeHandle;
|
|
54
59
|
private releaseIdentityProbeHandle;
|
|
55
60
|
private closeOpenDevice;
|
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.16",
|
|
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.16",
|
|
25
|
+
"@onekeyfe/hd-transport": "1.2.8-alpha.16"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@onekeyfe/hd-transport-electron": "1.2.8-alpha.
|
|
28
|
+
"@onekeyfe/hd-transport-electron": "1.2.8-alpha.16",
|
|
29
29
|
"@types/w3c-web-usb": "^1.0.6",
|
|
30
30
|
"@types/web-bluetooth": "^0.0.17"
|
|
31
31
|
},
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "7ccc3f507e2b536e6b5832e1cbb24a5fc1970ebe"
|
|
33
33
|
}
|
package/src/webusb.ts
CHANGED
|
@@ -44,6 +44,7 @@ const REPORT_ID = PROTOCOL_V1_REPORT_ID;
|
|
|
44
44
|
const HEADER_LENGTH = PROTOCOL_V1_MESSAGE_HEADER_SIZE;
|
|
45
45
|
const PACKET_IO_MAX_RETRIES = 3;
|
|
46
46
|
const PACKET_IO_RETRY_DELAY = 300;
|
|
47
|
+
const USB_DEVICE_IN_USE = 'USB device is already in use';
|
|
47
48
|
// Legacy devices can take longer to answer Initialize after a WebUSB reset.
|
|
48
49
|
// Keep this aligned with Node USB so a slow Protocol V1 response is not
|
|
49
50
|
// misreported as a protocol mismatch during acquire.
|
|
@@ -86,6 +87,8 @@ let usbDisconnectListenerAttached = false;
|
|
|
86
87
|
// open them, including while native open() is still pending.
|
|
87
88
|
const identityProbeHandles = new WeakSet<USBDevice>();
|
|
88
89
|
const openingHandles = new WeakSet<USBDevice>();
|
|
90
|
+
// A normal acquire keeps ownership even while recovery temporarily closes the handle.
|
|
91
|
+
const normalHandleOwners = new WeakMap<USBDevice, WebUsbTransport>();
|
|
89
92
|
|
|
90
93
|
function registerActiveWebUsbTransport(instance: WebUsbTransport, usb: USB) {
|
|
91
94
|
activeWebUsbTransport = instance;
|
|
@@ -130,6 +133,10 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
130
133
|
|
|
131
134
|
private identityProbeDevices = new Map<string, USBDevice>();
|
|
132
135
|
|
|
136
|
+
private normalDevices = new Map<USBDevice, string>();
|
|
137
|
+
|
|
138
|
+
private identityProbeCleanupPaths = new Set<string>();
|
|
139
|
+
|
|
133
140
|
/**
|
|
134
141
|
* The exact USBDevice object each cached protocol was probed against. The
|
|
135
142
|
* browser returns the same object identity for a device as long as it stays
|
|
@@ -273,6 +280,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
273
280
|
* It does NOT prompt the user to select a device
|
|
274
281
|
*/
|
|
275
282
|
async enumerate() {
|
|
283
|
+
// Retry only our failed cleanup before Core skips already-open USB handles.
|
|
284
|
+
for (const path of this.identityProbeCleanupPaths) {
|
|
285
|
+
try {
|
|
286
|
+
await this.release(path);
|
|
287
|
+
} catch {
|
|
288
|
+
// Keep ownership while native close still fails; retry on the next probe.
|
|
289
|
+
}
|
|
290
|
+
}
|
|
276
291
|
await this.getConnectedDevices();
|
|
277
292
|
return this.deviceList;
|
|
278
293
|
}
|
|
@@ -320,6 +335,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
320
335
|
if (this.identityProbe) {
|
|
321
336
|
await this.getConnectedDevices();
|
|
322
337
|
this.claimIdentityProbeHandle(input.path, await this.findDevice(input.path));
|
|
338
|
+
} else {
|
|
339
|
+
this.claimNormalHandle(input.path, await this.findDevice(input.path));
|
|
323
340
|
}
|
|
324
341
|
try {
|
|
325
342
|
await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
|
|
@@ -408,6 +425,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
408
425
|
await this.closeOpenDevice(input.path);
|
|
409
426
|
} finally {
|
|
410
427
|
this.releaseIdentityProbeHandle(input.path);
|
|
428
|
+
this.releaseNormalHandles(input.path);
|
|
411
429
|
}
|
|
412
430
|
throw e;
|
|
413
431
|
}
|
|
@@ -588,6 +606,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
588
606
|
this.releaseIdentityProbeHandle(path);
|
|
589
607
|
this.claimIdentityProbeHandle(path, device);
|
|
590
608
|
}
|
|
609
|
+
if (!this.identityProbe) this.claimNormalHandle(path, device);
|
|
591
610
|
this.assertHandleAvailable(device);
|
|
592
611
|
if (!device.opened) {
|
|
593
612
|
openingHandles.add(device);
|
|
@@ -615,6 +634,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
615
634
|
this.releaseIdentityProbeHandle(path);
|
|
616
635
|
this.claimIdentityProbeHandle(path, device);
|
|
617
636
|
}
|
|
637
|
+
if (!this.identityProbe) this.claimNormalHandle(path, device);
|
|
618
638
|
this.assertHandleAvailable(device);
|
|
619
639
|
if (!device.opened) {
|
|
620
640
|
openingHandles.add(device);
|
|
@@ -648,17 +668,47 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
648
668
|
}
|
|
649
669
|
|
|
650
670
|
private assertHandleAvailable(device: USBDevice) {
|
|
671
|
+
const normalOwner = normalHandleOwners.get(device);
|
|
651
672
|
if (
|
|
652
|
-
|
|
653
|
-
|
|
673
|
+
(normalOwner && normalOwner !== this) ||
|
|
674
|
+
(identityProbeHandles.has(device) &&
|
|
675
|
+
![...this.identityProbeDevices.values()].includes(device))
|
|
654
676
|
) {
|
|
655
|
-
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError,
|
|
677
|
+
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, USB_DEVICE_IN_USE);
|
|
656
678
|
}
|
|
657
679
|
}
|
|
658
680
|
|
|
681
|
+
private claimNormalHandle(path: string, device: USBDevice) {
|
|
682
|
+
this.assertHandleAvailable(device);
|
|
683
|
+
normalHandleOwners.set(device, this);
|
|
684
|
+
this.normalDevices.set(device, path);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
private releaseNormalHandles(path: string) {
|
|
688
|
+
for (const [device, ownedPath] of this.normalDevices) {
|
|
689
|
+
if (ownedPath === path) {
|
|
690
|
+
normalHandleOwners.delete(device);
|
|
691
|
+
this.normalDevices.delete(device);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
private isHandleOwnershipError(error: unknown): error is HardwareError {
|
|
697
|
+
return (
|
|
698
|
+
error instanceof HardwareError &&
|
|
699
|
+
error.errorCode === HardwareErrorCode.RuntimeError &&
|
|
700
|
+
this.getErrorMessage(error).includes(USB_DEVICE_IN_USE)
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
659
704
|
private claimIdentityProbeHandle(path: string, device: USBDevice) {
|
|
660
|
-
if (
|
|
661
|
-
|
|
705
|
+
if (
|
|
706
|
+
device.opened ||
|
|
707
|
+
openingHandles.has(device) ||
|
|
708
|
+
identityProbeHandles.has(device) ||
|
|
709
|
+
normalHandleOwners.has(device)
|
|
710
|
+
) {
|
|
711
|
+
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, USB_DEVICE_IN_USE);
|
|
662
712
|
}
|
|
663
713
|
identityProbeHandles.add(device);
|
|
664
714
|
this.identityProbeDevices.set(path, device);
|
|
@@ -667,9 +717,13 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
667
717
|
private releaseIdentityProbeHandle(path: string) {
|
|
668
718
|
const device = this.identityProbeDevices.get(path);
|
|
669
719
|
// A failed native close must not make an open probe handle available to other callers.
|
|
670
|
-
if (device?.opened)
|
|
720
|
+
if (device?.opened) {
|
|
721
|
+
this.identityProbeCleanupPaths.add(path);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
671
724
|
if (device) identityProbeHandles.delete(device);
|
|
672
725
|
this.identityProbeDevices.delete(path);
|
|
726
|
+
this.identityProbeCleanupPaths.delete(path);
|
|
673
727
|
}
|
|
674
728
|
|
|
675
729
|
private async closeOpenDevice(path: string) {
|
|
@@ -686,6 +740,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
686
740
|
} catch (error) {
|
|
687
741
|
this.Log?.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
|
|
688
742
|
}
|
|
743
|
+
this.assertHandleAvailable(current);
|
|
689
744
|
try {
|
|
690
745
|
await current.close();
|
|
691
746
|
} catch (error) {
|
|
@@ -699,6 +754,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
699
754
|
direction: USBDirection,
|
|
700
755
|
endpointNumber: number
|
|
701
756
|
) {
|
|
757
|
+
this.assertHandleAvailable(device);
|
|
702
758
|
try {
|
|
703
759
|
await device.clearHalt(direction, endpointNumber);
|
|
704
760
|
} catch (error) {
|
|
@@ -774,7 +830,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
774
830
|
operation: WebUsbOperation,
|
|
775
831
|
error: unknown
|
|
776
832
|
): Promise<never> {
|
|
777
|
-
if (this.isWebUsbIoHardwareError(error)) throw error;
|
|
833
|
+
if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error)) throw error;
|
|
778
834
|
if (error instanceof ProtocolV2LinkError && error.code !== 'io') throw error;
|
|
779
835
|
|
|
780
836
|
const nativeError = error instanceof ProtocolV2LinkError ? error.cause ?? error : error;
|
|
@@ -814,7 +870,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
814
870
|
}
|
|
815
871
|
|
|
816
872
|
private isRetryablePacketIoError(error: unknown) {
|
|
817
|
-
if (this.isWebUsbIoHardwareError(error)) return false;
|
|
873
|
+
if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error)) return false;
|
|
818
874
|
const message = this.getErrorMessage(error).toLowerCase();
|
|
819
875
|
if (this.isWebUsbPermissionDeniedMessage(message)) return false;
|
|
820
876
|
return (
|
|
@@ -848,6 +904,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
848
904
|
|
|
849
905
|
try {
|
|
850
906
|
const currentDevice = await this.findDevice(path);
|
|
907
|
+
this.assertHandleAvailable(currentDevice);
|
|
851
908
|
if (currentDevice.opened) {
|
|
852
909
|
const endpoints = this.deviceEndpoints.get(path);
|
|
853
910
|
const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
|
|
@@ -856,9 +913,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
856
913
|
} catch (releaseError) {
|
|
857
914
|
this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
|
|
858
915
|
}
|
|
916
|
+
this.assertHandleAvailable(currentDevice);
|
|
859
917
|
await currentDevice.close();
|
|
860
918
|
}
|
|
861
919
|
} catch (closeError) {
|
|
920
|
+
if (this.isHandleOwnershipError(closeError)) throw closeError;
|
|
862
921
|
this.Log.debug('[WebUsbTransport] close device before retry error:', closeError);
|
|
863
922
|
}
|
|
864
923
|
|
|
@@ -900,6 +959,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
900
959
|
try {
|
|
901
960
|
await this.reconnectForPacketIoRetry(path, 'out', attempt, error);
|
|
902
961
|
} catch (reconnectError) {
|
|
962
|
+
if (this.isHandleOwnershipError(reconnectError)) throw reconnectError;
|
|
903
963
|
lastError = reconnectError;
|
|
904
964
|
this.Log.debug(
|
|
905
965
|
`[WebUsbTransport] transferout reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
|
|
@@ -922,6 +982,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
922
982
|
const transferBuffer = this.toArrayBuffer(
|
|
923
983
|
packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
|
|
924
984
|
);
|
|
985
|
+
this.assertHandleAvailable(device);
|
|
925
986
|
const result = await device.transferOut(endpointOut, transferBuffer);
|
|
926
987
|
if (result.status !== 'ok') {
|
|
927
988
|
return this.throwWebUsbIoError(
|
|
@@ -949,6 +1010,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
949
1010
|
}
|
|
950
1011
|
const endpoints = this.deviceEndpoints.get(path);
|
|
951
1012
|
const endpointIn = endpoints?.endpointIn ?? this.endpointId;
|
|
1013
|
+
this.assertHandleAvailable(device);
|
|
952
1014
|
const result = await device.transferIn(endpointIn, length);
|
|
953
1015
|
return this.getTransferInData(result);
|
|
954
1016
|
} catch (error) {
|
|
@@ -963,6 +1025,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
963
1025
|
try {
|
|
964
1026
|
await this.reconnectForPacketIoRetry(path, 'in', attempt, error);
|
|
965
1027
|
} catch (reconnectError) {
|
|
1028
|
+
if (this.isHandleOwnershipError(reconnectError)) throw reconnectError;
|
|
966
1029
|
lastError = reconnectError;
|
|
967
1030
|
this.Log.debug(
|
|
968
1031
|
`[WebUsbTransport] transferin reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
|
|
@@ -982,6 +1045,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
982
1045
|
}
|
|
983
1046
|
const endpoints = this.deviceEndpoints.get(path);
|
|
984
1047
|
const endpointIn = endpoints?.endpointIn ?? this.endpointId;
|
|
1048
|
+
this.assertHandleAvailable(device);
|
|
985
1049
|
const result = await device.transferIn(endpointIn, length);
|
|
986
1050
|
return this.getTransferInData(result);
|
|
987
1051
|
}
|
|
@@ -1215,6 +1279,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
1215
1279
|
await this.closeOpenDevice(path);
|
|
1216
1280
|
} finally {
|
|
1217
1281
|
this.releaseIdentityProbeHandle(path);
|
|
1282
|
+
this.releaseNormalHandles(path);
|
|
1218
1283
|
}
|
|
1219
1284
|
// Keep deviceProtocol/deviceProtocolHints across release: the probe result is a
|
|
1220
1285
|
// physical-device property, so the next acquire can skip the wire-level probe.
|