commit 2cfa3bd6c82b5f2df0332307240fd202523cb522
parent 98d2ab5bd6f47961316c607f2a7ccd01fe38c472
Author: SomberNight <somber.night@protonmail.com>
Date: Fri, 17 Apr 2020 19:05:56 +0200
hww hidapi usage: try to mitigate some thread-safety issues
related: #6097
Diffstat:
6 files changed, 66 insertions(+), 25 deletions(-)
diff --git a/electrum/plugin.py b/electrum/plugin.py
@@ -30,6 +30,8 @@ import threading
import sys
from typing import (NamedTuple, Any, Union, TYPE_CHECKING, Optional, Tuple,
Dict, Iterable, List, Sequence)
+import concurrent
+from concurrent import futures
from .i18n import _
from .util import (profiler, DaemonThread, UserCancelled, ThreadJob, UserFacingException)
@@ -321,6 +323,20 @@ class HardwarePluginToScan(NamedTuple):
PLACEHOLDER_HW_CLIENT_LABELS = {None, "", " "}
+# hidapi is not thread-safe
+# see https://github.com/signal11/hidapi/issues/205#issuecomment-527654560
+# https://github.com/libusb/hidapi/issues/45
+# https://github.com/signal11/hidapi/issues/45#issuecomment-4434598
+# https://github.com/signal11/hidapi/pull/414#issuecomment-445164238
+# It is not entirely clear to me, exactly what is safe and what isn't, when
+# using multiple threads...
+# For now, we use a dedicated thread to enumerate devices (_hid_executor),
+# and we synchronize all device opens/closes/enumeration (_hid_lock).
+# FIXME there are still probably threading issues with how we use hidapi...
+_hid_executor = None # type: Optional[concurrent.futures.Executor]
+_hid_lock = threading.Lock()
+
+
class DeviceMgr(ThreadJob):
'''Manages hardware clients. A client communicates over a hardware
channel with the device.
@@ -367,9 +383,15 @@ class DeviceMgr(ThreadJob):
# locks: if you need to take multiple ones, acquire them in the order they are defined here!
self._scan_lock = threading.RLock()
self.lock = threading.RLock()
+ self.hid_lock = _hid_lock
self.config = config
+ global _hid_executor
+ if _hid_executor is None:
+ _hid_executor = concurrent.futures.ThreadPoolExecutor(max_workers=1,
+ thread_name_prefix='hid_enumerate_thread')
+
def with_scan_lock(func):
def func_wrapper(self: 'DeviceMgr', *args, **kwargs):
with self._scan_lock:
@@ -636,7 +658,15 @@ class DeviceMgr(ThreadJob):
except ImportError:
return []
- hid_list = hid.enumerate(0, 0)
+ def hid_enumerate():
+ with self.hid_lock:
+ return hid.enumerate(0, 0)
+
+ hid_list_fut = _hid_executor.submit(hid_enumerate)
+ try:
+ hid_list = hid_list_fut.result()
+ except (concurrent.futures.CancelledError, concurrent.futures.TimeoutError) as e:
+ return []
devices = []
for d in hid_list:
diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py
@@ -46,7 +46,7 @@ class BitBox02Client(HardwareClientBase):
# handler is a BitBox02_Handler, importing it would lead to a circular dependency
def __init__(self, handler: Any, device: Device, config: SimpleConfig, *, plugin: HW_PluginBase):
HardwareClientBase.__init__(self, plugin=plugin)
- self.bitbox02_device = None
+ self.bitbox02_device = None # type: Optional[bitbox02.BitBox02]
self.handler = handler
self.device_descriptor = device
self.config = config
@@ -73,10 +73,11 @@ class BitBox02Client(HardwareClientBase):
return True
def close(self):
- try:
- self.bitbox02_device.close()
- except:
- pass
+ with self.device_manager().hid_lock:
+ try:
+ self.bitbox02_device.close()
+ except:
+ pass
def has_usable_connection_with_device(self) -> bool:
if self.bitbox_hid_info is None:
@@ -91,7 +92,8 @@ class BitBox02Client(HardwareClientBase):
res = device_response()
except:
# Close the hid device on exception
- hid_device.close()
+ with self.device_manager().hid_lock:
+ hid_device.close()
raise
finally:
self.handler.finished()
@@ -155,8 +157,9 @@ class BitBox02Client(HardwareClientBase):
return set_noise_privkey(privkey)
if self.bitbox02_device is None:
- hid_device = hid.device()
- hid_device.open_path(self.bitbox_hid_info["path"])
+ with self.device_manager().hid_lock:
+ hid_device = hid.device()
+ hid_device.open_path(self.bitbox_hid_info["path"])
self.bitbox02_device = bitbox02.BitBox02(
transport=u2fhid.U2FHid(hid_device),
diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py
@@ -72,9 +72,9 @@ class CKCCClient(HardwareClientBase):
self.dev = ElectrumColdcardDevice(dev_path, encrypt=True)
else:
# open the real HID device
- import hid
- hd = hid.device(path=dev_path)
- hd.open_path(dev_path)
+ with self.device_manager().hid_lock:
+ hd = hid.device(path=dev_path)
+ hd.open_path(dev_path)
self.dev = ElectrumColdcardDevice(dev=hd, encrypt=True)
@@ -127,7 +127,8 @@ class CKCCClient(HardwareClientBase):
def close(self):
# close the HID device (so can be reused)
- self.dev.close()
+ with self.device_manager().hid_lock:
+ self.dev.close()
self.dev = None
def is_initialized(self):
diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py
@@ -77,10 +77,11 @@ class DigitalBitbox_Client(HardwareClientBase):
def close(self):
if self.opened:
- try:
- self.dbb_hid.close()
- except:
- pass
+ with self.device_manager().hid_lock:
+ try:
+ self.dbb_hid.close()
+ except:
+ pass
self.opened = False
@@ -681,8 +682,9 @@ class DigitalBitboxPlugin(HW_PluginBase):
def get_dbb_device(self, device):
- dev = hid.device()
- dev.open_path(device.path)
+ with self.device_manager().hid_lock:
+ dev = hid.device()
+ dev.open_path(device.path)
return dev
diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py
@@ -196,6 +196,9 @@ class HardwareClientBase:
def __init__(self, *, plugin: 'HW_PluginBase'):
self.plugin = plugin
+ def device_manager(self) -> 'DeviceMgr':
+ return self.plugin.device_manager()
+
def is_pairable(self) -> bool:
raise NotImplementedError()
diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py
@@ -74,7 +74,8 @@ class Ledger_Client(HardwareClientBase):
return True
def close(self):
- self.dongleObject.dongle.close()
+ with self.device_manager().hid_lock:
+ self.dongleObject.dongle.close()
def timeout(self, cutoff):
pass
@@ -184,13 +185,13 @@ class Ledger_Client(HardwareClientBase):
self.segwitSupported = self.nativeSegwitSupported or (firmwareInfo['specialVersion'] == 0x20 and versiontuple(firmware) >= versiontuple(SEGWIT_SUPPORT_SPECIAL))
if not checkFirmware(firmwareInfo):
- self.dongleObject.dongle.close()
+ self.close()
raise UserFacingException(MSG_NEEDS_FW_UPDATE_GENERIC)
try:
self.dongleObject.getOperationMode()
except BTChipException as e:
if (e.sw == 0x6985):
- self.dongleObject.dongle.close()
+ self.close()
self.handler.get_setup( )
# Acquire the new client on the next run
else:
@@ -593,9 +594,10 @@ class LedgerPlugin(HW_PluginBase):
ledger = True
else:
return None # non-compatible interface of a Nano S or Blue
- dev = hid.device()
- dev.open_path(device.path)
- dev.set_nonblocking(True)
+ with self.device_manager().hid_lock:
+ dev = hid.device()
+ dev.open_path(device.path)
+ dev.set_nonblocking(True)
return HIDDongleHIDAPI(dev, ledger, BTCHIP_DEBUG)
def create_client(self, device, handler):