@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.
@@ -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
- if (epIn.timeout === 50) {
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
- const seq = data[6];
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((callback: () => void) => callback()),
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.transfer).not.toHaveBeenCalled();
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.transfer.mockClear();
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.transfer).toHaveBeenCalledTimes(1);
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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
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
- this.disposeProtocolV2UsbLinks('Node USB transport stopped').catch(error => { var _a; return (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[NodeUsbTransport] stop link cleanup failed:', error); });
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.19",
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.19",
24
- "@onekeyfe/hd-transport": "1.2.0-alpha.19",
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": "4e79f6a400463f342285b9a761b6b84523f1d6b9"
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.disposeProtocolV2UsbLinks('Node USB transport stopped').catch(error =>
256
- this.Log?.debug('[NodeUsbTransport] stop link cleanup failed:', error)
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
- // 固件升级重启后的 bootloader 不保证响应 Ping。上层显式传入 V2
748
- // 表示协议已在重启前确认;直接建立 V2 链路,由首个业务命令验证
749
- // bootloader 是否已经完成 USB 初始化。
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