URI: 
       thardware devices: run all device communication on dedicated thread (#6561) - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 21c35726009295412b0d5d51e8dff6c2ac064842
   DIR parent 53a5a21ee89b791c351ad39a47e69837ed347af8
  HTML Author: ghost43 <somber.night@protonmail.com>
       Date:   Tue,  8 Sep 2020 15:52:53 +0000
       
       hardware devices: run all device communication on dedicated thread (#6561)
       
       hidapi/libusb etc are not thread-safe.
       
       related: #6554
       Diffstat:
         M electrum/plugin.py                  |      84 ++++++++++++++++---------------
         M electrum/plugins/bitbox02/bitbox02… |      35 ++++++++++++++++++++-----------
         M electrum/plugins/coldcard/coldcard… |      30 +++++++++++++++++++-----------
         M electrum/plugins/digitalbitbox/dig… |      36 +++++++++++++------------------
         M electrum/plugins/hw_wallet/plugin.… |       6 +++++-
         M electrum/plugins/keepkey/clientbas… |      12 ++++++++++++
         M electrum/plugins/keepkey/keepkey.py |      14 +++++++++++++-
         M electrum/plugins/ledger/ledger.py   |      27 ++++++++++++++++++---------
         M electrum/plugins/safe_t/clientbase… |      12 ++++++++++++
         M electrum/plugins/safe_t/safe_t.py   |      10 +++++++++-
         M electrum/plugins/trezor/clientbase… |      17 +++++++++++++++++
         M electrum/plugins/trezor/trezor.py   |       9 ++++++++-
       
       12 files changed, 195 insertions(+), 97 deletions(-)
       ---
   DIR diff --git a/electrum/plugin.py b/electrum/plugin.py
       t@@ -29,9 +29,10 @@ import time
        import threading
        import sys
        from typing import (NamedTuple, Any, Union, TYPE_CHECKING, Optional, Tuple,
       -                    Dict, Iterable, List, Sequence)
       +                    Dict, Iterable, List, Sequence, Callable, TypeVar)
        import concurrent
        from concurrent import futures
       +from functools import wraps, partial
        
        from .i18n import _
        from .util import (profiler, DaemonThread, UserCancelled, ThreadJob, UserFacingException)
       t@@ -334,11 +335,37 @@ PLACEHOLDER_HW_CLIENT_LABELS = {None, "", " "}
        #     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()
       +# Hence, we use a single thread for all device communications, including
       +# enumeration. Everything that uses hidapi, libusb, etc, MUST run on
       +# the following thread:
       +_hwd_comms_executor = concurrent.futures.ThreadPoolExecutor(
       +    max_workers=1,
       +    thread_name_prefix='hwd_comms_thread'
       +)
       +
       +
       +T = TypeVar('T')
       +
       +
       +def run_in_hwd_thread(func: Callable[[], T]) -> T:
       +    if threading.current_thread().name.startswith("hwd_comms_thread"):
       +        return func()
       +    else:
       +        fut = _hwd_comms_executor.submit(func)
       +        return fut.result()
       +        #except (concurrent.futures.CancelledError, concurrent.futures.TimeoutError) as e:
       +
       +
       +def runs_in_hwd_thread(func):
       +    @wraps(func)
       +    def wrapper(*args, **kwargs):
       +        return run_in_hwd_thread(partial(func, *args, **kwargs))
       +    return wrapper
       +
       +
       +def assert_runs_in_hwd_thread():
       +    if not threading.current_thread().name.startswith("hwd_comms_thread"):
       +        raise Exception("must only be called from HWD communication thread")
        
        
        class DeviceMgr(ThreadJob):
       t@@ -384,24 +411,11 @@ class DeviceMgr(ThreadJob):
                self._recognised_hardware = {}  # type: Dict[Tuple[int, int], HW_PluginBase]
                # Custom enumerate functions for devices we don't know about.
                self._enumerate_func = set()  # Needs self.lock.
       -        # 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:
       -                return func(self, *args, **kwargs)
       -        return func_wrapper
       -
            def thread_jobs(self):
                # Thread job to handle device timeouts
                return [self]
       t@@ -423,6 +437,7 @@ class DeviceMgr(ThreadJob):
                with self.lock:
                    self._enumerate_func.add(func)
        
       +    @runs_in_hwd_thread
            def create_client(self, device: 'Device', handler: Optional['HardwareHandlerBase'],
                              plugin: 'HW_PluginBase') -> Optional['HardwareClientBase']:
                # Get from cache first
       t@@ -452,7 +467,7 @@ class DeviceMgr(ThreadJob):
                    if xpub not in self.xpub_ids:
                        return
                    _id = self.xpub_ids.pop(xpub)
       -            self._close_client(_id)
       +        self._close_client(_id)
        
            def unpair_id(self, id_):
                xpub = self.xpub_by_id(id_)
       t@@ -462,8 +477,9 @@ class DeviceMgr(ThreadJob):
                    self._close_client(id_)
        
            def _close_client(self, id_):
       -        client = self._client_by_id(id_)
       -        self.clients.pop(client, None)
       +        with self.lock:
       +            client = self._client_by_id(id_)
       +            self.clients.pop(client, None)
                if client:
                    client.close()
        
       t@@ -486,7 +502,7 @@ class DeviceMgr(ThreadJob):
                    self.scan_devices()
                return self._client_by_id(id_)
        
       -    @with_scan_lock
       +    @runs_in_hwd_thread
            def client_for_keystore(self, plugin: 'HW_PluginBase', handler: Optional['HardwareHandlerBase'],
                                    keystore: 'Hardware_KeyStore',
                                    force_pair: bool, *,
       t@@ -655,25 +671,15 @@ class DeviceMgr(ThreadJob):
                # note: updated label/soft_device_id will be saved after pairing succeeds
                return info
        
       -    @with_scan_lock
       +    @runs_in_hwd_thread
            def _scan_devices_with_hid(self) -> List['Device']:
                try:
                    import hid
                except ImportError:
                    return []
        
       -        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:
       +        for d in hid.enumerate(0, 0):
                    product_key = (d['vendor_id'], d['product_id'])
                    if product_key in self._recognised_hardware:
                        plugin = self._recognised_hardware[product_key]
       t@@ -681,7 +687,7 @@ class DeviceMgr(ThreadJob):
                        devices.append(device)
                return devices
        
       -    @with_scan_lock
       +    @runs_in_hwd_thread
            @profiler
            def scan_devices(self) -> Sequence['Device']:
                self.logger.info("scanning devices...")
       t@@ -693,10 +699,8 @@ class DeviceMgr(ThreadJob):
                with self.lock:
                    enumerate_funcs = list(self._enumerate_func)
                for f in enumerate_funcs:
       -            # custom enumerate functions might use hidapi, so use hid thread to be safe
       -            new_devices_fut = _hid_executor.submit(f)
                    try:
       -                new_devices = new_devices_fut.result()
       +                new_devices = f()
                    except BaseException as e:
                        self.logger.error('custom device enum failed. func {}, error {}'
                                          .format(str(f), repr(e)))
   DIR diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py
       t@@ -13,7 +13,7 @@ from electrum.wallet import Standard_Wallet, Multisig_Wallet, Deterministic_Wall
        from electrum.util import bh2u, UserFacingException
        from electrum.base_wizard import ScriptTypeNotSupported, BaseWizard
        from electrum.logging import get_logger
       -from electrum.plugin import Device, DeviceInfo
       +from electrum.plugin import Device, DeviceInfo, runs_in_hwd_thread
        from electrum.simple_config import SimpleConfig
        from electrum.json_db import StoredDict
        from electrum.storage import get_derivation_used_for_hw_device_encryption
       t@@ -73,18 +73,19 @@ class BitBox02Client(HardwareClientBase):
            def is_initialized(self) -> bool:
                return True
        
       +    @runs_in_hwd_thread
            def close(self):
       -        with self.device_manager().hid_lock:
       -            try:
       -                self.bitbox02_device.close()
       -            except:
       -                pass
       +        try:
       +            self.bitbox02_device.close()
       +        except:
       +            pass
        
            def has_usable_connection_with_device(self) -> bool:
                if self.bitbox_hid_info is None:
                    return False
                return True
        
       +    @runs_in_hwd_thread
            def get_soft_device_id(self) -> Optional[str]:
                if self.handler is None:
                    # Can't do the pairing without the handler. This happens at wallet creation time, when
       t@@ -94,6 +95,7 @@ class BitBox02Client(HardwareClientBase):
                    self.pairing_dialog()
                return self.bitbox02_device.root_fingerprint().hex()
        
       +    @runs_in_hwd_thread
            def pairing_dialog(self):
                def pairing_step(code: str, device_response: Callable[[], bool]) -> bool:
                    msg = "Please compare and confirm the pairing code on your BitBox02:\n" + code
       t@@ -102,8 +104,7 @@ class BitBox02Client(HardwareClientBase):
                        res = device_response()
                    except:
                        # Close the hid device on exception
       -                with self.device_manager().hid_lock:
       -                    hid_device.close()
       +                hid_device.close()
                        raise
                    finally:
                        self.handler.finished()
       t@@ -167,10 +168,8 @@ class BitBox02Client(HardwareClientBase):
                        return set_noise_privkey(privkey)
        
                if self.bitbox02_device is None:
       -            with self.device_manager().hid_lock:
       -                hid_device = hid.device()
       -                hid_device.open_path(self.bitbox_hid_info["path"])
       -
       +            hid_device = hid.device()
       +            hid_device.open_path(self.bitbox_hid_info["path"])
        
                    bitbox02_device = bitbox02.BitBox02(
                        transport=u2fhid.U2FHid(hid_device),
       t@@ -197,6 +196,7 @@ class BitBox02Client(HardwareClientBase):
                    return bitbox02.btc.TBTC
                return bitbox02.btc.BTC
        
       +    @runs_in_hwd_thread
            def get_password_for_storage_encryption(self) -> str:
                derivation = get_derivation_used_for_hw_device_encryption()
                derivation_list = bip32.convert_bip32_path_to_list_of_uint32(derivation)
       t@@ -204,6 +204,7 @@ class BitBox02Client(HardwareClientBase):
                node = bip32.BIP32Node.from_xkey(xpub, net = constants.BitcoinMainnet()).subkey_at_public_derivation(())
                return node.eckey.get_public_key_bytes(compressed=True).hex()
        
       +    @runs_in_hwd_thread
            def get_xpub(self, bip32_path: str, xtype: str, *, display: bool = False) -> str:
                if self.bitbox02_device is None:
                    self.pairing_dialog()
       t@@ -244,6 +245,7 @@ class BitBox02Client(HardwareClientBase):
                    display=display,
                )
        
       +    @runs_in_hwd_thread
            def label(self) -> str:
                if self.handler is None:
                    # Can't do the pairing without the handler. This happens at wallet creation time, when
       t@@ -258,6 +260,7 @@ class BitBox02Client(HardwareClientBase):
                    self.bitbox02_device.root_fingerprint().hex(),
                )
        
       +    @runs_in_hwd_thread
            def request_root_fingerprint_from_device(self) -> str:
                if self.bitbox02_device is None:
                    raise Exception(
       t@@ -271,6 +274,7 @@ class BitBox02Client(HardwareClientBase):
                    return False
                return True
        
       +    @runs_in_hwd_thread
            def btc_multisig_config(
                self, coin, bip32_path: List[int], wallet: Multisig_Wallet
            ):
       t@@ -316,6 +320,7 @@ class BitBox02Client(HardwareClientBase):
                        raise UserFacingException("Failed to register multisig\naccount configuration on BitBox02")
                return multisig_config
        
       +    @runs_in_hwd_thread
            def show_address(
                self, bip32_path: str, address_type: str, wallet: Deterministic_Wallet
            ) -> str:
       t@@ -357,6 +362,7 @@ class BitBox02Client(HardwareClientBase):
                    display=True,
                )
        
       +    @runs_in_hwd_thread
            def sign_transaction(
                self,
                keystore: Hardware_KeyStore,
       t@@ -553,6 +559,7 @@ class BitBox02_KeyStore(Hardware_KeyStore):
                    ).format(self.device)
                )
        
       +    @runs_in_hwd_thread
            def sign_transaction(self, tx: PartialTransaction, password: str):
                if tx.is_complete():
                    return
       t@@ -572,6 +579,7 @@ class BitBox02_KeyStore(Hardware_KeyStore):
                    self.give_error(e, True)
                    return
        
       +    @runs_in_hwd_thread
            def show_address(
                self, sequence: Tuple[int, int], txin_type: str, wallet: Deterministic_Wallet
            ):
       t@@ -616,6 +624,7 @@ class BitBox02Plugin(HW_PluginBase):
                    raise ImportError()
        
            # handler is a BitBox02_Handler
       +    @runs_in_hwd_thread
            def create_client(self, device: Device, handler: Any) -> BitBox02Client:
                if not handler:
                    self.handler = handler
       t@@ -645,6 +654,7 @@ class BitBox02Plugin(HW_PluginBase):
                assert client.bitbox02_device is not None
                return client.get_xpub(derivation, xtype)
        
       +    @runs_in_hwd_thread
            def show_address(
                self,
                wallet: Deterministic_Wallet,
       t@@ -660,6 +670,7 @@ class BitBox02Plugin(HW_PluginBase):
                sequence = wallet.get_address_index(address)
                keystore.show_address(sequence, txin_type, wallet)
        
       +    @runs_in_hwd_thread
            def show_xpub(self, keystore: BitBox02_KeyStore):
                client = keystore.get_client()
                assert isinstance(client, BitBox02Client)
   DIR diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py
       t@@ -10,7 +10,7 @@ import struct
        from electrum import bip32
        from electrum.bip32 import BIP32Node, InvalidMasterKeyVersionBytes
        from electrum.i18n import _
       -from electrum.plugin import Device, hook
       +from electrum.plugin import Device, hook, runs_in_hwd_thread
        from electrum.keystore import Hardware_KeyStore, KeyStoreWithMPK
        from electrum.transaction import PartialTransaction
        from electrum.wallet import Standard_Wallet, Multisig_Wallet, Abstract_Wallet
       t@@ -72,9 +72,8 @@ class CKCCClient(HardwareClientBase):
                    self.dev = ElectrumColdcardDevice(dev_path, encrypt=True)
                else:
                    # open the real HID device
       -            with self.device_manager().hid_lock:
       -                hd = hid.device(path=dev_path)
       -                hd.open_path(dev_path)
       +            hd = hid.device(path=dev_path)
       +            hd.open_path(dev_path)
        
                    self.dev = ElectrumColdcardDevice(dev=hd, encrypt=True)
        
       t@@ -85,6 +84,7 @@ class CKCCClient(HardwareClientBase):
                return '<CKCCClient: xfp=%s label=%r>' % (xfp2str(self.dev.master_fingerprint),
                                                                self.label())
        
       +    @runs_in_hwd_thread
            def verify_connection(self, expected_xfp: int, expected_xpub=None):
                ex = (expected_xfp, expected_xpub)
        
       t@@ -121,14 +121,10 @@ class CKCCClient(HardwareClientBase):
                # can't do anything w/ devices that aren't setup (this code not normally reachable)
                return bool(self.dev.master_xpub)
        
       -    def timeout(self, cutoff):
       -        # nothing to do?
       -        pass
       -
       +    @runs_in_hwd_thread
            def close(self):
                # close the HID device (so can be reused)
       -        with self.device_manager().hid_lock:
       -            self.dev.close()
       +        self.dev.close()
                self.dev = None
        
            def is_initialized(self):
       t@@ -160,6 +156,7 @@ class CKCCClient(HardwareClientBase):
        
                return LabelStr(lab, self.dev.master_fingerprint, self.dev.master_xpub)
        
       +    @runs_in_hwd_thread
            def has_usable_connection_with_device(self):
                # Do end-to-end ping test
                try:
       t@@ -168,6 +165,7 @@ class CKCCClient(HardwareClientBase):
                except:
                    return False
        
       +    @runs_in_hwd_thread
            def get_xpub(self, bip32_path, xtype):
                assert xtype in ColdcardPlugin.SUPPORTED_XTYPES
                _logger.info('Derive xtype = %r' % xtype)
       t@@ -183,6 +181,7 @@ class CKCCClient(HardwareClientBase):
                    xpub = node._replace(xtype=xtype).to_xpub()
                return xpub
        
       +    @runs_in_hwd_thread
            def ping_check(self):
                # check connection is working
                assert self.dev.session_key, 'not encrypted?'
       t@@ -193,26 +192,32 @@ class CKCCClient(HardwareClientBase):
                except:
                    raise RuntimeError("Communication trouble with Coldcard")
        
       +    @runs_in_hwd_thread
            def show_address(self, path, addr_fmt):
                # prompt user w/ address, also returns it immediately.
                return self.dev.send_recv(CCProtocolPacker.show_address(path, addr_fmt), timeout=None)
        
       +    @runs_in_hwd_thread
            def show_p2sh_address(self, *args, **kws):
                # prompt user w/ p2sh address, also returns it immediately.
                return self.dev.send_recv(CCProtocolPacker.show_p2sh_address(*args, **kws), timeout=None)
        
       +    @runs_in_hwd_thread
            def get_version(self):
                # gives list of strings
                return self.dev.send_recv(CCProtocolPacker.version(), timeout=1000).split('\n')
        
       +    @runs_in_hwd_thread
            def sign_message_start(self, path, msg):
                # this starts the UX experience.
                self.dev.send_recv(CCProtocolPacker.sign_message(msg, path), timeout=None)
        
       +    @runs_in_hwd_thread
            def sign_message_poll(self):
                # poll device... if user has approved, will get tuple: (addr, sig) else None
                return self.dev.send_recv(CCProtocolPacker.get_signed_msg(), timeout=None)
        
       +    @runs_in_hwd_thread
            def sign_transaction_start(self, raw_psbt: bytes, *, finalize: bool = False):
                # Multiple steps to sign:
                # - upload binary
       t@@ -228,10 +233,12 @@ class CKCCClient(HardwareClientBase):
                if resp != None:
                    raise ValueError(resp)
        
       +    @runs_in_hwd_thread
            def sign_transaction_poll(self):
                # poll device... if user has approved, will get tuple: (legnth, checksum) else None
                return self.dev.send_recv(CCProtocolPacker.get_signed_txn(), timeout=None)
        
       +    @runs_in_hwd_thread
            def download_file(self, length, checksum, file_number=1):
                # get a file
                return self.dev.download_file(length, checksum, file_number=file_number)
       t@@ -317,7 +324,6 @@ class Coldcard_KeyStore(Hardware_KeyStore):
                                                    % MSG_SIGNING_MAX_LENGTH)
                    return b''
        
       -        client = self.get_client()
                path = self.get_derivation_prefix() + ("/%d/%d" % sequence)
                try:
                    cl = self.get_client()
       t@@ -508,6 +514,7 @@ class ColdcardPlugin(HW_PluginBase):
        
                return []
        
       +    @runs_in_hwd_thread
            def create_client(self, device, handler):
                if handler:
                    self.handler = handler
       t@@ -539,6 +546,7 @@ class ColdcardPlugin(HW_PluginBase):
                xpub = client.get_xpub(derivation, xtype)
                return xpub
        
       +    @runs_in_hwd_thread
            def get_client(self, keystore, force_pair=True, *,
                           devices=None, allow_user_interaction=True) -> Optional['CKCCClient']:
                # Acquire a connection to the hardware device (via USB)
   DIR diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py
       t@@ -30,6 +30,7 @@ from electrum.util import to_string, UserCancelled, UserFacingException, bfh
        from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET
        from electrum.network import Network
        from electrum.logging import get_logger
       +from electrum.plugin import runs_in_hwd_thread, run_in_hwd_thread
        
        from ..hw_wallet import HW_PluginBase, HardwareClientBase
        
       t@@ -74,21 +75,16 @@ class DigitalBitbox_Client(HardwareClientBase):
                self.setupRunning = False
                self.usbReportSize = 64 # firmware > v2.0.0
        
       -
       +    @runs_in_hwd_thread
            def close(self):
                if self.opened:
       -            with self.device_manager().hid_lock:
       -                try:
       -                    self.dbb_hid.close()
       -                except:
       -                    pass
       +            try:
       +                self.dbb_hid.close()
       +            except:
       +                pass
                self.opened = False
        
        
       -    def timeout(self, cutoff):
       -        pass
       -
       -
            def is_pairable(self):
                return True
        
       t@@ -111,7 +107,6 @@ class DigitalBitbox_Client(HardwareClientBase):
                if self.check_device_dialog():
                    return self.hid_send_encrypt(('{"xpub": "%s"}' % bip32_path).encode('utf8'))
        
       -
            def get_xpub(self, bip32_path, xtype):
                assert xtype in self.plugin.SUPPORTED_XTYPES
                reply = self._get_xpub(bip32_path)
       t@@ -171,9 +166,9 @@ class DigitalBitbox_Client(HardwareClientBase):
                        self.password = password.encode('utf8')
                        return True
        
       -
            def check_device_dialog(self):
       -        match = re.search(r'v([0-9])+\.[0-9]+\.[0-9]+', self.dbb_hid.get_serial_number_string())
       +        match = re.search(r'v([0-9])+\.[0-9]+\.[0-9]+',
       +                          run_in_hwd_thread(self.dbb_hid.get_serial_number_string))
                if match is None:
                    raise Exception("error detecting firmware version")
                major_version = int(match.group(1))
       t@@ -350,7 +345,7 @@ class DigitalBitbox_Client(HardwareClientBase):
                    raise UserFacingException(hid_reply['error']['message'])
                return True
        
       -
       +    @runs_in_hwd_thread
            def hid_send_frame(self, data):
                HWW_CID = 0xFF000000
                HWW_CMD = 0x80 + 0x40 + 0x01
       t@@ -370,7 +365,7 @@ class DigitalBitbox_Client(HardwareClientBase):
                        seq += 1
                    idx += len(write)
        
       -
       +    @runs_in_hwd_thread
            def hid_read_frame(self):
                # INIT response
                read = bytearray(self.dbb_hid.read(self.usbReportSize))
       t@@ -386,7 +381,7 @@ class DigitalBitbox_Client(HardwareClientBase):
                    idx += len(read) - 5
                return data
        
       -
       +    @runs_in_hwd_thread
            def hid_send_plain(self, msg):
                reply = ""
                try:
       t@@ -408,7 +403,7 @@ class DigitalBitbox_Client(HardwareClientBase):
                    _logger.info(f'Exception caught {repr(e)}')
                return reply
        
       -
       +    @runs_in_hwd_thread
            def hid_send_encrypt(self, msg):
                sha256_byte_len = 32
                reply = ""
       t@@ -680,11 +675,10 @@ class DigitalBitboxPlugin(HW_PluginBase):
        
                self.digitalbitbox_config = self.config.get('digitalbitbox', {})
        
       -
       +    @runs_in_hwd_thread
            def get_dbb_device(self, device):
       -        with self.device_manager().hid_lock:
       -            dev = hid.device()
       -            dev.open_path(device.path)
       +        dev = hid.device()
       +        dev.open_path(device.path)
                return dev
        
        
   DIR diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py
       t@@ -27,7 +27,8 @@
        from typing import TYPE_CHECKING, Dict, List, Union, Tuple, Sequence, Optional, Type
        from functools import partial
        
       -from electrum.plugin import BasePlugin, hook, Device, DeviceMgr, DeviceInfo
       +from electrum.plugin import (BasePlugin, hook, Device, DeviceMgr, DeviceInfo,
       +                             assert_runs_in_hwd_thread, runs_in_hwd_thread)
        from electrum.i18n import _
        from electrum.bitcoin import is_address, opcodes
        from electrum.util import bfh, versiontuple, UserFacingException
       t@@ -197,6 +198,7 @@ class HardwareClientBase:
            handler = None  # type: Optional['HardwareHandlerBase']
        
            def __init__(self, *, plugin: 'HW_PluginBase'):
       +        assert_runs_in_hwd_thread()
                self.plugin = plugin
        
            def device_manager(self) -> 'DeviceMgr':
       t@@ -242,6 +244,7 @@ class HardwareClientBase:
            def get_xpub(self, bip32_path: str, xtype) -> str:
                raise NotImplementedError()
        
       +    @runs_in_hwd_thread
            def request_root_fingerprint_from_device(self) -> str:
                # digitalbitbox (at least) does not reveal xpubs corresponding to unhardened paths
                # so ask for a direct child, and read out fingerprint from that:
       t@@ -249,6 +252,7 @@ class HardwareClientBase:
                root_fingerprint = BIP32Node.from_xkey(child_of_root_xpub).fingerprint.hex().lower()
                return root_fingerprint
        
       +    @runs_in_hwd_thread
            def get_password_for_storage_encryption(self) -> str:
                # note: using a different password based on hw device type is highly undesirable! see #5993
                derivation = get_derivation_used_for_hw_device_encryption()
   DIR diff --git a/electrum/plugins/keepkey/clientbase.py b/electrum/plugins/keepkey/clientbase.py
       t@@ -8,6 +8,7 @@ from electrum.util import UserCancelled
        from electrum.keystore import bip39_normalize_passphrase
        from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32
        from electrum.logging import Logger
       +from electrum.plugin import runs_in_hwd_thread
        from electrum.plugins.hw_wallet.plugin import HardwareClientBase, HardwareHandlerBase
        
        
       t@@ -129,6 +130,7 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger):
            def is_pairable(self):
                return not self.features.bootloader_mode
        
       +    @runs_in_hwd_thread
            def has_usable_connection_with_device(self):
                try:
                    res = self.ping("electrum pinging device")
       t@@ -143,6 +145,7 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger):
            def prevent_timeouts(self):
                self.last_operation = float('inf')
        
       +    @runs_in_hwd_thread
            def timeout(self, cutoff):
                '''Time out the client if the last operation was before cutoff.'''
                if self.last_operation < cutoff:
       t@@ -153,6 +156,7 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger):
            def expand_path(n):
                return convert_bip32_path_to_list_of_uint32(n)
        
       +    @runs_in_hwd_thread
            def cancel(self):
                '''Provided here as in keepkeylib but not trezorlib.'''
                self.transport.write(self.proto.Cancel())
       t@@ -160,6 +164,7 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger):
            def i4b(self, x):
                return pack('>I', x)
        
       +    @runs_in_hwd_thread
            def get_xpub(self, bip32_path, xtype):
                address_n = self.expand_path(bip32_path)
                creating = False
       t@@ -171,6 +176,7 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger):
                                 fingerprint=self.i4b(node.fingerprint),
                                 child_number=self.i4b(node.child_num)).to_xpub()
        
       +    @runs_in_hwd_thread
            def toggle_passphrase(self):
                if self.features.passphrase_protection:
                    self.msg = _("Confirm on your {} device to disable passphrases")
       t@@ -179,14 +185,17 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger):
                enabled = not self.features.passphrase_protection
                self.apply_settings(use_passphrase=enabled)
        
       +    @runs_in_hwd_thread
            def change_label(self, label):
                self.msg = _("Confirm the new label on your {} device")
                self.apply_settings(label=label)
        
       +    @runs_in_hwd_thread
            def change_homescreen(self, homescreen):
                self.msg = _("Confirm on your {} device to change your home screen")
                self.apply_settings(homescreen=homescreen)
        
       +    @runs_in_hwd_thread
            def set_pin(self, remove):
                if remove:
                    self.msg = _("Confirm on your {} device to disable PIN protection")
       t@@ -196,6 +205,7 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger):
                    self.msg = _("Confirm on your {} device to set a PIN")
                self.change_pin(remove)
        
       +    @runs_in_hwd_thread
            def clear_session(self):
                '''Clear the session to force pin (and passphrase if enabled)
                re-entry.  Does not leak exceptions.'''
       t@@ -207,10 +217,12 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger):
                    # If the device was removed it has the same effect...
                    self.logger.info(f"clear_session: ignoring error {e}")
        
       +    @runs_in_hwd_thread
            def get_public_node(self, address_n, creating):
                self.creating_wallet = creating
                return super(KeepKeyClientBase, self).get_public_node(address_n)
        
       +    @runs_in_hwd_thread
            def close(self):
                '''Called when Our wallet was closed or the device removed.'''
                self.logger.info("closing client")
   DIR diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py
       t@@ -9,7 +9,7 @@ from electrum import constants
        from electrum.i18n import _
        from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
        from electrum.keystore import Hardware_KeyStore
       -from electrum.plugin import Device
       +from electrum.plugin import Device, runs_in_hwd_thread
        from electrum.base_wizard import ScriptTypeNotSupported
        
        from ..hw_wallet import HW_PluginBase
       t@@ -37,6 +37,7 @@ class KeepKey_KeyStore(Hardware_KeyStore):
            def decrypt_message(self, sequence, message, password):
                raise UserFacingException(_('Encryption and decryption are not implemented by {}').format(self.device))
        
       +    @runs_in_hwd_thread
            def sign_message(self, sequence, message, password):
                client = self.get_client()
                address_path = self.get_derivation_prefix() + "/%d/%d"%sequence
       t@@ -44,6 +45,7 @@ class KeepKey_KeyStore(Hardware_KeyStore):
                msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message)
                return msg_sig.signature
        
       +    @runs_in_hwd_thread
            def sign_transaction(self, tx, password):
                if tx.is_complete():
                    return
       t@@ -95,6 +97,7 @@ class KeepKeyPlugin(HW_PluginBase):
                except ImportError:
                    self.libraries_available = False
        
       +    @runs_in_hwd_thread
            def enumerate(self):
                from keepkeylib.transport_webusb import WebUsbTransport
                results = []
       t@@ -112,16 +115,19 @@ class KeepKeyPlugin(HW_PluginBase):
            def _dev_to_str(dev: "usb1.USBDevice") -> str:
                return ":".join(str(x) for x in ["%03i" % (dev.getBusNumber(),)] + dev.getPortNumberList())
        
       +    @runs_in_hwd_thread
            def hid_transport(self, pair):
                from keepkeylib.transport_hid import HidTransport
                return HidTransport(pair)
        
       +    @runs_in_hwd_thread
            def webusb_transport(self, device):
                from keepkeylib.transport_webusb import WebUsbTransport
                for dev in WebUsbTransport.enumerate():
                    if device.path == self._dev_to_str(dev):
                        return WebUsbTransport(dev)
        
       +    @runs_in_hwd_thread
            def _try_hid(self, device):
                self.logger.info("Trying to connect over USB...")
                if device.interface_number == 1:
       t@@ -137,6 +143,7 @@ class KeepKeyPlugin(HW_PluginBase):
                    self.logger.info(f"cannot connect at {device.path} {e}")
                    return None
        
       +    @runs_in_hwd_thread
            def _try_webusb(self, device):
                self.logger.info("Trying to connect over WebUSB...")
                try:
       t@@ -145,6 +152,7 @@ class KeepKeyPlugin(HW_PluginBase):
                    self.logger.info(f"cannot connect at {device.path} {e}")
                    return None
        
       +    @runs_in_hwd_thread
            def create_client(self, device, handler):
                if device.product_key[1] == 2:
                    transport = self._try_webusb(device)
       t@@ -179,6 +187,7 @@ class KeepKeyPlugin(HW_PluginBase):
        
                return client
        
       +    @runs_in_hwd_thread
            def get_client(self, keystore, force_pair=True, *,
                           devices=None, allow_user_interaction=True) -> Optional['KeepKeyClient']:
                client = super().get_client(keystore, force_pair,
       t@@ -236,6 +245,7 @@ class KeepKeyPlugin(HW_PluginBase):
                finally:
                    wizard.loop.exit(exit_code)
        
       +    @runs_in_hwd_thread
            def _initialize_device(self, settings, method, device_id, wizard, handler):
                item, label, pin_protection, passphrase_protection = settings
        
       t@@ -315,6 +325,7 @@ class KeepKeyPlugin(HW_PluginBase):
                    return self.types.PAYTOMULTISIG
                raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))
        
       +    @runs_in_hwd_thread
            def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx):
                self.prev_tx = prev_tx
                client = self.get_client(keystore)
       t@@ -325,6 +336,7 @@ class KeepKeyPlugin(HW_PluginBase):
                signatures = [(bh2u(x) + '01') for x in signatures]
                tx.update_signatures(signatures)
        
       +    @runs_in_hwd_thread
            def show_address(self, wallet, address, keystore=None):
                if keystore is None:
                    keystore = wallet.get_keystore()
   DIR diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py
       t@@ -16,6 +16,7 @@ from electrum.wallet import Standard_Wallet
        from electrum.util import bfh, bh2u, versiontuple, UserFacingException
        from electrum.base_wizard import ScriptTypeNotSupported
        from electrum.logging import get_logger
       +from electrum.plugin import runs_in_hwd_thread
        
        from ..hw_wallet import HW_PluginBase, HardwareClientBase
        from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, validate_op_return_output, LibraryFoundButUnusable
       t@@ -74,16 +75,14 @@ class Ledger_Client(HardwareClientBase):
            def is_pairable(self):
                return True
        
       +    @runs_in_hwd_thread
            def close(self):
       -        with self.device_manager().hid_lock:
       -            self.dongleObject.dongle.close()
       -
       -    def timeout(self, cutoff):
       -        pass
       +        self.dongleObject.dongle.close()
        
            def is_initialized(self):
                return True
        
       +    @runs_in_hwd_thread
            def get_soft_device_id(self):
                if self._soft_device_id is None:
                    # modern ledger can provide xpub without user interaction
       t@@ -106,6 +105,7 @@ class Ledger_Client(HardwareClientBase):
                    return "Ledger Nano X"
                return None
        
       +    @runs_in_hwd_thread
            def has_usable_connection_with_device(self):
                try:
                    self.dongleObject.getFirmwareVersion()
       t@@ -113,6 +113,7 @@ class Ledger_Client(HardwareClientBase):
                    return False
                return True
        
       +    @runs_in_hwd_thread
            @test_pin_unlocked
            def get_xpub(self, bip32_path, xtype):
                self.checkDevice()
       t@@ -180,6 +181,7 @@ class Ledger_Client(HardwareClientBase):
            def supports_segwit_trustedInputs(self):
                return self.segwitTrustedInputs
        
       +    @runs_in_hwd_thread
            def perform_hw1_preflight(self):
                try:
                    firmwareInfo = self.dongleObject.getFirmwareVersion()
       t@@ -224,6 +226,7 @@ class Ledger_Client(HardwareClientBase):
                                                  "Please make sure that 'Browser support' is disabled on your device.")
                    raise e
        
       +    @runs_in_hwd_thread
            def checkDevice(self):
                if not self.preflightDone:
                    try:
       t@@ -290,6 +293,7 @@ class Ledger_KeyStore(Hardware_KeyStore):
            def decrypt_message(self, pubkey, message, password):
                raise UserFacingException(_('Encryption and decryption are currently not supported for {}').format(self.device))
        
       +    @runs_in_hwd_thread
            @test_pin_unlocked
            @set_and_unset_signing
            def sign_message(self, sequence, message, password):
       t@@ -336,6 +340,7 @@ class Ledger_KeyStore(Hardware_KeyStore):
                # And convert it
                return bytes([27 + 4 + (signature[0] & 0x01)]) + r + s
        
       +    @runs_in_hwd_thread
            @test_pin_unlocked
            @set_and_unset_signing
            def sign_transaction(self, tx, password):
       t@@ -536,6 +541,7 @@ class Ledger_KeyStore(Hardware_KeyStore):
                finally:
                    self.handler.finished()
        
       +    @runs_in_hwd_thread
            @test_pin_unlocked
            @set_and_unset_signing
            def show_address(self, sequence, txin_type):
       t@@ -607,6 +613,7 @@ class LedgerPlugin(HW_PluginBase):
                else:
                    raise LibraryFoundButUnusable(library_version=version)
        
       +    @runs_in_hwd_thread
            def get_btchip_device(self, device):
                ledger = False
                if device.product_key[0] == 0x2581 and device.product_key[1] == 0x3b7c:
       t@@ -618,12 +625,12 @@ class LedgerPlugin(HW_PluginBase):
                        ledger = True
                    else:
                        return None  # non-compatible interface of a Nano S or Blue
       -        with self.device_manager().hid_lock:
       -            dev = hid.device()
       -            dev.open_path(device.path)
       -            dev.set_nonblocking(True)
       +        dev = hid.device()
       +        dev.open_path(device.path)
       +        dev.set_nonblocking(True)
                return HIDDongleHIDAPI(dev, ledger, BTCHIP_DEBUG)
        
       +    @runs_in_hwd_thread
            def create_client(self, device, handler):
                if handler:
                    self.handler = handler
       t@@ -648,6 +655,7 @@ class LedgerPlugin(HW_PluginBase):
                xpub = client.get_xpub(derivation, xtype)
                return xpub
        
       +    @runs_in_hwd_thread
            def get_client(self, keystore, force_pair=True, *,
                           devices=None, allow_user_interaction=True):
                # All client interaction should not be in the main GUI thread
       t@@ -661,6 +669,7 @@ class LedgerPlugin(HW_PluginBase):
                    client.checkDevice()
                return client
        
       +    @runs_in_hwd_thread
            def show_address(self, wallet, address, keystore=None):
                if keystore is None:
                    keystore = wallet.get_keystore()
   DIR diff --git a/electrum/plugins/safe_t/clientbase.py b/electrum/plugins/safe_t/clientbase.py
       t@@ -8,6 +8,7 @@ from electrum.util import UserCancelled
        from electrum.keystore import bip39_normalize_passphrase
        from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32
        from electrum.logging import Logger
       +from electrum.plugin import runs_in_hwd_thread
        from electrum.plugins.hw_wallet.plugin import HardwareClientBase, HardwareHandlerBase
        
        
       t@@ -131,6 +132,7 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger):
            def is_pairable(self):
                return not self.features.bootloader_mode
        
       +    @runs_in_hwd_thread
            def has_usable_connection_with_device(self):
                try:
                    res = self.ping("electrum pinging device")
       t@@ -145,6 +147,7 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger):
            def prevent_timeouts(self):
                self.last_operation = float('inf')
        
       +    @runs_in_hwd_thread
            def timeout(self, cutoff):
                '''Time out the client if the last operation was before cutoff.'''
                if self.last_operation < cutoff:
       t@@ -155,6 +158,7 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger):
            def expand_path(n):
                return convert_bip32_path_to_list_of_uint32(n)
        
       +    @runs_in_hwd_thread
            def cancel(self):
                '''Provided here as in keepkeylib but not safetlib.'''
                self.transport.write(self.proto.Cancel())
       t@@ -162,6 +166,7 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger):
            def i4b(self, x):
                return pack('>I', x)
        
       +    @runs_in_hwd_thread
            def get_xpub(self, bip32_path, xtype):
                address_n = self.expand_path(bip32_path)
                creating = False
       t@@ -173,6 +178,7 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger):
                                 fingerprint=self.i4b(node.fingerprint),
                                 child_number=self.i4b(node.child_num)).to_xpub()
        
       +    @runs_in_hwd_thread
            def toggle_passphrase(self):
                if self.features.passphrase_protection:
                    self.msg = _("Confirm on your {} device to disable passphrases")
       t@@ -181,14 +187,17 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger):
                enabled = not self.features.passphrase_protection
                self.apply_settings(use_passphrase=enabled)
        
       +    @runs_in_hwd_thread
            def change_label(self, label):
                self.msg = _("Confirm the new label on your {} device")
                self.apply_settings(label=label)
        
       +    @runs_in_hwd_thread
            def change_homescreen(self, homescreen):
                self.msg = _("Confirm on your {} device to change your home screen")
                self.apply_settings(homescreen=homescreen)
        
       +    @runs_in_hwd_thread
            def set_pin(self, remove):
                if remove:
                    self.msg = _("Confirm on your {} device to disable PIN protection")
       t@@ -198,6 +207,7 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger):
                    self.msg = _("Confirm on your {} device to set a PIN")
                self.change_pin(remove)
        
       +    @runs_in_hwd_thread
            def clear_session(self):
                '''Clear the session to force pin (and passphrase if enabled)
                re-entry.  Does not leak exceptions.'''
       t@@ -209,10 +219,12 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger):
                    # If the device was removed it has the same effect...
                    self.logger.info(f"clear_session: ignoring error {e}")
        
       +    @runs_in_hwd_thread
            def get_public_node(self, address_n, creating):
                self.creating_wallet = creating
                return super(SafeTClientBase, self).get_public_node(address_n)
        
       +    @runs_in_hwd_thread
            def close(self):
                '''Called when Our wallet was closed or the device removed.'''
                self.logger.info("closing client")
   DIR diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py
       t@@ -7,7 +7,7 @@ from electrum.util import bfh, bh2u, versiontuple, UserCancelled, UserFacingExce
        from electrum.bip32 import BIP32Node
        from electrum import constants
        from electrum.i18n import _
       -from electrum.plugin import Device
       +from electrum.plugin import Device, runs_in_hwd_thread
        from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
        from electrum.keystore import Hardware_KeyStore
        from electrum.base_wizard import ScriptTypeNotSupported
       t@@ -35,6 +35,7 @@ class SafeTKeyStore(Hardware_KeyStore):
            def decrypt_message(self, sequence, message, password):
                raise UserFacingException(_('Encryption and decryption are not implemented by {}').format(self.device))
        
       +    @runs_in_hwd_thread
            def sign_message(self, sequence, message, password):
                client = self.get_client()
                address_path = self.get_derivation_prefix() + "/%d/%d"%sequence
       t@@ -42,6 +43,7 @@ class SafeTKeyStore(Hardware_KeyStore):
                msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message)
                return msg_sig.signature
        
       +    @runs_in_hwd_thread
            def sign_transaction(self, tx, password):
                if tx.is_complete():
                    return
       t@@ -96,6 +98,7 @@ class SafeTPlugin(HW_PluginBase):
                except AttributeError:
                    return 'unknown'
        
       +    @runs_in_hwd_thread
            def enumerate(self):
                devices = self.transport_handler.enumerate_devices()
                return [Device(path=d.get_path(),
       t@@ -106,6 +109,7 @@ class SafeTPlugin(HW_PluginBase):
                               transport_ui_string=d.get_path())
                        for d in devices]
        
       +    @runs_in_hwd_thread
            def create_client(self, device, handler):
                try:
                    self.logger.info(f"connecting to device at {device.path}")
       t@@ -141,6 +145,7 @@ class SafeTPlugin(HW_PluginBase):
        
                return client
        
       +    @runs_in_hwd_thread
            def get_client(self, keystore, force_pair=True, *,
                           devices=None, allow_user_interaction=True) -> Optional['SafeTClient']:
                client = super().get_client(keystore, force_pair,
       t@@ -198,6 +203,7 @@ class SafeTPlugin(HW_PluginBase):
                finally:
                    wizard.loop.exit(exit_code)
        
       +    @runs_in_hwd_thread
            def _initialize_device(self, settings, method, device_id, wizard, handler):
                item, label, pin_protection, passphrase_protection = settings
        
       t@@ -289,6 +295,7 @@ class SafeTPlugin(HW_PluginBase):
                    return self.types.OutputScriptType.PAYTOMULTISIG
                raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))
        
       +    @runs_in_hwd_thread
            def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx):
                self.prev_tx = prev_tx
                client = self.get_client(keystore)
       t@@ -299,6 +306,7 @@ class SafeTPlugin(HW_PluginBase):
                signatures = [(bh2u(x) + '01') for x in signatures]
                tx.update_signatures(signatures)
        
       +    @runs_in_hwd_thread
            def show_address(self, wallet, address, keystore=None):
                if keystore is None:
                    keystore = wallet.get_keystore()
   DIR diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py
       t@@ -7,6 +7,7 @@ from electrum.util import UserCancelled, UserFacingException
        from electrum.keystore import bip39_normalize_passphrase
        from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 as parse_path
        from electrum.logging import Logger
       +from electrum.plugin import runs_in_hwd_thread
        from electrum.plugins.hw_wallet.plugin import OutdatedHwFirmwareException, HardwareClientBase
        
        from trezorlib.client import TrezorClient, PASSPHRASE_ON_DEVICE
       t@@ -107,6 +108,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
            def is_pairable(self):
                return not self.features.bootloader_mode
        
       +    @runs_in_hwd_thread
            def has_usable_connection_with_device(self):
                if self.in_flow:
                    return True
       t@@ -123,6 +125,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
            def prevent_timeouts(self):
                self.last_operation = float('inf')
        
       +    @runs_in_hwd_thread
            def timeout(self, cutoff):
                '''Time out the client if the last operation was before cutoff.'''
                if self.last_operation < cutoff:
       t@@ -132,6 +135,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
            def i4b(self, x):
                return pack('>I', x)
        
       +    @runs_in_hwd_thread
            def get_xpub(self, bip32_path, xtype, creating=False):
                address_n = parse_path(bip32_path)
                with self.run_flow(creating_wallet=creating):
       t@@ -143,6 +147,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
                                 fingerprint=self.i4b(node.fingerprint),
                                 child_number=self.i4b(node.child_num)).to_xpub()
        
       +    @runs_in_hwd_thread
            def toggle_passphrase(self):
                if self.features.passphrase_protection:
                    msg = _("Confirm on your {} device to disable passphrases")
       t@@ -152,14 +157,17 @@ class TrezorClientBase(HardwareClientBase, Logger):
                with self.run_flow(msg):
                    trezorlib.device.apply_settings(self.client, use_passphrase=enabled)
        
       +    @runs_in_hwd_thread
            def change_label(self, label):
                with self.run_flow(_("Confirm the new label on your {} device")):
                    trezorlib.device.apply_settings(self.client, label=label)
        
       +    @runs_in_hwd_thread
            def change_homescreen(self, homescreen):
                with self.run_flow(_("Confirm on your {} device to change your home screen")):
                    trezorlib.device.apply_settings(self.client, homescreen=homescreen)
        
       +    @runs_in_hwd_thread
            def set_pin(self, remove):
                if remove:
                    msg = _("Confirm on your {} device to disable PIN protection")
       t@@ -170,6 +178,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
                with self.run_flow(msg):
                    trezorlib.device.change_pin(self.client, remove)
        
       +    @runs_in_hwd_thread
            def clear_session(self):
                '''Clear the session to force pin (and passphrase if enabled)
                re-entry.  Does not leak exceptions.'''
       t@@ -181,11 +190,13 @@ class TrezorClientBase(HardwareClientBase, Logger):
                    # If the device was removed it has the same effect...
                    self.logger.info(f"clear_session: ignoring error {e}")
        
       +    @runs_in_hwd_thread
            def close(self):
                '''Called when Our wallet was closed or the device removed.'''
                self.logger.info("closing client")
                self.clear_session()
        
       +    @runs_in_hwd_thread
            def is_uptodate(self):
                if self.client.is_outdated():
                    return False
       t@@ -203,6 +214,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
                    return "Trezor T"
                return None
        
       +    @runs_in_hwd_thread
            def show_address(self, address_str, script_type, multisig=None):
                coin_name = self.plugin.get_coin_name()
                address_n = parse_path(address_str)
       t@@ -215,6 +227,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
                        script_type=script_type,
                        multisig=multisig)
        
       +    @runs_in_hwd_thread
            def sign_message(self, address_str, message):
                coin_name = self.plugin.get_coin_name()
                address_n = parse_path(address_str)
       t@@ -225,6 +238,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
                        address_n,
                        message)
        
       +    @runs_in_hwd_thread
            def recover_device(self, recovery_type, *args, **kwargs):
                input_callback = self.mnemonic_callback(recovery_type)
                with self.run_flow():
       t@@ -237,14 +251,17 @@ class TrezorClientBase(HardwareClientBase, Logger):
        
            # ========= Unmodified trezorlib methods =========
        
       +    @runs_in_hwd_thread
            def sign_tx(self, *args, **kwargs):
                with self.run_flow():
                    return trezorlib.btc.sign_tx(self.client, *args, **kwargs)
        
       +    @runs_in_hwd_thread
            def reset_device(self, *args, **kwargs):
                with self.run_flow():
                    return trezorlib.device.reset(self.client, *args, **kwargs)
        
       +    @runs_in_hwd_thread
            def wipe_device(self, *args, **kwargs):
                with self.run_flow():
                    return trezorlib.device.wipe(self.client, *args, **kwargs)
   DIR diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py
       t@@ -6,7 +6,7 @@ from electrum.util import bfh, bh2u, versiontuple, UserCancelled, UserFacingExce
        from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 as parse_path
        from electrum import constants
        from electrum.i18n import _
       -from electrum.plugin import Device
       +from electrum.plugin import Device, runs_in_hwd_thread
        from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
        from electrum.keystore import Hardware_KeyStore
        from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET
       t@@ -143,6 +143,7 @@ class TrezorPlugin(HW_PluginBase):
                else:
                    raise LibraryFoundButUnusable(library_version=version)
        
       +    @runs_in_hwd_thread
            def is_bridge_available(self) -> bool:
                # Testing whether the Bridge is available can take several seconds
                # (when it is not), as it is slow to timeout, hence we cache it.
       t@@ -157,6 +158,7 @@ class TrezorPlugin(HW_PluginBase):
                        self._is_bridge_available = True
                return self._is_bridge_available
        
       +    @runs_in_hwd_thread
            def enumerate(self):
                # If there is a bridge, prefer that.
                # On Windows, the bridge runs as Admin (and Electrum usually does not),
       t@@ -174,6 +176,7 @@ class TrezorPlugin(HW_PluginBase):
                               transport_ui_string=d.get_path())
                        for d in devices]
        
       +    @runs_in_hwd_thread
            def create_client(self, device, handler):
                try:
                    self.logger.info(f"connecting to device at {device.path}")
       t@@ -190,6 +193,7 @@ class TrezorPlugin(HW_PluginBase):
                # note that this call can still raise!
                return TrezorClientBase(transport, handler, self)
        
       +    @runs_in_hwd_thread
            def get_client(self, keystore, force_pair=True, *,
                           devices=None, allow_user_interaction=True) -> Optional['TrezorClientBase']:
                client = super().get_client(keystore, force_pair,
       t@@ -238,6 +242,7 @@ class TrezorPlugin(HW_PluginBase):
                finally:
                    wizard.loop.exit(exit_code)
        
       +    @runs_in_hwd_thread
            def _initialize_device(self, settings: TrezorInitSettings, method, device_id, wizard, handler):
                if method == TIM_RECOVER and settings.recovery_type == RecoveryDeviceType.ScrambledWords:
                    handler.show_error(_(
       t@@ -333,6 +338,7 @@ class TrezorPlugin(HW_PluginBase):
                    return OutputScriptType.PAYTOMULTISIG
                raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))
        
       +    @runs_in_hwd_thread
            def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx):
                prev_tx = { bfh(txhash): self.electrum_tx_to_txtype(tx) for txhash, tx in prev_tx.items() }
                client = self.get_client(keystore)
       t@@ -343,6 +349,7 @@ class TrezorPlugin(HW_PluginBase):
                signatures = [(bh2u(x) + '01') for x in signatures]
                tx.update_signatures(signatures)
        
       +    @runs_in_hwd_thread
            def show_address(self, wallet, address, keystore=None):
                if keystore is None:
                    keystore = wallet.get_keystore()