@onekeyfe/hd-transport-web-device 1.2.8-alpha.16 → 1.2.8-alpha.18

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.
@@ -2,8 +2,11 @@ import { ERRORS, HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
2
2
 
3
3
  import WebUsbTransport from '../src/webusb';
4
4
  import { probeUsbDeviceIdentities } from '@onekeyfe/hd-common-connect-sdk/src';
5
+ import Core from '@onekeyfe/hd-core/src/core';
6
+ import TransportManager from '@onekeyfe/hd-core/src/data-manager/TransportManager';
7
+ import { Device } from '@onekeyfe/hd-core/src/device/Device';
5
8
 
6
- import type { MessageFromOneKey, TransportCallOptions } from '@onekeyfe/hd-transport';
9
+ import type { MessageFromOneKey, Transport, TransportCallOptions } from '@onekeyfe/hd-transport';
7
10
 
8
11
  jest.mock('@onekeyfe/hd-core', () => jest.requireActual('../../core/src'));
9
12
  jest.mock('../../core/src/data/config', () => ({
@@ -19,7 +22,7 @@ jest.mock('@onekeyfe/hd-transport-emulator', () => ({}));
19
22
 
20
23
  type PacketIoTransport = {
21
24
  rotateProtocolV2UsbGeneration: () => Promise<void>;
22
- detectProtocol: () => Promise<'V1'>;
25
+ detectProtocol: () => Promise<'V1' | 'V2'>;
23
26
  transferInWithRetry: (path: string, length: number) => Promise<DataView>;
24
27
  transferInOnce: (path: string, length: number) => Promise<DataView>;
25
28
  transferOutWithRetry: (path: string, packet: Uint8Array) => Promise<void>;
@@ -64,6 +67,251 @@ describe('WebUSB identity probe ownership', () => {
64
67
  else delete (globalThis as { navigator?: Navigator }).navigator;
65
68
  });
66
69
 
70
+ function createAcquiredTransport(protocol: 'V1' | 'V2' = 'V1') {
71
+ const active = new WebUsbTransport();
72
+ active.init({ debug: jest.fn() });
73
+ jest
74
+ .spyOn(active as unknown as PacketIoTransport, 'detectProtocol')
75
+ .mockResolvedValue(protocol);
76
+ jest.spyOn(active, 'connect').mockImplementation(() => {
77
+ device.opened = true;
78
+ return Promise.resolve();
79
+ });
80
+ return active;
81
+ }
82
+
83
+ test.each(['V1', 'V2'] as const)(
84
+ 'Core disposal releases a retained %s USB session for a new transport',
85
+ async protocol => {
86
+ const active = createAcquiredTransport(protocol);
87
+ const next = createAcquiredTransport(protocol);
88
+ const selected = new Device({
89
+ path: 'USB_PATH',
90
+ id: 'USB_PATH',
91
+ name: 'Mock device',
92
+ commType: 'webusb',
93
+ debug: false,
94
+ });
95
+ selected.keepSession = true;
96
+ selected.mainId = 'USB_PATH';
97
+ jest.spyOn(selected, 'isUsedHere').mockReturnValue(true);
98
+ const release = jest.spyOn(active, 'release');
99
+ jest.spyOn(TransportManager, 'getTransport').mockReturnValue(active as unknown as Transport);
100
+ const core = new Core();
101
+ try {
102
+ await active.acquire({ path: 'USB_PATH' });
103
+ await selected.release();
104
+ expect(release).not.toHaveBeenCalled();
105
+ expect(device.opened).toBe(true);
106
+ await core.dispose();
107
+ await core.dispose();
108
+ expect(device.opened).toBe(false);
109
+ expect(device.close).toHaveBeenCalledTimes(1);
110
+ await expect(next.acquire({ path: 'USB_PATH' })).resolves.toBe('USB_PATH');
111
+ } finally {
112
+ await active.release('USB_PATH');
113
+ await next.release('USB_PATH');
114
+ jest.restoreAllMocks();
115
+ }
116
+ }
117
+ );
118
+
119
+ test('stop waits for a pending native open and closes its captured handle', async () => {
120
+ const entered = createDeferred<void>();
121
+ const opening = createDeferred<void>();
122
+ const native = Object.assign(device, {
123
+ open: jest.fn(async () => {
124
+ entered.resolve();
125
+ await opening.promise;
126
+ device.opened = true;
127
+ }),
128
+ reset: jest.fn(),
129
+ configurations: [],
130
+ configuration: { configurationValue: 1 },
131
+ selectConfiguration: jest.fn(),
132
+ claimInterface: jest.fn(),
133
+ clearHalt: jest.fn(),
134
+ });
135
+ const active = new WebUsbTransport();
136
+ active.init({ debug: jest.fn() });
137
+ const connecting = active.connectToDevice('USB_PATH', true);
138
+ const result = expect(connecting).rejects.toThrow('WebUSB transport is stopped');
139
+ try {
140
+ await entered.promise;
141
+ let stopped = false;
142
+ const stopping = active.stop().then(() => {
143
+ stopped = true;
144
+ });
145
+ await Promise.resolve();
146
+ expect(stopped).toBe(false);
147
+ opening.resolve();
148
+ await result;
149
+ await stopping;
150
+ expect(native.reset).not.toHaveBeenCalled();
151
+ expect(native.claimInterface).not.toHaveBeenCalled();
152
+ expect(device.opened).toBe(false);
153
+ const next = createAcquiredTransport();
154
+ await expect(next.acquire({ path: 'USB_PATH' })).resolves.toBe('USB_PATH');
155
+ await next.release('USB_PATH');
156
+ } finally {
157
+ opening.resolve();
158
+ await connecting.catch(() => undefined);
159
+ await active.release('USB_PATH');
160
+ jest.restoreAllMocks();
161
+ }
162
+ });
163
+
164
+ test.each(['read', 'read retry', 'write', 'write retry'])(
165
+ 'stop rejects a late native %s result without touching the new owner',
166
+ async operation => {
167
+ const entered = createDeferred<void>();
168
+ const packet = createDeferred<{ status: 'ok'; data: DataView; bytesWritten: number }>();
169
+ Object.assign(device, {
170
+ transferIn: jest.fn(() => {
171
+ entered.resolve();
172
+ return packet.promise;
173
+ }),
174
+ transferOut: jest.fn(() => {
175
+ entered.resolve();
176
+ return packet.promise;
177
+ }),
178
+ });
179
+ const active = createAcquiredTransport();
180
+ const next = createAcquiredTransport();
181
+ await active.acquire({ path: 'USB_PATH' });
182
+ const state = active as unknown as PacketIoTransport;
183
+ let call: Promise<unknown>;
184
+ if (operation === 'read') call = state.transferInOnce('USB_PATH', 64);
185
+ else if (operation === 'read retry') call = state.transferInWithRetry('USB_PATH', 64);
186
+ else if (operation === 'write') call = state.transferOutOnce('USB_PATH', new Uint8Array([1]));
187
+ else call = state.transferOutWithRetry('USB_PATH', new Uint8Array([1]));
188
+ const result = expect(call).rejects.toThrow('WebUSB transport is stopped');
189
+ try {
190
+ await entered.promise;
191
+ await active.stop();
192
+ await next.acquire({ path: 'USB_PATH' });
193
+ packet.resolve({ status: 'ok', data: new DataView(new ArrayBuffer(64)), bytesWritten: 1 });
194
+ await result;
195
+ expect(device.close).toHaveBeenCalledTimes(1);
196
+ expect(device.opened).toBe(true);
197
+ } finally {
198
+ packet.resolve({ status: 'ok', data: new DataView(new ArrayBuffer(64)), bytesWritten: 1 });
199
+ await call.catch(() => undefined);
200
+ await active.release('USB_PATH');
201
+ await next.release('USB_PATH');
202
+ jest.restoreAllMocks();
203
+ }
204
+ }
205
+ );
206
+
207
+ test.each(['in', 'out'] as const)(
208
+ 'stop prevents a delayed transfer%s retry from closing or reopening the new owner',
209
+ async direction => {
210
+ const entered = createDeferred<void>();
211
+ const native = Object.assign(device, {
212
+ transferIn: jest.fn().mockRejectedValue(new Error('transferIn NetworkError')),
213
+ transferOut: jest.fn().mockRejectedValue(new Error('transferOut NetworkError')),
214
+ });
215
+ const active = createAcquiredTransport();
216
+ const next = createAcquiredTransport();
217
+ await active.acquire({ path: 'USB_PATH' });
218
+ const state = active as unknown as PacketIoTransport;
219
+ const retry = state.reconnectForPacketIoRetry.bind(active);
220
+ jest.spyOn(state, 'reconnectForPacketIoRetry').mockImplementation((...args) => {
221
+ entered.resolve();
222
+ return retry(...args);
223
+ });
224
+ const call =
225
+ direction === 'in'
226
+ ? state.transferInWithRetry('USB_PATH', 64)
227
+ : state.transferOutWithRetry('USB_PATH', new Uint8Array([1]));
228
+ const result = expect(call).rejects.toThrow('WebUSB transport is stopped');
229
+ try {
230
+ await entered.promise;
231
+ await active.stop();
232
+ await next.acquire({ path: 'USB_PATH' });
233
+ await result;
234
+ expect(native[direction === 'in' ? 'transferIn' : 'transferOut']).toHaveBeenCalledTimes(1);
235
+ expect(device.close).toHaveBeenCalledTimes(1);
236
+ expect(device.opened).toBe(true);
237
+ } finally {
238
+ await call.catch(() => undefined);
239
+ await active.release('USB_PATH');
240
+ await next.release('USB_PATH');
241
+ jest.restoreAllMocks();
242
+ }
243
+ }
244
+ );
245
+
246
+ test('failed stop keeps ownership until a later discovery confirms native closure', async () => {
247
+ const active = createAcquiredTransport();
248
+ const next = createAcquiredTransport();
249
+ const failure = new Error('native close failed');
250
+ await active.acquire({ path: 'USB_PATH' });
251
+ device.close.mockRejectedValueOnce(failure).mockRejectedValueOnce(failure);
252
+ try {
253
+ await expect(active.stop()).rejects.toBe(failure);
254
+ await next.getConnectedDevices();
255
+ await expect(next.acquire({ path: 'USB_PATH' })).rejects.toThrow(
256
+ 'USB device is already in use'
257
+ );
258
+ expect(device.opened).toBe(true);
259
+ await next.getConnectedDevices();
260
+ expect(device.opened).toBe(false);
261
+ await expect(next.acquire({ path: 'USB_PATH' })).resolves.toBe('USB_PATH');
262
+ expect(device.close).toHaveBeenCalledTimes(3);
263
+ } finally {
264
+ await active.release('USB_PATH');
265
+ await next.release('USB_PATH');
266
+ jest.restoreAllMocks();
267
+ }
268
+ });
269
+
270
+ test('stop closes captured handles across paths without closing re-enumerated or foreign handles', async () => {
271
+ const other = {
272
+ ...device,
273
+ serialNumber: 'OTHER_PATH',
274
+ close: jest.fn(() => {
275
+ other.opened = false;
276
+ return Promise.resolve();
277
+ }),
278
+ };
279
+ const foreign = { ...device, serialNumber: 'FOREIGN_PATH', close: jest.fn() };
280
+ usb.getDevices.mockResolvedValue([device, other, foreign]);
281
+ const active = createAcquiredTransport();
282
+ jest.spyOn(active, 'connect').mockImplementation(path => {
283
+ (path === 'USB_PATH' ? device : other).opened = true;
284
+ return Promise.resolve();
285
+ });
286
+ const current = createAcquiredTransport();
287
+ jest.spyOn(current, 'connect').mockImplementation(() => {
288
+ foreign.opened = true;
289
+ return Promise.resolve();
290
+ });
291
+ const currentDisconnect = jest.spyOn(current, 'markProtocolStale');
292
+ try {
293
+ await active.acquire({ path: 'USB_PATH' });
294
+ await active.acquire({ path: 'OTHER_PATH' });
295
+ await current.acquire({ path: 'FOREIGN_PATH' });
296
+ const replacement = { ...device, opened: false, close: jest.fn() };
297
+ usb.getDevices.mockResolvedValue([replacement, foreign]);
298
+ await active.getConnectedDevices();
299
+ await active.stop();
300
+ expect(device.opened).toBe(false);
301
+ expect(other.opened).toBe(false);
302
+ expect(replacement.close).not.toHaveBeenCalled();
303
+ expect(foreign.close).not.toHaveBeenCalled();
304
+ expect(foreign.opened).toBe(true);
305
+ disconnectListener({ device: replacement });
306
+ expect(currentDisconnect).toHaveBeenCalledWith('USB_PATH');
307
+ } finally {
308
+ await active.release('USB_PATH');
309
+ await active.release('OTHER_PATH');
310
+ await current.release('FOREIGN_PATH');
311
+ jest.restoreAllMocks();
312
+ }
313
+ });
314
+
67
315
  test('does not close a handle already opened by another operation', async () => {
68
316
  const probe = new WebUsbTransport(true);
69
317
  probe.init({ debug: jest.fn() });
package/dist/index.d.ts CHANGED
@@ -32,6 +32,8 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
32
32
  private acquiredPaths;
33
33
  private identityProbeDevices;
34
34
  private normalDevices;
35
+ private pendingHandleOperations;
36
+ private stopPromise?;
35
37
  private identityProbeCleanupPaths;
36
38
  /**
37
39
  * The exact USBDevice object each cached protocol was probed against. The
@@ -62,6 +64,10 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
62
64
  * Initialize WebUSB transport
63
65
  */
64
66
  init(logger: any, emitter?: EventEmitter): void;
67
+ stop(): Promise<void>;
68
+ private stopResources;
69
+ private assertRunning;
70
+ private runHandleOperation;
65
71
  /**
66
72
  * Announce that a USB device left. Called from the module-scoped disconnect
67
73
  * listener, which is why it is public rather than inlined.
package/dist/index.js CHANGED
@@ -90,6 +90,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
90
90
  this.acquiredPaths = new Set();
91
91
  this.identityProbeDevices = new Map();
92
92
  this.normalDevices = new Map();
93
+ this.pendingHandleOperations = new Set();
93
94
  this.identityProbeCleanupPaths = new Set();
94
95
  this.probedDeviceObjects = new Map();
95
96
  this.deviceEndpoints = new Map();
@@ -112,6 +113,102 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
112
113
  if (!this.identityProbe)
113
114
  registerActiveWebUsbTransport(this, usb);
114
115
  }
116
+ stop() {
117
+ this.stopped = true;
118
+ this.acquiredPaths.clear();
119
+ if (activeWebUsbTransport === this)
120
+ activeWebUsbTransport = undefined;
121
+ if (!this.stopPromise) {
122
+ this.stopPromise = this.stopResources().catch(error => {
123
+ this.stopPromise = undefined;
124
+ throw error;
125
+ });
126
+ }
127
+ return this.stopPromise;
128
+ }
129
+ stopResources() {
130
+ var _a, _b, _c;
131
+ return __awaiter(this, void 0, void 0, function* () {
132
+ let cleanupError;
133
+ try {
134
+ yield this.disposeProtocolV2UsbLinks('WebUSB transport stopped');
135
+ }
136
+ catch (error) {
137
+ cleanupError = error;
138
+ }
139
+ yield Promise.allSettled([...this.pendingHandleOperations]);
140
+ const handles = this.identityProbe
141
+ ? [...this.identityProbeDevices].map(([path, device]) => [device, path])
142
+ : [...this.normalDevices];
143
+ for (const [device, path] of handles) {
144
+ if (!this.identityProbe && normalHandleOwners.get(device) !== this) {
145
+ this.normalDevices.delete(device);
146
+ }
147
+ else {
148
+ try {
149
+ if (device.opened) {
150
+ const iface = (_b = (_a = this.deviceEndpoints.get(path)) === null || _a === void 0 ? void 0 : _a.interfaceNumber) !== null && _b !== void 0 ? _b : this.interfaceId;
151
+ try {
152
+ yield device.releaseInterface(iface);
153
+ }
154
+ catch (error) {
155
+ (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug('[WebUsbTransport] stop releaseInterface failed:', error);
156
+ }
157
+ yield device.close();
158
+ if (device.opened)
159
+ throw new Error('WebUSB handle remains open after stop');
160
+ }
161
+ if (this.identityProbe) {
162
+ identityProbeHandles.delete(device);
163
+ this.identityProbeDevices.delete(path);
164
+ this.identityProbeCleanupPaths.delete(path);
165
+ }
166
+ else {
167
+ normalHandleOwners.delete(device);
168
+ this.normalDevices.delete(device);
169
+ }
170
+ }
171
+ catch (error) {
172
+ cleanupError !== null && cleanupError !== void 0 ? cleanupError : (cleanupError = error);
173
+ }
174
+ }
175
+ }
176
+ this.deviceList = [];
177
+ this.deviceProtocol.clear();
178
+ this.confirmedDeviceProtocols.clear();
179
+ this.deviceProtocolHints.clear();
180
+ this.staleProtocolPaths.clear();
181
+ this.probedDeviceObjects.clear();
182
+ if (!this.normalDevices.size && !this.identityProbeDevices.size) {
183
+ this.deviceEndpoints.clear();
184
+ }
185
+ if (cleanupError) {
186
+ throw cleanupError instanceof Error
187
+ ? cleanupError
188
+ : new Error(this.getErrorMessage(cleanupError));
189
+ }
190
+ });
191
+ }
192
+ assertRunning() {
193
+ if (this.stopped) {
194
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'WebUSB transport is stopped');
195
+ }
196
+ }
197
+ runHandleOperation(operation) {
198
+ return __awaiter(this, void 0, void 0, function* () {
199
+ this.assertRunning();
200
+ const pending = Promise.resolve(operation());
201
+ this.pendingHandleOperations.add(pending);
202
+ try {
203
+ const result = yield pending;
204
+ this.assertRunning();
205
+ return result;
206
+ }
207
+ finally {
208
+ this.pendingHandleOperations.delete(pending);
209
+ }
210
+ });
211
+ }
115
212
  emitDeviceDisconnect(path, device) {
116
213
  var _a, _b;
117
214
  (_a = this.emitter) === null || _a === void 0 ? void 0 : _a.emit(transport.TRANSPORT_EVENT.DEVICE_DISCONNECT, {
@@ -171,10 +268,24 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
171
268
  });
172
269
  }
173
270
  getConnectedDevices() {
271
+ var _a;
174
272
  return __awaiter(this, void 0, void 0, function* () {
273
+ this.assertRunning();
175
274
  if (!this.usb)
176
275
  return [];
177
276
  const devices = yield this.usb.getDevices();
277
+ this.assertRunning();
278
+ for (const owner of new Set(devices.map(device => normalHandleOwners.get(device)))) {
279
+ if (owner === null || owner === void 0 ? void 0 : owner.stopped) {
280
+ try {
281
+ yield owner.stop();
282
+ }
283
+ catch (error) {
284
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[WebUsbTransport] stopped owner cleanup failed:', error);
285
+ }
286
+ }
287
+ }
288
+ this.assertRunning();
178
289
  const onekeyDevices = devices.filter(dev => {
179
290
  const isOneKey = hdShared.ONEKEY_WEBUSB_FILTER.some((desc) => dev.vendorId === desc.vendorId && dev.productId === desc.productId);
180
291
  return isOneKey && !hdShared.isKnownTrezorWebUsbDevice(dev);
@@ -201,6 +312,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
201
312
  acquire(input) {
202
313
  var _a, _b, _c, _d, _e, _f, _g;
203
314
  return __awaiter(this, void 0, void 0, function* () {
315
+ this.assertRunning();
204
316
  if (!input.path)
205
317
  return;
206
318
  if (this.identityProbe) {
@@ -259,12 +371,14 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
259
371
  this.deviceProtocolHints.set(input.path, protocolHint);
260
372
  }
261
373
  const detectedProtocol = yield this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
374
+ this.assertRunning();
262
375
  this.confirmedDeviceProtocols.set(input.path, detectedProtocol);
263
376
  const probedDevice = (_g = this.deviceList.find(d => d.path === input.path)) === null || _g === void 0 ? void 0 : _g.device;
264
377
  if (probedDevice) {
265
378
  this.probedDeviceObjects.set(input.path, probedDevice);
266
379
  }
267
380
  }
381
+ this.assertRunning();
268
382
  this.acquiredPaths.add(input.path);
269
383
  return yield Promise.resolve(input.path);
270
384
  }
@@ -357,10 +471,12 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
357
471
  return __awaiter(this, void 0, void 0, function* () {
358
472
  const maxRetries = 5;
359
473
  for (let i = 0; i < maxRetries; i++) {
474
+ this.assertRunning();
360
475
  try {
361
476
  return yield this.connectToDevice(path, first);
362
477
  }
363
478
  catch (e) {
479
+ this.assertRunning();
364
480
  if (e instanceof hdShared.HardwareError && e.errorCode === hdShared.HardwareErrorCode.BridgeNeedsPermission) {
365
481
  throw e;
366
482
  }
@@ -403,6 +519,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
403
519
  connectToDevice(path, first) {
404
520
  var _a;
405
521
  return __awaiter(this, void 0, void 0, function* () {
522
+ this.assertRunning();
406
523
  let device = yield this.findDevice(path);
407
524
  if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
408
525
  yield this.closeOpenDevice(path);
@@ -415,7 +532,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
415
532
  if (!device.opened) {
416
533
  openingHandles.add(device);
417
534
  try {
418
- yield device.open();
535
+ yield this.runHandleOperation(() => device.open());
419
536
  }
420
537
  catch (error) {
421
538
  return yield this.throwWebUsbIoError(path, 'open', error);
@@ -426,9 +543,10 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
426
543
  }
427
544
  if (first || this.deviceProtocol.get(path) !== 'V1') {
428
545
  try {
429
- yield device.reset();
546
+ yield this.runHandleOperation(() => device.reset());
430
547
  }
431
548
  catch (error) {
549
+ this.assertRunning();
432
550
  (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
433
551
  }
434
552
  }
@@ -445,7 +563,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
445
563
  if (!device.opened) {
446
564
  openingHandles.add(device);
447
565
  try {
448
- yield device.open();
566
+ yield this.runHandleOperation(() => device.open());
449
567
  }
450
568
  catch (error) {
451
569
  return yield this.throwWebUsbIoError(path, 'open', error);
@@ -457,12 +575,12 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
457
575
  if (first ||
458
576
  !device.configuration ||
459
577
  device.configuration.configurationValue !== this.configurationId) {
460
- yield device.selectConfiguration(this.configurationId);
578
+ yield this.runHandleOperation(() => device.selectConfiguration(this.configurationId));
461
579
  }
462
580
  const endpoints = this.discoverEndpoints(device);
463
581
  this.deviceEndpoints.set(path, endpoints);
464
582
  try {
465
- yield device.claimInterface(endpoints.interfaceNumber);
583
+ yield this.runHandleOperation(() => device.claimInterface(endpoints.interfaceNumber));
466
584
  }
467
585
  catch (error) {
468
586
  return this.throwWebUsbIoError(path, 'claimInterface', error);
@@ -472,6 +590,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
472
590
  });
473
591
  }
474
592
  assertHandleAvailable(device) {
593
+ this.assertRunning();
475
594
  const normalOwner = normalHandleOwners.get(device);
476
595
  if ((normalOwner && normalOwner !== this) ||
477
596
  (identityProbeHandles.has(device) &&
@@ -485,6 +604,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
485
604
  this.normalDevices.set(device, path);
486
605
  }
487
606
  releaseNormalHandles(path) {
607
+ if (this.stopped)
608
+ return;
488
609
  for (const [device, ownedPath] of this.normalDevices) {
489
610
  if (ownedPath === path) {
490
611
  normalHandleOwners.delete(device);
@@ -498,6 +619,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
498
619
  this.getErrorMessage(error).includes(USB_DEVICE_IN_USE));
499
620
  }
500
621
  claimIdentityProbeHandle(path, device) {
622
+ this.assertRunning();
501
623
  if (device.opened ||
502
624
  openingHandles.has(device) ||
503
625
  identityProbeHandles.has(device) ||
@@ -508,6 +630,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
508
630
  this.identityProbeDevices.set(path, device);
509
631
  }
510
632
  releaseIdentityProbeHandle(path) {
633
+ if (this.stopped)
634
+ return;
511
635
  const device = this.identityProbeDevices.get(path);
512
636
  if (device === null || device === void 0 ? void 0 : device.opened) {
513
637
  this.identityProbeCleanupPaths.add(path);
@@ -521,6 +645,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
521
645
  closeOpenDevice(path) {
522
646
  var _a, _b, _c, _d;
523
647
  return __awaiter(this, void 0, void 0, function* () {
648
+ if (this.stopped)
649
+ return;
524
650
  const current = this.identityProbe
525
651
  ? this.identityProbeDevices.get(path)
526
652
  : (_a = this.deviceList.find(device => device.path === path)) === null || _a === void 0 ? void 0 : _a.device;
@@ -530,14 +656,14 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
530
656
  const endpoints = this.deviceEndpoints.get(path);
531
657
  const ifaceNum = (_b = endpoints === null || endpoints === void 0 ? void 0 : endpoints.interfaceNumber) !== null && _b !== void 0 ? _b : this.interfaceId;
532
658
  try {
533
- yield current.releaseInterface(ifaceNum);
659
+ yield this.runHandleOperation(() => current.releaseInterface(ifaceNum));
534
660
  }
535
661
  catch (error) {
536
662
  (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
537
663
  }
538
664
  this.assertHandleAvailable(current);
539
665
  try {
540
- yield current.close();
666
+ yield this.runHandleOperation(() => current.close());
541
667
  }
542
668
  catch (error) {
543
669
  (_d = this.Log) === null || _d === void 0 ? void 0 : _d.debug('[WebUsbTransport] close before reconnect failed:', error);
@@ -551,14 +677,16 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
551
677
  return __awaiter(this, void 0, void 0, function* () {
552
678
  this.assertHandleAvailable(device);
553
679
  try {
554
- yield device.clearHalt(direction, endpointNumber);
680
+ yield this.runHandleOperation(() => device.clearHalt(direction, endpointNumber));
555
681
  }
556
682
  catch (error) {
683
+ this.assertRunning();
557
684
  (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[WebUsbTransport] clearHalt ${direction} endpoint ${endpointNumber} failed, continuing:`, error);
558
685
  }
559
686
  });
560
687
  }
561
688
  assertAcquired(path) {
689
+ this.assertRunning();
562
690
  if (!this.acquiredPaths.has(path)) {
563
691
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device is not acquired for ${path}`);
564
692
  }
@@ -615,6 +743,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
615
743
  throwWebUsbIoError(path, operation, error) {
616
744
  var _a;
617
745
  return __awaiter(this, void 0, void 0, function* () {
746
+ this.assertRunning();
618
747
  if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error))
619
748
  throw error;
620
749
  if (error instanceof transport.ProtocolV2LinkError && error.code !== 'io')
@@ -649,6 +778,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
649
778
  return /LIBUSB_ERROR_ACCESS|\b(?:access|permission) denied\b/i.test(message);
650
779
  }
651
780
  isRetryablePacketIoError(error) {
781
+ if (this.stopped)
782
+ return false;
652
783
  if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error))
653
784
  return false;
654
785
  const message = this.getErrorMessage(error).toLowerCase();
@@ -668,6 +799,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
668
799
  this.Log.debug(`[WebUsbTransport] transfer${direction} failed, retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(error)}`);
669
800
  this.staleProtocolPaths.add(path);
670
801
  yield hdShared.wait(attempt * PACKET_IO_RETRY_DELAY);
802
+ this.assertRunning();
671
803
  try {
672
804
  const currentDevice = yield this.findDevice(path);
673
805
  this.assertHandleAvailable(currentDevice);
@@ -675,16 +807,17 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
675
807
  const endpoints = this.deviceEndpoints.get(path);
676
808
  const ifaceNum = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.interfaceNumber) !== null && _a !== void 0 ? _a : this.interfaceId;
677
809
  try {
678
- yield currentDevice.releaseInterface(ifaceNum);
810
+ yield this.runHandleOperation(() => currentDevice.releaseInterface(ifaceNum));
679
811
  }
680
812
  catch (releaseError) {
681
813
  this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
682
814
  }
683
815
  this.assertHandleAvailable(currentDevice);
684
- yield currentDevice.close();
816
+ yield this.runHandleOperation(() => currentDevice.close());
685
817
  }
686
818
  }
687
819
  catch (closeError) {
820
+ this.assertRunning();
688
821
  if (this.isHandleOwnershipError(closeError))
689
822
  throw closeError;
690
823
  this.Log.debug('[WebUsbTransport] close device before retry error:', closeError);
@@ -728,6 +861,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
728
861
  yield this.reconnectForPacketIoRetry(path, 'out', attempt, error);
729
862
  }
730
863
  catch (reconnectError) {
864
+ this.assertRunning();
731
865
  if (this.isHandleOwnershipError(reconnectError))
732
866
  throw reconnectError;
733
867
  lastError = reconnectError;
@@ -750,6 +884,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
750
884
  const transferBuffer = this.toArrayBuffer(packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength));
751
885
  this.assertHandleAvailable(device);
752
886
  const result = yield device.transferOut(endpointOut, transferBuffer);
887
+ this.assertHandleAvailable(device);
753
888
  if (result.status !== 'ok') {
754
889
  return this.throwWebUsbIoError(path, 'transferOut', new Error(`transferOut status: ${String(result.status)}`));
755
890
  }
@@ -772,6 +907,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
772
907
  const endpointIn = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.endpointIn) !== null && _a !== void 0 ? _a : this.endpointId;
773
908
  this.assertHandleAvailable(device);
774
909
  const result = yield device.transferIn(endpointIn, length);
910
+ this.assertHandleAvailable(device);
775
911
  return this.getTransferInData(result);
776
912
  }
777
913
  catch (error) {
@@ -787,6 +923,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
787
923
  yield this.reconnectForPacketIoRetry(path, 'in', attempt, error);
788
924
  }
789
925
  catch (reconnectError) {
926
+ this.assertRunning();
790
927
  if (this.isHandleOwnershipError(reconnectError))
791
928
  throw reconnectError;
792
929
  lastError = reconnectError;
@@ -808,6 +945,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
808
945
  const endpointIn = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.endpointIn) !== null && _a !== void 0 ? _a : this.endpointId;
809
946
  this.assertHandleAvailable(device);
810
947
  const result = yield device.transferIn(endpointIn, length);
948
+ this.assertHandleAvailable(device);
811
949
  return this.getTransferInData(result);
812
950
  });
813
951
  }
package/dist/webusb.d.ts CHANGED
@@ -20,6 +20,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
20
20
  private acquiredPaths;
21
21
  private identityProbeDevices;
22
22
  private normalDevices;
23
+ private pendingHandleOperations;
24
+ private stopPromise?;
23
25
  private identityProbeCleanupPaths;
24
26
  private probedDeviceObjects;
25
27
  private deviceEndpoints;
@@ -35,6 +37,10 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
35
37
  interfaceId: number;
36
38
  constructor(identityProbe?: boolean);
37
39
  init(logger: any, emitter?: EventEmitter): void;
40
+ stop(): Promise<void>;
41
+ private stopResources;
42
+ private assertRunning;
43
+ private runHandleOperation;
38
44
  emitDeviceDisconnect(path: string, device?: USBDevice): void;
39
45
  markProtocolStale(path: string): void;
40
46
  configure(signedData: any): void;
@@ -1 +1 @@
1
- {"version":3,"file":"webusb.d.ts","sourceRoot":"","sources":["../src/webusb.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"webusb.d.ts","sourceRoot":"","sources":["../src/webusb.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-web-device",
3
- "version": "1.2.8-alpha.16",
3
+ "version": "1.2.8-alpha.18",
4
4
  "author": "OneKey",
5
5
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
6
6
  "license": "MIT",
@@ -21,13 +21,13 @@
21
21
  "lint:fix": "eslint . --fix"
22
22
  },
23
23
  "dependencies": {
24
- "@onekeyfe/hd-shared": "1.2.8-alpha.16",
25
- "@onekeyfe/hd-transport": "1.2.8-alpha.16"
24
+ "@onekeyfe/hd-shared": "1.2.8-alpha.18",
25
+ "@onekeyfe/hd-transport": "1.2.8-alpha.18"
26
26
  },
27
27
  "devDependencies": {
28
- "@onekeyfe/hd-transport-electron": "1.2.8-alpha.16",
28
+ "@onekeyfe/hd-transport-electron": "1.2.8-alpha.18",
29
29
  "@types/w3c-web-usb": "^1.0.6",
30
30
  "@types/web-bluetooth": "^0.0.17"
31
31
  },
32
- "gitHead": "7ccc3f507e2b536e6b5832e1cbb24a5fc1970ebe"
32
+ "gitHead": "1f66fc960c9d775272f989aa673d8efe2174729b"
33
33
  }
package/src/webusb.ts CHANGED
@@ -135,6 +135,10 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
135
135
 
136
136
  private normalDevices = new Map<USBDevice, string>();
137
137
 
138
+ private pendingHandleOperations = new Set<Promise<unknown>>();
139
+
140
+ private stopPromise?: Promise<void>;
141
+
138
142
  private identityProbeCleanupPaths = new Set<string>();
139
143
 
140
144
  /**
@@ -199,6 +203,96 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
199
203
  if (!this.identityProbe) registerActiveWebUsbTransport(this, usb);
200
204
  }
201
205
 
206
+ stop(): Promise<void> {
207
+ this.stopped = true;
208
+ this.acquiredPaths.clear();
209
+ if (activeWebUsbTransport === this) activeWebUsbTransport = undefined;
210
+ if (!this.stopPromise) {
211
+ this.stopPromise = this.stopResources().catch(error => {
212
+ // A later discovery can retry closure without dropping the handle guard.
213
+ this.stopPromise = undefined;
214
+ throw error;
215
+ });
216
+ }
217
+ return this.stopPromise;
218
+ }
219
+
220
+ private async stopResources() {
221
+ let cleanupError: unknown;
222
+ try {
223
+ await this.disposeProtocolV2UsbLinks('WebUSB transport stopped');
224
+ } catch (error) {
225
+ cleanupError = error;
226
+ }
227
+ // Wait for late open/reset/claim/close completions before releasing ownership.
228
+ // Reads are aborted by closing their handle and reject late results below.
229
+ await Promise.allSettled([...this.pendingHandleOperations]);
230
+ const handles = this.identityProbe
231
+ ? [...this.identityProbeDevices].map(([path, device]) => [device, path] as const)
232
+ : [...this.normalDevices];
233
+ for (const [device, path] of handles) {
234
+ if (!this.identityProbe && normalHandleOwners.get(device) !== this) {
235
+ this.normalDevices.delete(device);
236
+ } else {
237
+ try {
238
+ if (device.opened) {
239
+ const iface = this.deviceEndpoints.get(path)?.interfaceNumber ?? this.interfaceId;
240
+ try {
241
+ await device.releaseInterface(iface);
242
+ } catch (error) {
243
+ this.Log?.debug('[WebUsbTransport] stop releaseInterface failed:', error);
244
+ }
245
+ await device.close();
246
+ if (device.opened) throw new Error('WebUSB handle remains open after stop');
247
+ }
248
+ if (this.identityProbe) {
249
+ identityProbeHandles.delete(device);
250
+ this.identityProbeDevices.delete(path);
251
+ this.identityProbeCleanupPaths.delete(path);
252
+ } else {
253
+ normalHandleOwners.delete(device);
254
+ this.normalDevices.delete(device);
255
+ }
256
+ } catch (error) {
257
+ cleanupError ??= error;
258
+ }
259
+ }
260
+ }
261
+ this.deviceList = [];
262
+ this.deviceProtocol.clear();
263
+ this.confirmedDeviceProtocols.clear();
264
+ this.deviceProtocolHints.clear();
265
+ this.staleProtocolPaths.clear();
266
+ this.probedDeviceObjects.clear();
267
+ if (!this.normalDevices.size && !this.identityProbeDevices.size) {
268
+ this.deviceEndpoints.clear();
269
+ }
270
+ if (cleanupError) {
271
+ throw cleanupError instanceof Error
272
+ ? cleanupError
273
+ : new Error(this.getErrorMessage(cleanupError));
274
+ }
275
+ }
276
+
277
+ private assertRunning() {
278
+ if (this.stopped) {
279
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'WebUSB transport is stopped');
280
+ }
281
+ }
282
+
283
+ private async runHandleOperation<T>(operation: () => Promise<T>): Promise<T> {
284
+ this.assertRunning();
285
+ const pending = Promise.resolve(operation());
286
+ this.pendingHandleOperations.add(pending);
287
+ try {
288
+ const result = await pending;
289
+ this.assertRunning();
290
+ return result;
291
+ } finally {
292
+ this.pendingHandleOperations.delete(pending);
293
+ }
294
+ }
295
+
202
296
  /**
203
297
  * Announce that a USB device left. Called from the module-scoped disconnect
204
298
  * listener, which is why it is public rather than inlined.
@@ -296,9 +390,21 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
296
390
  * Get list of connected devices
297
391
  */
298
392
  async getConnectedDevices() {
393
+ this.assertRunning();
299
394
  if (!this.usb) return [];
300
395
 
301
396
  const devices = await this.usb.getDevices();
397
+ this.assertRunning();
398
+ for (const owner of new Set(devices.map(device => normalHandleOwners.get(device)))) {
399
+ if (owner?.stopped) {
400
+ try {
401
+ await owner.stop();
402
+ } catch (error) {
403
+ this.Log?.debug('[WebUsbTransport] stopped owner cleanup failed:', error);
404
+ }
405
+ }
406
+ }
407
+ this.assertRunning();
302
408
  const onekeyDevices = devices.filter(dev => {
303
409
  const isOneKey = ONEKEY_WEBUSB_FILTER.some(
304
410
  (desc: { vendorId: number; productId: number }) =>
@@ -331,6 +437,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
331
437
  * Acquire device control
332
438
  */
333
439
  async acquire(input: AcquireInput) {
440
+ this.assertRunning();
334
441
  if (!input.path) return;
335
442
  if (this.identityProbe) {
336
443
  await this.getConnectedDevices();
@@ -410,12 +517,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
410
517
  input.expectedProtocol,
411
518
  protocolHint
412
519
  );
520
+ this.assertRunning();
413
521
  this.confirmedDeviceProtocols.set(input.path, detectedProtocol);
414
522
  const probedDevice = this.deviceList.find(d => d.path === input.path)?.device;
415
523
  if (probedDevice) {
416
524
  this.probedDeviceObjects.set(input.path, probedDevice);
417
525
  }
418
526
  }
527
+ this.assertRunning();
419
528
  this.acquiredPaths.add(input.path);
420
529
  return await Promise.resolve(input.path);
421
530
  } catch (e) {
@@ -548,9 +657,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
548
657
  async connect(path: string, first: boolean) {
549
658
  const maxRetries = 5;
550
659
  for (let i = 0; i < maxRetries; i++) {
660
+ this.assertRunning();
551
661
  try {
552
662
  return await this.connectToDevice(path, first);
553
663
  } catch (e) {
664
+ this.assertRunning();
554
665
  if (e instanceof HardwareError && e.errorCode === HardwareErrorCode.BridgeNeedsPermission) {
555
666
  throw e;
556
667
  }
@@ -600,6 +711,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
600
711
  * Discovers interface/endpoint numbers from USB descriptors on first connection.
601
712
  */
602
713
  async connectToDevice(path: string, first: boolean) {
714
+ this.assertRunning();
603
715
  let device: USBDevice = await this.findDevice(path);
604
716
  if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
605
717
  await this.closeOpenDevice(path);
@@ -611,7 +723,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
611
723
  if (!device.opened) {
612
724
  openingHandles.add(device);
613
725
  try {
614
- await device.open();
726
+ await this.runHandleOperation(() => device.open());
615
727
  } catch (error) {
616
728
  return await this.throwWebUsbIoError(path, 'open', error);
617
729
  } finally {
@@ -622,8 +734,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
622
734
  // state as in the legacy transport; probing and V2 recovery still reset.
623
735
  if (first || this.deviceProtocol.get(path) !== 'V1') {
624
736
  try {
625
- await device.reset();
737
+ await this.runHandleOperation(() => device.reset());
626
738
  } catch (error) {
739
+ this.assertRunning();
627
740
  this.Log?.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
628
741
  }
629
742
  }
@@ -639,7 +752,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
639
752
  if (!device.opened) {
640
753
  openingHandles.add(device);
641
754
  try {
642
- await device.open();
755
+ await this.runHandleOperation(() => device.open());
643
756
  } catch (error) {
644
757
  return await this.throwWebUsbIoError(path, 'open', error);
645
758
  } finally {
@@ -652,14 +765,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
652
765
  !device.configuration ||
653
766
  device.configuration.configurationValue !== this.configurationId
654
767
  ) {
655
- await device.selectConfiguration(this.configurationId);
768
+ await this.runHandleOperation(() => device.selectConfiguration(this.configurationId));
656
769
  }
657
770
 
658
771
  // Discover endpoints from USB descriptors; descriptors are not used for protocol selection.
659
772
  const endpoints = this.discoverEndpoints(device);
660
773
  this.deviceEndpoints.set(path, endpoints);
661
774
  try {
662
- await device.claimInterface(endpoints.interfaceNumber);
775
+ await this.runHandleOperation(() => device.claimInterface(endpoints.interfaceNumber));
663
776
  } catch (error) {
664
777
  return this.throwWebUsbIoError(path, 'claimInterface', error);
665
778
  }
@@ -668,6 +781,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
668
781
  }
669
782
 
670
783
  private assertHandleAvailable(device: USBDevice) {
784
+ this.assertRunning();
671
785
  const normalOwner = normalHandleOwners.get(device);
672
786
  if (
673
787
  (normalOwner && normalOwner !== this) ||
@@ -685,6 +799,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
685
799
  }
686
800
 
687
801
  private releaseNormalHandles(path: string) {
802
+ if (this.stopped) return;
688
803
  for (const [device, ownedPath] of this.normalDevices) {
689
804
  if (ownedPath === path) {
690
805
  normalHandleOwners.delete(device);
@@ -702,6 +817,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
702
817
  }
703
818
 
704
819
  private claimIdentityProbeHandle(path: string, device: USBDevice) {
820
+ this.assertRunning();
705
821
  if (
706
822
  device.opened ||
707
823
  openingHandles.has(device) ||
@@ -715,6 +831,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
715
831
  }
716
832
 
717
833
  private releaseIdentityProbeHandle(path: string) {
834
+ if (this.stopped) return;
718
835
  const device = this.identityProbeDevices.get(path);
719
836
  // A failed native close must not make an open probe handle available to other callers.
720
837
  if (device?.opened) {
@@ -727,6 +844,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
727
844
  }
728
845
 
729
846
  private async closeOpenDevice(path: string) {
847
+ if (this.stopped) return;
730
848
  const current = this.identityProbe
731
849
  ? this.identityProbeDevices.get(path)
732
850
  : this.deviceList.find(device => device.path === path)?.device;
@@ -736,13 +854,13 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
736
854
  const endpoints = this.deviceEndpoints.get(path);
737
855
  const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
738
856
  try {
739
- await current.releaseInterface(ifaceNum);
857
+ await this.runHandleOperation(() => current.releaseInterface(ifaceNum));
740
858
  } catch (error) {
741
859
  this.Log?.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
742
860
  }
743
861
  this.assertHandleAvailable(current);
744
862
  try {
745
- await current.close();
863
+ await this.runHandleOperation(() => current.close());
746
864
  } catch (error) {
747
865
  this.Log?.debug('[WebUsbTransport] close before reconnect failed:', error);
748
866
  if (this.identityProbe) throw error;
@@ -756,8 +874,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
756
874
  ) {
757
875
  this.assertHandleAvailable(device);
758
876
  try {
759
- await device.clearHalt(direction, endpointNumber);
877
+ await this.runHandleOperation(() => device.clearHalt(direction, endpointNumber));
760
878
  } catch (error) {
879
+ this.assertRunning();
761
880
  this.Log?.debug(
762
881
  `[WebUsbTransport] clearHalt ${direction} endpoint ${endpointNumber} failed, continuing:`,
763
882
  error
@@ -773,6 +892,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
773
892
  * protocol entry produced the same fail-fast).
774
893
  */
775
894
  private assertAcquired(path: string) {
895
+ this.assertRunning();
776
896
  if (!this.acquiredPaths.has(path)) {
777
897
  throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, `Device is not acquired for ${path}`);
778
898
  }
@@ -830,6 +950,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
830
950
  operation: WebUsbOperation,
831
951
  error: unknown
832
952
  ): Promise<never> {
953
+ this.assertRunning();
833
954
  if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error)) throw error;
834
955
  if (error instanceof ProtocolV2LinkError && error.code !== 'io') throw error;
835
956
 
@@ -870,6 +991,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
870
991
  }
871
992
 
872
993
  private isRetryablePacketIoError(error: unknown) {
994
+ if (this.stopped) return false;
873
995
  if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error)) return false;
874
996
  const message = this.getErrorMessage(error).toLowerCase();
875
997
  if (this.isWebUsbPermissionDeniedMessage(message)) return false;
@@ -901,6 +1023,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
901
1023
  // USB disconnect event itself was missed.
902
1024
  this.staleProtocolPaths.add(path);
903
1025
  await wait(attempt * PACKET_IO_RETRY_DELAY);
1026
+ this.assertRunning();
904
1027
 
905
1028
  try {
906
1029
  const currentDevice = await this.findDevice(path);
@@ -909,14 +1032,15 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
909
1032
  const endpoints = this.deviceEndpoints.get(path);
910
1033
  const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
911
1034
  try {
912
- await currentDevice.releaseInterface(ifaceNum);
1035
+ await this.runHandleOperation(() => currentDevice.releaseInterface(ifaceNum));
913
1036
  } catch (releaseError) {
914
1037
  this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
915
1038
  }
916
1039
  this.assertHandleAvailable(currentDevice);
917
- await currentDevice.close();
1040
+ await this.runHandleOperation(() => currentDevice.close());
918
1041
  }
919
1042
  } catch (closeError) {
1043
+ this.assertRunning();
920
1044
  if (this.isHandleOwnershipError(closeError)) throw closeError;
921
1045
  this.Log.debug('[WebUsbTransport] close device before retry error:', closeError);
922
1046
  }
@@ -959,6 +1083,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
959
1083
  try {
960
1084
  await this.reconnectForPacketIoRetry(path, 'out', attempt, error);
961
1085
  } catch (reconnectError) {
1086
+ this.assertRunning();
962
1087
  if (this.isHandleOwnershipError(reconnectError)) throw reconnectError;
963
1088
  lastError = reconnectError;
964
1089
  this.Log.debug(
@@ -984,6 +1109,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
984
1109
  );
985
1110
  this.assertHandleAvailable(device);
986
1111
  const result = await device.transferOut(endpointOut, transferBuffer);
1112
+ this.assertHandleAvailable(device);
987
1113
  if (result.status !== 'ok') {
988
1114
  return this.throwWebUsbIoError(
989
1115
  path,
@@ -1012,6 +1138,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1012
1138
  const endpointIn = endpoints?.endpointIn ?? this.endpointId;
1013
1139
  this.assertHandleAvailable(device);
1014
1140
  const result = await device.transferIn(endpointIn, length);
1141
+ this.assertHandleAvailable(device);
1015
1142
  return this.getTransferInData(result);
1016
1143
  } catch (error) {
1017
1144
  lastError = error;
@@ -1025,6 +1152,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1025
1152
  try {
1026
1153
  await this.reconnectForPacketIoRetry(path, 'in', attempt, error);
1027
1154
  } catch (reconnectError) {
1155
+ this.assertRunning();
1028
1156
  if (this.isHandleOwnershipError(reconnectError)) throw reconnectError;
1029
1157
  lastError = reconnectError;
1030
1158
  this.Log.debug(
@@ -1047,6 +1175,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1047
1175
  const endpointIn = endpoints?.endpointIn ?? this.endpointId;
1048
1176
  this.assertHandleAvailable(device);
1049
1177
  const result = await device.transferIn(endpointIn, length);
1178
+ this.assertHandleAvailable(device);
1050
1179
  return this.getTransferInData(result);
1051
1180
  }
1052
1181