@onekeyfe/hd-transport-web-device 1.2.8-alpha.15 → 1.2.8-alpha.17

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/src/webusb.ts CHANGED
@@ -44,6 +44,7 @@ const REPORT_ID = PROTOCOL_V1_REPORT_ID;
44
44
  const HEADER_LENGTH = PROTOCOL_V1_MESSAGE_HEADER_SIZE;
45
45
  const PACKET_IO_MAX_RETRIES = 3;
46
46
  const PACKET_IO_RETRY_DELAY = 300;
47
+ const USB_DEVICE_IN_USE = 'USB device is already in use';
47
48
  // Legacy devices can take longer to answer Initialize after a WebUSB reset.
48
49
  // Keep this aligned with Node USB so a slow Protocol V1 response is not
49
50
  // misreported as a protocol mismatch during acquire.
@@ -86,6 +87,8 @@ let usbDisconnectListenerAttached = false;
86
87
  // open them, including while native open() is still pending.
87
88
  const identityProbeHandles = new WeakSet<USBDevice>();
88
89
  const openingHandles = new WeakSet<USBDevice>();
90
+ // A normal acquire keeps ownership even while recovery temporarily closes the handle.
91
+ const normalHandleOwners = new WeakMap<USBDevice, WebUsbTransport>();
89
92
 
90
93
  function registerActiveWebUsbTransport(instance: WebUsbTransport, usb: USB) {
91
94
  activeWebUsbTransport = instance;
@@ -130,6 +133,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
130
133
 
131
134
  private identityProbeDevices = new Map<string, USBDevice>();
132
135
 
136
+ private normalDevices = new Map<USBDevice, string>();
137
+
138
+ private pendingHandleOperations = new Set<Promise<unknown>>();
139
+
140
+ private stopPromise?: Promise<void>;
141
+
142
+ private identityProbeCleanupPaths = new Set<string>();
143
+
133
144
  /**
134
145
  * The exact USBDevice object each cached protocol was probed against. The
135
146
  * browser returns the same object identity for a device as long as it stays
@@ -192,6 +203,96 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
192
203
  if (!this.identityProbe) registerActiveWebUsbTransport(this, usb);
193
204
  }
194
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
+
195
296
  /**
196
297
  * Announce that a USB device left. Called from the module-scoped disconnect
197
298
  * listener, which is why it is public rather than inlined.
@@ -273,6 +374,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
273
374
  * It does NOT prompt the user to select a device
274
375
  */
275
376
  async enumerate() {
377
+ // Retry only our failed cleanup before Core skips already-open USB handles.
378
+ for (const path of this.identityProbeCleanupPaths) {
379
+ try {
380
+ await this.release(path);
381
+ } catch {
382
+ // Keep ownership while native close still fails; retry on the next probe.
383
+ }
384
+ }
276
385
  await this.getConnectedDevices();
277
386
  return this.deviceList;
278
387
  }
@@ -281,9 +390,21 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
281
390
  * Get list of connected devices
282
391
  */
283
392
  async getConnectedDevices() {
393
+ this.assertRunning();
284
394
  if (!this.usb) return [];
285
395
 
286
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();
287
408
  const onekeyDevices = devices.filter(dev => {
288
409
  const isOneKey = ONEKEY_WEBUSB_FILTER.some(
289
410
  (desc: { vendorId: number; productId: number }) =>
@@ -316,10 +437,13 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
316
437
  * Acquire device control
317
438
  */
318
439
  async acquire(input: AcquireInput) {
440
+ this.assertRunning();
319
441
  if (!input.path) return;
320
442
  if (this.identityProbe) {
321
443
  await this.getConnectedDevices();
322
444
  this.claimIdentityProbeHandle(input.path, await this.findDevice(input.path));
445
+ } else {
446
+ this.claimNormalHandle(input.path, await this.findDevice(input.path));
323
447
  }
324
448
  try {
325
449
  await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
@@ -393,12 +517,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
393
517
  input.expectedProtocol,
394
518
  protocolHint
395
519
  );
520
+ this.assertRunning();
396
521
  this.confirmedDeviceProtocols.set(input.path, detectedProtocol);
397
522
  const probedDevice = this.deviceList.find(d => d.path === input.path)?.device;
398
523
  if (probedDevice) {
399
524
  this.probedDeviceObjects.set(input.path, probedDevice);
400
525
  }
401
526
  }
527
+ this.assertRunning();
402
528
  this.acquiredPaths.add(input.path);
403
529
  return await Promise.resolve(input.path);
404
530
  } catch (e) {
@@ -408,6 +534,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
408
534
  await this.closeOpenDevice(input.path);
409
535
  } finally {
410
536
  this.releaseIdentityProbeHandle(input.path);
537
+ this.releaseNormalHandles(input.path);
411
538
  }
412
539
  throw e;
413
540
  }
@@ -530,9 +657,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
530
657
  async connect(path: string, first: boolean) {
531
658
  const maxRetries = 5;
532
659
  for (let i = 0; i < maxRetries; i++) {
660
+ this.assertRunning();
533
661
  try {
534
662
  return await this.connectToDevice(path, first);
535
663
  } catch (e) {
664
+ this.assertRunning();
536
665
  if (e instanceof HardwareError && e.errorCode === HardwareErrorCode.BridgeNeedsPermission) {
537
666
  throw e;
538
667
  }
@@ -582,17 +711,19 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
582
711
  * Discovers interface/endpoint numbers from USB descriptors on first connection.
583
712
  */
584
713
  async connectToDevice(path: string, first: boolean) {
714
+ this.assertRunning();
585
715
  let device: USBDevice = await this.findDevice(path);
586
716
  if (this.identityProbe && this.identityProbeDevices.get(path) !== device) {
587
717
  await this.closeOpenDevice(path);
588
718
  this.releaseIdentityProbeHandle(path);
589
719
  this.claimIdentityProbeHandle(path, device);
590
720
  }
721
+ if (!this.identityProbe) this.claimNormalHandle(path, device);
591
722
  this.assertHandleAvailable(device);
592
723
  if (!device.opened) {
593
724
  openingHandles.add(device);
594
725
  try {
595
- await device.open();
726
+ await this.runHandleOperation(() => device.open());
596
727
  } catch (error) {
597
728
  return await this.throwWebUsbIoError(path, 'open', error);
598
729
  } finally {
@@ -603,8 +734,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
603
734
  // state as in the legacy transport; probing and V2 recovery still reset.
604
735
  if (first || this.deviceProtocol.get(path) !== 'V1') {
605
736
  try {
606
- await device.reset();
737
+ await this.runHandleOperation(() => device.reset());
607
738
  } catch (error) {
739
+ this.assertRunning();
608
740
  this.Log?.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
609
741
  }
610
742
  }
@@ -615,11 +747,12 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
615
747
  this.releaseIdentityProbeHandle(path);
616
748
  this.claimIdentityProbeHandle(path, device);
617
749
  }
750
+ if (!this.identityProbe) this.claimNormalHandle(path, device);
618
751
  this.assertHandleAvailable(device);
619
752
  if (!device.opened) {
620
753
  openingHandles.add(device);
621
754
  try {
622
- await device.open();
755
+ await this.runHandleOperation(() => device.open());
623
756
  } catch (error) {
624
757
  return await this.throwWebUsbIoError(path, 'open', error);
625
758
  } finally {
@@ -632,14 +765,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
632
765
  !device.configuration ||
633
766
  device.configuration.configurationValue !== this.configurationId
634
767
  ) {
635
- await device.selectConfiguration(this.configurationId);
768
+ await this.runHandleOperation(() => device.selectConfiguration(this.configurationId));
636
769
  }
637
770
 
638
771
  // Discover endpoints from USB descriptors; descriptors are not used for protocol selection.
639
772
  const endpoints = this.discoverEndpoints(device);
640
773
  this.deviceEndpoints.set(path, endpoints);
641
774
  try {
642
- await device.claimInterface(endpoints.interfaceNumber);
775
+ await this.runHandleOperation(() => device.claimInterface(endpoints.interfaceNumber));
643
776
  } catch (error) {
644
777
  return this.throwWebUsbIoError(path, 'claimInterface', error);
645
778
  }
@@ -648,31 +781,70 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
648
781
  }
649
782
 
650
783
  private assertHandleAvailable(device: USBDevice) {
784
+ this.assertRunning();
785
+ const normalOwner = normalHandleOwners.get(device);
651
786
  if (
652
- identityProbeHandles.has(device) &&
653
- ![...this.identityProbeDevices.values()].includes(device)
787
+ (normalOwner && normalOwner !== this) ||
788
+ (identityProbeHandles.has(device) &&
789
+ ![...this.identityProbeDevices.values()].includes(device))
654
790
  ) {
655
- throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'USB device is already in use');
791
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, USB_DEVICE_IN_USE);
792
+ }
793
+ }
794
+
795
+ private claimNormalHandle(path: string, device: USBDevice) {
796
+ this.assertHandleAvailable(device);
797
+ normalHandleOwners.set(device, this);
798
+ this.normalDevices.set(device, path);
799
+ }
800
+
801
+ private releaseNormalHandles(path: string) {
802
+ if (this.stopped) return;
803
+ for (const [device, ownedPath] of this.normalDevices) {
804
+ if (ownedPath === path) {
805
+ normalHandleOwners.delete(device);
806
+ this.normalDevices.delete(device);
807
+ }
656
808
  }
657
809
  }
658
810
 
811
+ private isHandleOwnershipError(error: unknown): error is HardwareError {
812
+ return (
813
+ error instanceof HardwareError &&
814
+ error.errorCode === HardwareErrorCode.RuntimeError &&
815
+ this.getErrorMessage(error).includes(USB_DEVICE_IN_USE)
816
+ );
817
+ }
818
+
659
819
  private claimIdentityProbeHandle(path: string, device: USBDevice) {
660
- if (device.opened || openingHandles.has(device) || identityProbeHandles.has(device)) {
661
- throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'USB device is already in use');
820
+ this.assertRunning();
821
+ if (
822
+ device.opened ||
823
+ openingHandles.has(device) ||
824
+ identityProbeHandles.has(device) ||
825
+ normalHandleOwners.has(device)
826
+ ) {
827
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, USB_DEVICE_IN_USE);
662
828
  }
663
829
  identityProbeHandles.add(device);
664
830
  this.identityProbeDevices.set(path, device);
665
831
  }
666
832
 
667
833
  private releaseIdentityProbeHandle(path: string) {
834
+ if (this.stopped) return;
668
835
  const device = this.identityProbeDevices.get(path);
669
836
  // A failed native close must not make an open probe handle available to other callers.
670
- if (device?.opened) return;
837
+ if (device?.opened) {
838
+ this.identityProbeCleanupPaths.add(path);
839
+ return;
840
+ }
671
841
  if (device) identityProbeHandles.delete(device);
672
842
  this.identityProbeDevices.delete(path);
843
+ this.identityProbeCleanupPaths.delete(path);
673
844
  }
674
845
 
675
846
  private async closeOpenDevice(path: string) {
847
+ if (this.stopped) return;
676
848
  const current = this.identityProbe
677
849
  ? this.identityProbeDevices.get(path)
678
850
  : this.deviceList.find(device => device.path === path)?.device;
@@ -682,12 +854,13 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
682
854
  const endpoints = this.deviceEndpoints.get(path);
683
855
  const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
684
856
  try {
685
- await current.releaseInterface(ifaceNum);
857
+ await this.runHandleOperation(() => current.releaseInterface(ifaceNum));
686
858
  } catch (error) {
687
859
  this.Log?.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
688
860
  }
861
+ this.assertHandleAvailable(current);
689
862
  try {
690
- await current.close();
863
+ await this.runHandleOperation(() => current.close());
691
864
  } catch (error) {
692
865
  this.Log?.debug('[WebUsbTransport] close before reconnect failed:', error);
693
866
  if (this.identityProbe) throw error;
@@ -699,9 +872,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
699
872
  direction: USBDirection,
700
873
  endpointNumber: number
701
874
  ) {
875
+ this.assertHandleAvailable(device);
702
876
  try {
703
- await device.clearHalt(direction, endpointNumber);
877
+ await this.runHandleOperation(() => device.clearHalt(direction, endpointNumber));
704
878
  } catch (error) {
879
+ this.assertRunning();
705
880
  this.Log?.debug(
706
881
  `[WebUsbTransport] clearHalt ${direction} endpoint ${endpointNumber} failed, continuing:`,
707
882
  error
@@ -717,6 +892,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
717
892
  * protocol entry produced the same fail-fast).
718
893
  */
719
894
  private assertAcquired(path: string) {
895
+ this.assertRunning();
720
896
  if (!this.acquiredPaths.has(path)) {
721
897
  throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, `Device is not acquired for ${path}`);
722
898
  }
@@ -774,7 +950,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
774
950
  operation: WebUsbOperation,
775
951
  error: unknown
776
952
  ): Promise<never> {
777
- if (this.isWebUsbIoHardwareError(error)) throw error;
953
+ this.assertRunning();
954
+ if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error)) throw error;
778
955
  if (error instanceof ProtocolV2LinkError && error.code !== 'io') throw error;
779
956
 
780
957
  const nativeError = error instanceof ProtocolV2LinkError ? error.cause ?? error : error;
@@ -814,7 +991,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
814
991
  }
815
992
 
816
993
  private isRetryablePacketIoError(error: unknown) {
817
- if (this.isWebUsbIoHardwareError(error)) return false;
994
+ if (this.stopped) return false;
995
+ if (this.isHandleOwnershipError(error) || this.isWebUsbIoHardwareError(error)) return false;
818
996
  const message = this.getErrorMessage(error).toLowerCase();
819
997
  if (this.isWebUsbPermissionDeniedMessage(message)) return false;
820
998
  return (
@@ -845,20 +1023,25 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
845
1023
  // USB disconnect event itself was missed.
846
1024
  this.staleProtocolPaths.add(path);
847
1025
  await wait(attempt * PACKET_IO_RETRY_DELAY);
1026
+ this.assertRunning();
848
1027
 
849
1028
  try {
850
1029
  const currentDevice = await this.findDevice(path);
1030
+ this.assertHandleAvailable(currentDevice);
851
1031
  if (currentDevice.opened) {
852
1032
  const endpoints = this.deviceEndpoints.get(path);
853
1033
  const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
854
1034
  try {
855
- await currentDevice.releaseInterface(ifaceNum);
1035
+ await this.runHandleOperation(() => currentDevice.releaseInterface(ifaceNum));
856
1036
  } catch (releaseError) {
857
1037
  this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
858
1038
  }
859
- await currentDevice.close();
1039
+ this.assertHandleAvailable(currentDevice);
1040
+ await this.runHandleOperation(() => currentDevice.close());
860
1041
  }
861
1042
  } catch (closeError) {
1043
+ this.assertRunning();
1044
+ if (this.isHandleOwnershipError(closeError)) throw closeError;
862
1045
  this.Log.debug('[WebUsbTransport] close device before retry error:', closeError);
863
1046
  }
864
1047
 
@@ -900,6 +1083,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
900
1083
  try {
901
1084
  await this.reconnectForPacketIoRetry(path, 'out', attempt, error);
902
1085
  } catch (reconnectError) {
1086
+ this.assertRunning();
1087
+ if (this.isHandleOwnershipError(reconnectError)) throw reconnectError;
903
1088
  lastError = reconnectError;
904
1089
  this.Log.debug(
905
1090
  `[WebUsbTransport] transferout reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
@@ -922,7 +1107,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
922
1107
  const transferBuffer = this.toArrayBuffer(
923
1108
  packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
924
1109
  );
1110
+ this.assertHandleAvailable(device);
925
1111
  const result = await device.transferOut(endpointOut, transferBuffer);
1112
+ this.assertHandleAvailable(device);
926
1113
  if (result.status !== 'ok') {
927
1114
  return this.throwWebUsbIoError(
928
1115
  path,
@@ -949,7 +1136,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
949
1136
  }
950
1137
  const endpoints = this.deviceEndpoints.get(path);
951
1138
  const endpointIn = endpoints?.endpointIn ?? this.endpointId;
1139
+ this.assertHandleAvailable(device);
952
1140
  const result = await device.transferIn(endpointIn, length);
1141
+ this.assertHandleAvailable(device);
953
1142
  return this.getTransferInData(result);
954
1143
  } catch (error) {
955
1144
  lastError = error;
@@ -963,6 +1152,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
963
1152
  try {
964
1153
  await this.reconnectForPacketIoRetry(path, 'in', attempt, error);
965
1154
  } catch (reconnectError) {
1155
+ this.assertRunning();
1156
+ if (this.isHandleOwnershipError(reconnectError)) throw reconnectError;
966
1157
  lastError = reconnectError;
967
1158
  this.Log.debug(
968
1159
  `[WebUsbTransport] transferin reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
@@ -982,7 +1173,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
982
1173
  }
983
1174
  const endpoints = this.deviceEndpoints.get(path);
984
1175
  const endpointIn = endpoints?.endpointIn ?? this.endpointId;
1176
+ this.assertHandleAvailable(device);
985
1177
  const result = await device.transferIn(endpointIn, length);
1178
+ this.assertHandleAvailable(device);
986
1179
  return this.getTransferInData(result);
987
1180
  }
988
1181
 
@@ -1215,6 +1408,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
1215
1408
  await this.closeOpenDevice(path);
1216
1409
  } finally {
1217
1410
  this.releaseIdentityProbeHandle(path);
1411
+ this.releaseNormalHandles(path);
1218
1412
  }
1219
1413
  // Keep deviceProtocol/deviceProtocolHints across release: the probe result is a
1220
1414
  // physical-device property, so the next acquire can skip the wire-level probe.