1
0

plugin: make DeviceMgr.run non-blocking, fix lock

Prevents `DeviceMgr.run()` from blocking the `Plugins` `DaemonThread` by
scheduling the hww timeout check instead of awaiting its result on the
`Plugins` thread.

If something in the `_hwd_comms_executor` thread is waiting for user
input, e.g. when setting up a hww in the wizard the user needs to
unlock the hww for `HardwareClientBase.get_xpub()` to return, the
`_hwd_comms_executor` is blocked. If then `DeviceMgr.run()` gets called by
the `Plugins` `DaemonThread` concurrently and tries to check the hww
timeout on the `_hwd_comms_executor` as well the `DaemonThread` is
blocked too until the `_hwd_comms_executor` gets unblocked (and the
`DaemonThread.job_lock` is taken.

Now if something tries to take the `DaemonThread.job_lock` it blocks as
well, so if a user e.g. tries to load a new plugin from the plugins
dialog the whole gui thread will freeze until the hww gets unlocked.
This commit is contained in:
f321x
2026-01-21 13:57:04 +01:00
parent 64289d7e99
commit caff7db493
2 changed files with 17 additions and 3 deletions

View File

@@ -36,6 +36,7 @@ from urllib.parse import urlparse
from typing import (NamedTuple, Any, Union, TYPE_CHECKING, Optional, Tuple,
Dict, Iterable, List, Sequence, Callable, TypeVar, Mapping)
import concurrent
from concurrent.futures import Future
import zipimport
from functools import wraps, partial
from itertools import chain
@@ -1040,6 +1041,7 @@ class DeviceMgr(ThreadJob):
self._recognised_vendor = {} # type: Dict[int, HW_PluginBase] # vendor_id -> Plugin
# Custom enumerate functions for devices we don't know about.
self._enumerate_func = set() # Needs self.lock.
self._ongoing_timeout_checks = {} # type: Dict[str, Future]
self.lock = threading.RLock()
@@ -1053,10 +1055,16 @@ class DeviceMgr(ThreadJob):
"""Handle device timeouts. Runs in the context of the Plugins
thread."""
with self.lock:
clients = list(self.clients.keys())
clients = list(self.clients.items())
cutoff = time.time() - self.config.get_session_timeout()
for client in clients:
client.timeout(cutoff)
for client, client_id in clients:
if fut := self._ongoing_timeout_checks.get(client_id):
if not fut.done():
continue
# scheduling the timeout check prevents blocking the Plugins DaemonThread if the
# _hwd_comms_executor Thread is blocked (e.g. due to it awaiting user input).
fut = _hwd_comms_executor.submit(client.timeout, cutoff)
self._ongoing_timeout_checks[client_id] = fut
def register_devices(self, device_pairs, *, plugin: 'HW_PluginBase'):
for pair in device_pairs:
@@ -1113,6 +1121,8 @@ class DeviceMgr(ThreadJob):
with self.lock:
client = self._client_by_id(id_)
self.clients.pop(client, None)
if fut := self._ongoing_timeout_checks.pop(id_, None):
fut.cancel()
if client:
client.close()

View File

@@ -378,10 +378,14 @@ class DaemonThread(threading.Thread, Logger):
# malformed or malicious server responses
with self.job_lock:
for job in self.jobs:
start = time.perf_counter()
try:
job.run()
except Exception as e:
self.logger.exception('')
duration = time.perf_counter() - start
if duration > 0.5:
self.logger.warning(f"thread job {job} blocked {self} DaemonThread for {duration:.2f} s")
def remove_jobs(self, jobs):
with self.job_lock: