@onekeyfe/hd-transport-usb 1.2.0-alpha.13 → 1.2.0-alpha.131
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__/protocol-v2-link.test.ts +219 -40
- package/dist/index.d.ts +76 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +145 -62
- package/dist/transportLog.d.ts +1 -6
- package/dist/transportLog.d.ts.map +1 -1
- package/jest.config.js +5 -0
- package/package.json +5 -4
- package/src/index.ts +179 -63
- package/src/transportLog.ts +1 -11
|
@@ -62,6 +62,7 @@ const createHarness = () => {
|
|
|
62
62
|
const path = '6136';
|
|
63
63
|
const responseQueue: Buffer[] = [];
|
|
64
64
|
const sentSeqs: number[] = [];
|
|
65
|
+
let cancelledTransferCount = 0;
|
|
65
66
|
let writeError: Error | undefined;
|
|
66
67
|
let holdNextRead:
|
|
67
68
|
| {
|
|
@@ -71,29 +72,84 @@ const createHarness = () => {
|
|
|
71
72
|
}
|
|
72
73
|
| undefined;
|
|
73
74
|
|
|
75
|
+
const performInTransfer = (callback: (error?: Error, data?: Buffer) => void) => {
|
|
76
|
+
if (epIn.timeout === 50) {
|
|
77
|
+
callback(new Error('LIBUSB_TRANSFER_TIMED_OUT'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (holdNextRead) {
|
|
81
|
+
const pending = holdNextRead;
|
|
82
|
+
holdNextRead = undefined;
|
|
83
|
+
pending.callback = callback;
|
|
84
|
+
pending.markStarted();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const response = responseQueue.shift();
|
|
88
|
+
if (!response) {
|
|
89
|
+
callback(new Error('LIBUSB_TRANSFER_TIMED_OUT'));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
callback(undefined, response);
|
|
93
|
+
};
|
|
94
|
+
|
|
74
95
|
const epIn = {
|
|
75
96
|
direction: 'in',
|
|
76
97
|
address: 0x81,
|
|
77
98
|
timeout: 30_000,
|
|
78
99
|
transfer: jest.fn((_length: number, callback: (error?: Error, data?: Buffer) => void) => {
|
|
79
|
-
|
|
80
|
-
callback(new Error('LIBUSB_TRANSFER_TIMED_OUT'));
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
if (holdNextRead) {
|
|
84
|
-
const pending = holdNextRead;
|
|
85
|
-
holdNextRead = undefined;
|
|
86
|
-
pending.callback = callback;
|
|
87
|
-
pending.markStarted();
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const response = responseQueue.shift();
|
|
91
|
-
if (!response) {
|
|
92
|
-
callback(new Error('LIBUSB_TRANSFER_TIMED_OUT'));
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
callback(undefined, response);
|
|
100
|
+
performInTransfer(callback);
|
|
96
101
|
}),
|
|
102
|
+
makeTransfer: jest.fn(
|
|
103
|
+
(
|
|
104
|
+
_timeout: number,
|
|
105
|
+
callback: (error: Error | undefined, data: Buffer, actualLength: number) => void
|
|
106
|
+
) => {
|
|
107
|
+
let settled = false;
|
|
108
|
+
const finish = (error?: Error, data = Buffer.alloc(0)) => {
|
|
109
|
+
if (settled) return;
|
|
110
|
+
settled = true;
|
|
111
|
+
callback(error, data, data.length);
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
submit: jest.fn((buffer: Buffer) => {
|
|
115
|
+
performInTransfer((error, data) => {
|
|
116
|
+
if (error) {
|
|
117
|
+
finish(error);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
data?.copy(buffer);
|
|
121
|
+
finish(undefined, buffer.subarray(0, data?.length ?? 0));
|
|
122
|
+
});
|
|
123
|
+
}),
|
|
124
|
+
cancel: jest.fn(() => {
|
|
125
|
+
cancelledTransferCount += 1;
|
|
126
|
+
finish(new Error('LIBUSB_TRANSFER_CANCELLED'));
|
|
127
|
+
}),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const performOutTransfer = (data: Buffer, callback: (error?: Error) => void) => {
|
|
134
|
+
const seq = data[6];
|
|
135
|
+
sentSeqs.push(seq);
|
|
136
|
+
if (writeError) {
|
|
137
|
+
const error = writeError;
|
|
138
|
+
writeError = undefined;
|
|
139
|
+
callback(error);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
responseQueue.push(
|
|
143
|
+
Buffer.from(
|
|
144
|
+
ProtocolV2.encodeFrame(
|
|
145
|
+
schemas,
|
|
146
|
+
'Success',
|
|
147
|
+
{ message: 'ok' },
|
|
148
|
+
{ router: PROTOCOL_V2_CHANNEL_USB, seq }
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
);
|
|
152
|
+
callback();
|
|
97
153
|
};
|
|
98
154
|
|
|
99
155
|
const epOut = {
|
|
@@ -101,33 +157,40 @@ const createHarness = () => {
|
|
|
101
157
|
address: 0x01,
|
|
102
158
|
timeout: 30_000,
|
|
103
159
|
transfer: jest.fn((data: Buffer, callback: (error?: Error) => void) => {
|
|
104
|
-
|
|
105
|
-
sentSeqs.push(seq);
|
|
106
|
-
if (writeError) {
|
|
107
|
-
const error = writeError;
|
|
108
|
-
writeError = undefined;
|
|
109
|
-
callback(error);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
responseQueue.push(
|
|
113
|
-
Buffer.from(
|
|
114
|
-
ProtocolV2.encodeFrame(
|
|
115
|
-
schemas,
|
|
116
|
-
'Success',
|
|
117
|
-
{ message: 'ok' },
|
|
118
|
-
{ router: PROTOCOL_V2_CHANNEL_USB, seq }
|
|
119
|
-
)
|
|
120
|
-
)
|
|
121
|
-
);
|
|
122
|
-
callback();
|
|
160
|
+
performOutTransfer(data, callback);
|
|
123
161
|
}),
|
|
162
|
+
makeTransfer: jest.fn(
|
|
163
|
+
(
|
|
164
|
+
_timeout: number,
|
|
165
|
+
callback: (error: Error | undefined, data: Buffer, actualLength: number) => void
|
|
166
|
+
) => {
|
|
167
|
+
let settled = false;
|
|
168
|
+
const finish = (error: Error | undefined, data: Buffer) => {
|
|
169
|
+
if (settled) return;
|
|
170
|
+
settled = true;
|
|
171
|
+
callback(error, data, data.length);
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
submit: jest.fn((data: Buffer) => {
|
|
175
|
+
performOutTransfer(data, error => finish(error, data));
|
|
176
|
+
}),
|
|
177
|
+
cancel: jest.fn(() => {
|
|
178
|
+
cancelledTransferCount += 1;
|
|
179
|
+
finish(new Error('LIBUSB_TRANSFER_CANCELLED'), Buffer.alloc(0));
|
|
180
|
+
}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
),
|
|
124
184
|
};
|
|
125
185
|
|
|
126
186
|
const iface = {
|
|
127
187
|
descriptor: { bInterfaceClass: 0xff, bInterfaceNumber: 0 },
|
|
128
188
|
endpoints: [epIn, epOut],
|
|
129
189
|
claim: jest.fn(),
|
|
130
|
-
release: jest.fn((
|
|
190
|
+
release: jest.fn((closeEndpointsOrCallback: boolean | (() => void), callback?: () => void) => {
|
|
191
|
+
if (typeof closeEndpointsOrCallback === 'function') closeEndpointsOrCallback();
|
|
192
|
+
else callback?.();
|
|
193
|
+
}),
|
|
131
194
|
isKernelDriverActive: jest.fn(() => false),
|
|
132
195
|
detachKernelDriver: jest.fn(),
|
|
133
196
|
};
|
|
@@ -164,6 +227,7 @@ const createHarness = () => {
|
|
|
164
227
|
epIn,
|
|
165
228
|
epOut,
|
|
166
229
|
sentSeqs,
|
|
230
|
+
getCancelledTransferCount: () => cancelledTransferCount,
|
|
167
231
|
async acquire() {
|
|
168
232
|
await transport.enumerate();
|
|
169
233
|
await transport.acquire({ path, expectedProtocol: 'V2' });
|
|
@@ -189,7 +253,108 @@ const createHarness = () => {
|
|
|
189
253
|
};
|
|
190
254
|
|
|
191
255
|
describe('NodeUsbTransport Protocol V2 link lifecycle', () => {
|
|
192
|
-
test('
|
|
256
|
+
test('falls back to Protocol V2 when a cached V1 hint is stale', async () => {
|
|
257
|
+
const transport = new NodeUsbTransport() as any;
|
|
258
|
+
const events: string[] = [];
|
|
259
|
+
transport.probeProtocolV1 = jest.fn().mockImplementation(() => {
|
|
260
|
+
events.push('probe-v1');
|
|
261
|
+
return Promise.resolve(false);
|
|
262
|
+
});
|
|
263
|
+
transport.resetConnectionAfterProbe = jest.fn().mockImplementation(() => {
|
|
264
|
+
events.push('reset');
|
|
265
|
+
return Promise.resolve();
|
|
266
|
+
});
|
|
267
|
+
transport.probeProtocolV2 = jest.fn().mockImplementation(() => {
|
|
268
|
+
events.push('probe-v2');
|
|
269
|
+
return Promise.resolve(true);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
await expect(transport.detectProtocol('pro-usb', undefined, 'V1')).resolves.toBe('V2');
|
|
273
|
+
|
|
274
|
+
expect(events).toEqual(['probe-v1', 'reset', 'probe-v2']);
|
|
275
|
+
expect(transport.getProtocolType('pro-usb')).toBe('V2');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('keeps active links when the Protocol V2 schema is configured repeatedly', () => {
|
|
279
|
+
const transport = new NodeUsbTransport() as any;
|
|
280
|
+
transport.invalidateAllProtocolV2UsbLinks = jest.fn().mockResolvedValue(undefined);
|
|
281
|
+
const schemaSource = JSON.stringify(protocolV2Schema);
|
|
282
|
+
|
|
283
|
+
transport.configureProtocolV2(schemaSource);
|
|
284
|
+
transport.configureProtocolV2(schemaSource);
|
|
285
|
+
|
|
286
|
+
expect(transport.invalidateAllProtocolV2UsbLinks).not.toHaveBeenCalled();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('does not retry a native transfer cancelled by probe cleanup', () => {
|
|
290
|
+
const { transport } = createHarness();
|
|
291
|
+
|
|
292
|
+
expect((transport as any).isRetryableError(new Error('LIBUSB_TRANSFER_CANCELLED'))).toBe(false);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('cancels pending native transfers before closing a timed-out protocol probe', async () => {
|
|
296
|
+
const harness = createHarness();
|
|
297
|
+
const { transport, path } = harness;
|
|
298
|
+
await harness.acquire();
|
|
299
|
+
const cancelActiveTransfers = jest.spyOn(transport as any, 'cancelActiveTransfers');
|
|
300
|
+
const closeOpenDevice = jest.spyOn(transport as any, 'closeOpenDevice');
|
|
301
|
+
|
|
302
|
+
await (transport as any).resetConnectionAfterProbe(path);
|
|
303
|
+
|
|
304
|
+
expect(cancelActiveTransfers).toHaveBeenCalledWith(path);
|
|
305
|
+
expect(closeOpenDevice).toHaveBeenCalledWith(path);
|
|
306
|
+
expect(cancelActiveTransfers.mock.invocationCallOrder[0]).toBeLessThan(
|
|
307
|
+
closeOpenDevice.mock.invocationCallOrder[0]
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test('releases the USB interface when protocol detection rejects acquire', async () => {
|
|
312
|
+
const harness = createHarness();
|
|
313
|
+
const { transport, path, device, iface } = harness;
|
|
314
|
+
await transport.enumerate();
|
|
315
|
+
device.close.mockClear();
|
|
316
|
+
iface.release.mockClear();
|
|
317
|
+
jest
|
|
318
|
+
.spyOn(transport as any, 'detectProtocol')
|
|
319
|
+
.mockRejectedValueOnce(new Error('terminal protocol probe failure'));
|
|
320
|
+
|
|
321
|
+
await expect(transport.acquire({ path })).rejects.toMatchObject({
|
|
322
|
+
errorCode: expect.any(Number),
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
expect(iface.release).toHaveBeenCalledTimes(1);
|
|
326
|
+
expect(device.close).toHaveBeenCalledTimes(1);
|
|
327
|
+
expect((transport as any).openDevices.has(path)).toBe(false);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test('stop releases an acquired USB interface even before a Protocol V2 call', async () => {
|
|
331
|
+
const harness = createHarness();
|
|
332
|
+
const { transport, path, device, iface } = harness;
|
|
333
|
+
await transport.enumerate();
|
|
334
|
+
await transport.acquire({ path, expectedProtocol: 'V2' });
|
|
335
|
+
device.close.mockClear();
|
|
336
|
+
iface.release.mockClear();
|
|
337
|
+
|
|
338
|
+
await transport.stop();
|
|
339
|
+
|
|
340
|
+
expect(iface.release).toHaveBeenCalledTimes(1);
|
|
341
|
+
expect(device.close).toHaveBeenCalledTimes(1);
|
|
342
|
+
expect(transport.getProtocolType(path)).toBeUndefined();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('actively probes explicit Protocol V2 during bootloader reconnect', async () => {
|
|
346
|
+
const harness = createHarness();
|
|
347
|
+
const { transport, path, epOut } = harness;
|
|
348
|
+
|
|
349
|
+
await transport.enumerate();
|
|
350
|
+
await transport.acquire({ path, expectedProtocol: 'V2' });
|
|
351
|
+
|
|
352
|
+
expect(epOut.makeTransfer).toHaveBeenCalledTimes(1);
|
|
353
|
+
expect(transport.getProtocolType(path)).toBe('V2');
|
|
354
|
+
await transport.release(path);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test('keeps seq across calls and actively probed reacquire', async () => {
|
|
193
358
|
const harness = createHarness();
|
|
194
359
|
const { transport, path, sentSeqs } = harness;
|
|
195
360
|
|
|
@@ -207,14 +372,14 @@ describe('NodeUsbTransport Protocol V2 link lifecycle', () => {
|
|
|
207
372
|
const harness = createHarness();
|
|
208
373
|
const { transport, path, epOut } = harness;
|
|
209
374
|
await harness.acquire();
|
|
210
|
-
epOut.
|
|
375
|
+
epOut.makeTransfer.mockClear();
|
|
211
376
|
harness.failNextWrite(new Error('LIBUSB_ERROR_IO'));
|
|
212
377
|
|
|
213
378
|
await expect(transport.call(path, 'Ping', { message: 'write-failure' })).rejects.toThrow(
|
|
214
379
|
'LIBUSB_ERROR_IO'
|
|
215
380
|
);
|
|
216
381
|
|
|
217
|
-
expect(epOut.
|
|
382
|
+
expect(epOut.makeTransfer).toHaveBeenCalledTimes(1);
|
|
218
383
|
});
|
|
219
384
|
|
|
220
385
|
test('rejects a pending read when release invalidates the link', async () => {
|
|
@@ -241,6 +406,20 @@ describe('NodeUsbTransport Protocol V2 link lifecycle', () => {
|
|
|
241
406
|
expect(settled).not.toBe('still pending');
|
|
242
407
|
});
|
|
243
408
|
|
|
409
|
+
test('stop cancels an in-flight native USB transfer before releasing the interface', async () => {
|
|
410
|
+
const harness = createHarness();
|
|
411
|
+
const { transport, path } = harness;
|
|
412
|
+
await harness.acquire();
|
|
413
|
+
const pendingRead = harness.holdRead();
|
|
414
|
+
|
|
415
|
+
const call = transport.call(path, 'Ping', { message: 'pending' }, { timeoutMs: 5000 });
|
|
416
|
+
await pendingRead.started;
|
|
417
|
+
await transport.stop();
|
|
418
|
+
|
|
419
|
+
await expect(call).rejects.toThrow();
|
|
420
|
+
expect(harness.getCancelledTransferCount()).toBe(1);
|
|
421
|
+
});
|
|
422
|
+
|
|
244
423
|
test('keeps the cursor after a response timeout rebuilds the USB connection', async () => {
|
|
245
424
|
const harness = createHarness();
|
|
246
425
|
const { transport, path, sentSeqs } = harness;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,48 +2,119 @@ import * as transport from '@onekeyfe/hd-transport';
|
|
|
2
2
|
import transport__default, { ProtocolV2UsbTransportBase, OneKeyDeviceInfo, AcquireInput, TransportCallOptions, ProtocolV2Schemas, ProtocolV2CallContext, ProtocolType } from '@onekeyfe/hd-transport';
|
|
3
3
|
import EventEmitter from 'events';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Node.js USB Transport — complete transport implementation using libusb.
|
|
7
|
+
*
|
|
8
|
+
* Unlike the old UsbPlugin (which was a LowlevelTransportSharedPlugin piped
|
|
9
|
+
* through LowlevelTransport), this class is a standalone transport that handles
|
|
10
|
+
* both protocol encoding/decoding and USB I/O directly.
|
|
11
|
+
*
|
|
12
|
+
* Modeled after WebUsbTransport.
|
|
13
|
+
*/
|
|
5
14
|
declare class NodeUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
6
15
|
messages: ReturnType<typeof transport__default.parseConfigure> | undefined;
|
|
16
|
+
/** Protobuf schema for Protocol V2 transports. */
|
|
7
17
|
messagesV2: ReturnType<typeof transport__default.parseConfigure> | undefined;
|
|
18
|
+
private protocolV2SchemaSource;
|
|
8
19
|
name: string;
|
|
9
20
|
version: string;
|
|
10
21
|
configured: boolean;
|
|
11
22
|
isOutdated: boolean;
|
|
12
23
|
Log?: any;
|
|
13
24
|
emitter?: EventEmitter;
|
|
25
|
+
/** serial → bus id, built during enumerate */
|
|
14
26
|
private serialToBusId;
|
|
27
|
+
/** path → opened device state */
|
|
15
28
|
private openDevices;
|
|
29
|
+
/** Per-path protocol type detected by active wire-level probe. */
|
|
16
30
|
private deviceProtocol;
|
|
31
|
+
/** per-path reconnect lock to prevent concurrent reconnects */
|
|
17
32
|
private reconnectLocks;
|
|
33
|
+
/**
|
|
34
|
+
* Retain the low-level Transfer so release()/stop() can cancel native pending reads.
|
|
35
|
+
* Endpoint.transfer() hides it and may keep the CLI alive after output completes.
|
|
36
|
+
*/
|
|
37
|
+
private activeTransfers;
|
|
38
|
+
/** set to true when cancel() is called; checked by retry loops */
|
|
18
39
|
private cancelled;
|
|
19
40
|
constructor();
|
|
41
|
+
/**
|
|
42
|
+
* Initialize transport.
|
|
43
|
+
* Signature matches the Transport.init interface (logger, emitter).
|
|
44
|
+
*/
|
|
20
45
|
init(logger: any, emitter?: EventEmitter): Promise<string>;
|
|
21
46
|
configure(signedData: any): Promise<void>;
|
|
22
47
|
configureProtocolV2(signedData: any): void;
|
|
23
48
|
listen(): void;
|
|
24
|
-
stop(): void
|
|
49
|
+
stop(): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Low-level post (send only, no response). Not used by NodeUsbTransport
|
|
52
|
+
* since call() handles the full send+receive cycle, but required by the Transport interface.
|
|
53
|
+
*/
|
|
25
54
|
post(path: string, name: string, data: Record<string, unknown>): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Low-level read (receive only). Not used by NodeUsbTransport
|
|
57
|
+
* since call() handles the full send+receive cycle, but required by the Transport interface.
|
|
58
|
+
*/
|
|
26
59
|
read(path: string): Promise<{
|
|
27
60
|
message: {
|
|
28
61
|
[key: string]: any;
|
|
29
62
|
};
|
|
30
63
|
type: string;
|
|
31
64
|
}>;
|
|
65
|
+
/**
|
|
66
|
+
* Enumerate connected OneKey USB devices.
|
|
67
|
+
* Opens each device briefly to read its serial number (used as `path`),
|
|
68
|
+
* then closes it. acquire() re-opens from a fresh getDeviceList().
|
|
69
|
+
*/
|
|
32
70
|
enumerate(): Promise<OneKeyDeviceInfo[]>;
|
|
71
|
+
/**
|
|
72
|
+
* Acquire device — open USB device, claim interface, return path (string).
|
|
73
|
+
*/
|
|
33
74
|
acquire(input: AcquireInput): Promise<string>;
|
|
75
|
+
/**
|
|
76
|
+
* Release device — release interface and close.
|
|
77
|
+
*/
|
|
34
78
|
release(path: string, _onclose?: boolean): Promise<void>;
|
|
79
|
+
private cancelActiveTransfers;
|
|
80
|
+
private createTrackedTransfer;
|
|
81
|
+
private transferInOnce;
|
|
82
|
+
private transferOutOnce;
|
|
35
83
|
private closeOpenDevice;
|
|
84
|
+
/**
|
|
85
|
+
* Call device method — encode protobuf, send packets, receive response.
|
|
86
|
+
* This is the core method that replaces LowlevelTransport's call + UsbPlugin's send/receive.
|
|
87
|
+
*/
|
|
36
88
|
call(path: string, name: string, data: Record<string, unknown>, options?: TransportCallOptions): Promise<transport.MessageFromOneKey>;
|
|
37
89
|
private callProtocolV1;
|
|
38
90
|
cancel(): void;
|
|
91
|
+
/**
|
|
92
|
+
* Get the current open device for a path, re-resolving from the map
|
|
93
|
+
* so callers always use a fresh reference after reconnect.
|
|
94
|
+
*/
|
|
39
95
|
private getOpenDevice;
|
|
40
96
|
private getErrorMessage;
|
|
41
97
|
private isRetryableError;
|
|
42
98
|
private isUsbTransferTimeout;
|
|
43
99
|
private getDeviceInterface;
|
|
100
|
+
/**
|
|
101
|
+
* Reconnect device before retrying a failed transfer (aligned with WebUsbTransport).
|
|
102
|
+
* Uses per-path lock to prevent concurrent reconnects to the same device.
|
|
103
|
+
*/
|
|
44
104
|
private reconnectForRetry;
|
|
105
|
+
/**
|
|
106
|
+
* Send all encoded chunks to the device with retry.
|
|
107
|
+
* If a chunk fails and triggers reconnect, the entire sequence restarts
|
|
108
|
+
* from chunk 0 because the device resets protocol state on reconnect.
|
|
109
|
+
*/
|
|
45
110
|
private sendAllChunksWithRetry;
|
|
111
|
+
/**
|
|
112
|
+
* USB IN transfer with retry and reconnect (aligned with WebUsbTransport).
|
|
113
|
+
*/
|
|
46
114
|
private transferInWithRetry;
|
|
115
|
+
/**
|
|
116
|
+
* Open a USB device by path (serial number), claim interface, cache endpoints.
|
|
117
|
+
*/
|
|
47
118
|
private openDevice;
|
|
48
119
|
private drainStaleInput;
|
|
49
120
|
private createProtocolMismatchError;
|
|
@@ -61,6 +132,10 @@ declare class NodeUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
|
61
132
|
protected onProtocolV2UsbLinkInvalidated(path: string, reason: string): void;
|
|
62
133
|
protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error;
|
|
63
134
|
private callProtocolV2;
|
|
135
|
+
/**
|
|
136
|
+
* Receive a complete protobuf response from the device.
|
|
137
|
+
* Reads 64-byte packets, strips 0x3F marker, reassembles into hex string.
|
|
138
|
+
*/
|
|
64
139
|
private receiveData;
|
|
65
140
|
getProtocolType(path: string): ProtocolType | undefined;
|
|
66
141
|
}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,SAAS,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,SAAS,EAAE,EAQhB,0BAA0B,EAE3B,MAAM,wBAAwB,CAAC;AAGhC,OAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EACV,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACrB,MAAM,wBAAwB,CAAC;AAuIhC,MAAM,CAAC,OAAO,OAAO,gBAAiB,SAAQ,0BAA0B,CAAC,MAAM,CAAC;IAC9E,QAAQ,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAGlE,UAAU,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAEpE,OAAO,CAAC,sBAAsB,CAAqB;IAEnD,IAAI,SAAsB;IAE1B,OAAO,SAAM;IAEb,UAAU,UAAS;IAEnB,UAAU,UAAS;IAEnB,GAAG,CAAC,EAAE,GAAG,CAAC;IAEV,OAAO,CAAC,EAAE,YAAY,CAAC;IAGvB,OAAO,CAAC,aAAa,CAA6B;IAGlD,OAAO,CAAC,WAAW,CAAiC;IAGpD,OAAO,CAAC,cAAc,CAAwC;IAG9D,OAAO,CAAC,cAAc,CAA0C;IAMhE,OAAO,CAAC,eAAe,CAGnB;IAGJ,OAAO,CAAC,SAAS,CAAS;;IAc1B,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,YAAY;IAMxC,SAAS,CAAC,UAAU,EAAE,GAAG;IAOzB,mBAAmB,CAAC,UAAU,EAAE,GAAG;IAiBnC,MAAM;IAIA,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA2BrB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB9E,IAAI,CAAC,IAAI,EAAE,MAAM;;;;;;IAiBjB,SAAS,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IA8BxC,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IA4B7C,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;YAOhD,qBAAqB;IAcnC,OAAO,CAAC,qBAAqB;IAkB7B,OAAO,CAAC,cAAc;IA6BtB,OAAO,CAAC,eAAe;YAoBT,eAAe;IA6BvB,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,oBAAoB;YA0BlB,cAAc;IA0B5B,MAAM;IAUN,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,gBAAgB;IAsBxB,OAAO,CAAC,oBAAoB;IAK5B,OAAO,CAAC,kBAAkB;IAmB1B,OAAO,CAAC,iBAAiB;YAuDX,sBAAsB;YAyCtB,mBAAmB;YA2CnB,UAAU;YA+DV,eAAe;IAiB7B,OAAO,CAAC,2BAA2B;IAOnC,OAAO,CAAC,4BAA4B;YAOtB,cAAc;YAyCd,yBAAyB;YAiBzB,uBAAuB;YA0CvB,eAAe;YAaf,eAAe;IAc7B,SAAS,CAAC,uBAAuB,IAAI,iBAAiB;IAUtD,SAAS,CAAC,sBAAsB;cAIhB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,qBAAqB,GAC9B,OAAO,CAAC,IAAI,CAAC;cAOA,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,qBAAqB,GAC9B,OAAO,CAAC,UAAU,CAAC;cAsBN,4BAA4B,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK1F,SAAS,CAAC,8BAA8B,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAKrE,SAAS,CAAC,+BAA+B,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,KAAK;YAOnE,cAAc;YAad,WAAW;IA6CzB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;CAGxD"}
|
package/dist/index.js
CHANGED
|
@@ -59,14 +59,6 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
|
|
|
59
59
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
-
const HIGH_VOLUME_CALLS = new Set(['FileWrite', 'FilesystemFileWrite', 'EmmcFileWrite']);
|
|
63
|
-
function shouldSuppressHighVolumeCallLog(name) {
|
|
64
|
-
return HIGH_VOLUME_CALLS.has(name);
|
|
65
|
-
}
|
|
66
|
-
function createTransportCallLog(name, protocol) {
|
|
67
|
-
return { name, protocol };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
62
|
const { parseConfigure, ProtocolV1, check } = transport__default["default"];
|
|
71
63
|
const PACKET_SIZE = transport.PROTOCOL_V1_USB_PACKET_SIZE;
|
|
72
64
|
const REPORT_ID = transport.PROTOCOL_V1_REPORT_ID;
|
|
@@ -140,26 +132,6 @@ function readSerialNumber(dev, openDevices) {
|
|
|
140
132
|
}
|
|
141
133
|
});
|
|
142
134
|
}
|
|
143
|
-
function transferInOnce(ep, length) {
|
|
144
|
-
return new Promise((resolve, reject) => {
|
|
145
|
-
ep.transfer(length, (err, data) => {
|
|
146
|
-
if (err)
|
|
147
|
-
return reject(err);
|
|
148
|
-
if (!data || data.length === 0)
|
|
149
|
-
return reject(new Error('Empty USB transfer'));
|
|
150
|
-
resolve(data);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
function transferOutOnce(ep, data) {
|
|
155
|
-
return new Promise((resolve, reject) => {
|
|
156
|
-
ep.transfer(data, (err) => {
|
|
157
|
-
if (err)
|
|
158
|
-
return reject(err);
|
|
159
|
-
resolve();
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
135
|
function skipReportByte(packet) {
|
|
164
136
|
if (packet[0] === REPORT_ID) {
|
|
165
137
|
return packet.subarray(1);
|
|
@@ -184,6 +156,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
184
156
|
this.openDevices = new Map();
|
|
185
157
|
this.deviceProtocol = new Map();
|
|
186
158
|
this.reconnectLocks = new Map();
|
|
159
|
+
this.activeTransfers = new Map();
|
|
187
160
|
this.cancelled = false;
|
|
188
161
|
}
|
|
189
162
|
init(logger, emitter) {
|
|
@@ -198,19 +171,49 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
198
171
|
return Promise.resolve();
|
|
199
172
|
}
|
|
200
173
|
configureProtocolV2(signedData) {
|
|
174
|
+
var _a;
|
|
175
|
+
const schemaSource = typeof signedData === 'string'
|
|
176
|
+
? signedData
|
|
177
|
+
: (_a = JSON.stringify(signedData)) !== null && _a !== void 0 ? _a : String(signedData);
|
|
178
|
+
if (schemaSource === this.protocolV2SchemaSource)
|
|
179
|
+
return;
|
|
180
|
+
const hadProtocolV2Schema = this.protocolV2SchemaSource !== undefined;
|
|
201
181
|
this.messagesV2 = parseConfigure(signedData);
|
|
182
|
+
this.protocolV2SchemaSource = schemaSource;
|
|
183
|
+
if (!hadProtocolV2Schema)
|
|
184
|
+
return;
|
|
202
185
|
this.invalidateAllProtocolV2UsbLinks('Protocol V2 schema reconfigured').catch(error => { var _a; return (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[NodeUsbTransport] schema link cleanup failed:', error); });
|
|
203
186
|
}
|
|
204
187
|
listen() {
|
|
205
188
|
}
|
|
206
189
|
stop() {
|
|
207
|
-
|
|
190
|
+
var _a;
|
|
191
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
192
|
+
this.cancelled = true;
|
|
193
|
+
yield this.cancelActiveTransfers();
|
|
194
|
+
try {
|
|
195
|
+
yield this.disposeProtocolV2UsbLinks('Node USB transport stopped');
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[NodeUsbTransport] stop link cleanup failed:', error);
|
|
199
|
+
}
|
|
200
|
+
yield Promise.allSettled(this.reconnectLocks.values());
|
|
201
|
+
yield this.cancelActiveTransfers();
|
|
202
|
+
yield Promise.all(Array.from(this.openDevices.keys(), path => this.closeOpenDevice(path)));
|
|
203
|
+
this.reconnectLocks.clear();
|
|
204
|
+
this.deviceProtocol.clear();
|
|
205
|
+
this.serialToBusId.clear();
|
|
206
|
+
});
|
|
208
207
|
}
|
|
209
208
|
post(path, name, data) {
|
|
210
209
|
return __awaiter(this, void 0, void 0, function* () {
|
|
211
210
|
if (!this.messages) {
|
|
212
211
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
|
|
213
212
|
}
|
|
213
|
+
if (this.deviceProtocol.get(path) === 'V2') {
|
|
214
|
+
yield this.sendProtocolV2UsbFlowControl(path, name, data);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
214
217
|
const encodeBuffers = ProtocolV1.encodeMessageChunks(this.messages, name, data);
|
|
215
218
|
yield this.sendAllChunksWithRetry(path, encodeBuffers);
|
|
216
219
|
});
|
|
@@ -254,7 +257,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
254
257
|
});
|
|
255
258
|
}
|
|
256
259
|
acquire(input) {
|
|
257
|
-
var _a, _b, _c;
|
|
260
|
+
var _a, _b, _c, _d;
|
|
258
261
|
return __awaiter(this, void 0, void 0, function* () {
|
|
259
262
|
this.cancelled = false;
|
|
260
263
|
const path = (_a = input.path) !== null && _a !== void 0 ? _a : '';
|
|
@@ -265,22 +268,100 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
265
268
|
yield this.rotateProtocolV2UsbGeneration(path, 'Node USB transport acquired');
|
|
266
269
|
yield this.closeOpenDevice(path);
|
|
267
270
|
yield this.openDevice(path);
|
|
268
|
-
yield this.detectProtocol(path, input.expectedProtocol);
|
|
271
|
+
yield this.detectProtocol(path, input.expectedProtocol, input.protocolHint);
|
|
269
272
|
return path;
|
|
270
273
|
}
|
|
271
274
|
catch (error) {
|
|
272
275
|
(_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('NodeUsbTransport acquire error: ', error);
|
|
273
|
-
|
|
276
|
+
try {
|
|
277
|
+
yield this.release(path);
|
|
278
|
+
}
|
|
279
|
+
catch (cleanupError) {
|
|
280
|
+
(_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug('NodeUsbTransport acquire cleanup error: ', cleanupError);
|
|
281
|
+
}
|
|
282
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, (_d = error.message) !== null && _d !== void 0 ? _d : String(error));
|
|
274
283
|
}
|
|
275
284
|
});
|
|
276
285
|
}
|
|
277
286
|
release(path, _onclose) {
|
|
278
287
|
return __awaiter(this, void 0, void 0, function* () {
|
|
288
|
+
yield this.cancelActiveTransfers(path);
|
|
279
289
|
yield this.invalidateProtocolV2UsbLink(path, 'Node USB transport released');
|
|
280
290
|
yield this.closeOpenDevice(path);
|
|
281
291
|
this.deviceProtocol.delete(path);
|
|
282
292
|
});
|
|
283
293
|
}
|
|
294
|
+
cancelActiveTransfers(path) {
|
|
295
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
296
|
+
const transfers = Array.from(this.activeTransfers.entries()).filter(([, active]) => path === undefined || active.path === path);
|
|
297
|
+
transfers.forEach(([transfer]) => {
|
|
298
|
+
try {
|
|
299
|
+
transfer.cancel();
|
|
300
|
+
}
|
|
301
|
+
catch (_a) {
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
yield Promise.allSettled(transfers.map(([, active]) => active.settled));
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
createTrackedTransfer(path, endpoint, callback) {
|
|
308
|
+
let resolveSettled = () => undefined;
|
|
309
|
+
const settled = new Promise(resolve => {
|
|
310
|
+
resolveSettled = resolve;
|
|
311
|
+
});
|
|
312
|
+
const transfer = endpoint.makeTransfer(endpoint.timeout, (error, buffer, actualLength) => {
|
|
313
|
+
this.activeTransfers.delete(transfer);
|
|
314
|
+
resolveSettled();
|
|
315
|
+
callback(error, buffer, actualLength);
|
|
316
|
+
});
|
|
317
|
+
this.activeTransfers.set(transfer, { path, settled, resolveSettled });
|
|
318
|
+
return transfer;
|
|
319
|
+
}
|
|
320
|
+
transferInOnce(path, ep, length) {
|
|
321
|
+
return new Promise((resolve, reject) => {
|
|
322
|
+
const buffer = Buffer.alloc(length);
|
|
323
|
+
const transfer = this.createTrackedTransfer(path, ep, (error, _submittedBuffer, actualLength) => {
|
|
324
|
+
if (error) {
|
|
325
|
+
reject(error);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (actualLength <= 0) {
|
|
329
|
+
reject(new Error('Empty USB transfer'));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
resolve(buffer.subarray(0, actualLength));
|
|
333
|
+
});
|
|
334
|
+
try {
|
|
335
|
+
transfer.submit(buffer);
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
const active = this.activeTransfers.get(transfer);
|
|
339
|
+
this.activeTransfers.delete(transfer);
|
|
340
|
+
active === null || active === void 0 ? void 0 : active.resolveSettled();
|
|
341
|
+
reject(error);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
transferOutOnce(path, ep, data) {
|
|
346
|
+
return new Promise((resolve, reject) => {
|
|
347
|
+
const transfer = this.createTrackedTransfer(path, ep, error => {
|
|
348
|
+
if (error) {
|
|
349
|
+
reject(error);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
resolve();
|
|
353
|
+
});
|
|
354
|
+
try {
|
|
355
|
+
transfer.submit(data);
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
const active = this.activeTransfers.get(transfer);
|
|
359
|
+
this.activeTransfers.delete(transfer);
|
|
360
|
+
active === null || active === void 0 ? void 0 : active.resolveSettled();
|
|
361
|
+
reject(error);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
}
|
|
284
365
|
closeOpenDevice(path) {
|
|
285
366
|
return __awaiter(this, void 0, void 0, function* () {
|
|
286
367
|
const openDev = this.openDevices.get(path);
|
|
@@ -309,7 +390,6 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
309
390
|
});
|
|
310
391
|
}
|
|
311
392
|
call(path, name, data, options) {
|
|
312
|
-
var _a;
|
|
313
393
|
return __awaiter(this, void 0, void 0, function* () {
|
|
314
394
|
this.cancelled = false;
|
|
315
395
|
if (!this.messages) {
|
|
@@ -322,9 +402,6 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
322
402
|
if (!protocol) {
|
|
323
403
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol has not been detected for ${path}`);
|
|
324
404
|
}
|
|
325
|
-
if (!shouldSuppressHighVolumeCallLog(name)) {
|
|
326
|
-
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('transport call', createTransportCallLog(name, protocol));
|
|
327
|
-
}
|
|
328
405
|
if (protocol === 'V2') {
|
|
329
406
|
return this.callProtocolV2(path, name, data, options);
|
|
330
407
|
}
|
|
@@ -370,6 +447,9 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
370
447
|
}
|
|
371
448
|
isRetryableError(error) {
|
|
372
449
|
const message = this.getErrorMessage(error).toLowerCase();
|
|
450
|
+
if (message.includes('cancelled') || message.includes('canceled')) {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
373
453
|
return (message.includes('libusb') ||
|
|
374
454
|
message.includes('transfer') ||
|
|
375
455
|
message.includes('disconnected') ||
|
|
@@ -438,7 +518,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
438
518
|
const packet = new Uint8Array(PACKET_SIZE);
|
|
439
519
|
packet[0] = REPORT_ID;
|
|
440
520
|
packet.set(new Uint8Array(buffer), 1);
|
|
441
|
-
yield transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
521
|
+
yield this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
442
522
|
}
|
|
443
523
|
return;
|
|
444
524
|
}
|
|
@@ -471,7 +551,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
471
551
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
472
552
|
}
|
|
473
553
|
try {
|
|
474
|
-
return yield transferInOnce(currentDev.epIn, length);
|
|
554
|
+
return yield this.transferInOnce(path, currentDev.epIn, length);
|
|
475
555
|
}
|
|
476
556
|
catch (error) {
|
|
477
557
|
lastError = error;
|
|
@@ -530,7 +610,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
530
610
|
}
|
|
531
611
|
epIn.timeout = TRANSFER_TIMEOUT_MS;
|
|
532
612
|
epOut.timeout = TRANSFER_TIMEOUT_MS;
|
|
533
|
-
yield this.drainStaleInput(epIn);
|
|
613
|
+
yield this.drainStaleInput(path, epIn);
|
|
534
614
|
this.openDevices.set(path, { device: dev, iface, epIn, epOut });
|
|
535
615
|
}
|
|
536
616
|
catch (err) {
|
|
@@ -543,14 +623,14 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
543
623
|
}
|
|
544
624
|
});
|
|
545
625
|
}
|
|
546
|
-
drainStaleInput(epIn) {
|
|
626
|
+
drainStaleInput(path, epIn) {
|
|
547
627
|
return __awaiter(this, void 0, void 0, function* () {
|
|
548
628
|
const originalTimeout = epIn.timeout;
|
|
549
629
|
epIn.timeout = 50;
|
|
550
630
|
try {
|
|
551
631
|
for (let index = 0; index < 16; index += 1) {
|
|
552
632
|
try {
|
|
553
|
-
yield transferInOnce(epIn, PACKET_SIZE);
|
|
633
|
+
yield this.transferInOnce(path, epIn, PACKET_SIZE);
|
|
554
634
|
}
|
|
555
635
|
catch (_a) {
|
|
556
636
|
break;
|
|
@@ -568,33 +648,34 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
568
648
|
createProtocolDetectionError() {
|
|
569
649
|
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Unable to detect USB protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping');
|
|
570
650
|
}
|
|
571
|
-
detectProtocol(path, expectedProtocol) {
|
|
651
|
+
detectProtocol(path, expectedProtocol, protocolHint) {
|
|
652
|
+
var _a;
|
|
572
653
|
return __awaiter(this, void 0, void 0, function* () {
|
|
573
|
-
if (expectedProtocol === 'V1') {
|
|
574
|
-
if (yield this.probeProtocolV1(path)) {
|
|
575
|
-
this.deviceProtocol.set(path, 'V1');
|
|
576
|
-
return 'V1';
|
|
577
|
-
}
|
|
578
|
-
throw this.createProtocolMismatchError(expectedProtocol);
|
|
579
|
-
}
|
|
580
654
|
if (expectedProtocol === 'V2') {
|
|
581
655
|
if (yield this.probeProtocolV2(path)) {
|
|
582
656
|
this.deviceProtocol.set(path, 'V2');
|
|
657
|
+
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2 (expected)`);
|
|
583
658
|
return 'V2';
|
|
584
659
|
}
|
|
585
660
|
throw this.createProtocolMismatchError(expectedProtocol);
|
|
586
661
|
}
|
|
587
|
-
if (
|
|
588
|
-
this.
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
this.
|
|
593
|
-
return 'V1';
|
|
662
|
+
if (expectedProtocol === 'V1') {
|
|
663
|
+
if (yield this.probeProtocolV1(path)) {
|
|
664
|
+
this.deviceProtocol.set(path, 'V1');
|
|
665
|
+
return 'V1';
|
|
666
|
+
}
|
|
667
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
594
668
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
669
|
+
const probeOrder = protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
670
|
+
for (const [index, protocol] of probeOrder.entries()) {
|
|
671
|
+
if (index > 0) {
|
|
672
|
+
yield this.resetConnectionAfterProbe(path);
|
|
673
|
+
}
|
|
674
|
+
const detected = protocol === 'V1' ? yield this.probeProtocolV1(path) : yield this.probeProtocolV2(path);
|
|
675
|
+
if (detected) {
|
|
676
|
+
this.deviceProtocol.set(path, protocol);
|
|
677
|
+
return protocol;
|
|
678
|
+
}
|
|
598
679
|
}
|
|
599
680
|
this.deviceProtocol.delete(path);
|
|
600
681
|
throw this.createProtocolDetectionError();
|
|
@@ -605,6 +686,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
605
686
|
return __awaiter(this, void 0, void 0, function* () {
|
|
606
687
|
yield this.rotateProtocolV2UsbGeneration(path, 'Node USB protocol probe reset');
|
|
607
688
|
try {
|
|
689
|
+
yield this.cancelActiveTransfers(path);
|
|
608
690
|
yield this.closeOpenDevice(path);
|
|
609
691
|
}
|
|
610
692
|
catch (error) {
|
|
@@ -696,7 +778,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
696
778
|
if (this.cancelled) {
|
|
697
779
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
698
780
|
}
|
|
699
|
-
yield transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
781
|
+
yield this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
700
782
|
});
|
|
701
783
|
}
|
|
702
784
|
readProtocolV2UsbPacket(path, _context) {
|
|
@@ -706,7 +788,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
706
788
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
707
789
|
}
|
|
708
790
|
try {
|
|
709
|
-
const packet = yield transferInOnce(this.getOpenDevice(path).epIn, transport.PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
791
|
+
const packet = yield this.transferInOnce(path, this.getOpenDevice(path).epIn, transport.PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
710
792
|
return new Uint8Array(packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength));
|
|
711
793
|
}
|
|
712
794
|
catch (error) {
|
|
@@ -719,6 +801,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
719
801
|
}
|
|
720
802
|
resetProtocolV2UsbNativeLink(path, _reason) {
|
|
721
803
|
return __awaiter(this, void 0, void 0, function* () {
|
|
804
|
+
yield this.cancelActiveTransfers(path);
|
|
722
805
|
yield this.closeOpenDevice(path);
|
|
723
806
|
});
|
|
724
807
|
}
|
|
@@ -728,7 +811,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
728
811
|
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
|
|
729
812
|
}
|
|
730
813
|
createProtocolV2UsbTimeoutError(name, timeoutMs) {
|
|
731
|
-
return new
|
|
814
|
+
return new transport.ProtocolV2LinkError('response-timeout', `Protocol V2 response timeout after ${timeoutMs}ms for ${name}`);
|
|
732
815
|
}
|
|
733
816
|
callProtocolV2(path, name, data, options) {
|
|
734
817
|
return __awaiter(this, void 0, void 0, function* () {
|
package/dist/transportLog.d.ts
CHANGED
|
@@ -1,7 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function shouldSuppressHighVolumeCallLog(name: string): boolean;
|
|
3
|
-
export declare function createTransportCallLog(name: string, protocol: ProtocolType): {
|
|
4
|
-
name: string;
|
|
5
|
-
protocol: ProtocolType;
|
|
6
|
-
};
|
|
1
|
+
export { createTransportCallLog, shouldSuppressHighVolumeCallLog } from '@onekeyfe/hd-transport';
|
|
7
2
|
//# sourceMappingURL=transportLog.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transportLog.d.ts","sourceRoot":"","sources":["../src/transportLog.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"transportLog.d.ts","sourceRoot":"","sources":["../src/transportLog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,+BAA+B,EAAE,MAAM,wBAAwB,CAAC"}
|
package/jest.config.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport-usb",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.131",
|
|
4
4
|
"description": "OneKey hardware wallet direct USB transport plugin (libusb)",
|
|
5
5
|
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,14 +16,15 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"dev": "rimraf dist && rollup -c ../../build/rollup.config.js -w",
|
|
18
18
|
"build": "rimraf dist && rollup -c ../../build/rollup.config.js",
|
|
19
|
+
"test": "jest",
|
|
19
20
|
"lint": "eslint .",
|
|
20
21
|
"lint:fix": "eslint . --fix"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
|
-
"@onekeyfe/hd-shared": "1.2.0-alpha.
|
|
24
|
-
"@onekeyfe/hd-transport": "1.2.0-alpha.
|
|
24
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.131",
|
|
25
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.131",
|
|
25
26
|
"bytebuffer": "^5.0.1",
|
|
26
27
|
"usb": "^2.14.0"
|
|
27
28
|
},
|
|
28
|
-
"gitHead": "
|
|
29
|
+
"gitHead": "81b472e983e80e6aa771b991dc9c0c9b1bdcf07b"
|
|
29
30
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,13 +7,12 @@ import transport, {
|
|
|
7
7
|
PROTOCOL_V1_USB_PACKET_SIZE,
|
|
8
8
|
PROTOCOL_V2_CHANNEL_USB,
|
|
9
9
|
PROTOCOL_V2_FRAME_MAX_BYTES,
|
|
10
|
+
ProtocolV2LinkError,
|
|
10
11
|
ProtocolV2UsbTransportBase,
|
|
11
12
|
probeProtocolV2 as probeProtocolV2Helper,
|
|
12
13
|
} from '@onekeyfe/hd-transport';
|
|
13
14
|
import { ERRORS, HardwareErrorCode, ONEKEY_WEBUSB_FILTER, wait } from '@onekeyfe/hd-shared';
|
|
14
15
|
|
|
15
|
-
import { createTransportCallLog, shouldSuppressHighVolumeCallLog } from './transportLog';
|
|
16
|
-
|
|
17
16
|
import type EventEmitter from 'events';
|
|
18
17
|
import type {
|
|
19
18
|
AcquireInput,
|
|
@@ -131,31 +130,6 @@ function readSerialNumber(dev: usb.Device, openDevices?: Map<string, OpenDevice>
|
|
|
131
130
|
});
|
|
132
131
|
}
|
|
133
132
|
|
|
134
|
-
/**
|
|
135
|
-
* Promisified USB IN transfer (single attempt).
|
|
136
|
-
*/
|
|
137
|
-
function transferInOnce(ep: usb.InEndpoint, length: number): Promise<Buffer> {
|
|
138
|
-
return new Promise((resolve, reject) => {
|
|
139
|
-
ep.transfer(length, (err: Error | undefined, data: Buffer | undefined) => {
|
|
140
|
-
if (err) return reject(err);
|
|
141
|
-
if (!data || data.length === 0) return reject(new Error('Empty USB transfer'));
|
|
142
|
-
resolve(data);
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Promisified USB OUT transfer (single attempt).
|
|
149
|
-
*/
|
|
150
|
-
function transferOutOnce(ep: usb.OutEndpoint, data: Buffer): Promise<void> {
|
|
151
|
-
return new Promise((resolve, reject) => {
|
|
152
|
-
ep.transfer(data, (err: Error | undefined) => {
|
|
153
|
-
if (err) return reject(err);
|
|
154
|
-
resolve();
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
133
|
/**
|
|
160
134
|
* Skip the 0x3F protocol marker byte from a USB packet.
|
|
161
135
|
*/
|
|
@@ -188,6 +162,8 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
188
162
|
/** Protobuf schema for Protocol V2 transports. */
|
|
189
163
|
messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
190
164
|
|
|
165
|
+
private protocolV2SchemaSource: string | undefined;
|
|
166
|
+
|
|
191
167
|
name = 'NodeUsbTransport';
|
|
192
168
|
|
|
193
169
|
version = '';
|
|
@@ -212,6 +188,15 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
212
188
|
/** per-path reconnect lock to prevent concurrent reconnects */
|
|
213
189
|
private reconnectLocks = new Map<string, Promise<OpenDevice>>();
|
|
214
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Retain the low-level Transfer so release()/stop() can cancel native pending reads.
|
|
193
|
+
* Endpoint.transfer() hides it and may keep the CLI alive after output completes.
|
|
194
|
+
*/
|
|
195
|
+
private activeTransfers = new Map<
|
|
196
|
+
usb.Transfer,
|
|
197
|
+
{ path: string; settled: Promise<void>; resolveSettled: () => void }
|
|
198
|
+
>();
|
|
199
|
+
|
|
215
200
|
/** set to true when cancel() is called; checked by retry loops */
|
|
216
201
|
private cancelled = false;
|
|
217
202
|
|
|
@@ -241,7 +226,17 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
241
226
|
}
|
|
242
227
|
|
|
243
228
|
configureProtocolV2(signedData: any) {
|
|
229
|
+
const schemaSource =
|
|
230
|
+
typeof signedData === 'string'
|
|
231
|
+
? signedData
|
|
232
|
+
: JSON.stringify(signedData) ?? String(signedData);
|
|
233
|
+
if (schemaSource === this.protocolV2SchemaSource) return;
|
|
234
|
+
|
|
235
|
+
const hadProtocolV2Schema = this.protocolV2SchemaSource !== undefined;
|
|
244
236
|
this.messagesV2 = parseConfigure(signedData);
|
|
237
|
+
this.protocolV2SchemaSource = schemaSource;
|
|
238
|
+
if (!hadProtocolV2Schema) return;
|
|
239
|
+
|
|
245
240
|
this.invalidateAllProtocolV2UsbLinks('Protocol V2 schema reconfigured').catch(error =>
|
|
246
241
|
this.Log?.debug('[NodeUsbTransport] schema link cleanup failed:', error)
|
|
247
242
|
);
|
|
@@ -251,10 +246,27 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
251
246
|
// empty — could add hotplug events via usb.on('attach'/'detach')
|
|
252
247
|
}
|
|
253
248
|
|
|
254
|
-
stop() {
|
|
255
|
-
this.
|
|
256
|
-
|
|
257
|
-
);
|
|
249
|
+
async stop(): Promise<void> {
|
|
250
|
+
this.cancelled = true;
|
|
251
|
+
|
|
252
|
+
await this.cancelActiveTransfers();
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
await this.disposeProtocolV2UsbLinks('Node USB transport stopped');
|
|
256
|
+
} catch (error) {
|
|
257
|
+
this.Log?.debug('[NodeUsbTransport] stop link cleanup failed:', error);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Reconnect may already be in flight when dispose starts. Wait for it to
|
|
261
|
+
// settle, then close every remaining handle so it cannot reopen USB after
|
|
262
|
+
// the first cleanup pass.
|
|
263
|
+
await Promise.allSettled(this.reconnectLocks.values());
|
|
264
|
+
await this.cancelActiveTransfers();
|
|
265
|
+
await Promise.all(Array.from(this.openDevices.keys(), path => this.closeOpenDevice(path)));
|
|
266
|
+
|
|
267
|
+
this.reconnectLocks.clear();
|
|
268
|
+
this.deviceProtocol.clear();
|
|
269
|
+
this.serialToBusId.clear();
|
|
258
270
|
}
|
|
259
271
|
|
|
260
272
|
/**
|
|
@@ -265,6 +277,10 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
265
277
|
if (!this.messages) {
|
|
266
278
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
267
279
|
}
|
|
280
|
+
if (this.deviceProtocol.get(path) === 'V2') {
|
|
281
|
+
await this.sendProtocolV2UsbFlowControl(path, name, data);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
268
284
|
const encodeBuffers = ProtocolV1.encodeMessageChunks(this.messages, name, data);
|
|
269
285
|
await this.sendAllChunksWithRetry(path, encodeBuffers);
|
|
270
286
|
}
|
|
@@ -332,10 +348,15 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
332
348
|
await this.rotateProtocolV2UsbGeneration(path, 'Node USB transport acquired');
|
|
333
349
|
await this.closeOpenDevice(path);
|
|
334
350
|
await this.openDevice(path);
|
|
335
|
-
await this.detectProtocol(path, input.expectedProtocol);
|
|
351
|
+
await this.detectProtocol(path, input.expectedProtocol, input.protocolHint);
|
|
336
352
|
return path;
|
|
337
353
|
} catch (error: any) {
|
|
338
354
|
this.Log?.debug('NodeUsbTransport acquire error: ', error);
|
|
355
|
+
try {
|
|
356
|
+
await this.release(path);
|
|
357
|
+
} catch (cleanupError) {
|
|
358
|
+
this.Log?.debug('NodeUsbTransport acquire cleanup error: ', cleanupError);
|
|
359
|
+
}
|
|
339
360
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, error.message ?? String(error));
|
|
340
361
|
}
|
|
341
362
|
}
|
|
@@ -344,11 +365,93 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
344
365
|
* Release device — release interface and close.
|
|
345
366
|
*/
|
|
346
367
|
async release(path: string, _onclose?: boolean): Promise<void> {
|
|
368
|
+
await this.cancelActiveTransfers(path);
|
|
347
369
|
await this.invalidateProtocolV2UsbLink(path, 'Node USB transport released');
|
|
348
370
|
await this.closeOpenDevice(path);
|
|
349
371
|
this.deviceProtocol.delete(path);
|
|
350
372
|
}
|
|
351
373
|
|
|
374
|
+
private async cancelActiveTransfers(path?: string): Promise<void> {
|
|
375
|
+
const transfers = Array.from(this.activeTransfers.entries()).filter(
|
|
376
|
+
([, active]) => path === undefined || active.path === path
|
|
377
|
+
);
|
|
378
|
+
transfers.forEach(([transfer]) => {
|
|
379
|
+
try {
|
|
380
|
+
transfer.cancel();
|
|
381
|
+
} catch {
|
|
382
|
+
// A transfer may finish between the snapshot and cancel().
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
await Promise.allSettled(transfers.map(([, active]) => active.settled));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private createTrackedTransfer(
|
|
389
|
+
path: string,
|
|
390
|
+
endpoint: usb.InEndpoint | usb.OutEndpoint,
|
|
391
|
+
callback: (error: Error | undefined, buffer: Buffer, actualLength: number) => void
|
|
392
|
+
): usb.Transfer {
|
|
393
|
+
let resolveSettled: () => void = () => undefined;
|
|
394
|
+
const settled = new Promise<void>(resolve => {
|
|
395
|
+
resolveSettled = resolve;
|
|
396
|
+
});
|
|
397
|
+
const transfer = endpoint.makeTransfer(endpoint.timeout, (error, buffer, actualLength) => {
|
|
398
|
+
this.activeTransfers.delete(transfer);
|
|
399
|
+
resolveSettled();
|
|
400
|
+
callback(error, buffer, actualLength);
|
|
401
|
+
});
|
|
402
|
+
this.activeTransfers.set(transfer, { path, settled, resolveSettled });
|
|
403
|
+
return transfer;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private transferInOnce(path: string, ep: usb.InEndpoint, length: number): Promise<Buffer> {
|
|
407
|
+
return new Promise((resolve, reject) => {
|
|
408
|
+
const buffer = Buffer.alloc(length);
|
|
409
|
+
const transfer = this.createTrackedTransfer(
|
|
410
|
+
path,
|
|
411
|
+
ep,
|
|
412
|
+
(error, _submittedBuffer, actualLength) => {
|
|
413
|
+
if (error) {
|
|
414
|
+
reject(error);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (actualLength <= 0) {
|
|
418
|
+
reject(new Error('Empty USB transfer'));
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
resolve(buffer.subarray(0, actualLength));
|
|
422
|
+
}
|
|
423
|
+
);
|
|
424
|
+
try {
|
|
425
|
+
transfer.submit(buffer);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
const active = this.activeTransfers.get(transfer);
|
|
428
|
+
this.activeTransfers.delete(transfer);
|
|
429
|
+
active?.resolveSettled();
|
|
430
|
+
reject(error);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private transferOutOnce(path: string, ep: usb.OutEndpoint, data: Buffer): Promise<void> {
|
|
436
|
+
return new Promise((resolve, reject) => {
|
|
437
|
+
const transfer = this.createTrackedTransfer(path, ep, error => {
|
|
438
|
+
if (error) {
|
|
439
|
+
reject(error);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
resolve();
|
|
443
|
+
});
|
|
444
|
+
try {
|
|
445
|
+
transfer.submit(data);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
const active = this.activeTransfers.get(transfer);
|
|
448
|
+
this.activeTransfers.delete(transfer);
|
|
449
|
+
active?.resolveSettled();
|
|
450
|
+
reject(error);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
352
455
|
private async closeOpenDevice(path: string): Promise<void> {
|
|
353
456
|
const openDev = this.openDevices.get(path);
|
|
354
457
|
if (!openDev) return;
|
|
@@ -401,10 +504,6 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
401
504
|
`Device protocol has not been detected for ${path}`
|
|
402
505
|
);
|
|
403
506
|
}
|
|
404
|
-
if (!shouldSuppressHighVolumeCallLog(name)) {
|
|
405
|
-
this.Log?.debug('transport call', createTransportCallLog(name, protocol));
|
|
406
|
-
}
|
|
407
|
-
|
|
408
507
|
if (protocol === 'V2') {
|
|
409
508
|
return this.callProtocolV2(path, name, data, options);
|
|
410
509
|
}
|
|
@@ -468,6 +567,12 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
468
567
|
|
|
469
568
|
private isRetryableError(error: unknown): boolean {
|
|
470
569
|
const message = this.getErrorMessage(error).toLowerCase();
|
|
570
|
+
// cancelActiveTransfers() terminates timed-out or releasing native requests.
|
|
571
|
+
// LIBUSB_TRANSFER_CANCELLED is not transient. Retrying would start another pending
|
|
572
|
+
// read on the rebuilt interface and race protocol probing against release.
|
|
573
|
+
if (message.includes('cancelled') || message.includes('canceled')) {
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
471
576
|
return (
|
|
472
577
|
message.includes('libusb') ||
|
|
473
578
|
message.includes('transfer') ||
|
|
@@ -572,7 +677,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
572
677
|
const packet = new Uint8Array(PACKET_SIZE);
|
|
573
678
|
packet[0] = REPORT_ID;
|
|
574
679
|
packet.set(new Uint8Array(buffer), 1);
|
|
575
|
-
await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
680
|
+
await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
576
681
|
}
|
|
577
682
|
return; // all chunks sent successfully
|
|
578
683
|
} catch (error) {
|
|
@@ -615,7 +720,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
615
720
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
616
721
|
}
|
|
617
722
|
try {
|
|
618
|
-
return await transferInOnce(currentDev.epIn, length);
|
|
723
|
+
return await this.transferInOnce(path, currentDev.epIn, length);
|
|
619
724
|
} catch (error) {
|
|
620
725
|
lastError = error;
|
|
621
726
|
if (options?.waitIndefinitelyOnTimeout && this.isUsbTransferTimeout(error)) {
|
|
@@ -695,7 +800,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
695
800
|
epIn.timeout = TRANSFER_TIMEOUT_MS;
|
|
696
801
|
epOut.timeout = TRANSFER_TIMEOUT_MS;
|
|
697
802
|
|
|
698
|
-
await this.drainStaleInput(epIn);
|
|
803
|
+
await this.drainStaleInput(path, epIn);
|
|
699
804
|
|
|
700
805
|
this.openDevices.set(path, { device: dev, iface, epIn, epOut });
|
|
701
806
|
} catch (err) {
|
|
@@ -708,14 +813,14 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
708
813
|
}
|
|
709
814
|
}
|
|
710
815
|
|
|
711
|
-
private async drainStaleInput(epIn: usb.InEndpoint): Promise<void> {
|
|
816
|
+
private async drainStaleInput(path: string, epIn: usb.InEndpoint): Promise<void> {
|
|
712
817
|
const originalTimeout = epIn.timeout;
|
|
713
818
|
epIn.timeout = 50;
|
|
714
819
|
try {
|
|
715
820
|
// Drain a small bounded number of packets left by the previous USB session.
|
|
716
821
|
for (let index = 0; index < 16; index += 1) {
|
|
717
822
|
try {
|
|
718
|
-
await transferInOnce(epIn, PACKET_SIZE);
|
|
823
|
+
await this.transferInOnce(path, epIn, PACKET_SIZE);
|
|
719
824
|
} catch {
|
|
720
825
|
break;
|
|
721
826
|
}
|
|
@@ -741,37 +846,39 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
741
846
|
|
|
742
847
|
private async detectProtocol(
|
|
743
848
|
path: string,
|
|
744
|
-
expectedProtocol?: ProtocolType
|
|
849
|
+
expectedProtocol?: ProtocolType,
|
|
850
|
+
protocolHint?: ProtocolType
|
|
745
851
|
): Promise<ProtocolType> {
|
|
746
|
-
if (expectedProtocol === 'V1') {
|
|
747
|
-
if (await this.probeProtocolV1(path)) {
|
|
748
|
-
this.deviceProtocol.set(path, 'V1');
|
|
749
|
-
return 'V1';
|
|
750
|
-
}
|
|
751
|
-
throw this.createProtocolMismatchError(expectedProtocol);
|
|
752
|
-
}
|
|
753
|
-
|
|
754
852
|
if (expectedProtocol === 'V2') {
|
|
755
853
|
if (await this.probeProtocolV2(path)) {
|
|
756
854
|
this.deviceProtocol.set(path, 'V2');
|
|
855
|
+
this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2 (expected)`);
|
|
757
856
|
return 'V2';
|
|
758
857
|
}
|
|
759
858
|
throw this.createProtocolMismatchError(expectedProtocol);
|
|
760
859
|
}
|
|
761
860
|
|
|
762
|
-
if (
|
|
763
|
-
this.
|
|
764
|
-
|
|
861
|
+
if (expectedProtocol === 'V1') {
|
|
862
|
+
if (await this.probeProtocolV1(path)) {
|
|
863
|
+
this.deviceProtocol.set(path, 'V1');
|
|
864
|
+
return 'V1';
|
|
865
|
+
}
|
|
866
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
765
867
|
}
|
|
766
868
|
|
|
767
|
-
|
|
768
|
-
this.deviceProtocol.
|
|
769
|
-
return 'V1';
|
|
770
|
-
}
|
|
869
|
+
const probeOrder: ProtocolType[] =
|
|
870
|
+
protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
771
871
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
872
|
+
for (const [index, protocol] of probeOrder.entries()) {
|
|
873
|
+
if (index > 0) {
|
|
874
|
+
await this.resetConnectionAfterProbe(path);
|
|
875
|
+
}
|
|
876
|
+
const detected =
|
|
877
|
+
protocol === 'V1' ? await this.probeProtocolV1(path) : await this.probeProtocolV2(path);
|
|
878
|
+
if (detected) {
|
|
879
|
+
this.deviceProtocol.set(path, protocol);
|
|
880
|
+
return protocol;
|
|
881
|
+
}
|
|
775
882
|
}
|
|
776
883
|
|
|
777
884
|
this.deviceProtocol.delete(path);
|
|
@@ -782,6 +889,10 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
782
889
|
await this.rotateProtocolV2UsbGeneration(path, 'Node USB protocol probe reset');
|
|
783
890
|
|
|
784
891
|
try {
|
|
892
|
+
// A timed-out probe may leave an IN transfer pending. Cancel and await it before
|
|
893
|
+
// closing the interface, or libusb may never callback and release()/stop() will
|
|
894
|
+
// wait forever, surfacing as Polling timeout (809).
|
|
895
|
+
await this.cancelActiveTransfers(path);
|
|
785
896
|
await this.closeOpenDevice(path);
|
|
786
897
|
} catch (error) {
|
|
787
898
|
this.Log?.debug('[NodeUsbTransport] close after protocol probe error:', error);
|
|
@@ -882,7 +993,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
882
993
|
if (this.cancelled) {
|
|
883
994
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
884
995
|
}
|
|
885
|
-
await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
996
|
+
await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
886
997
|
}
|
|
887
998
|
|
|
888
999
|
protected async readProtocolV2UsbPacket(
|
|
@@ -894,7 +1005,8 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
894
1005
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
895
1006
|
}
|
|
896
1007
|
try {
|
|
897
|
-
const packet = await transferInOnce(
|
|
1008
|
+
const packet = await this.transferInOnce(
|
|
1009
|
+
path,
|
|
898
1010
|
this.getOpenDevice(path).epIn,
|
|
899
1011
|
PROTOCOL_V2_FRAME_MAX_BYTES
|
|
900
1012
|
);
|
|
@@ -910,6 +1022,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
910
1022
|
}
|
|
911
1023
|
|
|
912
1024
|
protected async resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise<void> {
|
|
1025
|
+
await this.cancelActiveTransfers(path);
|
|
913
1026
|
await this.closeOpenDevice(path);
|
|
914
1027
|
}
|
|
915
1028
|
|
|
@@ -919,7 +1032,10 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
919
1032
|
}
|
|
920
1033
|
|
|
921
1034
|
protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error {
|
|
922
|
-
return new
|
|
1035
|
+
return new ProtocolV2LinkError(
|
|
1036
|
+
'response-timeout',
|
|
1037
|
+
`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`
|
|
1038
|
+
);
|
|
923
1039
|
}
|
|
924
1040
|
|
|
925
1041
|
private async callProtocolV2(
|
package/src/transportLog.ts
CHANGED
|
@@ -1,11 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const HIGH_VOLUME_CALLS = new Set(['FileWrite', 'FilesystemFileWrite', 'EmmcFileWrite']);
|
|
4
|
-
|
|
5
|
-
export function shouldSuppressHighVolumeCallLog(name: string) {
|
|
6
|
-
return HIGH_VOLUME_CALLS.has(name);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function createTransportCallLog(name: string, protocol: ProtocolType) {
|
|
10
|
-
return { name, protocol };
|
|
11
|
-
}
|
|
1
|
+
export { createTransportCallLog, shouldSuppressHighVolumeCallLog } from '@onekeyfe/hd-transport';
|