@onekeyfe/hd-transport-web-device 1.2.8-alpha.100 → 1.2.8-alpha.102

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/dist/index.js CHANGED
@@ -51,11 +51,18 @@ const REPORT_ID = transport.PROTOCOL_V1_REPORT_ID;
51
51
  const HEADER_LENGTH = transport.PROTOCOL_V1_MESSAGE_HEADER_SIZE;
52
52
  const PACKET_IO_MAX_RETRIES = 3;
53
53
  const PACKET_IO_RETRY_DELAY = 300;
54
+ const USB_DEVICE_IN_USE = 'USB device is already in use';
55
+ const WEBUSB_LIFECYCLE_LOG_PREFIX = '[WebUsbTransport][lifecycle]';
56
+ const WEBUSB_ERROR_MESSAGE_LIMIT = 240;
54
57
  const PROTOCOL_V1_PROBE_TIMEOUT = 5000;
55
58
  const PROTOCOL_V2_PROBE_TIMEOUT = 1000;
56
59
  const EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS = 2;
57
60
  let activeWebUsbTransport;
58
61
  let usbDisconnectListenerAttached = false;
62
+ let nextWebUsbTransportInstanceId = 0;
63
+ const identityProbeHandles = new WeakSet();
64
+ const openingHandles = new WeakSet();
65
+ const normalHandleOwners = new WeakMap();
59
66
  function registerActiveWebUsbTransport(instance, usb) {
60
67
  activeWebUsbTransport = instance;
61
68
  if (usbDisconnectListenerAttached)
@@ -72,17 +79,26 @@ function registerActiveWebUsbTransport(instance, usb) {
72
79
  });
73
80
  }
74
81
  class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
75
- constructor() {
82
+ constructor(identityProbe = false) {
76
83
  super({
77
84
  router: transport.PROTOCOL_V2_CHANNEL_USB,
78
85
  maxFrameBytes: transport.PROTOCOL_V2_FRAME_MAX_BYTES,
79
86
  logPrefix: 'ProtocolV2 WebUSB',
80
87
  });
88
+ this.identityProbe = identityProbe;
89
+ this.transportInstanceId = ++nextWebUsbTransportInstanceId;
90
+ this.handleOperationSequence = 0;
91
+ this.deviceLogSequence = 0;
92
+ this.deviceLogIds = new WeakMap();
81
93
  this.deviceProtocol = new Map();
82
94
  this.confirmedDeviceProtocols = new Map();
83
95
  this.deviceProtocolHints = new Map();
84
96
  this.staleProtocolPaths = new Set();
85
97
  this.acquiredPaths = new Set();
98
+ this.identityProbeDevices = new Map();
99
+ this.normalDevices = new Map();
100
+ this.pendingHandleOperations = new Set();
101
+ this.identityProbeCleanupPaths = new Set();
86
102
  this.probedDeviceObjects = new Map();
87
103
  this.deviceEndpoints = new Map();
88
104
  this.name = 'WebUsbTransport';
@@ -101,7 +117,195 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
101
117
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'WebUSB is not supported by current browsers');
102
118
  }
103
119
  this.usb = usb;
104
- registerActiveWebUsbTransport(this, usb);
120
+ if (!this.identityProbe)
121
+ registerActiveWebUsbTransport(this, usb);
122
+ }
123
+ stop() {
124
+ const startedAt = Date.now();
125
+ this.stopped = true;
126
+ this.acquiredPaths.clear();
127
+ if (activeWebUsbTransport === this)
128
+ activeWebUsbTransport = undefined;
129
+ if (!this.stopPromise) {
130
+ this.logLifecycle('stop', { phase: 'start' });
131
+ this.stopPromise = this.stopResources().then(() => {
132
+ this.logLifecycle('stop', {
133
+ phase: 'success',
134
+ durationMs: Date.now() - startedAt,
135
+ });
136
+ }, error => {
137
+ this.logLifecycle('stop', Object.assign({ phase: 'error', durationMs: Date.now() - startedAt }, this.getDiagnosticError(error)));
138
+ this.stopPromise = undefined;
139
+ throw error;
140
+ });
141
+ }
142
+ return this.stopPromise;
143
+ }
144
+ stopResources() {
145
+ var _a, _b, _c;
146
+ return __awaiter(this, void 0, void 0, function* () {
147
+ let cleanupError;
148
+ try {
149
+ yield this.disposeProtocolV2UsbLinks('WebUSB transport stopped');
150
+ }
151
+ catch (error) {
152
+ cleanupError = error;
153
+ }
154
+ yield Promise.allSettled([...this.pendingHandleOperations]);
155
+ const handles = this.identityProbe
156
+ ? [...this.identityProbeDevices].map(([path, device]) => [device, path])
157
+ : [...this.normalDevices];
158
+ for (const [device, path] of handles) {
159
+ if (!this.identityProbe && normalHandleOwners.get(device) !== this) {
160
+ this.normalDevices.delete(device);
161
+ }
162
+ else {
163
+ try {
164
+ if (device.opened) {
165
+ const iface = (_b = (_a = this.deviceEndpoints.get(path)) === null || _a === void 0 ? void 0 : _a.interfaceNumber) !== null && _b !== void 0 ? _b : this.interfaceId;
166
+ if (this.getInterfaceClaimed(device, iface) === false) {
167
+ this.logSkippedHandleOperation('releaseInterface', device, 'interface-not-claimed', iface);
168
+ }
169
+ else {
170
+ try {
171
+ yield this.runLoggedHandleOperation({
172
+ operation: 'releaseInterface',
173
+ device,
174
+ path,
175
+ interfaceNumber: iface,
176
+ allowStopped: true,
177
+ task: () => device.releaseInterface(iface),
178
+ });
179
+ }
180
+ catch (error) {
181
+ (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug('[WebUsbTransport] stop releaseInterface failed:', error);
182
+ }
183
+ }
184
+ yield this.runLoggedHandleOperation({
185
+ operation: 'close',
186
+ device,
187
+ path,
188
+ allowStopped: true,
189
+ task: () => device.close(),
190
+ });
191
+ if (device.opened)
192
+ throw new Error('WebUSB handle remains open after stop');
193
+ }
194
+ if (this.identityProbe) {
195
+ identityProbeHandles.delete(device);
196
+ this.identityProbeDevices.delete(path);
197
+ this.identityProbeCleanupPaths.delete(path);
198
+ }
199
+ else {
200
+ normalHandleOwners.delete(device);
201
+ this.normalDevices.delete(device);
202
+ }
203
+ }
204
+ catch (error) {
205
+ cleanupError !== null && cleanupError !== void 0 ? cleanupError : (cleanupError = error);
206
+ }
207
+ }
208
+ }
209
+ this.deviceList = [];
210
+ this.deviceProtocol.clear();
211
+ this.confirmedDeviceProtocols.clear();
212
+ this.deviceProtocolHints.clear();
213
+ this.staleProtocolPaths.clear();
214
+ this.probedDeviceObjects.clear();
215
+ if (!this.normalDevices.size && !this.identityProbeDevices.size) {
216
+ this.deviceEndpoints.clear();
217
+ }
218
+ if (cleanupError) {
219
+ throw cleanupError instanceof Error
220
+ ? cleanupError
221
+ : new Error(this.getErrorMessage(cleanupError));
222
+ }
223
+ });
224
+ }
225
+ assertRunning() {
226
+ if (this.stopped) {
227
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'WebUSB transport is stopped');
228
+ }
229
+ }
230
+ runHandleOperation(operation) {
231
+ return __awaiter(this, void 0, void 0, function* () {
232
+ this.assertRunning();
233
+ const pending = Promise.resolve(operation());
234
+ this.pendingHandleOperations.add(pending);
235
+ try {
236
+ const result = yield pending;
237
+ this.assertRunning();
238
+ return result;
239
+ }
240
+ finally {
241
+ this.pendingHandleOperations.delete(pending);
242
+ }
243
+ });
244
+ }
245
+ getInterfaceClaimed(device, interfaceNumber) {
246
+ var _a, _b, _c;
247
+ return (_c = (_b = (_a = device.configuration) === null || _a === void 0 ? void 0 : _a.interfaces) === null || _b === void 0 ? void 0 : _b.find(usbInterface => usbInterface.interfaceNumber === interfaceNumber)) === null || _c === void 0 ? void 0 : _c.claimed;
248
+ }
249
+ getDeviceLogId(device) {
250
+ const existing = this.deviceLogIds.get(device);
251
+ if (existing)
252
+ return existing;
253
+ const next = ++this.deviceLogSequence;
254
+ this.deviceLogIds.set(device, next);
255
+ return next;
256
+ }
257
+ getHandleLogState(device, interfaceNumber) {
258
+ var _a, _b;
259
+ const interfaceClaimed = interfaceNumber === undefined ? undefined : this.getInterfaceClaimed(device, interfaceNumber);
260
+ return {
261
+ deviceRefId: this.getDeviceLogId(device),
262
+ opened: device.opened,
263
+ configurationValue: (_b = (_a = device.configuration) === null || _a === void 0 ? void 0 : _a.configurationValue) !== null && _b !== void 0 ? _b : null,
264
+ interfaceNumber: interfaceNumber !== null && interfaceNumber !== void 0 ? interfaceNumber : null,
265
+ interfaceClaimed: interfaceClaimed !== null && interfaceClaimed !== void 0 ? interfaceClaimed : null,
266
+ identityProbe: this.identityProbe,
267
+ stopped: this.stopped,
268
+ acquiredPathCount: this.acquiredPaths.size,
269
+ normalHandleCount: this.normalDevices.size,
270
+ identityProbeHandleCount: this.identityProbeDevices.size,
271
+ pendingHandleOperationCount: this.pendingHandleOperations.size,
272
+ };
273
+ }
274
+ getDiagnosticError(error, path) {
275
+ const errorMessage = this.getErrorMessage(error);
276
+ const redactedMessage = path ? errorMessage.split(path).join('<device>') : errorMessage;
277
+ return {
278
+ errorName: this.getErrorName(error) || 'Error',
279
+ errorMessage: redactedMessage.slice(0, WEBUSB_ERROR_MESSAGE_LIMIT),
280
+ errorCode: error instanceof hdShared.HardwareError ? error.errorCode : undefined,
281
+ };
282
+ }
283
+ logLifecycle(event, payload = {}) {
284
+ var _a;
285
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(WEBUSB_LIFECYCLE_LOG_PREFIX, Object.assign({ event, transportInstanceId: this.transportInstanceId }, payload));
286
+ }
287
+ logSkippedHandleOperation(operation, device, reason, interfaceNumber) {
288
+ const operationId = ++this.handleOperationSequence;
289
+ this.logLifecycle('handle-operation', Object.assign({ operationId,
290
+ operation, phase: 'skipped', reason }, this.getHandleLogState(device, interfaceNumber)));
291
+ }
292
+ runLoggedHandleOperation({ operation, device, task, path, interfaceNumber, endpointNumber, allowStopped = false, }) {
293
+ return __awaiter(this, void 0, void 0, function* () {
294
+ const operationId = ++this.handleOperationSequence;
295
+ const startedAt = Date.now();
296
+ const details = Object.assign({ operationId,
297
+ operation }, (endpointNumber === undefined ? {} : { endpointNumber }));
298
+ this.logLifecycle('handle-operation', Object.assign(Object.assign(Object.assign({}, details), { phase: 'start' }), this.getHandleLogState(device, interfaceNumber)));
299
+ try {
300
+ const result = allowStopped ? yield task() : yield this.runHandleOperation(task);
301
+ this.logLifecycle('handle-operation', Object.assign(Object.assign(Object.assign({}, details), { phase: 'success', durationMs: Date.now() - startedAt }), this.getHandleLogState(device, interfaceNumber)));
302
+ return result;
303
+ }
304
+ catch (error) {
305
+ this.logLifecycle('handle-operation', Object.assign(Object.assign(Object.assign(Object.assign({}, details), { phase: 'error', durationMs: Date.now() - startedAt }), this.getHandleLogState(device, interfaceNumber)), this.getDiagnosticError(error, path)));
306
+ throw error;
307
+ }
308
+ });
105
309
  }
106
310
  emitDeviceDisconnect(path, device) {
107
311
  var _a, _b;
@@ -138,11 +342,19 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
138
342
  return __awaiter(this, void 0, void 0, function* () {
139
343
  if (!this.usb)
140
344
  return null;
345
+ const startedAt = Date.now();
346
+ this.logLifecycle('prompt-device-access', { phase: 'start' });
141
347
  try {
142
348
  const device = yield this.usb.requestDevice({ filters: hdShared.ONEKEY_WEBUSB_FILTER });
349
+ this.logLifecycle('prompt-device-access', {
350
+ phase: 'success',
351
+ durationMs: Date.now() - startedAt,
352
+ selected: Boolean(device),
353
+ });
143
354
  return device;
144
355
  }
145
356
  catch (e) {
357
+ this.logLifecycle('prompt-device-access', Object.assign({ phase: 'error', durationMs: Date.now() - startedAt }, this.getDiagnosticError(e)));
146
358
  this.Log.debug('requestDevice error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
147
359
  return null;
148
360
  }
@@ -150,15 +362,66 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
150
362
  }
151
363
  enumerate() {
152
364
  return __awaiter(this, void 0, void 0, function* () {
153
- yield this.getConnectedDevices();
154
- return this.deviceList;
365
+ const startedAt = Date.now();
366
+ this.logLifecycle('enumerate', {
367
+ phase: 'start',
368
+ pendingProbeCleanupCount: this.identityProbeCleanupPaths.size,
369
+ });
370
+ try {
371
+ for (const path of this.identityProbeCleanupPaths) {
372
+ try {
373
+ yield this.release(path);
374
+ }
375
+ catch (_a) {
376
+ }
377
+ }
378
+ yield this.getConnectedDevices();
379
+ this.logLifecycle('enumerate', {
380
+ phase: 'success',
381
+ durationMs: Date.now() - startedAt,
382
+ deviceCount: this.deviceList.length,
383
+ });
384
+ return this.deviceList;
385
+ }
386
+ catch (error) {
387
+ this.logLifecycle('enumerate', Object.assign({ phase: 'error', durationMs: Date.now() - startedAt }, this.getDiagnosticError(error)));
388
+ throw error;
389
+ }
155
390
  });
156
391
  }
157
392
  getConnectedDevices() {
393
+ var _a;
158
394
  return __awaiter(this, void 0, void 0, function* () {
395
+ this.assertRunning();
159
396
  if (!this.usb)
160
397
  return [];
161
- const devices = yield this.usb.getDevices();
398
+ const startedAt = Date.now();
399
+ this.logLifecycle('get-devices', { phase: 'start' });
400
+ let devices;
401
+ try {
402
+ devices = yield this.usb.getDevices();
403
+ this.logLifecycle('get-devices', {
404
+ phase: 'success',
405
+ durationMs: Date.now() - startedAt,
406
+ authorizedDeviceCount: devices.length,
407
+ });
408
+ }
409
+ catch (error) {
410
+ this.logLifecycle('get-devices', Object.assign({ phase: 'error', durationMs: Date.now() - startedAt }, this.getDiagnosticError(error)));
411
+ throw error;
412
+ }
413
+ this.assertRunning();
414
+ for (const owner of new Set(devices.map(device => normalHandleOwners.get(device)))) {
415
+ if (owner === null || owner === void 0 ? void 0 : owner.stopped) {
416
+ try {
417
+ yield owner.stop();
418
+ }
419
+ catch (error) {
420
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[WebUsbTransport] stopped owner cleanup failed:', error);
421
+ }
422
+ }
423
+ }
424
+ this.assertRunning();
162
425
  const onekeyDevices = devices.filter(dev => {
163
426
  const isOneKey = hdShared.ONEKEY_WEBUSB_FILTER.some((desc) => dev.vendorId === desc.vendorId && dev.productId === desc.productId);
164
427
  return isOneKey && !hdShared.isKnownTrezorWebUsbDevice(dev);
@@ -183,14 +446,34 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
183
446
  });
184
447
  }
185
448
  acquire(input) {
186
- var _a, _b, _c, _d, _e, _f, _g;
449
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
187
450
  return __awaiter(this, void 0, void 0, function* () {
451
+ this.assertRunning();
188
452
  if (!input.path)
189
453
  return;
454
+ const startedAt = Date.now();
455
+ this.logLifecycle('acquire', {
456
+ phase: 'start',
457
+ expectedProtocol: (_a = input.expectedProtocol) !== null && _a !== void 0 ? _a : null,
458
+ protocolHint: (_b = input.protocolHint) !== null && _b !== void 0 ? _b : null,
459
+ forceProtocolDetection: Boolean(input.forceProtocolDetection),
460
+ skipProtocolProbe: Boolean(input.skipProtocolProbe),
461
+ hasCachedProtocol: this.deviceProtocol.has(input.path),
462
+ });
190
463
  try {
464
+ if (this.identityProbe) {
465
+ yield this.getConnectedDevices();
466
+ this.claimIdentityProbeHandle(input.path, yield this.findDevice(input.path));
467
+ }
468
+ else {
469
+ this.claimNormalHandle(input.path, yield this.findDevice(input.path));
470
+ }
191
471
  yield this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
192
472
  yield this.closeOpenDevice(input.path);
193
- yield this.connect((_a = input.path) !== null && _a !== void 0 ? _a : '', true);
473
+ yield this.connect((_c = input.path) !== null && _c !== void 0 ? _c : '', true, {
474
+ expectedProtocol: input.expectedProtocol,
475
+ forceProtocolDetection: input.forceProtocolDetection,
476
+ });
194
477
  if (input.skipProtocolProbe) {
195
478
  if (!input.expectedProtocol) {
196
479
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'skipProtocolProbe requires an expected protocol');
@@ -200,7 +483,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
200
483
  }
201
484
  this.staleProtocolPaths.delete(input.path);
202
485
  this.deviceProtocol.set(input.path, input.expectedProtocol);
203
- const currentDevice = (_b = this.deviceList.find(d => d.path === input.path)) === null || _b === void 0 ? void 0 : _b.device;
486
+ const currentDevice = (_d = this.deviceList.find(d => d.path === input.path)) === null || _d === void 0 ? void 0 : _d.device;
204
487
  if (currentDevice) {
205
488
  this.probedDeviceObjects.set(input.path, currentDevice);
206
489
  }
@@ -215,7 +498,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
215
498
  this.deviceProtocol.delete(input.path);
216
499
  }
217
500
  if (!input.skipProtocolProbe && this.deviceProtocol.has(input.path)) {
218
- const currentDevice = (_c = this.deviceList.find(d => d.path === input.path)) === null || _c === void 0 ? void 0 : _c.device;
501
+ const currentDevice = (_e = this.deviceList.find(d => d.path === input.path)) === null || _e === void 0 ? void 0 : _e.device;
219
502
  if (!currentDevice || currentDevice !== this.probedDeviceObjects.get(input.path)) {
220
503
  this.deviceProtocol.delete(input.path);
221
504
  }
@@ -228,39 +511,58 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
228
511
  this.deviceProtocol.delete(input.path);
229
512
  }
230
513
  if (!this.deviceProtocol.has(input.path)) {
231
- const usbDevice = (_d = this.deviceList.find(device => device.path === input.path)) === null || _d === void 0 ? void 0 : _d.device;
514
+ const usbDevice = (_f = this.deviceList.find(device => device.path === input.path)) === null || _f === void 0 ? void 0 : _f.device;
232
515
  const protocolHint = input.expectedProtocol
233
516
  ? undefined
234
- : (_f = (_e = input.protocolHint) !== null && _e !== void 0 ? _e : this.deviceProtocolHints.get(input.path)) !== null && _f !== void 0 ? _f : hdShared.inferProtocolHintFromUsbId(usbDevice === null || usbDevice === void 0 ? void 0 : usbDevice.vendorId, usbDevice === null || usbDevice === void 0 ? void 0 : usbDevice.productId);
517
+ : (_h = (_g = input.protocolHint) !== null && _g !== void 0 ? _g : this.deviceProtocolHints.get(input.path)) !== null && _h !== void 0 ? _h : hdShared.inferProtocolHintFromUsbId(usbDevice === null || usbDevice === void 0 ? void 0 : usbDevice.vendorId, usbDevice === null || usbDevice === void 0 ? void 0 : usbDevice.productId);
235
518
  if (protocolHint) {
236
519
  this.deviceProtocolHints.set(input.path, protocolHint);
237
520
  }
238
521
  const detectedProtocol = yield this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
522
+ this.assertRunning();
239
523
  this.confirmedDeviceProtocols.set(input.path, detectedProtocol);
240
- const probedDevice = (_g = this.deviceList.find(d => d.path === input.path)) === null || _g === void 0 ? void 0 : _g.device;
524
+ const probedDevice = (_j = this.deviceList.find(d => d.path === input.path)) === null || _j === void 0 ? void 0 : _j.device;
241
525
  if (probedDevice) {
242
526
  this.probedDeviceObjects.set(input.path, probedDevice);
243
527
  }
244
528
  }
529
+ this.assertRunning();
245
530
  this.acquiredPaths.add(input.path);
531
+ this.logLifecycle('acquire', {
532
+ phase: 'success',
533
+ durationMs: Date.now() - startedAt,
534
+ protocol: (_k = this.deviceProtocol.get(input.path)) !== null && _k !== void 0 ? _k : null,
535
+ });
246
536
  return yield Promise.resolve(input.path);
247
537
  }
248
538
  catch (e) {
539
+ this.logLifecycle('acquire', Object.assign({ phase: 'error', durationMs: Date.now() - startedAt }, this.getDiagnosticError(e, input.path)));
249
540
  this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
250
541
  this.acquiredPaths.delete(input.path);
251
- yield this.closeOpenDevice(input.path);
542
+ try {
543
+ yield this.closeOpenDevice(input.path);
544
+ }
545
+ finally {
546
+ this.releaseIdentityProbeHandle(input.path);
547
+ this.releaseNormalHandles(input.path);
548
+ }
252
549
  throw e;
253
550
  }
254
551
  });
255
552
  }
256
553
  createProtocolMismatchError(expected) {
257
- return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`);
554
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportProtocolProbeFailed, undefined, {
555
+ expectedProtocol: expected,
556
+ });
258
557
  }
259
558
  createProtocolProbeTimeoutError(expected, attempts) {
260
- return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInitializeFailed, `Protocol ${expected} probe timeout after ${attempts} attempts`);
559
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportProtocolProbeFailed, undefined, {
560
+ expectedProtocol: expected,
561
+ attempts,
562
+ });
261
563
  }
262
564
  createProtocolDetectionError() {
263
- return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound);
565
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportProtocolProbeFailed);
264
566
  }
265
567
  detectProtocol(path, expectedProtocol, protocolHint) {
266
568
  var _a;
@@ -324,18 +626,48 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
324
626
  return device.device;
325
627
  });
326
628
  }
327
- connect(path, first) {
629
+ connect(path, first, options = {}) {
328
630
  return __awaiter(this, void 0, void 0, function* () {
329
631
  const maxRetries = 5;
330
632
  for (let i = 0; i < maxRetries; i++) {
633
+ this.assertRunning();
634
+ const attempt = i + 1;
635
+ const startedAt = Date.now();
636
+ const connectionStrategy = this.shouldUseLegacyV1Connection(path, options)
637
+ ? 'protocol-v1-legacy-1.1.31'
638
+ : 'current';
639
+ this.logLifecycle('connect-attempt', {
640
+ phase: 'start',
641
+ attempt,
642
+ maxAttempts: maxRetries,
643
+ first,
644
+ connectionStrategy,
645
+ });
331
646
  try {
332
- return yield this.connectToDevice(path, first);
647
+ const result = yield this.connectToDevice(path, first, options);
648
+ this.logLifecycle('connect-attempt', {
649
+ phase: 'success',
650
+ attempt,
651
+ maxAttempts: maxRetries,
652
+ first,
653
+ connectionStrategy,
654
+ durationMs: Date.now() - startedAt,
655
+ });
656
+ return result;
333
657
  }
334
658
  catch (e) {
335
- if (e instanceof hdShared.HardwareError && e.errorCode === hdShared.HardwareErrorCode.BridgeNeedsPermission) {
659
+ this.assertRunning();
660
+ const isTerminalError = this.isHandleOwnershipError(e) ||
661
+ (e instanceof hdShared.HardwareError &&
662
+ (e.errorCode === hdShared.HardwareErrorCode.BridgeNeedsPermission ||
663
+ e.errorCode === hdShared.HardwareErrorCode.WebUsbDeviceAccessError));
664
+ const willRetry = !isTerminalError && attempt < maxRetries;
665
+ this.logLifecycle('connect-attempt', Object.assign({ phase: 'error', attempt, maxAttempts: maxRetries, first,
666
+ connectionStrategy, durationMs: Date.now() - startedAt, willRetry }, this.getDiagnosticError(e, path)));
667
+ if (isTerminalError) {
336
668
  throw e;
337
669
  }
338
- if (i === maxRetries - 1) {
670
+ if (!willRetry) {
339
671
  throw e;
340
672
  }
341
673
  yield hdShared.wait(i * 200);
@@ -343,6 +675,11 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
343
675
  }
344
676
  });
345
677
  }
678
+ shouldUseLegacyV1Connection(path, options) {
679
+ if (options.expectedProtocol)
680
+ return options.expectedProtocol === 'V1';
681
+ return !options.forceProtocolDetection && this.deviceProtocol.get(path) === 'V1';
682
+ }
346
683
  discoverEndpoints(device) {
347
684
  var _a, _b;
348
685
  for (const config of device.configurations) {
@@ -371,87 +708,276 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
371
708
  endpointOut: this.endpointId,
372
709
  };
373
710
  }
374
- connectToDevice(path, first) {
375
- var _a;
711
+ connectToDevice(path, first, options = {}) {
376
712
  return __awaiter(this, void 0, void 0, function* () {
713
+ this.assertRunning();
377
714
  let device = yield this.findDevice(path);
715
+ if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
716
+ yield this.closeOpenDevice(path);
717
+ this.releaseIdentityProbeHandle(path);
718
+ this.claimIdentityProbeHandle(path, device);
719
+ }
720
+ if (!this.identityProbe)
721
+ this.claimNormalHandle(path, device);
722
+ this.assertHandleAvailable(device);
378
723
  if (!device.opened) {
724
+ openingHandles.add(device);
379
725
  try {
380
- yield device.open();
726
+ yield this.runLoggedHandleOperation({
727
+ operation: 'open',
728
+ device,
729
+ path,
730
+ task: () => device.open(),
731
+ });
381
732
  }
382
733
  catch (error) {
383
- return this.throwWebUsbIoError(path, 'open', error);
734
+ return yield this.throwWebUsbIoError(path, 'open', error);
735
+ }
736
+ finally {
737
+ openingHandles.delete(device);
738
+ }
739
+ }
740
+ if (this.shouldUseLegacyV1Connection(path, options)) {
741
+ if (first) {
742
+ yield this.runLoggedHandleOperation({
743
+ operation: 'selectConfiguration',
744
+ device,
745
+ path,
746
+ task: () => device.selectConfiguration(this.configurationId),
747
+ });
748
+ try {
749
+ yield this.runLoggedHandleOperation({
750
+ operation: 'reset',
751
+ device,
752
+ path,
753
+ task: () => device.reset(),
754
+ });
755
+ }
756
+ catch (_a) {
757
+ this.assertRunning();
758
+ }
384
759
  }
760
+ const legacyEndpoints = {
761
+ interfaceNumber: this.interfaceId,
762
+ endpointIn: this.endpointId,
763
+ endpointOut: this.endpointId,
764
+ };
765
+ this.deviceEndpoints.set(path, legacyEndpoints);
766
+ yield this.claimDeviceInterface(path, device, legacyEndpoints);
767
+ return;
385
768
  }
386
769
  if (first || this.deviceProtocol.get(path) !== 'V1') {
387
770
  try {
388
- yield device.reset();
771
+ yield this.runLoggedHandleOperation({
772
+ operation: 'reset',
773
+ device,
774
+ path,
775
+ task: () => device.reset(),
776
+ });
389
777
  }
390
- catch (error) {
391
- (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
778
+ catch (_b) {
779
+ this.assertRunning();
392
780
  }
393
781
  }
394
782
  yield this.getConnectedDevices();
395
783
  device = yield this.findDevice(path);
784
+ if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
785
+ yield this.closeOpenDevice(path);
786
+ this.releaseIdentityProbeHandle(path);
787
+ this.claimIdentityProbeHandle(path, device);
788
+ }
789
+ if (!this.identityProbe)
790
+ this.claimNormalHandle(path, device);
791
+ this.assertHandleAvailable(device);
396
792
  if (!device.opened) {
793
+ openingHandles.add(device);
397
794
  try {
398
- yield device.open();
795
+ yield this.runLoggedHandleOperation({
796
+ operation: 'open',
797
+ device,
798
+ path,
799
+ task: () => device.open(),
800
+ });
399
801
  }
400
802
  catch (error) {
401
- return this.throwWebUsbIoError(path, 'open', error);
803
+ return yield this.throwWebUsbIoError(path, 'open', error);
804
+ }
805
+ finally {
806
+ openingHandles.delete(device);
402
807
  }
403
808
  }
404
809
  if (first ||
405
810
  !device.configuration ||
406
811
  device.configuration.configurationValue !== this.configurationId) {
407
- yield device.selectConfiguration(this.configurationId);
812
+ yield this.runLoggedHandleOperation({
813
+ operation: 'selectConfiguration',
814
+ device,
815
+ path,
816
+ task: () => device.selectConfiguration(this.configurationId),
817
+ });
408
818
  }
409
819
  const endpoints = this.discoverEndpoints(device);
410
820
  this.deviceEndpoints.set(path, endpoints);
821
+ yield this.claimDeviceInterface(path, device, endpoints);
822
+ yield this.clearEndpointHalt(path, device, 'in', endpoints.endpointIn);
823
+ yield this.clearEndpointHalt(path, device, 'out', endpoints.endpointOut);
824
+ });
825
+ }
826
+ claimDeviceInterface(path, device, endpoints) {
827
+ return __awaiter(this, void 0, void 0, function* () {
411
828
  try {
412
- yield device.claimInterface(endpoints.interfaceNumber);
829
+ yield this.runLoggedHandleOperation({
830
+ operation: 'claimInterface',
831
+ device,
832
+ path,
833
+ interfaceNumber: endpoints.interfaceNumber,
834
+ task: () => device.claimInterface(endpoints.interfaceNumber),
835
+ });
413
836
  }
414
837
  catch (error) {
415
838
  return this.throwWebUsbIoError(path, 'claimInterface', error);
416
839
  }
417
- yield this.clearEndpointHalt(device, 'in', endpoints.endpointIn);
418
- yield this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
419
840
  });
420
841
  }
842
+ assertHandleAvailable(device) {
843
+ this.assertRunning();
844
+ const normalOwner = normalHandleOwners.get(device);
845
+ if ((normalOwner && normalOwner !== this) ||
846
+ (identityProbeHandles.has(device) &&
847
+ ![...this.identityProbeDevices.values()].includes(device))) {
848
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, USB_DEVICE_IN_USE);
849
+ }
850
+ }
851
+ claimNormalHandle(path, device) {
852
+ this.assertHandleAvailable(device);
853
+ normalHandleOwners.set(device, this);
854
+ this.normalDevices.set(device, path);
855
+ }
856
+ releaseNormalHandles(path) {
857
+ if (this.stopped)
858
+ return;
859
+ for (const [device, ownedPath] of this.normalDevices) {
860
+ if (ownedPath === path && !device.opened) {
861
+ normalHandleOwners.delete(device);
862
+ this.normalDevices.delete(device);
863
+ }
864
+ }
865
+ }
866
+ isHandleOwnershipError(error) {
867
+ return (error instanceof hdShared.HardwareError &&
868
+ error.errorCode === hdShared.HardwareErrorCode.RuntimeError &&
869
+ this.getErrorMessage(error).includes(USB_DEVICE_IN_USE));
870
+ }
871
+ claimIdentityProbeHandle(path, device) {
872
+ this.assertRunning();
873
+ if (device.opened ||
874
+ openingHandles.has(device) ||
875
+ identityProbeHandles.has(device) ||
876
+ normalHandleOwners.has(device)) {
877
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, USB_DEVICE_IN_USE);
878
+ }
879
+ identityProbeHandles.add(device);
880
+ this.identityProbeDevices.set(path, device);
881
+ }
882
+ releaseIdentityProbeHandle(path) {
883
+ if (this.stopped)
884
+ return;
885
+ const device = this.identityProbeDevices.get(path);
886
+ if (device === null || device === void 0 ? void 0 : device.opened) {
887
+ this.identityProbeCleanupPaths.add(path);
888
+ return;
889
+ }
890
+ if (device)
891
+ identityProbeHandles.delete(device);
892
+ this.identityProbeDevices.delete(path);
893
+ this.identityProbeCleanupPaths.delete(path);
894
+ }
421
895
  closeOpenDevice(path) {
422
- var _a, _b, _c, _d;
423
896
  return __awaiter(this, void 0, void 0, function* () {
424
- const current = (_a = this.deviceList.find(device => device.path === path)) === null || _a === void 0 ? void 0 : _a.device;
425
- if (!(current === null || current === void 0 ? void 0 : current.opened))
897
+ if (this.stopped)
898
+ return;
899
+ const devices = this.identityProbe
900
+ ? [this.identityProbeDevices.get(path)].filter((device) => device !== undefined)
901
+ : [...this.normalDevices]
902
+ .filter(([, ownedPath]) => ownedPath === path)
903
+ .map(([device]) => device);
904
+ let firstCloseError;
905
+ for (const device of devices) {
906
+ try {
907
+ yield this.closeDeviceHandle(path, device);
908
+ }
909
+ catch (error) {
910
+ firstCloseError !== null && firstCloseError !== void 0 ? firstCloseError : (firstCloseError = error);
911
+ }
912
+ }
913
+ if (firstCloseError) {
914
+ throw firstCloseError instanceof Error
915
+ ? firstCloseError
916
+ : new Error(this.getErrorMessage(firstCloseError));
917
+ }
918
+ });
919
+ }
920
+ closeDeviceHandle(path, current) {
921
+ var _a, _b, _c;
922
+ return __awaiter(this, void 0, void 0, function* () {
923
+ if (!current.opened)
426
924
  return;
925
+ this.assertHandleAvailable(current);
427
926
  const endpoints = this.deviceEndpoints.get(path);
428
- const ifaceNum = (_b = endpoints === null || endpoints === void 0 ? void 0 : endpoints.interfaceNumber) !== null && _b !== void 0 ? _b : this.interfaceId;
429
- try {
430
- yield current.releaseInterface(ifaceNum);
927
+ const ifaceNum = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.interfaceNumber) !== null && _a !== void 0 ? _a : this.interfaceId;
928
+ if (this.getInterfaceClaimed(current, ifaceNum) === false) {
929
+ this.logSkippedHandleOperation('releaseInterface', current, 'interface-not-claimed', ifaceNum);
431
930
  }
432
- catch (error) {
433
- (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
931
+ else {
932
+ try {
933
+ yield this.runLoggedHandleOperation({
934
+ operation: 'releaseInterface',
935
+ device: current,
936
+ path,
937
+ interfaceNumber: ifaceNum,
938
+ task: () => current.releaseInterface(ifaceNum),
939
+ });
940
+ }
941
+ catch (error) {
942
+ (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
943
+ }
434
944
  }
945
+ this.assertHandleAvailable(current);
435
946
  try {
436
- yield current.close();
947
+ yield this.runLoggedHandleOperation({
948
+ operation: 'close',
949
+ device: current,
950
+ path,
951
+ task: () => current.close(),
952
+ });
437
953
  }
438
954
  catch (error) {
439
- (_d = this.Log) === null || _d === void 0 ? void 0 : _d.debug('[WebUsbTransport] close before reconnect failed:', error);
955
+ (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug('[WebUsbTransport] close before reconnect failed:', error);
956
+ throw error;
440
957
  }
441
958
  });
442
959
  }
443
- clearEndpointHalt(device, direction, endpointNumber) {
960
+ clearEndpointHalt(path, device, direction, endpointNumber) {
444
961
  var _a;
445
962
  return __awaiter(this, void 0, void 0, function* () {
963
+ this.assertHandleAvailable(device);
446
964
  try {
447
- yield device.clearHalt(direction, endpointNumber);
965
+ yield this.runLoggedHandleOperation({
966
+ operation: direction === 'in' ? 'clearHaltIn' : 'clearHaltOut',
967
+ device,
968
+ path,
969
+ endpointNumber,
970
+ task: () => device.clearHalt(direction, endpointNumber),
971
+ });
448
972
  }
449
973
  catch (error) {
974
+ this.assertRunning();
450
975
  (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[WebUsbTransport] clearHalt ${direction} endpoint ${endpointNumber} failed, continuing:`, error);
451
976
  }
452
977
  });
453
978
  }
454
979
  assertAcquired(path) {
980
+ this.assertRunning();
455
981
  if (!this.acquiredPaths.has(path)) {
456
982
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device is not acquired for ${path}`);
457
983
  }
@@ -508,7 +1034,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
508
1034
  throwWebUsbIoError(path, operation, error) {
509
1035
  var _a;
510
1036
  return __awaiter(this, void 0, void 0, function* () {
511
- if (this.isWebUsbIoHardwareError(error))
1037
+ this.assertRunning();
1038
+ if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error))
512
1039
  throw error;
513
1040
  if (error instanceof transport.ProtocolV2LinkError && error.code !== 'io')
514
1041
  throw error;
@@ -542,7 +1069,9 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
542
1069
  return /LIBUSB_ERROR_ACCESS|\b(?:access|permission) denied\b/i.test(message);
543
1070
  }
544
1071
  isRetryablePacketIoError(error) {
545
- if (this.isWebUsbIoHardwareError(error))
1072
+ if (this.stopped)
1073
+ return false;
1074
+ if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error))
546
1075
  return false;
547
1076
  const message = this.getErrorMessage(error).toLowerCase();
548
1077
  if (this.isWebUsbPermissionDeniedMessage(message))
@@ -561,21 +1090,43 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
561
1090
  this.Log.debug(`[WebUsbTransport] transfer${direction} failed, retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(error)}`);
562
1091
  this.staleProtocolPaths.add(path);
563
1092
  yield hdShared.wait(attempt * PACKET_IO_RETRY_DELAY);
1093
+ this.assertRunning();
564
1094
  try {
565
1095
  const currentDevice = yield this.findDevice(path);
1096
+ this.assertHandleAvailable(currentDevice);
566
1097
  if (currentDevice.opened) {
567
1098
  const endpoints = this.deviceEndpoints.get(path);
568
1099
  const ifaceNum = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.interfaceNumber) !== null && _a !== void 0 ? _a : this.interfaceId;
569
- try {
570
- yield currentDevice.releaseInterface(ifaceNum);
1100
+ if (this.getInterfaceClaimed(currentDevice, ifaceNum) === false) {
1101
+ this.logSkippedHandleOperation('releaseInterface', currentDevice, 'interface-not-claimed', ifaceNum);
571
1102
  }
572
- catch (releaseError) {
573
- this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
1103
+ else {
1104
+ try {
1105
+ yield this.runLoggedHandleOperation({
1106
+ operation: 'releaseInterface',
1107
+ device: currentDevice,
1108
+ path,
1109
+ interfaceNumber: ifaceNum,
1110
+ task: () => currentDevice.releaseInterface(ifaceNum),
1111
+ });
1112
+ }
1113
+ catch (releaseError) {
1114
+ this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
1115
+ }
574
1116
  }
575
- yield currentDevice.close();
1117
+ this.assertHandleAvailable(currentDevice);
1118
+ yield this.runLoggedHandleOperation({
1119
+ operation: 'close',
1120
+ device: currentDevice,
1121
+ path,
1122
+ task: () => currentDevice.close(),
1123
+ });
576
1124
  }
577
1125
  }
578
1126
  catch (closeError) {
1127
+ this.assertRunning();
1128
+ if (this.isHandleOwnershipError(closeError))
1129
+ throw closeError;
579
1130
  this.Log.debug('[WebUsbTransport] close device before retry error:', closeError);
580
1131
  }
581
1132
  yield this.getConnectedDevices();
@@ -617,6 +1168,9 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
617
1168
  yield this.reconnectForPacketIoRetry(path, 'out', attempt, error);
618
1169
  }
619
1170
  catch (reconnectError) {
1171
+ this.assertRunning();
1172
+ if (this.isHandleOwnershipError(reconnectError))
1173
+ throw reconnectError;
620
1174
  lastError = reconnectError;
621
1175
  this.Log.debug(`[WebUsbTransport] transferout reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(reconnectError)}`);
622
1176
  }
@@ -635,7 +1189,9 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
635
1189
  const endpoints = this.deviceEndpoints.get(path);
636
1190
  const endpointOut = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.endpointOut) !== null && _a !== void 0 ? _a : this.endpointId;
637
1191
  const transferBuffer = this.toArrayBuffer(packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength));
1192
+ this.assertHandleAvailable(device);
638
1193
  const result = yield device.transferOut(endpointOut, transferBuffer);
1194
+ this.assertHandleAvailable(device);
639
1195
  if (result.status !== 'ok') {
640
1196
  return this.throwWebUsbIoError(path, 'transferOut', new Error(`transferOut status: ${String(result.status)}`));
641
1197
  }
@@ -656,7 +1212,9 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
656
1212
  }
657
1213
  const endpoints = this.deviceEndpoints.get(path);
658
1214
  const endpointIn = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.endpointIn) !== null && _a !== void 0 ? _a : this.endpointId;
1215
+ this.assertHandleAvailable(device);
659
1216
  const result = yield device.transferIn(endpointIn, length);
1217
+ this.assertHandleAvailable(device);
660
1218
  return this.getTransferInData(result);
661
1219
  }
662
1220
  catch (error) {
@@ -672,6 +1230,9 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
672
1230
  yield this.reconnectForPacketIoRetry(path, 'in', attempt, error);
673
1231
  }
674
1232
  catch (reconnectError) {
1233
+ this.assertRunning();
1234
+ if (this.isHandleOwnershipError(reconnectError))
1235
+ throw reconnectError;
675
1236
  lastError = reconnectError;
676
1237
  this.Log.debug(`[WebUsbTransport] transferin reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(reconnectError)}`);
677
1238
  }
@@ -689,7 +1250,9 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
689
1250
  }
690
1251
  const endpoints = this.deviceEndpoints.get(path);
691
1252
  const endpointIn = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.endpointIn) !== null && _a !== void 0 ? _a : this.endpointId;
1253
+ this.assertHandleAvailable(device);
692
1254
  const result = yield device.transferIn(endpointIn, length);
1255
+ this.assertHandleAvailable(device);
693
1256
  return this.getTransferInData(result);
694
1257
  });
695
1258
  }
@@ -749,10 +1312,10 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
749
1312
  return false;
750
1313
  }
751
1314
  try {
752
- yield this.callProtocolV1(path, 'Initialize', {}, {
1315
+ const response = yield this.callProtocolV1(path, this.identityProbe ? 'GetFeatures' : 'Initialize', {}, {
753
1316
  timeoutMs: PROTOCOL_V1_PROBE_TIMEOUT,
754
1317
  });
755
- return true;
1318
+ return !this.identityProbe || response.type === 'Features';
756
1319
  }
757
1320
  catch (error) {
758
1321
  if (this.isWebUsbIoHardwareError(error))
@@ -868,9 +1431,25 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
868
1431
  }
869
1432
  release(path) {
870
1433
  return __awaiter(this, void 0, void 0, function* () {
1434
+ const startedAt = Date.now();
1435
+ this.logLifecycle('release', { phase: 'start' });
871
1436
  this.acquiredPaths.delete(path);
872
- yield this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
873
- yield this.closeOpenDevice(path);
1437
+ try {
1438
+ yield this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
1439
+ yield this.closeOpenDevice(path);
1440
+ this.logLifecycle('release', {
1441
+ phase: 'success',
1442
+ durationMs: Date.now() - startedAt,
1443
+ });
1444
+ }
1445
+ catch (error) {
1446
+ this.logLifecycle('release', Object.assign({ phase: 'error', durationMs: Date.now() - startedAt }, this.getDiagnosticError(error, path)));
1447
+ throw error;
1448
+ }
1449
+ finally {
1450
+ this.releaseIdentityProbeHandle(path);
1451
+ this.releaseNormalHandles(path);
1452
+ }
874
1453
  this.deviceEndpoints.delete(path);
875
1454
  });
876
1455
  }
@@ -1277,10 +1856,12 @@ class ElectronBleTransport {
1277
1856
  });
1278
1857
  }
1279
1858
  createProtocolMismatchError(expected) {
1280
- return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`);
1859
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportProtocolProbeFailed, undefined, {
1860
+ expectedProtocol: expected,
1861
+ });
1281
1862
  }
1282
1863
  createProtocolDetectionError() {
1283
- return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleTimeoutError, 'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping');
1864
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportProtocolProbeFailed);
1284
1865
  }
1285
1866
  clearProbeProtocol(uuid, protocol) {
1286
1867
  if (this.deviceProtocol.get(uuid) === protocol) {