@onekeyfe/hd-transport-usb 1.2.0-alpha.19 → 1.2.0-alpha.20
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 +155 -40
- package/dist/index.d.ts +75 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +102 -28
- package/package.json +4 -4
- package/src/index.ts +134 -39
|
@@ -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,6 +253,43 @@ const createHarness = () => {
|
|
|
189
253
|
};
|
|
190
254
|
|
|
191
255
|
describe('NodeUsbTransport Protocol V2 link lifecycle', () => {
|
|
256
|
+
test('does not retry a native transfer cancelled by probe cleanup', () => {
|
|
257
|
+
const { transport } = createHarness();
|
|
258
|
+
|
|
259
|
+
expect((transport as any).isRetryableError(new Error('LIBUSB_TRANSFER_CANCELLED'))).toBe(false);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('cancels pending native transfers before closing a timed-out protocol probe', async () => {
|
|
263
|
+
const harness = createHarness();
|
|
264
|
+
const { transport, path } = harness;
|
|
265
|
+
await harness.acquire();
|
|
266
|
+
const cancelActiveTransfers = jest.spyOn(transport as any, 'cancelActiveTransfers');
|
|
267
|
+
const closeOpenDevice = jest.spyOn(transport as any, 'closeOpenDevice');
|
|
268
|
+
|
|
269
|
+
await (transport as any).resetConnectionAfterProbe(path);
|
|
270
|
+
|
|
271
|
+
expect(cancelActiveTransfers).toHaveBeenCalledWith(path);
|
|
272
|
+
expect(closeOpenDevice).toHaveBeenCalledWith(path);
|
|
273
|
+
expect(cancelActiveTransfers.mock.invocationCallOrder[0]).toBeLessThan(
|
|
274
|
+
closeOpenDevice.mock.invocationCallOrder[0]
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('stop releases an acquired USB interface even before a Protocol V2 call', async () => {
|
|
279
|
+
const harness = createHarness();
|
|
280
|
+
const { transport, path, device, iface } = harness;
|
|
281
|
+
await transport.enumerate();
|
|
282
|
+
await transport.acquire({ path, expectedProtocol: 'V2' });
|
|
283
|
+
device.close.mockClear();
|
|
284
|
+
iface.release.mockClear();
|
|
285
|
+
|
|
286
|
+
await transport.stop();
|
|
287
|
+
|
|
288
|
+
expect(iface.release).toHaveBeenCalledTimes(1);
|
|
289
|
+
expect(device.close).toHaveBeenCalledTimes(1);
|
|
290
|
+
expect(transport.getProtocolType(path)).toBeUndefined();
|
|
291
|
+
});
|
|
292
|
+
|
|
192
293
|
test('trusts explicit Protocol V2 during bootloader reconnect without probing Ping', async () => {
|
|
193
294
|
const harness = createHarness();
|
|
194
295
|
const { transport, path, epOut } = harness;
|
|
@@ -196,7 +297,7 @@ describe('NodeUsbTransport Protocol V2 link lifecycle', () => {
|
|
|
196
297
|
await transport.enumerate();
|
|
197
298
|
await transport.acquire({ path, expectedProtocol: 'V2' });
|
|
198
299
|
|
|
199
|
-
expect(epOut.
|
|
300
|
+
expect(epOut.makeTransfer).not.toHaveBeenCalled();
|
|
200
301
|
expect(transport.getProtocolType(path)).toBe('V2');
|
|
201
302
|
await transport.release(path);
|
|
202
303
|
});
|
|
@@ -219,14 +320,14 @@ describe('NodeUsbTransport Protocol V2 link lifecycle', () => {
|
|
|
219
320
|
const harness = createHarness();
|
|
220
321
|
const { transport, path, epOut } = harness;
|
|
221
322
|
await harness.acquire();
|
|
222
|
-
epOut.
|
|
323
|
+
epOut.makeTransfer.mockClear();
|
|
223
324
|
harness.failNextWrite(new Error('LIBUSB_ERROR_IO'));
|
|
224
325
|
|
|
225
326
|
await expect(transport.call(path, 'Ping', { message: 'write-failure' })).rejects.toThrow(
|
|
226
327
|
'LIBUSB_ERROR_IO'
|
|
227
328
|
);
|
|
228
329
|
|
|
229
|
-
expect(epOut.
|
|
330
|
+
expect(epOut.makeTransfer).toHaveBeenCalledTimes(1);
|
|
230
331
|
});
|
|
231
332
|
|
|
232
333
|
test('rejects a pending read when release invalidates the link', async () => {
|
|
@@ -253,6 +354,20 @@ describe('NodeUsbTransport Protocol V2 link lifecycle', () => {
|
|
|
253
354
|
expect(settled).not.toBe('still pending');
|
|
254
355
|
});
|
|
255
356
|
|
|
357
|
+
test('stop cancels an in-flight native USB transfer before releasing the interface', async () => {
|
|
358
|
+
const harness = createHarness();
|
|
359
|
+
const { transport, path } = harness;
|
|
360
|
+
await harness.acquire();
|
|
361
|
+
const pendingRead = harness.holdRead();
|
|
362
|
+
|
|
363
|
+
const call = transport.call(path, 'Ping', { message: 'pending' }, { timeoutMs: 5000 });
|
|
364
|
+
await pendingRead.started;
|
|
365
|
+
await transport.stop();
|
|
366
|
+
|
|
367
|
+
await expect(call).rejects.toThrow();
|
|
368
|
+
expect(harness.getCancelledTransferCount()).toBe(1);
|
|
369
|
+
});
|
|
370
|
+
|
|
256
371
|
test('keeps the cursor after a response timeout rebuilds the USB connection', async () => {
|
|
257
372
|
const harness = createHarness();
|
|
258
373
|
const { transport, path, sentSeqs } = harness;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,8 +2,18 @@ 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;
|
|
8
18
|
name: string;
|
|
9
19
|
version: string;
|
|
@@ -11,39 +21,99 @@ declare class NodeUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
|
11
21
|
isOutdated: boolean;
|
|
12
22
|
Log?: any;
|
|
13
23
|
emitter?: EventEmitter;
|
|
24
|
+
/** serial → bus id, built during enumerate */
|
|
14
25
|
private serialToBusId;
|
|
26
|
+
/** path → opened device state */
|
|
15
27
|
private openDevices;
|
|
28
|
+
/** Per-path protocol type detected by active wire-level probe. */
|
|
16
29
|
private deviceProtocol;
|
|
30
|
+
/** per-path reconnect lock to prevent concurrent reconnects */
|
|
17
31
|
private reconnectLocks;
|
|
32
|
+
/**
|
|
33
|
+
* Retain the low-level Transfer so release()/stop() can cancel native pending reads.
|
|
34
|
+
* Endpoint.transfer() hides it and may keep the CLI alive after output completes.
|
|
35
|
+
*/
|
|
36
|
+
private activeTransfers;
|
|
37
|
+
/** set to true when cancel() is called; checked by retry loops */
|
|
18
38
|
private cancelled;
|
|
19
39
|
constructor();
|
|
40
|
+
/**
|
|
41
|
+
* Initialize transport.
|
|
42
|
+
* Signature matches the Transport.init interface (logger, emitter).
|
|
43
|
+
*/
|
|
20
44
|
init(logger: any, emitter?: EventEmitter): Promise<string>;
|
|
21
45
|
configure(signedData: any): Promise<void>;
|
|
22
46
|
configureProtocolV2(signedData: any): void;
|
|
23
47
|
listen(): void;
|
|
24
|
-
stop(): void
|
|
48
|
+
stop(): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Low-level post (send only, no response). Not used by NodeUsbTransport
|
|
51
|
+
* since call() handles the full send+receive cycle, but required by the Transport interface.
|
|
52
|
+
*/
|
|
25
53
|
post(path: string, name: string, data: Record<string, unknown>): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Low-level read (receive only). Not used by NodeUsbTransport
|
|
56
|
+
* since call() handles the full send+receive cycle, but required by the Transport interface.
|
|
57
|
+
*/
|
|
26
58
|
read(path: string): Promise<{
|
|
27
59
|
message: {
|
|
28
60
|
[key: string]: any;
|
|
29
61
|
};
|
|
30
62
|
type: string;
|
|
31
63
|
}>;
|
|
64
|
+
/**
|
|
65
|
+
* Enumerate connected OneKey USB devices.
|
|
66
|
+
* Opens each device briefly to read its serial number (used as `path`),
|
|
67
|
+
* then closes it. acquire() re-opens from a fresh getDeviceList().
|
|
68
|
+
*/
|
|
32
69
|
enumerate(): Promise<OneKeyDeviceInfo[]>;
|
|
70
|
+
/**
|
|
71
|
+
* Acquire device — open USB device, claim interface, return path (string).
|
|
72
|
+
*/
|
|
33
73
|
acquire(input: AcquireInput): Promise<string>;
|
|
74
|
+
/**
|
|
75
|
+
* Release device — release interface and close.
|
|
76
|
+
*/
|
|
34
77
|
release(path: string, _onclose?: boolean): Promise<void>;
|
|
78
|
+
private cancelActiveTransfers;
|
|
79
|
+
private createTrackedTransfer;
|
|
80
|
+
private transferInOnce;
|
|
81
|
+
private transferOutOnce;
|
|
35
82
|
private closeOpenDevice;
|
|
83
|
+
/**
|
|
84
|
+
* Call device method — encode protobuf, send packets, receive response.
|
|
85
|
+
* This is the core method that replaces LowlevelTransport's call + UsbPlugin's send/receive.
|
|
86
|
+
*/
|
|
36
87
|
call(path: string, name: string, data: Record<string, unknown>, options?: TransportCallOptions): Promise<transport.MessageFromOneKey>;
|
|
37
88
|
private callProtocolV1;
|
|
38
89
|
cancel(): void;
|
|
90
|
+
/**
|
|
91
|
+
* Get the current open device for a path, re-resolving from the map
|
|
92
|
+
* so callers always use a fresh reference after reconnect.
|
|
93
|
+
*/
|
|
39
94
|
private getOpenDevice;
|
|
40
95
|
private getErrorMessage;
|
|
41
96
|
private isRetryableError;
|
|
42
97
|
private isUsbTransferTimeout;
|
|
43
98
|
private getDeviceInterface;
|
|
99
|
+
/**
|
|
100
|
+
* Reconnect device before retrying a failed transfer (aligned with WebUsbTransport).
|
|
101
|
+
* Uses per-path lock to prevent concurrent reconnects to the same device.
|
|
102
|
+
*/
|
|
44
103
|
private reconnectForRetry;
|
|
104
|
+
/**
|
|
105
|
+
* Send all encoded chunks to the device with retry.
|
|
106
|
+
* If a chunk fails and triggers reconnect, the entire sequence restarts
|
|
107
|
+
* from chunk 0 because the device resets protocol state on reconnect.
|
|
108
|
+
*/
|
|
45
109
|
private sendAllChunksWithRetry;
|
|
110
|
+
/**
|
|
111
|
+
* USB IN transfer with retry and reconnect (aligned with WebUsbTransport).
|
|
112
|
+
*/
|
|
46
113
|
private transferInWithRetry;
|
|
114
|
+
/**
|
|
115
|
+
* Open a USB device by path (serial number), claim interface, cache endpoints.
|
|
116
|
+
*/
|
|
47
117
|
private openDevice;
|
|
48
118
|
private drainStaleInput;
|
|
49
119
|
private createProtocolMismatchError;
|
|
@@ -61,6 +131,10 @@ declare class NodeUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
|
61
131
|
protected onProtocolV2UsbLinkInvalidated(path: string, reason: string): void;
|
|
62
132
|
protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error;
|
|
63
133
|
private callProtocolV2;
|
|
134
|
+
/**
|
|
135
|
+
* Receive a complete protobuf response from the device.
|
|
136
|
+
* Reads 64-byte packets, strips 0x3F marker, reassembles into hex string.
|
|
137
|
+
*/
|
|
64
138
|
private receiveData;
|
|
65
139
|
getProtocolType(path: string): ProtocolType | undefined;
|
|
66
140
|
}
|
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,EAOhB,0BAA0B,EAE3B,MAAM,wBAAwB,CAAC;AAKhC,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;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -140,26 +140,6 @@ function readSerialNumber(dev, openDevices) {
|
|
|
140
140
|
}
|
|
141
141
|
});
|
|
142
142
|
}
|
|
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
143
|
function skipReportByte(packet) {
|
|
164
144
|
if (packet[0] === REPORT_ID) {
|
|
165
145
|
return packet.subarray(1);
|
|
@@ -184,6 +164,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
184
164
|
this.openDevices = new Map();
|
|
185
165
|
this.deviceProtocol = new Map();
|
|
186
166
|
this.reconnectLocks = new Map();
|
|
167
|
+
this.activeTransfers = new Map();
|
|
187
168
|
this.cancelled = false;
|
|
188
169
|
}
|
|
189
170
|
init(logger, emitter) {
|
|
@@ -204,7 +185,23 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
204
185
|
listen() {
|
|
205
186
|
}
|
|
206
187
|
stop() {
|
|
207
|
-
|
|
188
|
+
var _a;
|
|
189
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
190
|
+
this.cancelled = true;
|
|
191
|
+
yield this.cancelActiveTransfers();
|
|
192
|
+
try {
|
|
193
|
+
yield this.disposeProtocolV2UsbLinks('Node USB transport stopped');
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[NodeUsbTransport] stop link cleanup failed:', error);
|
|
197
|
+
}
|
|
198
|
+
yield Promise.allSettled(this.reconnectLocks.values());
|
|
199
|
+
yield this.cancelActiveTransfers();
|
|
200
|
+
yield Promise.all(Array.from(this.openDevices.keys(), path => this.closeOpenDevice(path)));
|
|
201
|
+
this.reconnectLocks.clear();
|
|
202
|
+
this.deviceProtocol.clear();
|
|
203
|
+
this.serialToBusId.clear();
|
|
204
|
+
});
|
|
208
205
|
}
|
|
209
206
|
post(path, name, data) {
|
|
210
207
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -276,11 +273,83 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
276
273
|
}
|
|
277
274
|
release(path, _onclose) {
|
|
278
275
|
return __awaiter(this, void 0, void 0, function* () {
|
|
276
|
+
yield this.cancelActiveTransfers(path);
|
|
279
277
|
yield this.invalidateProtocolV2UsbLink(path, 'Node USB transport released');
|
|
280
278
|
yield this.closeOpenDevice(path);
|
|
281
279
|
this.deviceProtocol.delete(path);
|
|
282
280
|
});
|
|
283
281
|
}
|
|
282
|
+
cancelActiveTransfers(path) {
|
|
283
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
284
|
+
const transfers = Array.from(this.activeTransfers.entries()).filter(([, active]) => path === undefined || active.path === path);
|
|
285
|
+
transfers.forEach(([transfer]) => {
|
|
286
|
+
try {
|
|
287
|
+
transfer.cancel();
|
|
288
|
+
}
|
|
289
|
+
catch (_a) {
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
yield Promise.allSettled(transfers.map(([, active]) => active.settled));
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
createTrackedTransfer(path, endpoint, callback) {
|
|
296
|
+
let resolveSettled = () => undefined;
|
|
297
|
+
const settled = new Promise(resolve => {
|
|
298
|
+
resolveSettled = resolve;
|
|
299
|
+
});
|
|
300
|
+
const transfer = endpoint.makeTransfer(endpoint.timeout, (error, buffer, actualLength) => {
|
|
301
|
+
this.activeTransfers.delete(transfer);
|
|
302
|
+
resolveSettled();
|
|
303
|
+
callback(error, buffer, actualLength);
|
|
304
|
+
});
|
|
305
|
+
this.activeTransfers.set(transfer, { path, settled, resolveSettled });
|
|
306
|
+
return transfer;
|
|
307
|
+
}
|
|
308
|
+
transferInOnce(path, ep, length) {
|
|
309
|
+
return new Promise((resolve, reject) => {
|
|
310
|
+
const buffer = Buffer.alloc(length);
|
|
311
|
+
const transfer = this.createTrackedTransfer(path, ep, (error, _submittedBuffer, actualLength) => {
|
|
312
|
+
if (error) {
|
|
313
|
+
reject(error);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (actualLength <= 0) {
|
|
317
|
+
reject(new Error('Empty USB transfer'));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
resolve(buffer.subarray(0, actualLength));
|
|
321
|
+
});
|
|
322
|
+
try {
|
|
323
|
+
transfer.submit(buffer);
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
const active = this.activeTransfers.get(transfer);
|
|
327
|
+
this.activeTransfers.delete(transfer);
|
|
328
|
+
active === null || active === void 0 ? void 0 : active.resolveSettled();
|
|
329
|
+
reject(error);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
transferOutOnce(path, ep, data) {
|
|
334
|
+
return new Promise((resolve, reject) => {
|
|
335
|
+
const transfer = this.createTrackedTransfer(path, ep, error => {
|
|
336
|
+
if (error) {
|
|
337
|
+
reject(error);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
resolve();
|
|
341
|
+
});
|
|
342
|
+
try {
|
|
343
|
+
transfer.submit(data);
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
const active = this.activeTransfers.get(transfer);
|
|
347
|
+
this.activeTransfers.delete(transfer);
|
|
348
|
+
active === null || active === void 0 ? void 0 : active.resolveSettled();
|
|
349
|
+
reject(error);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
}
|
|
284
353
|
closeOpenDevice(path) {
|
|
285
354
|
return __awaiter(this, void 0, void 0, function* () {
|
|
286
355
|
const openDev = this.openDevices.get(path);
|
|
@@ -370,6 +439,9 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
370
439
|
}
|
|
371
440
|
isRetryableError(error) {
|
|
372
441
|
const message = this.getErrorMessage(error).toLowerCase();
|
|
442
|
+
if (message.includes('cancelled') || message.includes('canceled')) {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
373
445
|
return (message.includes('libusb') ||
|
|
374
446
|
message.includes('transfer') ||
|
|
375
447
|
message.includes('disconnected') ||
|
|
@@ -438,7 +510,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
438
510
|
const packet = new Uint8Array(PACKET_SIZE);
|
|
439
511
|
packet[0] = REPORT_ID;
|
|
440
512
|
packet.set(new Uint8Array(buffer), 1);
|
|
441
|
-
yield transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
513
|
+
yield this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
442
514
|
}
|
|
443
515
|
return;
|
|
444
516
|
}
|
|
@@ -471,7 +543,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
471
543
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
472
544
|
}
|
|
473
545
|
try {
|
|
474
|
-
return yield transferInOnce(currentDev.epIn, length);
|
|
546
|
+
return yield this.transferInOnce(path, currentDev.epIn, length);
|
|
475
547
|
}
|
|
476
548
|
catch (error) {
|
|
477
549
|
lastError = error;
|
|
@@ -530,7 +602,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
530
602
|
}
|
|
531
603
|
epIn.timeout = TRANSFER_TIMEOUT_MS;
|
|
532
604
|
epOut.timeout = TRANSFER_TIMEOUT_MS;
|
|
533
|
-
yield this.drainStaleInput(epIn);
|
|
605
|
+
yield this.drainStaleInput(path, epIn);
|
|
534
606
|
this.openDevices.set(path, { device: dev, iface, epIn, epOut });
|
|
535
607
|
}
|
|
536
608
|
catch (err) {
|
|
@@ -543,14 +615,14 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
543
615
|
}
|
|
544
616
|
});
|
|
545
617
|
}
|
|
546
|
-
drainStaleInput(epIn) {
|
|
618
|
+
drainStaleInput(path, epIn) {
|
|
547
619
|
return __awaiter(this, void 0, void 0, function* () {
|
|
548
620
|
const originalTimeout = epIn.timeout;
|
|
549
621
|
epIn.timeout = 50;
|
|
550
622
|
try {
|
|
551
623
|
for (let index = 0; index < 16; index += 1) {
|
|
552
624
|
try {
|
|
553
|
-
yield transferInOnce(epIn, PACKET_SIZE);
|
|
625
|
+
yield this.transferInOnce(path, epIn, PACKET_SIZE);
|
|
554
626
|
}
|
|
555
627
|
catch (_a) {
|
|
556
628
|
break;
|
|
@@ -604,6 +676,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
604
676
|
return __awaiter(this, void 0, void 0, function* () {
|
|
605
677
|
yield this.rotateProtocolV2UsbGeneration(path, 'Node USB protocol probe reset');
|
|
606
678
|
try {
|
|
679
|
+
yield this.cancelActiveTransfers(path);
|
|
607
680
|
yield this.closeOpenDevice(path);
|
|
608
681
|
}
|
|
609
682
|
catch (error) {
|
|
@@ -695,7 +768,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
695
768
|
if (this.cancelled) {
|
|
696
769
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
697
770
|
}
|
|
698
|
-
yield transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
771
|
+
yield this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
699
772
|
});
|
|
700
773
|
}
|
|
701
774
|
readProtocolV2UsbPacket(path, _context) {
|
|
@@ -705,7 +778,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
705
778
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
706
779
|
}
|
|
707
780
|
try {
|
|
708
|
-
const packet = yield transferInOnce(this.getOpenDevice(path).epIn, transport.PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
781
|
+
const packet = yield this.transferInOnce(path, this.getOpenDevice(path).epIn, transport.PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
709
782
|
return new Uint8Array(packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength));
|
|
710
783
|
}
|
|
711
784
|
catch (error) {
|
|
@@ -718,6 +791,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
718
791
|
}
|
|
719
792
|
resetProtocolV2UsbNativeLink(path, _reason) {
|
|
720
793
|
return __awaiter(this, void 0, void 0, function* () {
|
|
794
|
+
yield this.cancelActiveTransfers(path);
|
|
721
795
|
yield this.closeOpenDevice(path);
|
|
722
796
|
});
|
|
723
797
|
}
|
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.20",
|
|
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",
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
"lint:fix": "eslint . --fix"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@onekeyfe/hd-shared": "1.2.0-alpha.
|
|
24
|
-
"@onekeyfe/hd-transport": "1.2.0-alpha.
|
|
23
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.20",
|
|
24
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.20",
|
|
25
25
|
"bytebuffer": "^5.0.1",
|
|
26
26
|
"usb": "^2.14.0"
|
|
27
27
|
},
|
|
28
|
-
"gitHead": "
|
|
28
|
+
"gitHead": "5fbc1ada90fd3cfda7e5ef08be368cb452018733"
|
|
29
29
|
}
|
package/src/index.ts
CHANGED
|
@@ -131,31 +131,6 @@ function readSerialNumber(dev: usb.Device, openDevices?: Map<string, OpenDevice>
|
|
|
131
131
|
});
|
|
132
132
|
}
|
|
133
133
|
|
|
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
134
|
/**
|
|
160
135
|
* Skip the 0x3F protocol marker byte from a USB packet.
|
|
161
136
|
*/
|
|
@@ -212,6 +187,15 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
212
187
|
/** per-path reconnect lock to prevent concurrent reconnects */
|
|
213
188
|
private reconnectLocks = new Map<string, Promise<OpenDevice>>();
|
|
214
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Retain the low-level Transfer so release()/stop() can cancel native pending reads.
|
|
192
|
+
* Endpoint.transfer() hides it and may keep the CLI alive after output completes.
|
|
193
|
+
*/
|
|
194
|
+
private activeTransfers = new Map<
|
|
195
|
+
usb.Transfer,
|
|
196
|
+
{ path: string; settled: Promise<void>; resolveSettled: () => void }
|
|
197
|
+
>();
|
|
198
|
+
|
|
215
199
|
/** set to true when cancel() is called; checked by retry loops */
|
|
216
200
|
private cancelled = false;
|
|
217
201
|
|
|
@@ -251,10 +235,27 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
251
235
|
// empty — could add hotplug events via usb.on('attach'/'detach')
|
|
252
236
|
}
|
|
253
237
|
|
|
254
|
-
stop() {
|
|
255
|
-
this.
|
|
256
|
-
|
|
257
|
-
);
|
|
238
|
+
async stop(): Promise<void> {
|
|
239
|
+
this.cancelled = true;
|
|
240
|
+
|
|
241
|
+
await this.cancelActiveTransfers();
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
await this.disposeProtocolV2UsbLinks('Node USB transport stopped');
|
|
245
|
+
} catch (error) {
|
|
246
|
+
this.Log?.debug('[NodeUsbTransport] stop link cleanup failed:', error);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Reconnect may already be in flight when dispose starts. Wait for it to
|
|
250
|
+
// settle, then close every remaining handle so it cannot reopen USB after
|
|
251
|
+
// the first cleanup pass.
|
|
252
|
+
await Promise.allSettled(this.reconnectLocks.values());
|
|
253
|
+
await this.cancelActiveTransfers();
|
|
254
|
+
await Promise.all(Array.from(this.openDevices.keys(), path => this.closeOpenDevice(path)));
|
|
255
|
+
|
|
256
|
+
this.reconnectLocks.clear();
|
|
257
|
+
this.deviceProtocol.clear();
|
|
258
|
+
this.serialToBusId.clear();
|
|
258
259
|
}
|
|
259
260
|
|
|
260
261
|
/**
|
|
@@ -344,11 +345,93 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
344
345
|
* Release device — release interface and close.
|
|
345
346
|
*/
|
|
346
347
|
async release(path: string, _onclose?: boolean): Promise<void> {
|
|
348
|
+
await this.cancelActiveTransfers(path);
|
|
347
349
|
await this.invalidateProtocolV2UsbLink(path, 'Node USB transport released');
|
|
348
350
|
await this.closeOpenDevice(path);
|
|
349
351
|
this.deviceProtocol.delete(path);
|
|
350
352
|
}
|
|
351
353
|
|
|
354
|
+
private async cancelActiveTransfers(path?: string): Promise<void> {
|
|
355
|
+
const transfers = Array.from(this.activeTransfers.entries()).filter(
|
|
356
|
+
([, active]) => path === undefined || active.path === path
|
|
357
|
+
);
|
|
358
|
+
transfers.forEach(([transfer]) => {
|
|
359
|
+
try {
|
|
360
|
+
transfer.cancel();
|
|
361
|
+
} catch {
|
|
362
|
+
// A transfer may finish between the snapshot and cancel().
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
await Promise.allSettled(transfers.map(([, active]) => active.settled));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private createTrackedTransfer(
|
|
369
|
+
path: string,
|
|
370
|
+
endpoint: usb.InEndpoint | usb.OutEndpoint,
|
|
371
|
+
callback: (error: Error | undefined, buffer: Buffer, actualLength: number) => void
|
|
372
|
+
): usb.Transfer {
|
|
373
|
+
let resolveSettled: () => void = () => undefined;
|
|
374
|
+
const settled = new Promise<void>(resolve => {
|
|
375
|
+
resolveSettled = resolve;
|
|
376
|
+
});
|
|
377
|
+
const transfer = endpoint.makeTransfer(endpoint.timeout, (error, buffer, actualLength) => {
|
|
378
|
+
this.activeTransfers.delete(transfer);
|
|
379
|
+
resolveSettled();
|
|
380
|
+
callback(error, buffer, actualLength);
|
|
381
|
+
});
|
|
382
|
+
this.activeTransfers.set(transfer, { path, settled, resolveSettled });
|
|
383
|
+
return transfer;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private transferInOnce(path: string, ep: usb.InEndpoint, length: number): Promise<Buffer> {
|
|
387
|
+
return new Promise((resolve, reject) => {
|
|
388
|
+
const buffer = Buffer.alloc(length);
|
|
389
|
+
const transfer = this.createTrackedTransfer(
|
|
390
|
+
path,
|
|
391
|
+
ep,
|
|
392
|
+
(error, _submittedBuffer, actualLength) => {
|
|
393
|
+
if (error) {
|
|
394
|
+
reject(error);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (actualLength <= 0) {
|
|
398
|
+
reject(new Error('Empty USB transfer'));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
resolve(buffer.subarray(0, actualLength));
|
|
402
|
+
}
|
|
403
|
+
);
|
|
404
|
+
try {
|
|
405
|
+
transfer.submit(buffer);
|
|
406
|
+
} catch (error) {
|
|
407
|
+
const active = this.activeTransfers.get(transfer);
|
|
408
|
+
this.activeTransfers.delete(transfer);
|
|
409
|
+
active?.resolveSettled();
|
|
410
|
+
reject(error);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private transferOutOnce(path: string, ep: usb.OutEndpoint, data: Buffer): Promise<void> {
|
|
416
|
+
return new Promise((resolve, reject) => {
|
|
417
|
+
const transfer = this.createTrackedTransfer(path, ep, error => {
|
|
418
|
+
if (error) {
|
|
419
|
+
reject(error);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
resolve();
|
|
423
|
+
});
|
|
424
|
+
try {
|
|
425
|
+
transfer.submit(data);
|
|
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
|
+
|
|
352
435
|
private async closeOpenDevice(path: string): Promise<void> {
|
|
353
436
|
const openDev = this.openDevices.get(path);
|
|
354
437
|
if (!openDev) return;
|
|
@@ -468,6 +551,12 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
468
551
|
|
|
469
552
|
private isRetryableError(error: unknown): boolean {
|
|
470
553
|
const message = this.getErrorMessage(error).toLowerCase();
|
|
554
|
+
// cancelActiveTransfers() terminates timed-out or releasing native requests.
|
|
555
|
+
// LIBUSB_TRANSFER_CANCELLED is not transient. Retrying would start another pending
|
|
556
|
+
// read on the rebuilt interface and race protocol probing against release.
|
|
557
|
+
if (message.includes('cancelled') || message.includes('canceled')) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
471
560
|
return (
|
|
472
561
|
message.includes('libusb') ||
|
|
473
562
|
message.includes('transfer') ||
|
|
@@ -572,7 +661,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
572
661
|
const packet = new Uint8Array(PACKET_SIZE);
|
|
573
662
|
packet[0] = REPORT_ID;
|
|
574
663
|
packet.set(new Uint8Array(buffer), 1);
|
|
575
|
-
await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
664
|
+
await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
576
665
|
}
|
|
577
666
|
return; // all chunks sent successfully
|
|
578
667
|
} catch (error) {
|
|
@@ -615,7 +704,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
615
704
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
616
705
|
}
|
|
617
706
|
try {
|
|
618
|
-
return await transferInOnce(currentDev.epIn, length);
|
|
707
|
+
return await this.transferInOnce(path, currentDev.epIn, length);
|
|
619
708
|
} catch (error) {
|
|
620
709
|
lastError = error;
|
|
621
710
|
if (options?.waitIndefinitelyOnTimeout && this.isUsbTransferTimeout(error)) {
|
|
@@ -695,7 +784,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
695
784
|
epIn.timeout = TRANSFER_TIMEOUT_MS;
|
|
696
785
|
epOut.timeout = TRANSFER_TIMEOUT_MS;
|
|
697
786
|
|
|
698
|
-
await this.drainStaleInput(epIn);
|
|
787
|
+
await this.drainStaleInput(path, epIn);
|
|
699
788
|
|
|
700
789
|
this.openDevices.set(path, { device: dev, iface, epIn, epOut });
|
|
701
790
|
} catch (err) {
|
|
@@ -708,14 +797,14 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
708
797
|
}
|
|
709
798
|
}
|
|
710
799
|
|
|
711
|
-
private async drainStaleInput(epIn: usb.InEndpoint): Promise<void> {
|
|
800
|
+
private async drainStaleInput(path: string, epIn: usb.InEndpoint): Promise<void> {
|
|
712
801
|
const originalTimeout = epIn.timeout;
|
|
713
802
|
epIn.timeout = 50;
|
|
714
803
|
try {
|
|
715
804
|
// Drain a small bounded number of packets left by the previous USB session.
|
|
716
805
|
for (let index = 0; index < 16; index += 1) {
|
|
717
806
|
try {
|
|
718
|
-
await transferInOnce(epIn, PACKET_SIZE);
|
|
807
|
+
await this.transferInOnce(path, epIn, PACKET_SIZE);
|
|
719
808
|
} catch {
|
|
720
809
|
break;
|
|
721
810
|
}
|
|
@@ -744,9 +833,9 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
744
833
|
expectedProtocol?: ProtocolType
|
|
745
834
|
): Promise<ProtocolType> {
|
|
746
835
|
if (expectedProtocol === 'V2') {
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
//
|
|
836
|
+
// Bootloader may not answer Ping after an update reboot. An explicit V2 hint means
|
|
837
|
+
// the protocol was confirmed earlier; establish the link and let the first command
|
|
838
|
+
// verify that USB initialization completed.
|
|
750
839
|
this.deviceProtocol.set(path, 'V2');
|
|
751
840
|
this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2 (expected)`);
|
|
752
841
|
return 'V2';
|
|
@@ -783,6 +872,10 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
783
872
|
await this.rotateProtocolV2UsbGeneration(path, 'Node USB protocol probe reset');
|
|
784
873
|
|
|
785
874
|
try {
|
|
875
|
+
// A timed-out probe may leave an IN transfer pending. Cancel and await it before
|
|
876
|
+
// closing the interface, or libusb may never callback and release()/stop() will
|
|
877
|
+
// wait forever, surfacing as Polling timeout (809).
|
|
878
|
+
await this.cancelActiveTransfers(path);
|
|
786
879
|
await this.closeOpenDevice(path);
|
|
787
880
|
} catch (error) {
|
|
788
881
|
this.Log?.debug('[NodeUsbTransport] close after protocol probe error:', error);
|
|
@@ -883,7 +976,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
883
976
|
if (this.cancelled) {
|
|
884
977
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
885
978
|
}
|
|
886
|
-
await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
979
|
+
await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
887
980
|
}
|
|
888
981
|
|
|
889
982
|
protected async readProtocolV2UsbPacket(
|
|
@@ -895,7 +988,8 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
895
988
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
896
989
|
}
|
|
897
990
|
try {
|
|
898
|
-
const packet = await transferInOnce(
|
|
991
|
+
const packet = await this.transferInOnce(
|
|
992
|
+
path,
|
|
899
993
|
this.getOpenDevice(path).epIn,
|
|
900
994
|
PROTOCOL_V2_FRAME_MAX_BYTES
|
|
901
995
|
);
|
|
@@ -911,6 +1005,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
911
1005
|
}
|
|
912
1006
|
|
|
913
1007
|
protected async resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise<void> {
|
|
1008
|
+
await this.cancelActiveTransfers(path);
|
|
914
1009
|
await this.closeOpenDevice(path);
|
|
915
1010
|
}
|
|
916
1011
|
|