@onekeyfe/hd-transport-usb 1.2.0-alpha.17 → 1.2.0-alpha.170
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 +211 -44
- package/dist/index.d.ts +76 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +148 -60
- 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 +190 -64
- 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,19 +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 () => {
|
|
193
346
|
const harness = createHarness();
|
|
194
347
|
const { transport, path, epOut } = harness;
|
|
195
348
|
|
|
196
349
|
await transport.enumerate();
|
|
197
350
|
await transport.acquire({ path, expectedProtocol: 'V2' });
|
|
198
351
|
|
|
199
|
-
expect(epOut.
|
|
352
|
+
expect(epOut.makeTransfer).toHaveBeenCalledTimes(1);
|
|
200
353
|
expect(transport.getProtocolType(path)).toBe('V2');
|
|
201
354
|
await transport.release(path);
|
|
202
355
|
});
|
|
203
356
|
|
|
204
|
-
test('keeps seq across calls and
|
|
357
|
+
test('keeps seq across calls and actively probed reacquire', async () => {
|
|
205
358
|
const harness = createHarness();
|
|
206
359
|
const { transport, path, sentSeqs } = harness;
|
|
207
360
|
|
|
@@ -211,7 +364,7 @@ describe('NodeUsbTransport Protocol V2 link lifecycle', () => {
|
|
|
211
364
|
await harness.acquire();
|
|
212
365
|
await transport.call(path, 'Ping', { message: 'second' });
|
|
213
366
|
|
|
214
|
-
expect(sentSeqs).toEqual([1, 2]);
|
|
367
|
+
expect(sentSeqs).toEqual([1, 2, 3, 4]);
|
|
215
368
|
await transport.release(path);
|
|
216
369
|
});
|
|
217
370
|
|
|
@@ -219,14 +372,14 @@ describe('NodeUsbTransport Protocol V2 link lifecycle', () => {
|
|
|
219
372
|
const harness = createHarness();
|
|
220
373
|
const { transport, path, epOut } = harness;
|
|
221
374
|
await harness.acquire();
|
|
222
|
-
epOut.
|
|
375
|
+
epOut.makeTransfer.mockClear();
|
|
223
376
|
harness.failNextWrite(new Error('LIBUSB_ERROR_IO'));
|
|
224
377
|
|
|
225
378
|
await expect(transport.call(path, 'Ping', { message: 'write-failure' })).rejects.toThrow(
|
|
226
379
|
'LIBUSB_ERROR_IO'
|
|
227
380
|
);
|
|
228
381
|
|
|
229
|
-
expect(epOut.
|
|
382
|
+
expect(epOut.makeTransfer).toHaveBeenCalledTimes(1);
|
|
230
383
|
});
|
|
231
384
|
|
|
232
385
|
test('rejects a pending read when release invalidates the link', async () => {
|
|
@@ -253,6 +406,20 @@ describe('NodeUsbTransport Protocol V2 link lifecycle', () => {
|
|
|
253
406
|
expect(settled).not.toBe('still pending');
|
|
254
407
|
});
|
|
255
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
|
+
|
|
256
423
|
test('keeps the cursor after a response timeout rebuilds the USB connection', async () => {
|
|
257
424
|
const harness = createHarness();
|
|
258
425
|
const { transport, path, sentSeqs } = harness;
|
|
@@ -266,7 +433,7 @@ describe('NodeUsbTransport Protocol V2 link lifecycle', () => {
|
|
|
266
433
|
await harness.acquire();
|
|
267
434
|
await transport.call(path, 'Ping', { message: 'after-timeout' });
|
|
268
435
|
|
|
269
|
-
expect(sentSeqs).toEqual([1, 2]);
|
|
436
|
+
expect(sentSeqs).toEqual([1, 2, 3, 4]);
|
|
270
437
|
await transport.release(path);
|
|
271
438
|
});
|
|
272
439
|
});
|
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;AAShC,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;IAiC7C,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, _e;
|
|
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,104 @@ 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
|
-
|
|
271
|
+
const descriptor = this.getOpenDevice(path).device.deviceDescriptor;
|
|
272
|
+
const protocolHint = input.expectedProtocol
|
|
273
|
+
? undefined
|
|
274
|
+
: (_b = input.protocolHint) !== null && _b !== void 0 ? _b : hdShared.inferProtocolHintFromUsbId(descriptor.idVendor, descriptor.idProduct);
|
|
275
|
+
yield this.detectProtocol(path, input.expectedProtocol, protocolHint);
|
|
269
276
|
return path;
|
|
270
277
|
}
|
|
271
278
|
catch (error) {
|
|
272
|
-
(
|
|
273
|
-
|
|
279
|
+
(_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug('NodeUsbTransport acquire error: ', error);
|
|
280
|
+
try {
|
|
281
|
+
yield this.release(path);
|
|
282
|
+
}
|
|
283
|
+
catch (cleanupError) {
|
|
284
|
+
(_d = this.Log) === null || _d === void 0 ? void 0 : _d.debug('NodeUsbTransport acquire cleanup error: ', cleanupError);
|
|
285
|
+
}
|
|
286
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, (_e = error.message) !== null && _e !== void 0 ? _e : String(error));
|
|
274
287
|
}
|
|
275
288
|
});
|
|
276
289
|
}
|
|
277
290
|
release(path, _onclose) {
|
|
278
291
|
return __awaiter(this, void 0, void 0, function* () {
|
|
292
|
+
yield this.cancelActiveTransfers(path);
|
|
279
293
|
yield this.invalidateProtocolV2UsbLink(path, 'Node USB transport released');
|
|
280
294
|
yield this.closeOpenDevice(path);
|
|
281
295
|
this.deviceProtocol.delete(path);
|
|
282
296
|
});
|
|
283
297
|
}
|
|
298
|
+
cancelActiveTransfers(path) {
|
|
299
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
300
|
+
const transfers = Array.from(this.activeTransfers.entries()).filter(([, active]) => path === undefined || active.path === path);
|
|
301
|
+
transfers.forEach(([transfer]) => {
|
|
302
|
+
try {
|
|
303
|
+
transfer.cancel();
|
|
304
|
+
}
|
|
305
|
+
catch (_a) {
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
yield Promise.allSettled(transfers.map(([, active]) => active.settled));
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
createTrackedTransfer(path, endpoint, callback) {
|
|
312
|
+
let resolveSettled = () => undefined;
|
|
313
|
+
const settled = new Promise(resolve => {
|
|
314
|
+
resolveSettled = resolve;
|
|
315
|
+
});
|
|
316
|
+
const transfer = endpoint.makeTransfer(endpoint.timeout, (error, buffer, actualLength) => {
|
|
317
|
+
this.activeTransfers.delete(transfer);
|
|
318
|
+
resolveSettled();
|
|
319
|
+
callback(error, buffer, actualLength);
|
|
320
|
+
});
|
|
321
|
+
this.activeTransfers.set(transfer, { path, settled, resolveSettled });
|
|
322
|
+
return transfer;
|
|
323
|
+
}
|
|
324
|
+
transferInOnce(path, ep, length) {
|
|
325
|
+
return new Promise((resolve, reject) => {
|
|
326
|
+
const buffer = Buffer.alloc(length);
|
|
327
|
+
const transfer = this.createTrackedTransfer(path, ep, (error, _submittedBuffer, actualLength) => {
|
|
328
|
+
if (error) {
|
|
329
|
+
reject(error);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (actualLength <= 0) {
|
|
333
|
+
reject(new Error('Empty USB transfer'));
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
resolve(buffer.subarray(0, actualLength));
|
|
337
|
+
});
|
|
338
|
+
try {
|
|
339
|
+
transfer.submit(buffer);
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
const active = this.activeTransfers.get(transfer);
|
|
343
|
+
this.activeTransfers.delete(transfer);
|
|
344
|
+
active === null || active === void 0 ? void 0 : active.resolveSettled();
|
|
345
|
+
reject(error);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
transferOutOnce(path, ep, data) {
|
|
350
|
+
return new Promise((resolve, reject) => {
|
|
351
|
+
const transfer = this.createTrackedTransfer(path, ep, error => {
|
|
352
|
+
if (error) {
|
|
353
|
+
reject(error);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
resolve();
|
|
357
|
+
});
|
|
358
|
+
try {
|
|
359
|
+
transfer.submit(data);
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
const active = this.activeTransfers.get(transfer);
|
|
363
|
+
this.activeTransfers.delete(transfer);
|
|
364
|
+
active === null || active === void 0 ? void 0 : active.resolveSettled();
|
|
365
|
+
reject(error);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
284
369
|
closeOpenDevice(path) {
|
|
285
370
|
return __awaiter(this, void 0, void 0, function* () {
|
|
286
371
|
const openDev = this.openDevices.get(path);
|
|
@@ -309,7 +394,6 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
309
394
|
});
|
|
310
395
|
}
|
|
311
396
|
call(path, name, data, options) {
|
|
312
|
-
var _a;
|
|
313
397
|
return __awaiter(this, void 0, void 0, function* () {
|
|
314
398
|
this.cancelled = false;
|
|
315
399
|
if (!this.messages) {
|
|
@@ -322,9 +406,6 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
322
406
|
if (!protocol) {
|
|
323
407
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol has not been detected for ${path}`);
|
|
324
408
|
}
|
|
325
|
-
if (!shouldSuppressHighVolumeCallLog(name)) {
|
|
326
|
-
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('transport call', createTransportCallLog(name, protocol));
|
|
327
|
-
}
|
|
328
409
|
if (protocol === 'V2') {
|
|
329
410
|
return this.callProtocolV2(path, name, data, options);
|
|
330
411
|
}
|
|
@@ -370,6 +451,9 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
370
451
|
}
|
|
371
452
|
isRetryableError(error) {
|
|
372
453
|
const message = this.getErrorMessage(error).toLowerCase();
|
|
454
|
+
if (message.includes('cancelled') || message.includes('canceled')) {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
373
457
|
return (message.includes('libusb') ||
|
|
374
458
|
message.includes('transfer') ||
|
|
375
459
|
message.includes('disconnected') ||
|
|
@@ -438,7 +522,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
438
522
|
const packet = new Uint8Array(PACKET_SIZE);
|
|
439
523
|
packet[0] = REPORT_ID;
|
|
440
524
|
packet.set(new Uint8Array(buffer), 1);
|
|
441
|
-
yield transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
525
|
+
yield this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
442
526
|
}
|
|
443
527
|
return;
|
|
444
528
|
}
|
|
@@ -471,7 +555,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
471
555
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
472
556
|
}
|
|
473
557
|
try {
|
|
474
|
-
return yield transferInOnce(currentDev.epIn, length);
|
|
558
|
+
return yield this.transferInOnce(path, currentDev.epIn, length);
|
|
475
559
|
}
|
|
476
560
|
catch (error) {
|
|
477
561
|
lastError = error;
|
|
@@ -530,7 +614,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
530
614
|
}
|
|
531
615
|
epIn.timeout = TRANSFER_TIMEOUT_MS;
|
|
532
616
|
epOut.timeout = TRANSFER_TIMEOUT_MS;
|
|
533
|
-
yield this.drainStaleInput(epIn);
|
|
617
|
+
yield this.drainStaleInput(path, epIn);
|
|
534
618
|
this.openDevices.set(path, { device: dev, iface, epIn, epOut });
|
|
535
619
|
}
|
|
536
620
|
catch (err) {
|
|
@@ -543,14 +627,14 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
543
627
|
}
|
|
544
628
|
});
|
|
545
629
|
}
|
|
546
|
-
drainStaleInput(epIn) {
|
|
630
|
+
drainStaleInput(path, epIn) {
|
|
547
631
|
return __awaiter(this, void 0, void 0, function* () {
|
|
548
632
|
const originalTimeout = epIn.timeout;
|
|
549
633
|
epIn.timeout = 50;
|
|
550
634
|
try {
|
|
551
635
|
for (let index = 0; index < 16; index += 1) {
|
|
552
636
|
try {
|
|
553
|
-
yield transferInOnce(epIn, PACKET_SIZE);
|
|
637
|
+
yield this.transferInOnce(path, epIn, PACKET_SIZE);
|
|
554
638
|
}
|
|
555
639
|
catch (_a) {
|
|
556
640
|
break;
|
|
@@ -568,13 +652,16 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
568
652
|
createProtocolDetectionError() {
|
|
569
653
|
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
654
|
}
|
|
571
|
-
detectProtocol(path, expectedProtocol) {
|
|
655
|
+
detectProtocol(path, expectedProtocol, protocolHint) {
|
|
572
656
|
var _a;
|
|
573
657
|
return __awaiter(this, void 0, void 0, function* () {
|
|
574
658
|
if (expectedProtocol === 'V2') {
|
|
575
|
-
this.
|
|
576
|
-
|
|
577
|
-
|
|
659
|
+
if (yield this.probeProtocolV2(path)) {
|
|
660
|
+
this.deviceProtocol.set(path, 'V2');
|
|
661
|
+
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2 (expected)`);
|
|
662
|
+
return 'V2';
|
|
663
|
+
}
|
|
664
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
578
665
|
}
|
|
579
666
|
if (expectedProtocol === 'V1') {
|
|
580
667
|
if (yield this.probeProtocolV1(path)) {
|
|
@@ -583,17 +670,16 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
583
670
|
}
|
|
584
671
|
throw this.createProtocolMismatchError(expectedProtocol);
|
|
585
672
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
this.
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
return 'V2';
|
|
673
|
+
const probeOrder = protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
674
|
+
for (const [index, protocol] of probeOrder.entries()) {
|
|
675
|
+
if (index > 0) {
|
|
676
|
+
yield this.resetConnectionAfterProbe(path);
|
|
677
|
+
}
|
|
678
|
+
const detected = protocol === 'V1' ? yield this.probeProtocolV1(path) : yield this.probeProtocolV2(path);
|
|
679
|
+
if (detected) {
|
|
680
|
+
this.deviceProtocol.set(path, protocol);
|
|
681
|
+
return protocol;
|
|
682
|
+
}
|
|
597
683
|
}
|
|
598
684
|
this.deviceProtocol.delete(path);
|
|
599
685
|
throw this.createProtocolDetectionError();
|
|
@@ -604,6 +690,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
604
690
|
return __awaiter(this, void 0, void 0, function* () {
|
|
605
691
|
yield this.rotateProtocolV2UsbGeneration(path, 'Node USB protocol probe reset');
|
|
606
692
|
try {
|
|
693
|
+
yield this.cancelActiveTransfers(path);
|
|
607
694
|
yield this.closeOpenDevice(path);
|
|
608
695
|
}
|
|
609
696
|
catch (error) {
|
|
@@ -695,7 +782,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
695
782
|
if (this.cancelled) {
|
|
696
783
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
697
784
|
}
|
|
698
|
-
yield transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
785
|
+
yield this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
699
786
|
});
|
|
700
787
|
}
|
|
701
788
|
readProtocolV2UsbPacket(path, _context) {
|
|
@@ -705,7 +792,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
705
792
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
706
793
|
}
|
|
707
794
|
try {
|
|
708
|
-
const packet = yield transferInOnce(this.getOpenDevice(path).epIn, transport.PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
795
|
+
const packet = yield this.transferInOnce(path, this.getOpenDevice(path).epIn, transport.PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
709
796
|
return new Uint8Array(packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength));
|
|
710
797
|
}
|
|
711
798
|
catch (error) {
|
|
@@ -718,6 +805,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
718
805
|
}
|
|
719
806
|
resetProtocolV2UsbNativeLink(path, _reason) {
|
|
720
807
|
return __awaiter(this, void 0, void 0, function* () {
|
|
808
|
+
yield this.cancelActiveTransfers(path);
|
|
721
809
|
yield this.closeOpenDevice(path);
|
|
722
810
|
});
|
|
723
811
|
}
|
|
@@ -727,7 +815,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
727
815
|
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
|
|
728
816
|
}
|
|
729
817
|
createProtocolV2UsbTimeoutError(name, timeoutMs) {
|
|
730
|
-
return new
|
|
818
|
+
return new transport.ProtocolV2LinkError('response-timeout', `Protocol V2 response timeout after ${timeoutMs}ms for ${name}`);
|
|
731
819
|
}
|
|
732
820
|
callProtocolV2(path, name, data, options) {
|
|
733
821
|
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.170",
|
|
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.170",
|
|
25
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.170",
|
|
25
26
|
"bytebuffer": "^5.0.1",
|
|
26
27
|
"usb": "^2.14.0"
|
|
27
28
|
},
|
|
28
|
-
"gitHead": "
|
|
29
|
+
"gitHead": "d702db069d6b19f74cd5f1cf42277a7152beb3a8"
|
|
29
30
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,12 +7,17 @@ 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
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
import {
|
|
15
|
+
ERRORS,
|
|
16
|
+
HardwareErrorCode,
|
|
17
|
+
ONEKEY_WEBUSB_FILTER,
|
|
18
|
+
inferProtocolHintFromUsbId,
|
|
19
|
+
wait,
|
|
20
|
+
} from '@onekeyfe/hd-shared';
|
|
16
21
|
|
|
17
22
|
import type EventEmitter from 'events';
|
|
18
23
|
import type {
|
|
@@ -131,31 +136,6 @@ function readSerialNumber(dev: usb.Device, openDevices?: Map<string, OpenDevice>
|
|
|
131
136
|
});
|
|
132
137
|
}
|
|
133
138
|
|
|
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
139
|
/**
|
|
160
140
|
* Skip the 0x3F protocol marker byte from a USB packet.
|
|
161
141
|
*/
|
|
@@ -188,6 +168,8 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
188
168
|
/** Protobuf schema for Protocol V2 transports. */
|
|
189
169
|
messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
190
170
|
|
|
171
|
+
private protocolV2SchemaSource: string | undefined;
|
|
172
|
+
|
|
191
173
|
name = 'NodeUsbTransport';
|
|
192
174
|
|
|
193
175
|
version = '';
|
|
@@ -212,6 +194,15 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
212
194
|
/** per-path reconnect lock to prevent concurrent reconnects */
|
|
213
195
|
private reconnectLocks = new Map<string, Promise<OpenDevice>>();
|
|
214
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Retain the low-level Transfer so release()/stop() can cancel native pending reads.
|
|
199
|
+
* Endpoint.transfer() hides it and may keep the CLI alive after output completes.
|
|
200
|
+
*/
|
|
201
|
+
private activeTransfers = new Map<
|
|
202
|
+
usb.Transfer,
|
|
203
|
+
{ path: string; settled: Promise<void>; resolveSettled: () => void }
|
|
204
|
+
>();
|
|
205
|
+
|
|
215
206
|
/** set to true when cancel() is called; checked by retry loops */
|
|
216
207
|
private cancelled = false;
|
|
217
208
|
|
|
@@ -241,7 +232,17 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
241
232
|
}
|
|
242
233
|
|
|
243
234
|
configureProtocolV2(signedData: any) {
|
|
235
|
+
const schemaSource =
|
|
236
|
+
typeof signedData === 'string'
|
|
237
|
+
? signedData
|
|
238
|
+
: JSON.stringify(signedData) ?? String(signedData);
|
|
239
|
+
if (schemaSource === this.protocolV2SchemaSource) return;
|
|
240
|
+
|
|
241
|
+
const hadProtocolV2Schema = this.protocolV2SchemaSource !== undefined;
|
|
244
242
|
this.messagesV2 = parseConfigure(signedData);
|
|
243
|
+
this.protocolV2SchemaSource = schemaSource;
|
|
244
|
+
if (!hadProtocolV2Schema) return;
|
|
245
|
+
|
|
245
246
|
this.invalidateAllProtocolV2UsbLinks('Protocol V2 schema reconfigured').catch(error =>
|
|
246
247
|
this.Log?.debug('[NodeUsbTransport] schema link cleanup failed:', error)
|
|
247
248
|
);
|
|
@@ -251,10 +252,27 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
251
252
|
// empty — could add hotplug events via usb.on('attach'/'detach')
|
|
252
253
|
}
|
|
253
254
|
|
|
254
|
-
stop() {
|
|
255
|
-
this.
|
|
256
|
-
|
|
257
|
-
);
|
|
255
|
+
async stop(): Promise<void> {
|
|
256
|
+
this.cancelled = true;
|
|
257
|
+
|
|
258
|
+
await this.cancelActiveTransfers();
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
await this.disposeProtocolV2UsbLinks('Node USB transport stopped');
|
|
262
|
+
} catch (error) {
|
|
263
|
+
this.Log?.debug('[NodeUsbTransport] stop link cleanup failed:', error);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Reconnect may already be in flight when dispose starts. Wait for it to
|
|
267
|
+
// settle, then close every remaining handle so it cannot reopen USB after
|
|
268
|
+
// the first cleanup pass.
|
|
269
|
+
await Promise.allSettled(this.reconnectLocks.values());
|
|
270
|
+
await this.cancelActiveTransfers();
|
|
271
|
+
await Promise.all(Array.from(this.openDevices.keys(), path => this.closeOpenDevice(path)));
|
|
272
|
+
|
|
273
|
+
this.reconnectLocks.clear();
|
|
274
|
+
this.deviceProtocol.clear();
|
|
275
|
+
this.serialToBusId.clear();
|
|
258
276
|
}
|
|
259
277
|
|
|
260
278
|
/**
|
|
@@ -265,6 +283,10 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
265
283
|
if (!this.messages) {
|
|
266
284
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
267
285
|
}
|
|
286
|
+
if (this.deviceProtocol.get(path) === 'V2') {
|
|
287
|
+
await this.sendProtocolV2UsbFlowControl(path, name, data);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
268
290
|
const encodeBuffers = ProtocolV1.encodeMessageChunks(this.messages, name, data);
|
|
269
291
|
await this.sendAllChunksWithRetry(path, encodeBuffers);
|
|
270
292
|
}
|
|
@@ -332,10 +354,20 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
332
354
|
await this.rotateProtocolV2UsbGeneration(path, 'Node USB transport acquired');
|
|
333
355
|
await this.closeOpenDevice(path);
|
|
334
356
|
await this.openDevice(path);
|
|
335
|
-
|
|
357
|
+
const descriptor = this.getOpenDevice(path).device.deviceDescriptor;
|
|
358
|
+
const protocolHint = input.expectedProtocol
|
|
359
|
+
? undefined
|
|
360
|
+
: input.protocolHint ??
|
|
361
|
+
inferProtocolHintFromUsbId(descriptor.idVendor, descriptor.idProduct);
|
|
362
|
+
await this.detectProtocol(path, input.expectedProtocol, protocolHint);
|
|
336
363
|
return path;
|
|
337
364
|
} catch (error: any) {
|
|
338
365
|
this.Log?.debug('NodeUsbTransport acquire error: ', error);
|
|
366
|
+
try {
|
|
367
|
+
await this.release(path);
|
|
368
|
+
} catch (cleanupError) {
|
|
369
|
+
this.Log?.debug('NodeUsbTransport acquire cleanup error: ', cleanupError);
|
|
370
|
+
}
|
|
339
371
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, error.message ?? String(error));
|
|
340
372
|
}
|
|
341
373
|
}
|
|
@@ -344,11 +376,93 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
344
376
|
* Release device — release interface and close.
|
|
345
377
|
*/
|
|
346
378
|
async release(path: string, _onclose?: boolean): Promise<void> {
|
|
379
|
+
await this.cancelActiveTransfers(path);
|
|
347
380
|
await this.invalidateProtocolV2UsbLink(path, 'Node USB transport released');
|
|
348
381
|
await this.closeOpenDevice(path);
|
|
349
382
|
this.deviceProtocol.delete(path);
|
|
350
383
|
}
|
|
351
384
|
|
|
385
|
+
private async cancelActiveTransfers(path?: string): Promise<void> {
|
|
386
|
+
const transfers = Array.from(this.activeTransfers.entries()).filter(
|
|
387
|
+
([, active]) => path === undefined || active.path === path
|
|
388
|
+
);
|
|
389
|
+
transfers.forEach(([transfer]) => {
|
|
390
|
+
try {
|
|
391
|
+
transfer.cancel();
|
|
392
|
+
} catch {
|
|
393
|
+
// A transfer may finish between the snapshot and cancel().
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
await Promise.allSettled(transfers.map(([, active]) => active.settled));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private createTrackedTransfer(
|
|
400
|
+
path: string,
|
|
401
|
+
endpoint: usb.InEndpoint | usb.OutEndpoint,
|
|
402
|
+
callback: (error: Error | undefined, buffer: Buffer, actualLength: number) => void
|
|
403
|
+
): usb.Transfer {
|
|
404
|
+
let resolveSettled: () => void = () => undefined;
|
|
405
|
+
const settled = new Promise<void>(resolve => {
|
|
406
|
+
resolveSettled = resolve;
|
|
407
|
+
});
|
|
408
|
+
const transfer = endpoint.makeTransfer(endpoint.timeout, (error, buffer, actualLength) => {
|
|
409
|
+
this.activeTransfers.delete(transfer);
|
|
410
|
+
resolveSettled();
|
|
411
|
+
callback(error, buffer, actualLength);
|
|
412
|
+
});
|
|
413
|
+
this.activeTransfers.set(transfer, { path, settled, resolveSettled });
|
|
414
|
+
return transfer;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private transferInOnce(path: string, ep: usb.InEndpoint, length: number): Promise<Buffer> {
|
|
418
|
+
return new Promise((resolve, reject) => {
|
|
419
|
+
const buffer = Buffer.alloc(length);
|
|
420
|
+
const transfer = this.createTrackedTransfer(
|
|
421
|
+
path,
|
|
422
|
+
ep,
|
|
423
|
+
(error, _submittedBuffer, actualLength) => {
|
|
424
|
+
if (error) {
|
|
425
|
+
reject(error);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (actualLength <= 0) {
|
|
429
|
+
reject(new Error('Empty USB transfer'));
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
resolve(buffer.subarray(0, actualLength));
|
|
433
|
+
}
|
|
434
|
+
);
|
|
435
|
+
try {
|
|
436
|
+
transfer.submit(buffer);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
const active = this.activeTransfers.get(transfer);
|
|
439
|
+
this.activeTransfers.delete(transfer);
|
|
440
|
+
active?.resolveSettled();
|
|
441
|
+
reject(error);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private transferOutOnce(path: string, ep: usb.OutEndpoint, data: Buffer): Promise<void> {
|
|
447
|
+
return new Promise((resolve, reject) => {
|
|
448
|
+
const transfer = this.createTrackedTransfer(path, ep, error => {
|
|
449
|
+
if (error) {
|
|
450
|
+
reject(error);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
resolve();
|
|
454
|
+
});
|
|
455
|
+
try {
|
|
456
|
+
transfer.submit(data);
|
|
457
|
+
} catch (error) {
|
|
458
|
+
const active = this.activeTransfers.get(transfer);
|
|
459
|
+
this.activeTransfers.delete(transfer);
|
|
460
|
+
active?.resolveSettled();
|
|
461
|
+
reject(error);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
352
466
|
private async closeOpenDevice(path: string): Promise<void> {
|
|
353
467
|
const openDev = this.openDevices.get(path);
|
|
354
468
|
if (!openDev) return;
|
|
@@ -401,10 +515,6 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
401
515
|
`Device protocol has not been detected for ${path}`
|
|
402
516
|
);
|
|
403
517
|
}
|
|
404
|
-
if (!shouldSuppressHighVolumeCallLog(name)) {
|
|
405
|
-
this.Log?.debug('transport call', createTransportCallLog(name, protocol));
|
|
406
|
-
}
|
|
407
|
-
|
|
408
518
|
if (protocol === 'V2') {
|
|
409
519
|
return this.callProtocolV2(path, name, data, options);
|
|
410
520
|
}
|
|
@@ -468,6 +578,12 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
468
578
|
|
|
469
579
|
private isRetryableError(error: unknown): boolean {
|
|
470
580
|
const message = this.getErrorMessage(error).toLowerCase();
|
|
581
|
+
// cancelActiveTransfers() terminates timed-out or releasing native requests.
|
|
582
|
+
// LIBUSB_TRANSFER_CANCELLED is not transient. Retrying would start another pending
|
|
583
|
+
// read on the rebuilt interface and race protocol probing against release.
|
|
584
|
+
if (message.includes('cancelled') || message.includes('canceled')) {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
471
587
|
return (
|
|
472
588
|
message.includes('libusb') ||
|
|
473
589
|
message.includes('transfer') ||
|
|
@@ -572,7 +688,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
572
688
|
const packet = new Uint8Array(PACKET_SIZE);
|
|
573
689
|
packet[0] = REPORT_ID;
|
|
574
690
|
packet.set(new Uint8Array(buffer), 1);
|
|
575
|
-
await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
691
|
+
await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
576
692
|
}
|
|
577
693
|
return; // all chunks sent successfully
|
|
578
694
|
} catch (error) {
|
|
@@ -615,7 +731,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
615
731
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
616
732
|
}
|
|
617
733
|
try {
|
|
618
|
-
return await transferInOnce(currentDev.epIn, length);
|
|
734
|
+
return await this.transferInOnce(path, currentDev.epIn, length);
|
|
619
735
|
} catch (error) {
|
|
620
736
|
lastError = error;
|
|
621
737
|
if (options?.waitIndefinitelyOnTimeout && this.isUsbTransferTimeout(error)) {
|
|
@@ -695,7 +811,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
695
811
|
epIn.timeout = TRANSFER_TIMEOUT_MS;
|
|
696
812
|
epOut.timeout = TRANSFER_TIMEOUT_MS;
|
|
697
813
|
|
|
698
|
-
await this.drainStaleInput(epIn);
|
|
814
|
+
await this.drainStaleInput(path, epIn);
|
|
699
815
|
|
|
700
816
|
this.openDevices.set(path, { device: dev, iface, epIn, epOut });
|
|
701
817
|
} catch (err) {
|
|
@@ -708,14 +824,14 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
708
824
|
}
|
|
709
825
|
}
|
|
710
826
|
|
|
711
|
-
private async drainStaleInput(epIn: usb.InEndpoint): Promise<void> {
|
|
827
|
+
private async drainStaleInput(path: string, epIn: usb.InEndpoint): Promise<void> {
|
|
712
828
|
const originalTimeout = epIn.timeout;
|
|
713
829
|
epIn.timeout = 50;
|
|
714
830
|
try {
|
|
715
831
|
// Drain a small bounded number of packets left by the previous USB session.
|
|
716
832
|
for (let index = 0; index < 16; index += 1) {
|
|
717
833
|
try {
|
|
718
|
-
await transferInOnce(epIn, PACKET_SIZE);
|
|
834
|
+
await this.transferInOnce(path, epIn, PACKET_SIZE);
|
|
719
835
|
} catch {
|
|
720
836
|
break;
|
|
721
837
|
}
|
|
@@ -741,15 +857,16 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
741
857
|
|
|
742
858
|
private async detectProtocol(
|
|
743
859
|
path: string,
|
|
744
|
-
expectedProtocol?: ProtocolType
|
|
860
|
+
expectedProtocol?: ProtocolType,
|
|
861
|
+
protocolHint?: ProtocolType
|
|
745
862
|
): Promise<ProtocolType> {
|
|
746
863
|
if (expectedProtocol === 'V2') {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
864
|
+
if (await this.probeProtocolV2(path)) {
|
|
865
|
+
this.deviceProtocol.set(path, 'V2');
|
|
866
|
+
this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2 (expected)`);
|
|
867
|
+
return 'V2';
|
|
868
|
+
}
|
|
869
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
753
870
|
}
|
|
754
871
|
|
|
755
872
|
if (expectedProtocol === 'V1') {
|
|
@@ -760,19 +877,19 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
760
877
|
throw this.createProtocolMismatchError(expectedProtocol);
|
|
761
878
|
}
|
|
762
879
|
|
|
763
|
-
|
|
764
|
-
this.deviceProtocol.
|
|
765
|
-
return 'V2';
|
|
766
|
-
}
|
|
880
|
+
const probeOrder: ProtocolType[] =
|
|
881
|
+
protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
767
882
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
883
|
+
for (const [index, protocol] of probeOrder.entries()) {
|
|
884
|
+
if (index > 0) {
|
|
885
|
+
await this.resetConnectionAfterProbe(path);
|
|
886
|
+
}
|
|
887
|
+
const detected =
|
|
888
|
+
protocol === 'V1' ? await this.probeProtocolV1(path) : await this.probeProtocolV2(path);
|
|
889
|
+
if (detected) {
|
|
890
|
+
this.deviceProtocol.set(path, protocol);
|
|
891
|
+
return protocol;
|
|
892
|
+
}
|
|
776
893
|
}
|
|
777
894
|
|
|
778
895
|
this.deviceProtocol.delete(path);
|
|
@@ -783,6 +900,10 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
783
900
|
await this.rotateProtocolV2UsbGeneration(path, 'Node USB protocol probe reset');
|
|
784
901
|
|
|
785
902
|
try {
|
|
903
|
+
// A timed-out probe may leave an IN transfer pending. Cancel and await it before
|
|
904
|
+
// closing the interface, or libusb may never callback and release()/stop() will
|
|
905
|
+
// wait forever, surfacing as Polling timeout (809).
|
|
906
|
+
await this.cancelActiveTransfers(path);
|
|
786
907
|
await this.closeOpenDevice(path);
|
|
787
908
|
} catch (error) {
|
|
788
909
|
this.Log?.debug('[NodeUsbTransport] close after protocol probe error:', error);
|
|
@@ -883,7 +1004,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
883
1004
|
if (this.cancelled) {
|
|
884
1005
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
885
1006
|
}
|
|
886
|
-
await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
1007
|
+
await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
887
1008
|
}
|
|
888
1009
|
|
|
889
1010
|
protected async readProtocolV2UsbPacket(
|
|
@@ -895,7 +1016,8 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
895
1016
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
896
1017
|
}
|
|
897
1018
|
try {
|
|
898
|
-
const packet = await transferInOnce(
|
|
1019
|
+
const packet = await this.transferInOnce(
|
|
1020
|
+
path,
|
|
899
1021
|
this.getOpenDevice(path).epIn,
|
|
900
1022
|
PROTOCOL_V2_FRAME_MAX_BYTES
|
|
901
1023
|
);
|
|
@@ -911,6 +1033,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
911
1033
|
}
|
|
912
1034
|
|
|
913
1035
|
protected async resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise<void> {
|
|
1036
|
+
await this.cancelActiveTransfers(path);
|
|
914
1037
|
await this.closeOpenDevice(path);
|
|
915
1038
|
}
|
|
916
1039
|
|
|
@@ -920,7 +1043,10 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
920
1043
|
}
|
|
921
1044
|
|
|
922
1045
|
protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error {
|
|
923
|
-
return new
|
|
1046
|
+
return new ProtocolV2LinkError(
|
|
1047
|
+
'response-timeout',
|
|
1048
|
+
`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`
|
|
1049
|
+
);
|
|
924
1050
|
}
|
|
925
1051
|
|
|
926
1052
|
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';
|