@onekeyfe/hd-transport-web-device 1.2.8-alpha.5 → 1.2.8-alpha.51
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 +268 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +86 -10
- package/dist/webusb.d.ts +7 -1
- package/dist/webusb.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/webusb.ts +90 -11
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import WebUsbTransport from '../src/webusb';
|
|
2
|
+
import { probeUsbDeviceIdentities } from '@onekeyfe/hd-common-connect-sdk/src';
|
|
3
|
+
|
|
4
|
+
import type { MessageFromOneKey, TransportCallOptions } from '@onekeyfe/hd-transport';
|
|
5
|
+
|
|
6
|
+
jest.mock('@onekeyfe/hd-core', () => jest.requireActual('../../core/src'));
|
|
7
|
+
jest.mock('../../core/src/data/config', () => ({
|
|
8
|
+
getSDKVersion: () => '1.0.0-test',
|
|
9
|
+
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0-test/',
|
|
10
|
+
}));
|
|
11
|
+
jest.mock('@onekeyfe/hd-transport-web-device', () => ({
|
|
12
|
+
WebUsbTransport: jest.requireActual('../src/webusb').default,
|
|
13
|
+
}));
|
|
14
|
+
jest.mock('@onekeyfe/hd-transport-http', () => ({ __esModule: true, default: jest.fn() }));
|
|
15
|
+
jest.mock('@onekeyfe/hd-transport-lowlevel', () => ({}));
|
|
16
|
+
jest.mock('@onekeyfe/hd-transport-emulator', () => ({}));
|
|
17
|
+
|
|
18
|
+
describe('WebUSB identity probe ownership', () => {
|
|
19
|
+
let originalNavigator: PropertyDescriptor | undefined;
|
|
20
|
+
const device = {
|
|
21
|
+
vendorId: 0x1209,
|
|
22
|
+
productId: 0x53c1,
|
|
23
|
+
serialNumber: 'USB_PATH',
|
|
24
|
+
opened: false,
|
|
25
|
+
close: jest.fn(),
|
|
26
|
+
};
|
|
27
|
+
let disconnectListener: (event: { device: typeof device }) => void;
|
|
28
|
+
const usb = {
|
|
29
|
+
getDevices: jest.fn(),
|
|
30
|
+
addEventListener: jest.fn((_type, callback) => {
|
|
31
|
+
disconnectListener = callback;
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
|
|
36
|
+
device.opened = false;
|
|
37
|
+
device.close.mockReset().mockImplementation(() => {
|
|
38
|
+
device.opened = false;
|
|
39
|
+
return Promise.resolve();
|
|
40
|
+
});
|
|
41
|
+
usb.getDevices.mockResolvedValue([device]);
|
|
42
|
+
usb.addEventListener.mockClear();
|
|
43
|
+
Object.defineProperty(globalThis, 'navigator', { configurable: true, value: { usb } });
|
|
44
|
+
});
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
if (originalNavigator) Object.defineProperty(globalThis, 'navigator', originalNavigator);
|
|
47
|
+
else delete (globalThis as { navigator?: Navigator }).navigator;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('does not close a handle already opened by another operation', async () => {
|
|
51
|
+
const probe = new WebUsbTransport(true);
|
|
52
|
+
probe.init({ debug: jest.fn() });
|
|
53
|
+
device.opened = true;
|
|
54
|
+
await expect(probe.acquire({ path: 'USB_PATH' })).rejects.toThrow(
|
|
55
|
+
'USB device is already in use'
|
|
56
|
+
);
|
|
57
|
+
expect(device.close).not.toHaveBeenCalled();
|
|
58
|
+
expect(usb.addEventListener).not.toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('does not borrow a handle while a normal native open is pending', async () => {
|
|
62
|
+
const active = new WebUsbTransport();
|
|
63
|
+
const probe = new WebUsbTransport(true);
|
|
64
|
+
active.init({ debug: jest.fn() });
|
|
65
|
+
probe.init({ debug: jest.fn() });
|
|
66
|
+
let finishOpen!: () => void;
|
|
67
|
+
let started!: () => void;
|
|
68
|
+
const opening = new Promise<void>(resolve => {
|
|
69
|
+
finishOpen = resolve;
|
|
70
|
+
});
|
|
71
|
+
const entered = new Promise<void>(resolve => {
|
|
72
|
+
started = resolve;
|
|
73
|
+
});
|
|
74
|
+
const native = Object.assign(device, {
|
|
75
|
+
open: jest.fn(async () => {
|
|
76
|
+
started();
|
|
77
|
+
await opening;
|
|
78
|
+
device.opened = true;
|
|
79
|
+
}),
|
|
80
|
+
reset: jest.fn(),
|
|
81
|
+
configurations: [],
|
|
82
|
+
configuration: { configurationValue: 1 },
|
|
83
|
+
selectConfiguration: jest.fn(),
|
|
84
|
+
claimInterface: jest.fn(),
|
|
85
|
+
clearHalt: jest.fn(),
|
|
86
|
+
});
|
|
87
|
+
jest.spyOn(active, 'findDevice').mockResolvedValue(native as unknown as USBDevice);
|
|
88
|
+
const connecting = active.connectToDevice('USB_PATH', true);
|
|
89
|
+
await entered;
|
|
90
|
+
await expect(probe.acquire({ path: 'USB_PATH' })).rejects.toThrow(
|
|
91
|
+
'USB device is already in use'
|
|
92
|
+
);
|
|
93
|
+
expect(device.close).not.toHaveBeenCalled();
|
|
94
|
+
finishOpen();
|
|
95
|
+
await connecting;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('a normal connection cannot close an isolated probe handle and release closes the captured handle after re-enumeration', async () => {
|
|
99
|
+
const probe = new WebUsbTransport(true);
|
|
100
|
+
probe.init({ debug: jest.fn() });
|
|
101
|
+
const internals = probe as unknown as {
|
|
102
|
+
rotateProtocolV2UsbGeneration: () => Promise<void>;
|
|
103
|
+
detectProtocol: () => Promise<'V1'>;
|
|
104
|
+
};
|
|
105
|
+
jest.spyOn(internals, 'rotateProtocolV2UsbGeneration').mockResolvedValue(undefined);
|
|
106
|
+
jest.spyOn(internals, 'detectProtocol').mockResolvedValue('V1');
|
|
107
|
+
jest.spyOn(probe, 'connect').mockImplementation(() => {
|
|
108
|
+
device.opened = true;
|
|
109
|
+
return Promise.resolve();
|
|
110
|
+
});
|
|
111
|
+
await probe.acquire({ path: 'USB_PATH' });
|
|
112
|
+
const active = new WebUsbTransport();
|
|
113
|
+
active.init({ debug: jest.fn() });
|
|
114
|
+
await expect(active.connectToDevice('USB_PATH', true)).rejects.toThrow(
|
|
115
|
+
'USB device is already in use'
|
|
116
|
+
);
|
|
117
|
+
expect(device.close).not.toHaveBeenCalled();
|
|
118
|
+
const replacement = { ...device, close: jest.fn() };
|
|
119
|
+
usb.getDevices.mockResolvedValue([replacement]);
|
|
120
|
+
await probe.getConnectedDevices();
|
|
121
|
+
device.close.mockRejectedValueOnce(new Error('native close failed'));
|
|
122
|
+
await expect(probe.release('USB_PATH')).rejects.toThrow('native close failed');
|
|
123
|
+
await probe.enumerate();
|
|
124
|
+
expect(device.close).toHaveBeenCalledTimes(2);
|
|
125
|
+
expect(replacement.close).not.toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('does not replace the active SDK disconnect observer', () => {
|
|
129
|
+
const active = new WebUsbTransport();
|
|
130
|
+
active.init({ debug: jest.fn() });
|
|
131
|
+
const probe = new WebUsbTransport(true);
|
|
132
|
+
probe.init({ debug: jest.fn() });
|
|
133
|
+
const activeDisconnect = jest.spyOn(active, 'markProtocolStale');
|
|
134
|
+
const probeDisconnect = jest.spyOn(probe, 'markProtocolStale');
|
|
135
|
+
disconnectListener({ device });
|
|
136
|
+
expect(activeDisconnect).toHaveBeenCalledWith('USB_PATH');
|
|
137
|
+
expect(probeDisconnect).not.toHaveBeenCalled();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test.each(['release', 'acquire failure'])(
|
|
141
|
+
'keeps a probe handle protected after close fails during %s and recovers after cleanup',
|
|
142
|
+
async phase => {
|
|
143
|
+
Object.assign(device, {
|
|
144
|
+
open: jest.fn(() => {
|
|
145
|
+
device.opened = true;
|
|
146
|
+
return Promise.resolve();
|
|
147
|
+
}),
|
|
148
|
+
releaseInterface: jest.fn().mockResolvedValue(undefined),
|
|
149
|
+
reset: jest.fn(),
|
|
150
|
+
configurations: [],
|
|
151
|
+
configuration: { configurationValue: 1 },
|
|
152
|
+
selectConfiguration: jest.fn(),
|
|
153
|
+
claimInterface: jest.fn(),
|
|
154
|
+
clearHalt: jest.fn(),
|
|
155
|
+
});
|
|
156
|
+
const probe = new WebUsbTransport(true);
|
|
157
|
+
probe.init({ debug: jest.fn() });
|
|
158
|
+
const internals = probe as unknown as {
|
|
159
|
+
rotateProtocolV2UsbGeneration: () => Promise<void>;
|
|
160
|
+
detectProtocol: () => Promise<'V1'>;
|
|
161
|
+
};
|
|
162
|
+
jest.spyOn(internals, 'rotateProtocolV2UsbGeneration').mockResolvedValue(undefined);
|
|
163
|
+
jest.spyOn(internals, 'detectProtocol').mockResolvedValue('V1');
|
|
164
|
+
jest.spyOn(probe, 'connect').mockImplementation(() => {
|
|
165
|
+
device.opened = true;
|
|
166
|
+
if (phase === 'acquire failure') return Promise.reject(new Error('probe failed'));
|
|
167
|
+
return Promise.resolve();
|
|
168
|
+
});
|
|
169
|
+
const closeFailure = new Error('native close failed');
|
|
170
|
+
device.close.mockRejectedValueOnce(closeFailure);
|
|
171
|
+
if (phase === 'release') {
|
|
172
|
+
await probe.acquire({ path: 'USB_PATH' });
|
|
173
|
+
await expect(probe.release('USB_PATH')).rejects.toBe(closeFailure);
|
|
174
|
+
} else {
|
|
175
|
+
await expect(probe.acquire({ path: 'USB_PATH' })).rejects.toBe(closeFailure);
|
|
176
|
+
}
|
|
177
|
+
expect(device.opened).toBe(true);
|
|
178
|
+
const active = new WebUsbTransport();
|
|
179
|
+
active.init({ debug: jest.fn() });
|
|
180
|
+
await expect(active.connectToDevice('USB_PATH', true)).rejects.toThrow(
|
|
181
|
+
'USB device is already in use'
|
|
182
|
+
);
|
|
183
|
+
expect(device.close).toHaveBeenCalledTimes(1);
|
|
184
|
+
|
|
185
|
+
await probe.release('USB_PATH');
|
|
186
|
+
expect(device.opened).toBe(false);
|
|
187
|
+
await expect(active.connectToDevice('USB_PATH', true)).resolves.toBeUndefined();
|
|
188
|
+
expect(device.opened).toBe(true);
|
|
189
|
+
await active.release('USB_PATH');
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
test.each(['release', 'acquire failure'])(
|
|
194
|
+
'the public probe retries failed %s cleanup on its next call',
|
|
195
|
+
async phase => {
|
|
196
|
+
const internals = WebUsbTransport.prototype as unknown as {
|
|
197
|
+
rotateProtocolV2UsbGeneration: () => Promise<void>;
|
|
198
|
+
detectProtocol: () => Promise<'V1'>;
|
|
199
|
+
};
|
|
200
|
+
jest.spyOn(internals, 'rotateProtocolV2UsbGeneration').mockResolvedValue(undefined);
|
|
201
|
+
jest.spyOn(internals, 'detectProtocol').mockResolvedValue('V1');
|
|
202
|
+
jest.spyOn(WebUsbTransport.prototype, 'getProtocolType').mockReturnValue('V1');
|
|
203
|
+
jest.spyOn(WebUsbTransport.prototype, 'configure').mockImplementation(() => undefined);
|
|
204
|
+
jest
|
|
205
|
+
.spyOn(WebUsbTransport.prototype, 'configureProtocolV2')
|
|
206
|
+
.mockImplementation(() => undefined);
|
|
207
|
+
const connect = jest.spyOn(WebUsbTransport.prototype, 'connect').mockImplementation(() => {
|
|
208
|
+
device.opened = true;
|
|
209
|
+
return Promise.resolve();
|
|
210
|
+
});
|
|
211
|
+
if (phase === 'acquire failure') {
|
|
212
|
+
connect.mockImplementationOnce(() => {
|
|
213
|
+
device.opened = true;
|
|
214
|
+
return Promise.reject(new Error('probe failed'));
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
jest.spyOn(WebUsbTransport.prototype, 'call').mockResolvedValue({
|
|
218
|
+
type: 'Features',
|
|
219
|
+
message: { onekey_serial_no: 'PHYSICAL_SERIAL', onekey_device_type: 'CLASSIC1S' },
|
|
220
|
+
});
|
|
221
|
+
device.close.mockRejectedValueOnce(new Error('native close failed'));
|
|
222
|
+
try {
|
|
223
|
+
await expect(probeUsbDeviceIdentities({ commType: 'webusb' })).resolves.toEqual([]);
|
|
224
|
+
expect(device.opened).toBe(true);
|
|
225
|
+
const active = new WebUsbTransport();
|
|
226
|
+
active.init({ debug: jest.fn() });
|
|
227
|
+
await expect(active.connectToDevice('USB_PATH', true)).rejects.toThrow(
|
|
228
|
+
'USB device is already in use'
|
|
229
|
+
);
|
|
230
|
+
device.close.mockRejectedValueOnce(new Error('native close still failing'));
|
|
231
|
+
await expect(probeUsbDeviceIdentities({ commType: 'webusb' })).resolves.toEqual([]);
|
|
232
|
+
expect(device.opened).toBe(true);
|
|
233
|
+
expect(connect).toHaveBeenCalledTimes(1);
|
|
234
|
+
expect(device.close).toHaveBeenCalledTimes(2);
|
|
235
|
+
await expect(probeUsbDeviceIdentities({ commType: 'webusb' })).resolves.toMatchObject([
|
|
236
|
+
{ path: 'USB_PATH', serialNo: 'PHYSICAL_SERIAL' },
|
|
237
|
+
]);
|
|
238
|
+
expect(connect).toHaveBeenCalledTimes(2);
|
|
239
|
+
expect(device.opened).toBe(false);
|
|
240
|
+
} finally {
|
|
241
|
+
jest.restoreAllMocks();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
test.each([false, true])(
|
|
247
|
+
'uses a read-only V1 probe only for isolated discovery (%s)',
|
|
248
|
+
async identityProbe => {
|
|
249
|
+
const probe = new WebUsbTransport(identityProbe);
|
|
250
|
+
const internals = probe as unknown as {
|
|
251
|
+
messages: object;
|
|
252
|
+
callProtocolV1: (
|
|
253
|
+
path: string,
|
|
254
|
+
name: string,
|
|
255
|
+
data: object,
|
|
256
|
+
options: TransportCallOptions
|
|
257
|
+
) => Promise<MessageFromOneKey>;
|
|
258
|
+
probeProtocolV1: (path: string) => Promise<boolean>;
|
|
259
|
+
};
|
|
260
|
+
internals.messages = {};
|
|
261
|
+
const call = jest
|
|
262
|
+
.spyOn(internals, 'callProtocolV1')
|
|
263
|
+
.mockResolvedValue({ type: 'Features', message: {} });
|
|
264
|
+
await expect(internals.probeProtocolV1('USB_PATH')).resolves.toBe(true);
|
|
265
|
+
expect(call.mock.calls[0][1]).toBe(identityProbe ? 'GetFeatures' : 'Initialize');
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ interface DeviceInfo extends OneKeyDeviceInfoBase {
|
|
|
13
13
|
protocolType?: ProtocolType;
|
|
14
14
|
}
|
|
15
15
|
declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
16
|
+
private readonly identityProbe;
|
|
16
17
|
messages: ReturnType<typeof _onekeyfe_hd_transport__default.parseConfigure> | undefined;
|
|
17
18
|
/** Protobuf schema for Protocol V2 transports. */
|
|
18
19
|
messagesV2: ReturnType<typeof _onekeyfe_hd_transport__default.parseConfigure> | undefined;
|
|
@@ -29,6 +30,8 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
|
29
30
|
private staleProtocolPaths;
|
|
30
31
|
/** Paths currently acquired (between a successful acquire() and its release()). */
|
|
31
32
|
private acquiredPaths;
|
|
33
|
+
private identityProbeDevices;
|
|
34
|
+
private identityProbeCleanupPaths;
|
|
32
35
|
/**
|
|
33
36
|
* The exact USBDevice object each cached protocol was probed against. The
|
|
34
37
|
* browser returns the same object identity for a device as long as it stays
|
|
@@ -53,7 +56,7 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
|
53
56
|
configurationId: number;
|
|
54
57
|
endpointId: number;
|
|
55
58
|
interfaceId: number;
|
|
56
|
-
constructor();
|
|
59
|
+
constructor(identityProbe?: boolean);
|
|
57
60
|
/**
|
|
58
61
|
* Initialize WebUSB transport
|
|
59
62
|
*/
|
|
@@ -129,6 +132,9 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
|
129
132
|
* Discovers interface/endpoint numbers from USB descriptors on first connection.
|
|
130
133
|
*/
|
|
131
134
|
connectToDevice(path: string, first: boolean): Promise<undefined>;
|
|
135
|
+
private assertHandleAvailable;
|
|
136
|
+
private claimIdentityProbeHandle;
|
|
137
|
+
private releaseIdentityProbeHandle;
|
|
132
138
|
private closeOpenDevice;
|
|
133
139
|
private clearEndpointHalt;
|
|
134
140
|
/**
|
package/dist/index.js
CHANGED
|
@@ -56,6 +56,8 @@ const PROTOCOL_V2_PROBE_TIMEOUT = 1000;
|
|
|
56
56
|
const EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS = 2;
|
|
57
57
|
let activeWebUsbTransport;
|
|
58
58
|
let usbDisconnectListenerAttached = false;
|
|
59
|
+
const identityProbeHandles = new WeakSet();
|
|
60
|
+
const openingHandles = new WeakSet();
|
|
59
61
|
function registerActiveWebUsbTransport(instance, usb) {
|
|
60
62
|
activeWebUsbTransport = instance;
|
|
61
63
|
if (usbDisconnectListenerAttached)
|
|
@@ -72,17 +74,20 @@ function registerActiveWebUsbTransport(instance, usb) {
|
|
|
72
74
|
});
|
|
73
75
|
}
|
|
74
76
|
class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
75
|
-
constructor() {
|
|
77
|
+
constructor(identityProbe = false) {
|
|
76
78
|
super({
|
|
77
79
|
router: transport.PROTOCOL_V2_CHANNEL_USB,
|
|
78
80
|
maxFrameBytes: transport.PROTOCOL_V2_FRAME_MAX_BYTES,
|
|
79
81
|
logPrefix: 'ProtocolV2 WebUSB',
|
|
80
82
|
});
|
|
83
|
+
this.identityProbe = identityProbe;
|
|
81
84
|
this.deviceProtocol = new Map();
|
|
82
85
|
this.confirmedDeviceProtocols = new Map();
|
|
83
86
|
this.deviceProtocolHints = new Map();
|
|
84
87
|
this.staleProtocolPaths = new Set();
|
|
85
88
|
this.acquiredPaths = new Set();
|
|
89
|
+
this.identityProbeDevices = new Map();
|
|
90
|
+
this.identityProbeCleanupPaths = new Set();
|
|
86
91
|
this.probedDeviceObjects = new Map();
|
|
87
92
|
this.deviceEndpoints = new Map();
|
|
88
93
|
this.name = 'WebUsbTransport';
|
|
@@ -101,7 +106,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
101
106
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'WebUSB is not supported by current browsers');
|
|
102
107
|
}
|
|
103
108
|
this.usb = usb;
|
|
104
|
-
|
|
109
|
+
if (!this.identityProbe)
|
|
110
|
+
registerActiveWebUsbTransport(this, usb);
|
|
105
111
|
}
|
|
106
112
|
emitDeviceDisconnect(path, device) {
|
|
107
113
|
var _a, _b;
|
|
@@ -150,6 +156,13 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
150
156
|
}
|
|
151
157
|
enumerate() {
|
|
152
158
|
return __awaiter(this, void 0, void 0, function* () {
|
|
159
|
+
for (const path of this.identityProbeCleanupPaths) {
|
|
160
|
+
try {
|
|
161
|
+
yield this.release(path);
|
|
162
|
+
}
|
|
163
|
+
catch (_a) {
|
|
164
|
+
}
|
|
165
|
+
}
|
|
153
166
|
yield this.getConnectedDevices();
|
|
154
167
|
return this.deviceList;
|
|
155
168
|
});
|
|
@@ -187,6 +200,10 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
187
200
|
return __awaiter(this, void 0, void 0, function* () {
|
|
188
201
|
if (!input.path)
|
|
189
202
|
return;
|
|
203
|
+
if (this.identityProbe) {
|
|
204
|
+
yield this.getConnectedDevices();
|
|
205
|
+
this.claimIdentityProbeHandle(input.path, yield this.findDevice(input.path));
|
|
206
|
+
}
|
|
190
207
|
try {
|
|
191
208
|
yield this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
|
|
192
209
|
yield this.closeOpenDevice(input.path);
|
|
@@ -248,7 +265,12 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
248
265
|
catch (e) {
|
|
249
266
|
this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
|
|
250
267
|
this.acquiredPaths.delete(input.path);
|
|
251
|
-
|
|
268
|
+
try {
|
|
269
|
+
yield this.closeOpenDevice(input.path);
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
this.releaseIdentityProbeHandle(input.path);
|
|
273
|
+
}
|
|
252
274
|
throw e;
|
|
253
275
|
}
|
|
254
276
|
});
|
|
@@ -375,12 +397,22 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
375
397
|
var _a;
|
|
376
398
|
return __awaiter(this, void 0, void 0, function* () {
|
|
377
399
|
let device = yield this.findDevice(path);
|
|
400
|
+
if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
|
|
401
|
+
yield this.closeOpenDevice(path);
|
|
402
|
+
this.releaseIdentityProbeHandle(path);
|
|
403
|
+
this.claimIdentityProbeHandle(path, device);
|
|
404
|
+
}
|
|
405
|
+
this.assertHandleAvailable(device);
|
|
378
406
|
if (!device.opened) {
|
|
407
|
+
openingHandles.add(device);
|
|
379
408
|
try {
|
|
380
409
|
yield device.open();
|
|
381
410
|
}
|
|
382
411
|
catch (error) {
|
|
383
|
-
return this.throwWebUsbIoError(path, 'open', error);
|
|
412
|
+
return yield this.throwWebUsbIoError(path, 'open', error);
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
openingHandles.delete(device);
|
|
384
416
|
}
|
|
385
417
|
}
|
|
386
418
|
if (first || this.deviceProtocol.get(path) !== 'V1') {
|
|
@@ -393,12 +425,22 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
393
425
|
}
|
|
394
426
|
yield this.getConnectedDevices();
|
|
395
427
|
device = yield this.findDevice(path);
|
|
428
|
+
if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
|
|
429
|
+
yield this.closeOpenDevice(path);
|
|
430
|
+
this.releaseIdentityProbeHandle(path);
|
|
431
|
+
this.claimIdentityProbeHandle(path, device);
|
|
432
|
+
}
|
|
433
|
+
this.assertHandleAvailable(device);
|
|
396
434
|
if (!device.opened) {
|
|
435
|
+
openingHandles.add(device);
|
|
397
436
|
try {
|
|
398
437
|
yield device.open();
|
|
399
438
|
}
|
|
400
439
|
catch (error) {
|
|
401
|
-
return this.throwWebUsbIoError(path, 'open', error);
|
|
440
|
+
return yield this.throwWebUsbIoError(path, 'open', error);
|
|
441
|
+
}
|
|
442
|
+
finally {
|
|
443
|
+
openingHandles.delete(device);
|
|
402
444
|
}
|
|
403
445
|
}
|
|
404
446
|
if (first ||
|
|
@@ -418,12 +460,39 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
418
460
|
yield this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
|
|
419
461
|
});
|
|
420
462
|
}
|
|
463
|
+
assertHandleAvailable(device) {
|
|
464
|
+
if (identityProbeHandles.has(device) &&
|
|
465
|
+
![...this.identityProbeDevices.values()].includes(device)) {
|
|
466
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'USB device is already in use');
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
claimIdentityProbeHandle(path, device) {
|
|
470
|
+
if (device.opened || openingHandles.has(device) || identityProbeHandles.has(device)) {
|
|
471
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'USB device is already in use');
|
|
472
|
+
}
|
|
473
|
+
identityProbeHandles.add(device);
|
|
474
|
+
this.identityProbeDevices.set(path, device);
|
|
475
|
+
}
|
|
476
|
+
releaseIdentityProbeHandle(path) {
|
|
477
|
+
const device = this.identityProbeDevices.get(path);
|
|
478
|
+
if (device === null || device === void 0 ? void 0 : device.opened) {
|
|
479
|
+
this.identityProbeCleanupPaths.add(path);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (device)
|
|
483
|
+
identityProbeHandles.delete(device);
|
|
484
|
+
this.identityProbeDevices.delete(path);
|
|
485
|
+
this.identityProbeCleanupPaths.delete(path);
|
|
486
|
+
}
|
|
421
487
|
closeOpenDevice(path) {
|
|
422
488
|
var _a, _b, _c, _d;
|
|
423
489
|
return __awaiter(this, void 0, void 0, function* () {
|
|
424
|
-
const current =
|
|
490
|
+
const current = this.identityProbe
|
|
491
|
+
? this.identityProbeDevices.get(path)
|
|
492
|
+
: (_a = this.deviceList.find(device => device.path === path)) === null || _a === void 0 ? void 0 : _a.device;
|
|
425
493
|
if (!(current === null || current === void 0 ? void 0 : current.opened))
|
|
426
494
|
return;
|
|
495
|
+
this.assertHandleAvailable(current);
|
|
427
496
|
const endpoints = this.deviceEndpoints.get(path);
|
|
428
497
|
const ifaceNum = (_b = endpoints === null || endpoints === void 0 ? void 0 : endpoints.interfaceNumber) !== null && _b !== void 0 ? _b : this.interfaceId;
|
|
429
498
|
try {
|
|
@@ -437,6 +506,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
437
506
|
}
|
|
438
507
|
catch (error) {
|
|
439
508
|
(_d = this.Log) === null || _d === void 0 ? void 0 : _d.debug('[WebUsbTransport] close before reconnect failed:', error);
|
|
509
|
+
if (this.identityProbe)
|
|
510
|
+
throw error;
|
|
440
511
|
}
|
|
441
512
|
});
|
|
442
513
|
}
|
|
@@ -749,10 +820,10 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
749
820
|
return false;
|
|
750
821
|
}
|
|
751
822
|
try {
|
|
752
|
-
yield this.callProtocolV1(path, 'Initialize', {}, {
|
|
823
|
+
const response = yield this.callProtocolV1(path, this.identityProbe ? 'GetFeatures' : 'Initialize', {}, {
|
|
753
824
|
timeoutMs: PROTOCOL_V1_PROBE_TIMEOUT,
|
|
754
825
|
});
|
|
755
|
-
return
|
|
826
|
+
return !this.identityProbe || response.type === 'Features';
|
|
756
827
|
}
|
|
757
828
|
catch (error) {
|
|
758
829
|
if (this.isWebUsbIoHardwareError(error))
|
|
@@ -869,8 +940,13 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
869
940
|
release(path) {
|
|
870
941
|
return __awaiter(this, void 0, void 0, function* () {
|
|
871
942
|
this.acquiredPaths.delete(path);
|
|
872
|
-
|
|
873
|
-
|
|
943
|
+
try {
|
|
944
|
+
yield this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
|
|
945
|
+
yield this.closeOpenDevice(path);
|
|
946
|
+
}
|
|
947
|
+
finally {
|
|
948
|
+
this.releaseIdentityProbeHandle(path);
|
|
949
|
+
}
|
|
874
950
|
this.deviceEndpoints.delete(path);
|
|
875
951
|
});
|
|
876
952
|
}
|
package/dist/webusb.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface DeviceInfo extends OneKeyDeviceInfoBase {
|
|
|
9
9
|
protocolType?: ProtocolType;
|
|
10
10
|
}
|
|
11
11
|
export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
12
|
+
private readonly identityProbe;
|
|
12
13
|
messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
13
14
|
messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
14
15
|
private protocolV2SchemaSource;
|
|
@@ -17,6 +18,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
17
18
|
private deviceProtocolHints;
|
|
18
19
|
private staleProtocolPaths;
|
|
19
20
|
private acquiredPaths;
|
|
21
|
+
private identityProbeDevices;
|
|
22
|
+
private identityProbeCleanupPaths;
|
|
20
23
|
private probedDeviceObjects;
|
|
21
24
|
private deviceEndpoints;
|
|
22
25
|
name: string;
|
|
@@ -29,7 +32,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
29
32
|
configurationId: number;
|
|
30
33
|
endpointId: number;
|
|
31
34
|
interfaceId: number;
|
|
32
|
-
constructor();
|
|
35
|
+
constructor(identityProbe?: boolean);
|
|
33
36
|
init(logger: any, emitter?: EventEmitter): void;
|
|
34
37
|
emitDeviceDisconnect(path: string, device?: USBDevice): void;
|
|
35
38
|
markProtocolStale(path: string): void;
|
|
@@ -47,6 +50,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
47
50
|
connect(path: string, first: boolean): Promise<undefined>;
|
|
48
51
|
private discoverEndpoints;
|
|
49
52
|
connectToDevice(path: string, first: boolean): Promise<undefined>;
|
|
53
|
+
private assertHandleAvailable;
|
|
54
|
+
private claimIdentityProbeHandle;
|
|
55
|
+
private releaseIdentityProbeHandle;
|
|
50
56
|
private closeOpenDevice;
|
|
51
57
|
private clearEndpointHalt;
|
|
52
58
|
private assertAcquired;
|
package/dist/webusb.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webusb.d.ts","sourceRoot":"","sources":["../src/webusb.ts"],"names":[],"mappings":";;AACA,OAAO,SAAS,EAAE,EAQhB,0BAA0B,EAG3B,MAAM,wBAAwB,CAAC;AAahC,OAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EACpB,YAAY,EACZ,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACrB,MAAM,wBAAwB,CAAC;AAuBhC,MAAM,WAAW,UAAW,SAAQ,oBAAoB;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,CAAC;IAClB,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;
|
|
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.51",
|
|
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.51",
|
|
25
|
+
"@onekeyfe/hd-transport": "1.2.8-alpha.51"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@onekeyfe/hd-transport-electron": "1.2.8-alpha.
|
|
28
|
+
"@onekeyfe/hd-transport-electron": "1.2.8-alpha.51",
|
|
29
29
|
"@types/w3c-web-usb": "^1.0.6",
|
|
30
30
|
"@types/web-bluetooth": "^0.0.17"
|
|
31
31
|
},
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "0ccbc08199c62e96f6084e864e558055100b66ed"
|
|
33
33
|
}
|
package/src/webusb.ts
CHANGED
|
@@ -82,6 +82,10 @@ type WebUsbOperation = 'open' | 'claimInterface' | 'transferIn' | 'transferOut';
|
|
|
82
82
|
*/
|
|
83
83
|
let activeWebUsbTransport: WebUsbTransport | undefined;
|
|
84
84
|
let usbDisconnectListenerAttached = false;
|
|
85
|
+
// Only isolated discovery owns these handles. Normal calls must never close or
|
|
86
|
+
// open them, including while native open() is still pending.
|
|
87
|
+
const identityProbeHandles = new WeakSet<USBDevice>();
|
|
88
|
+
const openingHandles = new WeakSet<USBDevice>();
|
|
85
89
|
|
|
86
90
|
function registerActiveWebUsbTransport(instance: WebUsbTransport, usb: USB) {
|
|
87
91
|
activeWebUsbTransport = instance;
|
|
@@ -124,6 +128,10 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
124
128
|
/** Paths currently acquired (between a successful acquire() and its release()). */
|
|
125
129
|
private acquiredPaths: Set<string> = new Set();
|
|
126
130
|
|
|
131
|
+
private identityProbeDevices = new Map<string, USBDevice>();
|
|
132
|
+
|
|
133
|
+
private identityProbeCleanupPaths = new Set<string>();
|
|
134
|
+
|
|
127
135
|
/**
|
|
128
136
|
* The exact USBDevice object each cached protocol was probed against. The
|
|
129
137
|
* browser returns the same object identity for a device as long as it stays
|
|
@@ -160,7 +168,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
160
168
|
|
|
161
169
|
interfaceId = INTERFACE_ID;
|
|
162
170
|
|
|
163
|
-
constructor() {
|
|
171
|
+
constructor(private readonly identityProbe = false) {
|
|
164
172
|
super({
|
|
165
173
|
router: PROTOCOL_V2_CHANNEL_USB,
|
|
166
174
|
maxFrameBytes: PROTOCOL_V2_FRAME_MAX_BYTES,
|
|
@@ -183,7 +191,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
183
191
|
);
|
|
184
192
|
}
|
|
185
193
|
this.usb = usb;
|
|
186
|
-
registerActiveWebUsbTransport(this, usb);
|
|
194
|
+
if (!this.identityProbe) registerActiveWebUsbTransport(this, usb);
|
|
187
195
|
}
|
|
188
196
|
|
|
189
197
|
/**
|
|
@@ -267,6 +275,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
267
275
|
* It does NOT prompt the user to select a device
|
|
268
276
|
*/
|
|
269
277
|
async enumerate() {
|
|
278
|
+
// Retry only our failed cleanup before Core skips already-open USB handles.
|
|
279
|
+
for (const path of this.identityProbeCleanupPaths) {
|
|
280
|
+
try {
|
|
281
|
+
await this.release(path);
|
|
282
|
+
} catch {
|
|
283
|
+
// Keep ownership while native close still fails; retry on the next probe.
|
|
284
|
+
}
|
|
285
|
+
}
|
|
270
286
|
await this.getConnectedDevices();
|
|
271
287
|
return this.deviceList;
|
|
272
288
|
}
|
|
@@ -311,6 +327,10 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
311
327
|
*/
|
|
312
328
|
async acquire(input: AcquireInput) {
|
|
313
329
|
if (!input.path) return;
|
|
330
|
+
if (this.identityProbe) {
|
|
331
|
+
await this.getConnectedDevices();
|
|
332
|
+
this.claimIdentityProbeHandle(input.path, await this.findDevice(input.path));
|
|
333
|
+
}
|
|
314
334
|
try {
|
|
315
335
|
await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
|
|
316
336
|
await this.closeOpenDevice(input.path);
|
|
@@ -394,7 +414,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
394
414
|
} catch (e) {
|
|
395
415
|
this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
|
|
396
416
|
this.acquiredPaths.delete(input.path);
|
|
397
|
-
|
|
417
|
+
try {
|
|
418
|
+
await this.closeOpenDevice(input.path);
|
|
419
|
+
} finally {
|
|
420
|
+
this.releaseIdentityProbeHandle(input.path);
|
|
421
|
+
}
|
|
398
422
|
throw e;
|
|
399
423
|
}
|
|
400
424
|
}
|
|
@@ -569,11 +593,20 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
569
593
|
*/
|
|
570
594
|
async connectToDevice(path: string, first: boolean) {
|
|
571
595
|
let device: USBDevice = await this.findDevice(path);
|
|
596
|
+
if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
|
|
597
|
+
await this.closeOpenDevice(path);
|
|
598
|
+
this.releaseIdentityProbeHandle(path);
|
|
599
|
+
this.claimIdentityProbeHandle(path, device);
|
|
600
|
+
}
|
|
601
|
+
this.assertHandleAvailable(device);
|
|
572
602
|
if (!device.opened) {
|
|
603
|
+
openingHandles.add(device);
|
|
573
604
|
try {
|
|
574
605
|
await device.open();
|
|
575
606
|
} catch (error) {
|
|
576
|
-
return this.throwWebUsbIoError(path, 'open', error);
|
|
607
|
+
return await this.throwWebUsbIoError(path, 'open', error);
|
|
608
|
+
} finally {
|
|
609
|
+
openingHandles.delete(device);
|
|
577
610
|
}
|
|
578
611
|
}
|
|
579
612
|
// A V1 packet retry continues an in-flight exchange. Preserve its endpoint
|
|
@@ -587,11 +620,20 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
587
620
|
}
|
|
588
621
|
await this.getConnectedDevices();
|
|
589
622
|
device = await this.findDevice(path);
|
|
623
|
+
if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
|
|
624
|
+
await this.closeOpenDevice(path);
|
|
625
|
+
this.releaseIdentityProbeHandle(path);
|
|
626
|
+
this.claimIdentityProbeHandle(path, device);
|
|
627
|
+
}
|
|
628
|
+
this.assertHandleAvailable(device);
|
|
590
629
|
if (!device.opened) {
|
|
630
|
+
openingHandles.add(device);
|
|
591
631
|
try {
|
|
592
632
|
await device.open();
|
|
593
633
|
} catch (error) {
|
|
594
|
-
return this.throwWebUsbIoError(path, 'open', error);
|
|
634
|
+
return await this.throwWebUsbIoError(path, 'open', error);
|
|
635
|
+
} finally {
|
|
636
|
+
openingHandles.delete(device);
|
|
595
637
|
}
|
|
596
638
|
}
|
|
597
639
|
|
|
@@ -615,9 +657,41 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
615
657
|
await this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
|
|
616
658
|
}
|
|
617
659
|
|
|
660
|
+
private assertHandleAvailable(device: USBDevice) {
|
|
661
|
+
if (
|
|
662
|
+
identityProbeHandles.has(device) &&
|
|
663
|
+
![...this.identityProbeDevices.values()].includes(device)
|
|
664
|
+
) {
|
|
665
|
+
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'USB device is already in use');
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
private claimIdentityProbeHandle(path: string, device: USBDevice) {
|
|
670
|
+
if (device.opened || openingHandles.has(device) || identityProbeHandles.has(device)) {
|
|
671
|
+
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'USB device is already in use');
|
|
672
|
+
}
|
|
673
|
+
identityProbeHandles.add(device);
|
|
674
|
+
this.identityProbeDevices.set(path, device);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
private releaseIdentityProbeHandle(path: string) {
|
|
678
|
+
const device = this.identityProbeDevices.get(path);
|
|
679
|
+
// A failed native close must not make an open probe handle available to other callers.
|
|
680
|
+
if (device?.opened) {
|
|
681
|
+
this.identityProbeCleanupPaths.add(path);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (device) identityProbeHandles.delete(device);
|
|
685
|
+
this.identityProbeDevices.delete(path);
|
|
686
|
+
this.identityProbeCleanupPaths.delete(path);
|
|
687
|
+
}
|
|
688
|
+
|
|
618
689
|
private async closeOpenDevice(path: string) {
|
|
619
|
-
const current = this.
|
|
690
|
+
const current = this.identityProbe
|
|
691
|
+
? this.identityProbeDevices.get(path)
|
|
692
|
+
: this.deviceList.find(device => device.path === path)?.device;
|
|
620
693
|
if (!current?.opened) return;
|
|
694
|
+
this.assertHandleAvailable(current);
|
|
621
695
|
|
|
622
696
|
const endpoints = this.deviceEndpoints.get(path);
|
|
623
697
|
const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
|
|
@@ -630,6 +704,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
630
704
|
await current.close();
|
|
631
705
|
} catch (error) {
|
|
632
706
|
this.Log?.debug('[WebUsbTransport] close before reconnect failed:', error);
|
|
707
|
+
if (this.identityProbe) throw error;
|
|
633
708
|
}
|
|
634
709
|
}
|
|
635
710
|
|
|
@@ -984,15 +1059,15 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
984
1059
|
}
|
|
985
1060
|
|
|
986
1061
|
try {
|
|
987
|
-
await this.callProtocolV1(
|
|
1062
|
+
const response = await this.callProtocolV1(
|
|
988
1063
|
path,
|
|
989
|
-
'Initialize',
|
|
1064
|
+
this.identityProbe ? 'GetFeatures' : 'Initialize',
|
|
990
1065
|
{},
|
|
991
1066
|
{
|
|
992
1067
|
timeoutMs: PROTOCOL_V1_PROBE_TIMEOUT,
|
|
993
1068
|
}
|
|
994
1069
|
);
|
|
995
|
-
return
|
|
1070
|
+
return !this.identityProbe || response.type === 'Features';
|
|
996
1071
|
} catch (error) {
|
|
997
1072
|
if (this.isWebUsbIoHardwareError(error)) throw error;
|
|
998
1073
|
return false;
|
|
@@ -1149,8 +1224,12 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
1149
1224
|
*/
|
|
1150
1225
|
async release(path: string) {
|
|
1151
1226
|
this.acquiredPaths.delete(path);
|
|
1152
|
-
|
|
1153
|
-
|
|
1227
|
+
try {
|
|
1228
|
+
await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
|
|
1229
|
+
await this.closeOpenDevice(path);
|
|
1230
|
+
} finally {
|
|
1231
|
+
this.releaseIdentityProbeHandle(path);
|
|
1232
|
+
}
|
|
1154
1233
|
// Keep deviceProtocol/deviceProtocolHints across release: the probe result is a
|
|
1155
1234
|
// physical-device property, so the next acquire can skip the wire-level probe.
|
|
1156
1235
|
// V2 entries are still dropped by onProtocolV2UsbLinkInvalidated via the link
|