@onekeyfe/hd-transport-web-device 1.2.0-alpha.135 → 1.2.0-alpha.136
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 +228 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +62 -10
- package/dist/webusb.d.ts +5 -0
- package/dist/webusb.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/webusb.ts +120 -12
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import transport from '@onekeyfe/hd-transport';
|
|
2
|
+
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
3
|
+
|
|
4
|
+
import WebUsbTransport from '../src/webusb';
|
|
5
|
+
|
|
6
|
+
const schema = {
|
|
7
|
+
nested: {
|
|
8
|
+
Ping: { fields: { message: { type: 'string', id: 1 } } },
|
|
9
|
+
Success: { fields: { message: { type: 'string', id: 1 } } },
|
|
10
|
+
MessageType: {
|
|
11
|
+
values: {
|
|
12
|
+
MessageType_Ping: 60206,
|
|
13
|
+
MessageType_Success: 60207,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function buildAcquirableTransport(path = 'pro-webusb') {
|
|
20
|
+
const webusb = new WebUsbTransport() as any;
|
|
21
|
+
webusb.Log = { debug: jest.fn() };
|
|
22
|
+
webusb.rotateProtocolV2UsbGeneration = jest.fn().mockResolvedValue(undefined);
|
|
23
|
+
webusb.closeOpenDevice = jest.fn().mockResolvedValue(undefined);
|
|
24
|
+
webusb.connect = jest.fn().mockResolvedValue(undefined);
|
|
25
|
+
// Simulate a device that stayed connected since the probe: same USBDevice
|
|
26
|
+
// object present in the device list and recorded as the probed object.
|
|
27
|
+
const deviceObject = { serialNumber: path } as unknown as USBDevice;
|
|
28
|
+
webusb.deviceList = [{ path, device: deviceObject, commType: 'webusb' }];
|
|
29
|
+
webusb.probedDeviceObjects.set(path, deviceObject);
|
|
30
|
+
return webusb;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('WebUsbTransport protocol probe cache', () => {
|
|
34
|
+
test('acquire skips the wire probe when the protocol is already cached', async () => {
|
|
35
|
+
const webusb = buildAcquirableTransport();
|
|
36
|
+
const path = 'pro-webusb';
|
|
37
|
+
webusb.deviceProtocol.set(path, 'V1');
|
|
38
|
+
webusb.detectProtocol = jest.fn();
|
|
39
|
+
|
|
40
|
+
await expect(webusb.acquire({ path, expectedProtocol: 'V1' })).resolves.toBe(path);
|
|
41
|
+
|
|
42
|
+
expect(webusb.detectProtocol).not.toHaveBeenCalled();
|
|
43
|
+
expect(webusb.deviceProtocol.get(path)).toBe('V1');
|
|
44
|
+
expect(webusb.acquiredPaths.has(path)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('acquire re-probes when the caller expects a different protocol than cached', async () => {
|
|
48
|
+
const webusb = buildAcquirableTransport();
|
|
49
|
+
const path = 'pro-webusb';
|
|
50
|
+
webusb.deviceProtocol.set(path, 'V1');
|
|
51
|
+
webusb.detectProtocol = jest.fn().mockImplementation((p: string) => {
|
|
52
|
+
webusb.deviceProtocol.set(p, 'V2');
|
|
53
|
+
return Promise.resolve('V2');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await expect(webusb.acquire({ path, expectedProtocol: 'V2' })).resolves.toBe(path);
|
|
57
|
+
|
|
58
|
+
expect(webusb.detectProtocol).toHaveBeenCalledWith(path, 'V2', undefined);
|
|
59
|
+
expect(webusb.deviceProtocol.get(path)).toBe('V2');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('acquire probes when nothing is cached for the path', async () => {
|
|
63
|
+
const webusb = buildAcquirableTransport();
|
|
64
|
+
const path = 'pro-webusb';
|
|
65
|
+
webusb.detectProtocol = jest.fn().mockResolvedValue('V1');
|
|
66
|
+
|
|
67
|
+
await expect(webusb.acquire({ path, expectedProtocol: 'V1' })).resolves.toBe(path);
|
|
68
|
+
|
|
69
|
+
expect(webusb.detectProtocol).toHaveBeenCalledWith(path, 'V1', undefined);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('acquire re-probes when the USBDevice object identity changed since the probe', async () => {
|
|
73
|
+
const webusb = buildAcquirableTransport();
|
|
74
|
+
const path = 'pro-webusb';
|
|
75
|
+
webusb.deviceProtocol.set(path, 'V1');
|
|
76
|
+
// Simulate a replug the transport never saw a disconnect event for: the OS
|
|
77
|
+
// re-enumerated the device, so the list now holds a NEW USBDevice object.
|
|
78
|
+
webusb.deviceList = [
|
|
79
|
+
{ path, device: { serialNumber: path } as unknown as USBDevice, commType: 'webusb' },
|
|
80
|
+
];
|
|
81
|
+
webusb.detectProtocol = jest.fn().mockImplementation((p: string) => {
|
|
82
|
+
webusb.deviceProtocol.set(p, 'V1');
|
|
83
|
+
return Promise.resolve('V1');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await expect(webusb.acquire({ path, expectedProtocol: 'V1' })).resolves.toBe(path);
|
|
87
|
+
|
|
88
|
+
expect(webusb.detectProtocol).toHaveBeenCalledTimes(1);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('acquire re-probes a stale-marked path even when a protocol is cached', async () => {
|
|
92
|
+
const webusb = buildAcquirableTransport();
|
|
93
|
+
const path = 'pro-webusb';
|
|
94
|
+
webusb.deviceProtocol.set(path, 'V1');
|
|
95
|
+
webusb.markProtocolStale(path);
|
|
96
|
+
webusb.detectProtocol = jest.fn().mockImplementation((p: string) => {
|
|
97
|
+
webusb.deviceProtocol.set(p, 'V1');
|
|
98
|
+
return Promise.resolve('V1');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await expect(webusb.acquire({ path, expectedProtocol: 'V1' })).resolves.toBe(path);
|
|
102
|
+
|
|
103
|
+
expect(webusb.detectProtocol).toHaveBeenCalledTimes(1);
|
|
104
|
+
expect(webusb.staleProtocolPaths.has(path)).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('release keeps the cached V1 protocol so the next acquire can reuse it', async () => {
|
|
108
|
+
const webusb = new WebUsbTransport() as any;
|
|
109
|
+
const path = 'pro-webusb';
|
|
110
|
+
webusb.deviceProtocol.set(path, 'V1');
|
|
111
|
+
webusb.deviceProtocolHints.set(path, 'V2');
|
|
112
|
+
webusb.deviceEndpoints.set(path, { interfaceNumber: 0, endpointIn: 1, endpointOut: 1 });
|
|
113
|
+
webusb.acquiredPaths.add(path);
|
|
114
|
+
webusb.closeOpenDevice = jest.fn().mockResolvedValue(undefined);
|
|
115
|
+
|
|
116
|
+
await webusb.release(path);
|
|
117
|
+
|
|
118
|
+
expect(webusb.deviceProtocol.get(path)).toBe('V1');
|
|
119
|
+
expect(webusb.deviceProtocolHints.get(path)).toBe('V2');
|
|
120
|
+
expect(webusb.deviceEndpoints.has(path)).toBe(false);
|
|
121
|
+
expect(webusb.acquiredPaths.has(path)).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('release still drops a cached V2 protocol through link invalidation', async () => {
|
|
125
|
+
const webusb = new WebUsbTransport() as any;
|
|
126
|
+
const path = 'pro2-webusb';
|
|
127
|
+
webusb.Log = { debug: jest.fn() };
|
|
128
|
+
webusb.messages = transport.parseConfigure(schema);
|
|
129
|
+
webusb.messagesV2 = transport.parseConfigure(schema);
|
|
130
|
+
webusb.writeProtocolV2UsbPacket = jest.fn().mockResolvedValue(undefined);
|
|
131
|
+
let markReadStarted: () => void = () => undefined;
|
|
132
|
+
const readStarted = new Promise<void>(resolve => {
|
|
133
|
+
markReadStarted = resolve;
|
|
134
|
+
});
|
|
135
|
+
webusb.readProtocolV2UsbPacket = jest.fn().mockImplementation(() => {
|
|
136
|
+
markReadStarted();
|
|
137
|
+
return new Promise<void>(() => {});
|
|
138
|
+
});
|
|
139
|
+
webusb.closeOpenDevice = jest.fn().mockResolvedValue(undefined);
|
|
140
|
+
webusb.deviceProtocol.set(path, 'V2');
|
|
141
|
+
await webusb.rotateProtocolV2UsbGeneration(path, 'test connection');
|
|
142
|
+
|
|
143
|
+
const call = webusb.callProtocolV2(path, 'Ping', { message: 'release' });
|
|
144
|
+
await readStarted;
|
|
145
|
+
await webusb.release(path);
|
|
146
|
+
|
|
147
|
+
await expect(call).rejects.toThrow('WebUSB transport released');
|
|
148
|
+
expect(webusb.deviceProtocol.has(path)).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('call and post fail fast for a path that is not acquired', async () => {
|
|
152
|
+
const webusb = new WebUsbTransport() as any;
|
|
153
|
+
const path = 'pro-webusb';
|
|
154
|
+
webusb.Log = { debug: jest.fn() };
|
|
155
|
+
webusb.messages = transport.parseConfigure(schema);
|
|
156
|
+
webusb.messagesV2 = transport.parseConfigure(schema);
|
|
157
|
+
// A surviving protocol cache entry must NOT act as a session token.
|
|
158
|
+
webusb.deviceProtocol.set(path, 'V1');
|
|
159
|
+
|
|
160
|
+
await expect(webusb.call(path, 'Ping', {})).rejects.toMatchObject({
|
|
161
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
162
|
+
message: expect.stringContaining('not acquired'),
|
|
163
|
+
});
|
|
164
|
+
await expect(webusb.post(path, 'Ping', {})).rejects.toMatchObject({
|
|
165
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
166
|
+
message: expect.stringContaining('not acquired'),
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('a transfer-level reconnect marks the protocol stale for the next acquire', async () => {
|
|
171
|
+
const webusb = new WebUsbTransport() as any;
|
|
172
|
+
const path = 'pro-webusb';
|
|
173
|
+
webusb.Log = { debug: jest.fn() };
|
|
174
|
+
webusb.deviceProtocol.set(path, 'V1');
|
|
175
|
+
webusb.findDevice = jest.fn().mockResolvedValue({ opened: false });
|
|
176
|
+
webusb.getConnectedDevices = jest.fn().mockResolvedValue([]);
|
|
177
|
+
webusb.connect = jest.fn().mockResolvedValue(undefined);
|
|
178
|
+
|
|
179
|
+
await webusb.reconnectForPacketIoRetry(path, 'in', 0, new Error('transferIn failed'));
|
|
180
|
+
|
|
181
|
+
// The in-flight call keeps the cached protocol; only the next acquire re-probes.
|
|
182
|
+
expect(webusb.deviceProtocol.get(path)).toBe('V1');
|
|
183
|
+
expect(webusb.staleProtocolPaths.has(path)).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('USB disconnect marks the serial stale and the listener attaches only once', () => {
|
|
187
|
+
const addEventListener = jest.fn();
|
|
188
|
+
const usb = { addEventListener } as unknown as USB;
|
|
189
|
+
const originalNavigator = (globalThis as any).navigator;
|
|
190
|
+
Object.defineProperty(globalThis, 'navigator', {
|
|
191
|
+
value: { usb },
|
|
192
|
+
configurable: true,
|
|
193
|
+
});
|
|
194
|
+
try {
|
|
195
|
+
const first = new WebUsbTransport() as any;
|
|
196
|
+
first.init({ debug: jest.fn() });
|
|
197
|
+
const second = new WebUsbTransport() as any;
|
|
198
|
+
second.init({ debug: jest.fn() });
|
|
199
|
+
|
|
200
|
+
// Module-level listener: one registration across instances.
|
|
201
|
+
expect(addEventListener).toHaveBeenCalledTimes(1);
|
|
202
|
+
const handler = addEventListener.mock.calls[0][1] as (event: {
|
|
203
|
+
device?: { serialNumber?: string | null };
|
|
204
|
+
}) => void;
|
|
205
|
+
|
|
206
|
+
const path = 'pro-webusb';
|
|
207
|
+
first.deviceProtocol.set(path, 'V1');
|
|
208
|
+
second.deviceProtocol.set(path, 'V1');
|
|
209
|
+
handler({ device: { serialNumber: path } });
|
|
210
|
+
|
|
211
|
+
// Routed to the most recently initialized instance; the cached value is
|
|
212
|
+
// retained (in-flight sessions keep working) but marked stale.
|
|
213
|
+
expect(second.staleProtocolPaths.has(path)).toBe(true);
|
|
214
|
+
expect(second.deviceProtocol.get(path)).toBe('V1');
|
|
215
|
+
expect(first.staleProtocolPaths.has(path)).toBe(false);
|
|
216
|
+
|
|
217
|
+
// Events without a usable serial are ignored.
|
|
218
|
+
handler({ device: { serialNumber: null } });
|
|
219
|
+
handler({});
|
|
220
|
+
expect(second.staleProtocolPaths.size).toBe(1);
|
|
221
|
+
} finally {
|
|
222
|
+
Object.defineProperty(globalThis, 'navigator', {
|
|
223
|
+
value: originalNavigator,
|
|
224
|
+
configurable: true,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,21 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
|
20
20
|
/** Per-path protocol type detected by active wire-level probe. */
|
|
21
21
|
private deviceProtocol;
|
|
22
22
|
private deviceProtocolHints;
|
|
23
|
+
/**
|
|
24
|
+
* Paths whose cached protocol must be re-probed on the next acquire (a USB
|
|
25
|
+
* disconnect was seen, or a transfer-level reconnect happened mid-call).
|
|
26
|
+
*/
|
|
27
|
+
private staleProtocolPaths;
|
|
28
|
+
/** Paths currently acquired (between a successful acquire() and its release()). */
|
|
29
|
+
private acquiredPaths;
|
|
30
|
+
/**
|
|
31
|
+
* The exact USBDevice object each cached protocol was probed against. The
|
|
32
|
+
* browser returns the same object identity for a device as long as it stays
|
|
33
|
+
* connected, and a replug/reboot always yields a new object — so an identity
|
|
34
|
+
* mismatch proves the device was re-enumerated since the probe, even when the
|
|
35
|
+
* disconnect event itself was delayed or missed.
|
|
36
|
+
*/
|
|
37
|
+
private probedDeviceObjects;
|
|
23
38
|
/** Per-path USB endpoint / interface numbers (discovered from USB descriptors) */
|
|
24
39
|
private deviceEndpoints;
|
|
25
40
|
name: string;
|
|
@@ -40,6 +55,17 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
|
40
55
|
* Initialize WebUSB transport
|
|
41
56
|
*/
|
|
42
57
|
init(logger: any): void;
|
|
58
|
+
/**
|
|
59
|
+
* Protocol type is a property of the physical device keyed by USB serial number.
|
|
60
|
+
* It can only change across a device reboot (e.g. normal ↔ bootloader mode), and
|
|
61
|
+
* a reboot always surfaces as a USB disconnect. Disconnects (and transfer-level
|
|
62
|
+
* reconnects, which cover a missed disconnect event) only MARK the cached probe
|
|
63
|
+
* result stale instead of deleting it: an in-flight session keeps using the old
|
|
64
|
+
* value so the transfer-level reconnect retries can absorb a transient
|
|
65
|
+
* re-enumeration exactly as they did before the cache existed, while the next
|
|
66
|
+
* acquire re-probes from scratch.
|
|
67
|
+
*/
|
|
68
|
+
markProtocolStale(path: string): void;
|
|
43
69
|
/**
|
|
44
70
|
* Configure Protocol V1 protobuf schema (legacy chunked 0x3F framing).
|
|
45
71
|
*/
|
|
@@ -97,6 +123,14 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
|
97
123
|
connectToDevice(path: string, first: boolean): Promise<void>;
|
|
98
124
|
private closeOpenDevice;
|
|
99
125
|
private clearEndpointHalt;
|
|
126
|
+
/**
|
|
127
|
+
* With the protocol cache surviving release(), deviceProtocol presence no
|
|
128
|
+
* longer implies an active session. Guard call()/post() explicitly so a
|
|
129
|
+
* post-release straggler fails fast instead of silently reopening the device
|
|
130
|
+
* and driving it outside any session (pre-cache behavior: the deleted
|
|
131
|
+
* protocol entry produced the same fail-fast).
|
|
132
|
+
*/
|
|
133
|
+
private assertAcquired;
|
|
100
134
|
post(session: string, name: string, data: Record<string, unknown>): Promise<void>;
|
|
101
135
|
private getErrorMessage;
|
|
102
136
|
private isRetryablePacketIoError;
|
package/dist/index.js
CHANGED
|
@@ -57,6 +57,21 @@ const EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS = 2;
|
|
|
57
57
|
function inferProtocolHintFromDeviceName$1(name) {
|
|
58
58
|
return /\bpro\s*2\b/i.test(name !== null && name !== void 0 ? name : '') ? 'V2' : undefined;
|
|
59
59
|
}
|
|
60
|
+
let activeWebUsbTransport;
|
|
61
|
+
let usbDisconnectListenerAttached = false;
|
|
62
|
+
function registerActiveWebUsbTransport(instance, usb) {
|
|
63
|
+
activeWebUsbTransport = instance;
|
|
64
|
+
if (usbDisconnectListenerAttached)
|
|
65
|
+
return;
|
|
66
|
+
usbDisconnectListenerAttached = true;
|
|
67
|
+
usb.addEventListener('disconnect', event => {
|
|
68
|
+
var _a;
|
|
69
|
+
const serial = (_a = event.device) === null || _a === void 0 ? void 0 : _a.serialNumber;
|
|
70
|
+
if (typeof serial !== 'string' || serial.length === 0)
|
|
71
|
+
return;
|
|
72
|
+
activeWebUsbTransport === null || activeWebUsbTransport === void 0 ? void 0 : activeWebUsbTransport.markProtocolStale(serial);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
60
75
|
class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
61
76
|
constructor() {
|
|
62
77
|
super({
|
|
@@ -66,6 +81,9 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
66
81
|
});
|
|
67
82
|
this.deviceProtocol = new Map();
|
|
68
83
|
this.deviceProtocolHints = new Map();
|
|
84
|
+
this.staleProtocolPaths = new Set();
|
|
85
|
+
this.acquiredPaths = new Set();
|
|
86
|
+
this.probedDeviceObjects = new Map();
|
|
69
87
|
this.deviceEndpoints = new Map();
|
|
70
88
|
this.name = 'WebUsbTransport';
|
|
71
89
|
this.stopped = false;
|
|
@@ -82,6 +100,11 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
82
100
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'WebUSB is not supported by current browsers');
|
|
83
101
|
}
|
|
84
102
|
this.usb = usb;
|
|
103
|
+
registerActiveWebUsbTransport(this, usb);
|
|
104
|
+
}
|
|
105
|
+
markProtocolStale(path) {
|
|
106
|
+
this.staleProtocolPaths.add(path);
|
|
107
|
+
this.probedDeviceObjects.delete(path);
|
|
85
108
|
}
|
|
86
109
|
configure(signedData) {
|
|
87
110
|
const messages = parseConfigure$1(signedData);
|
|
@@ -148,7 +171,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
148
171
|
});
|
|
149
172
|
}
|
|
150
173
|
acquire(input) {
|
|
151
|
-
var _a, _b, _c, _d;
|
|
174
|
+
var _a, _b, _c, _d, _e, _f;
|
|
152
175
|
return __awaiter(this, void 0, void 0, function* () {
|
|
153
176
|
if (!input.path)
|
|
154
177
|
return;
|
|
@@ -156,18 +179,40 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
156
179
|
yield this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
|
|
157
180
|
yield this.closeOpenDevice(input.path);
|
|
158
181
|
yield this.connect((_a = input.path) !== null && _a !== void 0 ? _a : '', true);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (
|
|
164
|
-
this.
|
|
182
|
+
if (this.staleProtocolPaths.has(input.path)) {
|
|
183
|
+
this.staleProtocolPaths.delete(input.path);
|
|
184
|
+
this.deviceProtocol.delete(input.path);
|
|
185
|
+
}
|
|
186
|
+
if (this.deviceProtocol.has(input.path)) {
|
|
187
|
+
const currentDevice = (_b = this.deviceList.find(d => d.path === input.path)) === null || _b === void 0 ? void 0 : _b.device;
|
|
188
|
+
if (!currentDevice || currentDevice !== this.probedDeviceObjects.get(input.path)) {
|
|
189
|
+
this.deviceProtocol.delete(input.path);
|
|
190
|
+
}
|
|
165
191
|
}
|
|
166
|
-
|
|
192
|
+
const cachedProtocol = this.deviceProtocol.get(input.path);
|
|
193
|
+
if (cachedProtocol && input.expectedProtocol && cachedProtocol !== input.expectedProtocol) {
|
|
194
|
+
this.deviceProtocol.delete(input.path);
|
|
195
|
+
}
|
|
196
|
+
if (!this.deviceProtocol.has(input.path)) {
|
|
197
|
+
const deviceName = (_c = this.deviceList.find(device => device.path === input.path)) === null || _c === void 0 ? void 0 : _c.device.productName;
|
|
198
|
+
const protocolHint = input.expectedProtocol
|
|
199
|
+
? undefined
|
|
200
|
+
: (_e = (_d = input.protocolHint) !== null && _d !== void 0 ? _d : this.deviceProtocolHints.get(input.path)) !== null && _e !== void 0 ? _e : inferProtocolHintFromDeviceName$1(deviceName);
|
|
201
|
+
if (protocolHint) {
|
|
202
|
+
this.deviceProtocolHints.set(input.path, protocolHint);
|
|
203
|
+
}
|
|
204
|
+
yield this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
|
|
205
|
+
const probedDevice = (_f = this.deviceList.find(d => d.path === input.path)) === null || _f === void 0 ? void 0 : _f.device;
|
|
206
|
+
if (probedDevice) {
|
|
207
|
+
this.probedDeviceObjects.set(input.path, probedDevice);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
this.acquiredPaths.add(input.path);
|
|
167
211
|
return yield Promise.resolve(input.path);
|
|
168
212
|
}
|
|
169
213
|
catch (e) {
|
|
170
214
|
this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
|
|
215
|
+
this.acquiredPaths.delete(input.path);
|
|
171
216
|
yield this.closeOpenDevice(input.path);
|
|
172
217
|
throw e;
|
|
173
218
|
}
|
|
@@ -351,8 +396,14 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
351
396
|
}
|
|
352
397
|
});
|
|
353
398
|
}
|
|
399
|
+
assertAcquired(path) {
|
|
400
|
+
if (!this.acquiredPaths.has(path)) {
|
|
401
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device is not acquired for ${path}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
354
404
|
post(session, name, data) {
|
|
355
405
|
return __awaiter(this, void 0, void 0, function* () {
|
|
406
|
+
this.assertAcquired(session);
|
|
356
407
|
if (this.deviceProtocol.get(session) === 'V2') {
|
|
357
408
|
yield this.sendProtocolV2UsbFlowControl(session, name, data);
|
|
358
409
|
return;
|
|
@@ -385,6 +436,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
385
436
|
var _a;
|
|
386
437
|
return __awaiter(this, void 0, void 0, function* () {
|
|
387
438
|
this.Log.debug(`[WebUsbTransport] transfer${direction} failed, retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(error)}`);
|
|
439
|
+
this.staleProtocolPaths.add(path);
|
|
388
440
|
yield hdShared.wait(attempt * PACKET_IO_RETRY_DELAY);
|
|
389
441
|
try {
|
|
390
442
|
const currentDevice = yield this.findDevice(path);
|
|
@@ -599,6 +651,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
599
651
|
if (this.messages == null) {
|
|
600
652
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
|
|
601
653
|
}
|
|
654
|
+
this.assertAcquired(path);
|
|
602
655
|
const device = yield this.findDevice(path);
|
|
603
656
|
if (!device) {
|
|
604
657
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound);
|
|
@@ -678,10 +731,9 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
678
731
|
}
|
|
679
732
|
release(path) {
|
|
680
733
|
return __awaiter(this, void 0, void 0, function* () {
|
|
734
|
+
this.acquiredPaths.delete(path);
|
|
681
735
|
yield this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
|
|
682
736
|
yield this.closeOpenDevice(path);
|
|
683
|
-
this.deviceProtocol.delete(path);
|
|
684
|
-
this.deviceProtocolHints.delete(path);
|
|
685
737
|
this.deviceEndpoints.delete(path);
|
|
686
738
|
});
|
|
687
739
|
}
|
package/dist/webusb.d.ts
CHANGED
|
@@ -12,6 +12,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
12
12
|
private protocolV2SchemaSource;
|
|
13
13
|
private deviceProtocol;
|
|
14
14
|
private deviceProtocolHints;
|
|
15
|
+
private staleProtocolPaths;
|
|
16
|
+
private acquiredPaths;
|
|
17
|
+
private probedDeviceObjects;
|
|
15
18
|
private deviceEndpoints;
|
|
16
19
|
name: string;
|
|
17
20
|
stopped: boolean;
|
|
@@ -24,6 +27,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
24
27
|
interfaceId: number;
|
|
25
28
|
constructor();
|
|
26
29
|
init(logger: any): void;
|
|
30
|
+
markProtocolStale(path: string): void;
|
|
27
31
|
configure(signedData: any): void;
|
|
28
32
|
configureProtocolV2(signedData: any): void;
|
|
29
33
|
promptDeviceAccess(): Promise<USBDevice | null>;
|
|
@@ -40,6 +44,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
40
44
|
connectToDevice(path: string, first: boolean): Promise<void>;
|
|
41
45
|
private closeOpenDevice;
|
|
42
46
|
private clearEndpointHalt;
|
|
47
|
+
private assertAcquired;
|
|
43
48
|
post(session: string, name: string, data: Record<string, unknown>): Promise<void>;
|
|
44
49
|
private getErrorMessage;
|
|
45
50
|
private isRetryablePacketIoError;
|
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,EAE3B,MAAM,wBAAwB,CAAC;AAUhC,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EACpB,YAAY,EACZ,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACrB,MAAM,wBAAwB,CAAC;AA0BhC,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.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.136",
|
|
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.0-alpha.
|
|
25
|
-
"@onekeyfe/hd-transport": "1.2.0-alpha.
|
|
24
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.136",
|
|
25
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.136"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@onekeyfe/hd-transport-electron": "1.2.0-alpha.
|
|
28
|
+
"@onekeyfe/hd-transport-electron": "1.2.0-alpha.136",
|
|
29
29
|
"@types/w3c-web-usb": "^1.0.6",
|
|
30
30
|
"@types/web-bluetooth": "^0.0.17"
|
|
31
31
|
},
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "92945bc4aae3d6602dd44c2e028e902e6bce7b55"
|
|
33
33
|
}
|
package/src/webusb.ts
CHANGED
|
@@ -69,6 +69,27 @@ interface TransferCancelToken {
|
|
|
69
69
|
cancelled: boolean;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* The navigator.usb disconnect listener is module-scoped and attached at most
|
|
74
|
+
* once: listeners on navigator.usb are global and never garbage collected, so a
|
|
75
|
+
* per-instance listener would retain every transport instance created across
|
|
76
|
+
* SDK re-initializations. Events are routed to the most recently initialized
|
|
77
|
+
* transport instance instead.
|
|
78
|
+
*/
|
|
79
|
+
let activeWebUsbTransport: WebUsbTransport | undefined;
|
|
80
|
+
let usbDisconnectListenerAttached = false;
|
|
81
|
+
|
|
82
|
+
function registerActiveWebUsbTransport(instance: WebUsbTransport, usb: USB) {
|
|
83
|
+
activeWebUsbTransport = instance;
|
|
84
|
+
if (usbDisconnectListenerAttached) return;
|
|
85
|
+
usbDisconnectListenerAttached = true;
|
|
86
|
+
usb.addEventListener('disconnect', event => {
|
|
87
|
+
const serial = event.device?.serialNumber;
|
|
88
|
+
if (typeof serial !== 'string' || serial.length === 0) return;
|
|
89
|
+
activeWebUsbTransport?.markProtocolStale(serial);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
72
93
|
export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
73
94
|
messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
74
95
|
|
|
@@ -82,6 +103,24 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
82
103
|
|
|
83
104
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
84
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Paths whose cached protocol must be re-probed on the next acquire (a USB
|
|
108
|
+
* disconnect was seen, or a transfer-level reconnect happened mid-call).
|
|
109
|
+
*/
|
|
110
|
+
private staleProtocolPaths: Set<string> = new Set();
|
|
111
|
+
|
|
112
|
+
/** Paths currently acquired (between a successful acquire() and its release()). */
|
|
113
|
+
private acquiredPaths: Set<string> = new Set();
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The exact USBDevice object each cached protocol was probed against. The
|
|
117
|
+
* browser returns the same object identity for a device as long as it stays
|
|
118
|
+
* connected, and a replug/reboot always yields a new object — so an identity
|
|
119
|
+
* mismatch proves the device was re-enumerated since the probe, even when the
|
|
120
|
+
* disconnect event itself was delayed or missed.
|
|
121
|
+
*/
|
|
122
|
+
private probedDeviceObjects: Map<string, USBDevice> = new Map();
|
|
123
|
+
|
|
85
124
|
/** Per-path USB endpoint / interface numbers (discovered from USB descriptors) */
|
|
86
125
|
private deviceEndpoints: Map<string, DeviceEndpoints> = new Map();
|
|
87
126
|
|
|
@@ -129,6 +168,22 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
129
168
|
);
|
|
130
169
|
}
|
|
131
170
|
this.usb = usb;
|
|
171
|
+
registerActiveWebUsbTransport(this, usb);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Protocol type is a property of the physical device keyed by USB serial number.
|
|
176
|
+
* It can only change across a device reboot (e.g. normal ↔ bootloader mode), and
|
|
177
|
+
* a reboot always surfaces as a USB disconnect. Disconnects (and transfer-level
|
|
178
|
+
* reconnects, which cover a missed disconnect event) only MARK the cached probe
|
|
179
|
+
* result stale instead of deleting it: an in-flight session keeps using the old
|
|
180
|
+
* value so the transfer-level reconnect retries can absorb a transient
|
|
181
|
+
* re-enumeration exactly as they did before the cache existed, while the next
|
|
182
|
+
* acquire re-probes from scratch.
|
|
183
|
+
*/
|
|
184
|
+
markProtocolStale(path: string) {
|
|
185
|
+
this.staleProtocolPaths.add(path);
|
|
186
|
+
this.probedDeviceObjects.delete(path);
|
|
132
187
|
}
|
|
133
188
|
|
|
134
189
|
/**
|
|
@@ -231,20 +286,49 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
231
286
|
await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
|
|
232
287
|
await this.closeOpenDevice(input.path);
|
|
233
288
|
await this.connect(input.path ?? '', true);
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
this.
|
|
289
|
+
if (this.staleProtocolPaths.has(input.path)) {
|
|
290
|
+
// The device disconnected (possibly rebooting into another mode) since
|
|
291
|
+
// the protocol was probed — drop the cache so it is re-probed below.
|
|
292
|
+
this.staleProtocolPaths.delete(input.path);
|
|
293
|
+
this.deviceProtocol.delete(input.path);
|
|
294
|
+
}
|
|
295
|
+
if (this.deviceProtocol.has(input.path)) {
|
|
296
|
+
const currentDevice = this.deviceList.find(d => d.path === input.path)?.device;
|
|
297
|
+
if (!currentDevice || currentDevice !== this.probedDeviceObjects.get(input.path)) {
|
|
298
|
+
// The OS re-enumerated the device since the probe (a replug/reboot we
|
|
299
|
+
// may not have seen a disconnect event for) — the cached protocol can
|
|
300
|
+
// no longer be trusted; re-probe on the wire below.
|
|
301
|
+
this.deviceProtocol.delete(input.path);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const cachedProtocol = this.deviceProtocol.get(input.path);
|
|
305
|
+
if (cachedProtocol && input.expectedProtocol && cachedProtocol !== input.expectedProtocol) {
|
|
306
|
+
// The caller expects a different protocol than the cached probe result;
|
|
307
|
+
// the cache is stale — drop it and re-probe on the wire below.
|
|
308
|
+
this.deviceProtocol.delete(input.path);
|
|
309
|
+
}
|
|
310
|
+
if (!this.deviceProtocol.has(input.path)) {
|
|
311
|
+
const deviceName = this.deviceList.find(device => device.path === input.path)?.device
|
|
312
|
+
.productName;
|
|
313
|
+
const protocolHint = input.expectedProtocol
|
|
314
|
+
? undefined
|
|
315
|
+
: input.protocolHint ??
|
|
316
|
+
this.deviceProtocolHints.get(input.path) ??
|
|
317
|
+
inferProtocolHintFromDeviceName(deviceName);
|
|
318
|
+
if (protocolHint) {
|
|
319
|
+
this.deviceProtocolHints.set(input.path, protocolHint);
|
|
320
|
+
}
|
|
321
|
+
await this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
|
|
322
|
+
const probedDevice = this.deviceList.find(d => d.path === input.path)?.device;
|
|
323
|
+
if (probedDevice) {
|
|
324
|
+
this.probedDeviceObjects.set(input.path, probedDevice);
|
|
325
|
+
}
|
|
243
326
|
}
|
|
244
|
-
|
|
327
|
+
this.acquiredPaths.add(input.path);
|
|
245
328
|
return await Promise.resolve(input.path);
|
|
246
329
|
} catch (e) {
|
|
247
330
|
this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
|
|
331
|
+
this.acquiredPaths.delete(input.path);
|
|
248
332
|
await this.closeOpenDevice(input.path);
|
|
249
333
|
throw e;
|
|
250
334
|
}
|
|
@@ -480,7 +564,21 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
480
564
|
}
|
|
481
565
|
}
|
|
482
566
|
|
|
567
|
+
/**
|
|
568
|
+
* With the protocol cache surviving release(), deviceProtocol presence no
|
|
569
|
+
* longer implies an active session. Guard call()/post() explicitly so a
|
|
570
|
+
* post-release straggler fails fast instead of silently reopening the device
|
|
571
|
+
* and driving it outside any session (pre-cache behavior: the deleted
|
|
572
|
+
* protocol entry produced the same fail-fast).
|
|
573
|
+
*/
|
|
574
|
+
private assertAcquired(path: string) {
|
|
575
|
+
if (!this.acquiredPaths.has(path)) {
|
|
576
|
+
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, `Device is not acquired for ${path}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
483
580
|
async post(session: string, name: string, data: Record<string, unknown>) {
|
|
581
|
+
this.assertAcquired(session);
|
|
484
582
|
if (this.deviceProtocol.get(session) === 'V2') {
|
|
485
583
|
await this.sendProtocolV2UsbFlowControl(session, name, data);
|
|
486
584
|
return;
|
|
@@ -522,6 +620,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
522
620
|
error
|
|
523
621
|
)}`
|
|
524
622
|
);
|
|
623
|
+
// The device dropped off the bus mid-call and may have rebooted into a
|
|
624
|
+
// different mode. The in-flight retry keeps the known protocol, but the
|
|
625
|
+
// next acquire must re-probe — this also self-heals a stale cache when the
|
|
626
|
+
// USB disconnect event itself was missed.
|
|
627
|
+
this.staleProtocolPaths.add(path);
|
|
525
628
|
await wait(attempt * PACKET_IO_RETRY_DELAY);
|
|
526
629
|
|
|
527
630
|
try {
|
|
@@ -755,6 +858,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
755
858
|
if (this.messages == null) {
|
|
756
859
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
757
860
|
}
|
|
861
|
+
this.assertAcquired(path);
|
|
758
862
|
|
|
759
863
|
const device = await this.findDevice(path);
|
|
760
864
|
if (!device) {
|
|
@@ -871,10 +975,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
871
975
|
* Release device
|
|
872
976
|
*/
|
|
873
977
|
async release(path: string) {
|
|
978
|
+
this.acquiredPaths.delete(path);
|
|
874
979
|
await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
|
|
875
980
|
await this.closeOpenDevice(path);
|
|
876
|
-
|
|
877
|
-
|
|
981
|
+
// Keep deviceProtocol/deviceProtocolHints across release: the probe result is a
|
|
982
|
+
// physical-device property, so the next acquire can skip the wire-level probe.
|
|
983
|
+
// V2 entries are still dropped by onProtocolV2UsbLinkInvalidated via the link
|
|
984
|
+
// invalidation above, and any entry is re-probed after a USB disconnect or a
|
|
985
|
+
// transfer-level reconnect (see markProtocolStale).
|
|
878
986
|
this.deviceEndpoints.delete(path);
|
|
879
987
|
}
|
|
880
988
|
|