URI: 
       tplugin.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       tplugin.py (29046B)
       ---
            1 #!/usr/bin/env python
            2 #
            3 # Electrum - lightweight Bitcoin client
            4 # Copyright (C) 2015 Thomas Voegtlin
            5 #
            6 # Permission is hereby granted, free of charge, to any person
            7 # obtaining a copy of this software and associated documentation files
            8 # (the "Software"), to deal in the Software without restriction,
            9 # including without limitation the rights to use, copy, modify, merge,
           10 # publish, distribute, sublicense, and/or sell copies of the Software,
           11 # and to permit persons to whom the Software is furnished to do so,
           12 # subject to the following conditions:
           13 #
           14 # The above copyright notice and this permission notice shall be
           15 # included in all copies or substantial portions of the Software.
           16 #
           17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
           18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
           19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
           20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
           21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
           22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
           23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
           24 # SOFTWARE.
           25 import os
           26 import pkgutil
           27 import importlib.util
           28 import time
           29 import threading
           30 import sys
           31 from typing import (NamedTuple, Any, Union, TYPE_CHECKING, Optional, Tuple,
           32                     Dict, Iterable, List, Sequence, Callable, TypeVar)
           33 import concurrent
           34 from concurrent import futures
           35 from functools import wraps, partial
           36 
           37 from .i18n import _
           38 from .util import (profiler, DaemonThread, UserCancelled, ThreadJob, UserFacingException)
           39 from . import bip32
           40 from . import plugins
           41 from .simple_config import SimpleConfig
           42 from .logging import get_logger, Logger
           43 
           44 if TYPE_CHECKING:
           45     from .plugins.hw_wallet import HW_PluginBase, HardwareClientBase, HardwareHandlerBase
           46     from .keystore import Hardware_KeyStore
           47     from .wallet import Abstract_Wallet
           48 
           49 
           50 _logger = get_logger(__name__)
           51 plugin_loaders = {}
           52 hook_names = set()
           53 hooks = {}
           54 
           55 
           56 class Plugins(DaemonThread):
           57 
           58     LOGGING_SHORTCUT = 'p'
           59 
           60     @profiler
           61     def __init__(self, config: SimpleConfig, gui_name):
           62         DaemonThread.__init__(self)
           63         self.setName('Plugins')
           64         self.pkgpath = os.path.dirname(plugins.__file__)
           65         self.config = config
           66         self.hw_wallets = {}
           67         self.plugins = {}  # type: Dict[str, BasePlugin]
           68         self.gui_name = gui_name
           69         self.descriptions = {}
           70         self.device_manager = DeviceMgr(config)
           71         self.load_plugins()
           72         self.add_jobs(self.device_manager.thread_jobs())
           73         self.start()
           74 
           75     def load_plugins(self):
           76         for loader, name, ispkg in pkgutil.iter_modules([self.pkgpath]):
           77             full_name = f'electrum.plugins.{name}'
           78             spec = importlib.util.find_spec(full_name)
           79             if spec is None:  # pkgutil found it but importlib can't ?!
           80                 raise Exception(f"Error pre-loading {full_name}: no spec")
           81             try:
           82                 module = importlib.util.module_from_spec(spec)
           83                 # sys.modules needs to be modified for relative imports to work
           84                 # see https://stackoverflow.com/a/50395128
           85                 sys.modules[spec.name] = module
           86                 spec.loader.exec_module(module)
           87             except Exception as e:
           88                 raise Exception(f"Error pre-loading {full_name}: {repr(e)}") from e
           89             d = module.__dict__
           90             gui_good = self.gui_name in d.get('available_for', [])
           91             if not gui_good:
           92                 continue
           93             details = d.get('registers_wallet_type')
           94             if details:
           95                 self.register_wallet_type(name, gui_good, details)
           96             details = d.get('registers_keystore')
           97             if details:
           98                 self.register_keystore(name, gui_good, details)
           99             self.descriptions[name] = d
          100             if not d.get('requires_wallet_type') and self.config.get('use_' + name):
          101                 try:
          102                     self.load_plugin(name)
          103                 except BaseException as e:
          104                     self.logger.exception(f"cannot initialize plugin {name}: {e}")
          105 
          106     def get(self, name):
          107         return self.plugins.get(name)
          108 
          109     def count(self):
          110         return len(self.plugins)
          111 
          112     def load_plugin(self, name) -> 'BasePlugin':
          113         if name in self.plugins:
          114             return self.plugins[name]
          115         full_name = f'electrum.plugins.{name}.{self.gui_name}'
          116         spec = importlib.util.find_spec(full_name)
          117         if spec is None:
          118             raise RuntimeError("%s implementation for %s plugin not found"
          119                                % (self.gui_name, name))
          120         try:
          121             module = importlib.util.module_from_spec(spec)
          122             spec.loader.exec_module(module)
          123             plugin = module.Plugin(self, self.config, name)
          124         except Exception as e:
          125             raise Exception(f"Error loading {name} plugin: {repr(e)}") from e
          126         self.add_jobs(plugin.thread_jobs())
          127         self.plugins[name] = plugin
          128         self.logger.info(f"loaded {name}")
          129         return plugin
          130 
          131     def close_plugin(self, plugin):
          132         self.remove_jobs(plugin.thread_jobs())
          133 
          134     def enable(self, name: str) -> 'BasePlugin':
          135         self.config.set_key('use_' + name, True, True)
          136         p = self.get(name)
          137         if p:
          138             return p
          139         return self.load_plugin(name)
          140 
          141     def disable(self, name: str) -> None:
          142         self.config.set_key('use_' + name, False, True)
          143         p = self.get(name)
          144         if not p:
          145             return
          146         self.plugins.pop(name)
          147         p.close()
          148         self.logger.info(f"closed {name}")
          149 
          150     def toggle(self, name: str) -> Optional['BasePlugin']:
          151         p = self.get(name)
          152         return self.disable(name) if p else self.enable(name)
          153 
          154     def is_available(self, name: str, wallet: 'Abstract_Wallet') -> bool:
          155         d = self.descriptions.get(name)
          156         if not d:
          157             return False
          158         deps = d.get('requires', [])
          159         for dep, s in deps:
          160             try:
          161                 __import__(dep)
          162             except ImportError as e:
          163                 self.logger.warning(f'Plugin {name} unavailable: {repr(e)}')
          164                 return False
          165         requires = d.get('requires_wallet_type', [])
          166         return not requires or wallet.wallet_type in requires
          167 
          168     def get_hardware_support(self):
          169         out = []
          170         for name, (gui_good, details) in self.hw_wallets.items():
          171             if gui_good:
          172                 try:
          173                     p = self.get_plugin(name)
          174                     if p.is_enabled():
          175                         out.append(HardwarePluginToScan(name=name,
          176                                                         description=details[2],
          177                                                         plugin=p,
          178                                                         exception=None))
          179                 except Exception as e:
          180                     self.logger.exception(f"cannot load plugin for: {name}")
          181                     out.append(HardwarePluginToScan(name=name,
          182                                                     description=details[2],
          183                                                     plugin=None,
          184                                                     exception=e))
          185         return out
          186 
          187     def register_wallet_type(self, name, gui_good, wallet_type):
          188         from .wallet import register_wallet_type, register_constructor
          189         self.logger.info(f"registering wallet type {(wallet_type, name)}")
          190         def loader():
          191             plugin = self.get_plugin(name)
          192             register_constructor(wallet_type, plugin.wallet_class)
          193         register_wallet_type(wallet_type)
          194         plugin_loaders[wallet_type] = loader
          195 
          196     def register_keystore(self, name, gui_good, details):
          197         from .keystore import register_keystore
          198         def dynamic_constructor(d):
          199             return self.get_plugin(name).keystore_class(d)
          200         if details[0] == 'hardware':
          201             self.hw_wallets[name] = (gui_good, details)
          202             self.logger.info(f"registering hardware {name}: {details}")
          203             register_keystore(details[1], dynamic_constructor)
          204 
          205     def get_plugin(self, name: str) -> 'BasePlugin':
          206         if name not in self.plugins:
          207             self.load_plugin(name)
          208         return self.plugins[name]
          209 
          210     def run(self):
          211         while self.is_running():
          212             time.sleep(0.1)
          213             self.run_jobs()
          214         self.on_stop()
          215 
          216 
          217 def hook(func):
          218     hook_names.add(func.__name__)
          219     return func
          220 
          221 def run_hook(name, *args):
          222     results = []
          223     f_list = hooks.get(name, [])
          224     for p, f in f_list:
          225         if p.is_enabled():
          226             try:
          227                 r = f(*args)
          228             except Exception:
          229                 _logger.exception(f"Plugin error. plugin: {p}, hook: {name}")
          230                 r = False
          231             if r:
          232                 results.append(r)
          233 
          234     if results:
          235         assert len(results) == 1, results
          236         return results[0]
          237 
          238 
          239 class BasePlugin(Logger):
          240 
          241     def __init__(self, parent, config: 'SimpleConfig', name):
          242         self.parent = parent  # type: Plugins  # The plugins object
          243         self.name = name
          244         self.config = config
          245         self.wallet = None
          246         Logger.__init__(self)
          247         # add self to hooks
          248         for k in dir(self):
          249             if k in hook_names:
          250                 l = hooks.get(k, [])
          251                 l.append((self, getattr(self, k)))
          252                 hooks[k] = l
          253 
          254     def __str__(self):
          255         return self.name
          256 
          257     def close(self):
          258         # remove self from hooks
          259         for attr_name in dir(self):
          260             if attr_name in hook_names:
          261                 # found attribute in self that is also the name of a hook
          262                 l = hooks.get(attr_name, [])
          263                 try:
          264                     l.remove((self, getattr(self, attr_name)))
          265                 except ValueError:
          266                     # maybe attr name just collided with hook name and was not hook
          267                     continue
          268                 hooks[attr_name] = l
          269         self.parent.close_plugin(self)
          270         self.on_close()
          271 
          272     def on_close(self):
          273         pass
          274 
          275     def requires_settings(self) -> bool:
          276         return False
          277 
          278     def thread_jobs(self):
          279         return []
          280 
          281     def is_enabled(self):
          282         return self.is_available() and self.config.get('use_'+self.name) is True
          283 
          284     def is_available(self):
          285         return True
          286 
          287     def can_user_disable(self):
          288         return True
          289 
          290     def settings_widget(self, window):
          291         raise NotImplementedError()
          292 
          293     def settings_dialog(self, window):
          294         raise NotImplementedError()
          295 
          296 
          297 class DeviceUnpairableError(UserFacingException): pass
          298 class HardwarePluginLibraryUnavailable(Exception): pass
          299 class CannotAutoSelectDevice(Exception): pass
          300 
          301 
          302 class Device(NamedTuple):
          303     path: Union[str, bytes]
          304     interface_number: int
          305     id_: str
          306     product_key: Any   # when using hid, often Tuple[int, int]
          307     usage_page: int
          308     transport_ui_string: str
          309 
          310 
          311 class DeviceInfo(NamedTuple):
          312     device: Device
          313     label: Optional[str] = None
          314     initialized: Optional[bool] = None
          315     exception: Optional[Exception] = None
          316     plugin_name: Optional[str] = None  # manufacturer, e.g. "trezor"
          317     soft_device_id: Optional[str] = None  # if available, used to distinguish same-type hw devices
          318     model_name: Optional[str] = None  # e.g. "Ledger Nano S"
          319 
          320 
          321 class HardwarePluginToScan(NamedTuple):
          322     name: str
          323     description: str
          324     plugin: Optional['HW_PluginBase']
          325     exception: Optional[Exception]
          326 
          327 
          328 PLACEHOLDER_HW_CLIENT_LABELS = {None, "", " "}
          329 
          330 
          331 # hidapi is not thread-safe
          332 # see https://github.com/signal11/hidapi/issues/205#issuecomment-527654560
          333 #     https://github.com/libusb/hidapi/issues/45
          334 #     https://github.com/signal11/hidapi/issues/45#issuecomment-4434598
          335 #     https://github.com/signal11/hidapi/pull/414#issuecomment-445164238
          336 # It is not entirely clear to me, exactly what is safe and what isn't, when
          337 # using multiple threads...
          338 # Hence, we use a single thread for all device communications, including
          339 # enumeration. Everything that uses hidapi, libusb, etc, MUST run on
          340 # the following thread:
          341 _hwd_comms_executor = concurrent.futures.ThreadPoolExecutor(
          342     max_workers=1,
          343     thread_name_prefix='hwd_comms_thread'
          344 )
          345 
          346 
          347 T = TypeVar('T')
          348 
          349 
          350 def run_in_hwd_thread(func: Callable[[], T]) -> T:
          351     if threading.current_thread().name.startswith("hwd_comms_thread"):
          352         return func()
          353     else:
          354         fut = _hwd_comms_executor.submit(func)
          355         return fut.result()
          356         #except (concurrent.futures.CancelledError, concurrent.futures.TimeoutError) as e:
          357 
          358 
          359 def runs_in_hwd_thread(func):
          360     @wraps(func)
          361     def wrapper(*args, **kwargs):
          362         return run_in_hwd_thread(partial(func, *args, **kwargs))
          363     return wrapper
          364 
          365 
          366 def assert_runs_in_hwd_thread():
          367     if not threading.current_thread().name.startswith("hwd_comms_thread"):
          368         raise Exception("must only be called from HWD communication thread")
          369 
          370 
          371 class DeviceMgr(ThreadJob):
          372     '''Manages hardware clients.  A client communicates over a hardware
          373     channel with the device.
          374 
          375     In addition to tracking device HID IDs, the device manager tracks
          376     hardware wallets and manages wallet pairing.  A HID ID may be
          377     paired with a wallet when it is confirmed that the hardware device
          378     matches the wallet, i.e. they have the same master public key.  A
          379     HID ID can be unpaired if e.g. it is wiped.
          380 
          381     Because of hotplugging, a wallet must request its client
          382     dynamically each time it is required, rather than caching it
          383     itself.
          384 
          385     The device manager is shared across plugins, so just one place
          386     does hardware scans when needed.  By tracking HID IDs, if a device
          387     is plugged into a different port the wallet is automatically
          388     re-paired.
          389 
          390     Wallets are informed on connect / disconnect events.  It must
          391     implement connected(), disconnected() callbacks.  Being connected
          392     implies a pairing.  Callbacks can happen in any thread context,
          393     and we do them without holding the lock.
          394 
          395     Confusingly, the HID ID (serial number) reported by the HID system
          396     doesn't match the device ID reported by the device itself.  We use
          397     the HID IDs.
          398 
          399     This plugin is thread-safe.  Currently only devices supported by
          400     hidapi are implemented.'''
          401 
          402     def __init__(self, config: SimpleConfig):
          403         ThreadJob.__init__(self)
          404         # Keyed by xpub.  The value is the device id
          405         # has been paired, and None otherwise. Needs self.lock.
          406         self.xpub_ids = {}  # type: Dict[str, str]
          407         # A list of clients.  The key is the client, the value is
          408         # a (path, id_) pair. Needs self.lock.
          409         self.clients = {}  # type: Dict[HardwareClientBase, Tuple[Union[str, bytes], str]]
          410         # What we recognise.  (vendor_id, product_id) -> Plugin
          411         self._recognised_hardware = {}  # type: Dict[Tuple[int, int], HW_PluginBase]
          412         self._recognised_vendor = {}  # type: Dict[int, HW_PluginBase]  # vendor_id -> Plugin
          413         # Custom enumerate functions for devices we don't know about.
          414         self._enumerate_func = set()  # Needs self.lock.
          415 
          416         self.lock = threading.RLock()
          417 
          418         self.config = config
          419 
          420     def thread_jobs(self):
          421         # Thread job to handle device timeouts
          422         return [self]
          423 
          424     def run(self):
          425         '''Handle device timeouts.  Runs in the context of the Plugins
          426         thread.'''
          427         with self.lock:
          428             clients = list(self.clients.keys())
          429         cutoff = time.time() - self.config.get_session_timeout()
          430         for client in clients:
          431             client.timeout(cutoff)
          432 
          433     def register_devices(self, device_pairs, *, plugin: 'HW_PluginBase'):
          434         for pair in device_pairs:
          435             self._recognised_hardware[pair] = plugin
          436 
          437     def register_vendor_ids(self, vendor_ids: Iterable[int], *, plugin: 'HW_PluginBase'):
          438         for vendor_id in vendor_ids:
          439             self._recognised_vendor[vendor_id] = plugin
          440 
          441     def register_enumerate_func(self, func):
          442         with self.lock:
          443             self._enumerate_func.add(func)
          444 
          445     @runs_in_hwd_thread
          446     def create_client(self, device: 'Device', handler: Optional['HardwareHandlerBase'],
          447                       plugin: 'HW_PluginBase') -> Optional['HardwareClientBase']:
          448         # Get from cache first
          449         client = self._client_by_id(device.id_)
          450         if client:
          451             return client
          452         client = plugin.create_client(device, handler)
          453         if client:
          454             self.logger.info(f"Registering {client}")
          455             with self.lock:
          456                 self.clients[client] = (device.path, device.id_)
          457         return client
          458 
          459     def xpub_id(self, xpub):
          460         with self.lock:
          461             return self.xpub_ids.get(xpub)
          462 
          463     def xpub_by_id(self, id_):
          464         with self.lock:
          465             for xpub, xpub_id in self.xpub_ids.items():
          466                 if xpub_id == id_:
          467                     return xpub
          468             return None
          469 
          470     def unpair_xpub(self, xpub):
          471         with self.lock:
          472             if xpub not in self.xpub_ids:
          473                 return
          474             _id = self.xpub_ids.pop(xpub)
          475         self._close_client(_id)
          476 
          477     def unpair_id(self, id_):
          478         xpub = self.xpub_by_id(id_)
          479         if xpub:
          480             self.unpair_xpub(xpub)
          481         else:
          482             self._close_client(id_)
          483 
          484     def _close_client(self, id_):
          485         with self.lock:
          486             client = self._client_by_id(id_)
          487             self.clients.pop(client, None)
          488         if client:
          489             client.close()
          490 
          491     def pair_xpub(self, xpub, id_):
          492         with self.lock:
          493             self.xpub_ids[xpub] = id_
          494 
          495     def _client_by_id(self, id_) -> Optional['HardwareClientBase']:
          496         with self.lock:
          497             for client, (path, client_id) in self.clients.items():
          498                 if client_id == id_:
          499                     return client
          500         return None
          501 
          502     def client_by_id(self, id_, *, scan_now: bool = True) -> Optional['HardwareClientBase']:
          503         '''Returns a client for the device ID if one is registered.  If
          504         a device is wiped or in bootloader mode pairing is impossible;
          505         in such cases we communicate by device ID and not wallet.'''
          506         if scan_now:
          507             self.scan_devices()
          508         return self._client_by_id(id_)
          509 
          510     @runs_in_hwd_thread
          511     def client_for_keystore(self, plugin: 'HW_PluginBase', handler: Optional['HardwareHandlerBase'],
          512                             keystore: 'Hardware_KeyStore',
          513                             force_pair: bool, *,
          514                             devices: Sequence['Device'] = None,
          515                             allow_user_interaction: bool = True) -> Optional['HardwareClientBase']:
          516         self.logger.info("getting client for keystore")
          517         if handler is None:
          518             raise Exception(_("Handler not found for") + ' ' + plugin.name + '\n' + _("A library is probably missing."))
          519         handler.update_status(False)
          520         if devices is None:
          521             devices = self.scan_devices()
          522         xpub = keystore.xpub
          523         derivation = keystore.get_derivation_prefix()
          524         assert derivation is not None
          525         client = self.client_by_xpub(plugin, xpub, handler, devices)
          526         if client is None and force_pair:
          527             try:
          528                 info = self.select_device(plugin, handler, keystore, devices,
          529                                           allow_user_interaction=allow_user_interaction)
          530             except CannotAutoSelectDevice:
          531                 pass
          532             else:
          533                 client = self.force_pair_xpub(plugin, handler, info, xpub, derivation)
          534         if client:
          535             handler.update_status(True)
          536         if client:
          537             # note: if select_device was called, we might also update label etc here:
          538             keystore.opportunistically_fill_in_missing_info_from_device(client)
          539         self.logger.info("end client for keystore")
          540         return client
          541 
          542     def client_by_xpub(self, plugin: 'HW_PluginBase', xpub, handler: 'HardwareHandlerBase',
          543                        devices: Sequence['Device']) -> Optional['HardwareClientBase']:
          544         _id = self.xpub_id(xpub)
          545         client = self._client_by_id(_id)
          546         if client:
          547             # An unpaired client might have another wallet's handler
          548             # from a prior scan.  Replace to fix dialog parenting.
          549             client.handler = handler
          550             return client
          551 
          552         for device in devices:
          553             if device.id_ == _id:
          554                 return self.create_client(device, handler, plugin)
          555 
          556     def force_pair_xpub(self, plugin: 'HW_PluginBase', handler: 'HardwareHandlerBase',
          557                         info: 'DeviceInfo', xpub, derivation) -> Optional['HardwareClientBase']:
          558         # The wallet has not been previously paired, so let the user
          559         # choose an unpaired device and compare its first address.
          560         xtype = bip32.xpub_type(xpub)
          561         client = self._client_by_id(info.device.id_)
          562         if client and client.is_pairable():
          563             # See comment above for same code
          564             client.handler = handler
          565             # This will trigger a PIN/passphrase entry request
          566             try:
          567                 client_xpub = client.get_xpub(derivation, xtype)
          568             except (UserCancelled, RuntimeError):
          569                  # Bad / cancelled PIN / passphrase
          570                 client_xpub = None
          571             if client_xpub == xpub:
          572                 self.pair_xpub(xpub, info.device.id_)
          573                 return client
          574 
          575         # The user input has wrong PIN or passphrase, or cancelled input,
          576         # or it is not pairable
          577         raise DeviceUnpairableError(
          578             _('Electrum cannot pair with your {}.\n\n'
          579               'Before you request bitcoins to be sent to addresses in this '
          580               'wallet, ensure you can pair with your device, or that you have '
          581               'its seed (and passphrase, if any).  Otherwise all bitcoins you '
          582               'receive will be unspendable.').format(plugin.device))
          583 
          584     def unpaired_device_infos(self, handler: Optional['HardwareHandlerBase'], plugin: 'HW_PluginBase',
          585                               devices: Sequence['Device'] = None,
          586                               include_failing_clients=False) -> List['DeviceInfo']:
          587         '''Returns a list of DeviceInfo objects: one for each connected,
          588         unpaired device accepted by the plugin.'''
          589         if not plugin.libraries_available:
          590             message = plugin.get_library_not_available_message()
          591             raise HardwarePluginLibraryUnavailable(message)
          592         if devices is None:
          593             devices = self.scan_devices()
          594         devices = [dev for dev in devices if not self.xpub_by_id(dev.id_)]
          595         infos = []
          596         for device in devices:
          597             if not plugin.can_recognize_device(device):
          598                 continue
          599             try:
          600                 client = self.create_client(device, handler, plugin)
          601                 if not client:
          602                     continue
          603                 label = client.label()
          604                 is_initialized = client.is_initialized()
          605                 soft_device_id = client.get_soft_device_id()
          606                 model_name = client.device_model_name()
          607             except Exception as e:
          608                 self.logger.error(f'failed to create client for {plugin.name} at {device.path}: {repr(e)}')
          609                 if include_failing_clients:
          610                     infos.append(DeviceInfo(device=device, exception=e, plugin_name=plugin.name))
          611                 continue
          612             infos.append(DeviceInfo(device=device,
          613                                     label=label,
          614                                     initialized=is_initialized,
          615                                     plugin_name=plugin.name,
          616                                     soft_device_id=soft_device_id,
          617                                     model_name=model_name))
          618 
          619         return infos
          620 
          621     def select_device(self, plugin: 'HW_PluginBase', handler: 'HardwareHandlerBase',
          622                       keystore: 'Hardware_KeyStore', devices: Sequence['Device'] = None,
          623                       *, allow_user_interaction: bool = True) -> 'DeviceInfo':
          624         """Select the device to use for keystore."""
          625         # ideally this should not be called from the GUI thread...
          626         # assert handler.get_gui_thread() != threading.current_thread(), 'must not be called from GUI thread'
          627         while True:
          628             infos = self.unpaired_device_infos(handler, plugin, devices)
          629             if infos:
          630                 break
          631             if not allow_user_interaction:
          632                 raise CannotAutoSelectDevice()
          633             msg = _('Please insert your {}').format(plugin.device)
          634             if keystore.label:
          635                 msg += ' ({})'.format(keystore.label)
          636             msg += '. {}\n\n{}'.format(
          637                 _('Verify the cable is connected and that '
          638                   'no other application is using it.'),
          639                 _('Try to connect again?')
          640             )
          641             if not handler.yes_no_question(msg):
          642                 raise UserCancelled()
          643             devices = None
          644 
          645         # select device automatically. (but only if we have reasonable expectation it is the correct one)
          646         # method 1: select device by id
          647         if keystore.soft_device_id:
          648             for info in infos:
          649                 if info.soft_device_id == keystore.soft_device_id:
          650                     return info
          651         # method 2: select device by label
          652         #           but only if not a placeholder label and only if there is no collision
          653         device_labels = [info.label for info in infos]
          654         if (keystore.label not in PLACEHOLDER_HW_CLIENT_LABELS
          655                 and device_labels.count(keystore.label) == 1):
          656             for info in infos:
          657                 if info.label == keystore.label:
          658                     return info
          659         # method 3: if there is only one device connected, and we don't have useful label/soft_device_id
          660         #           saved for keystore anyway, select it
          661         if (len(infos) == 1
          662                 and keystore.label in PLACEHOLDER_HW_CLIENT_LABELS
          663                 and keystore.soft_device_id is None):
          664             return infos[0]
          665 
          666         if not allow_user_interaction:
          667             raise CannotAutoSelectDevice()
          668         # ask user to select device manually
          669         msg = _("Please select which {} device to use:").format(plugin.device)
          670         descriptions = ["{label} ({maybe_model}{init}, {transport})"
          671                         .format(label=info.label or _("An unnamed {}").format(info.plugin_name),
          672                                 init=(_("initialized") if info.initialized else _("wiped")),
          673                                 transport=info.device.transport_ui_string,
          674                                 maybe_model=f"{info.model_name}, " if info.model_name else "")
          675                         for info in infos]
          676         c = handler.query_choice(msg, descriptions)
          677         if c is None:
          678             raise UserCancelled()
          679         info = infos[c]
          680         # note: updated label/soft_device_id will be saved after pairing succeeds
          681         return info
          682 
          683     @runs_in_hwd_thread
          684     def _scan_devices_with_hid(self) -> List['Device']:
          685         try:
          686             import hid
          687         except ImportError:
          688             return []
          689 
          690         devices = []
          691         for d in hid.enumerate(0, 0):
          692             vendor_id = d['vendor_id']
          693             product_key = (vendor_id, d['product_id'])
          694             plugin = None
          695             if product_key in self._recognised_hardware:
          696                 plugin = self._recognised_hardware[product_key]
          697             elif vendor_id in self._recognised_vendor:
          698                 plugin = self._recognised_vendor[vendor_id]
          699             if plugin:
          700                 device = plugin.create_device_from_hid_enumeration(d, product_key=product_key)
          701                 if device:
          702                     devices.append(device)
          703         return devices
          704 
          705     @runs_in_hwd_thread
          706     @profiler
          707     def scan_devices(self) -> Sequence['Device']:
          708         self.logger.info("scanning devices...")
          709 
          710         # First see what's connected that we know about
          711         devices = self._scan_devices_with_hid()
          712 
          713         # Let plugin handlers enumerate devices we don't know about
          714         with self.lock:
          715             enumerate_funcs = list(self._enumerate_func)
          716         for f in enumerate_funcs:
          717             try:
          718                 new_devices = f()
          719             except BaseException as e:
          720                 self.logger.error('custom device enum failed. func {}, error {}'
          721                                   .format(str(f), repr(e)))
          722             else:
          723                 devices.extend(new_devices)
          724 
          725         # find out what was disconnected
          726         pairs = [(dev.path, dev.id_) for dev in devices]
          727         disconnected_clients = []
          728         with self.lock:
          729             connected = {}
          730             for client, pair in self.clients.items():
          731                 if pair in pairs and client.has_usable_connection_with_device():
          732                     connected[client] = pair
          733                 else:
          734                     disconnected_clients.append((client, pair[1]))
          735             self.clients = connected
          736 
          737         # Unpair disconnected devices
          738         for client, id_ in disconnected_clients:
          739             self.unpair_id(id_)
          740             if client.handler:
          741                 client.handler.update_status(False)
          742 
          743         return devices