fix(qt): auto-close Plugins manager, read-only field styling, RLock-safe heirs persistence

GUI / plugin lifecycle:
- Auto-close Electrum's native 'Electrum Plugins' manager dialog after the
  BAL plugin is hot-enabled. Electrum 4.7.x no longer calls the old init_qt
  hook, so the close is now triggered from the create_status_bar, init_menubar
  and load_wallet hooks (fired when reload_windows() recreates the window).
- Robust dialog matching (isinstance / class name / localized window title) to
  cope with zipimport module-identity mismatches.
- Robust dismissal of the modal dialog (reject()/done()/close()) with a retry
  schedule [400, 800, 1500] ms; if it still cannot be closed, fall back to
  bringing it to the front (showNormal/raise_/activateWindow) so it never
  lingers hidden in the background. Counting only visible top-levels avoids
  treating an already-closed dialog as still open.

Read-only field styling:
- Paint the locked Delivery time / Check Alive date editors and the mining-fee
  spinbox with a light-grey background (#f0f0f0) so the user can see at a glance
  that they are not editable outside the 'Build your will' wizard; the styling
  is cleared when the fields are made editable again.

Pickle/RLock crash on 'Build will':
- heirs.save() now sanitises the heirs mapping via _json_safe() before handing
  it to json_db.put(), which deep-copies the value. A live runtime object
  (holding a threading.RLock) slipping into an heir value previously raised
  'TypeError: cannot pickle _thread.RLock object' and aborted the task; such
  values are now coerced to str and logged with their path.
- init_heirs_to_locktime() coerces the locktime to a plain serializable scalar.
- log_error() now accepts both a sys.exc_info() triple and a single exception
  instance, fixing the secondary 'TypeError object is not subscriptable' that
  masked the real error.
This commit is contained in:
GenSpark AI Developer
2026-06-15 20:35:55 +00:00
parent a8155183d7
commit 714b17eacd
6 changed files with 342 additions and 28 deletions

View File

@@ -306,6 +306,42 @@ def get_change_output(wallet, in_amount, out_amount, fee):
return out
def _json_safe(value, _path="heirs", _depth=0):
"""Return a JSON-serializable deep copy of *value*.
The wallet DB persists the heirs dict via ``json_db.put``, which calls
``copy.deepcopy`` on the value. If any nested element is a live runtime
object (e.g. one holding a ``threading.RLock``), deepcopy raises
``TypeError: cannot pickle '_thread.RLock' object`` and the whole
"Build will" task fails.
To make persistence robust we coerce the structure to plain
JSON-compatible types (dict / list / str / int / float / bool / None).
Anything else is converted to ``str(value)`` and logged with its path so
the offending field can be identified, instead of crashing the task.
"""
# Primitive JSON scalars are kept as-is.
if value is None or isinstance(value, (bool, int, float, str)):
return value
if isinstance(value, dict):
return {
str(k): _json_safe(v, "{}[{!r}]".format(_path, k), _depth + 1)
for k, v in value.items()
}
if isinstance(value, (list, tuple)):
return [
_json_safe(v, "{}[{}]".format(_path, i), _depth + 1)
for i, v in enumerate(value)
]
# Unexpected runtime object: do not let it reach deepcopy. Log where it
# was found so the real source can be fixed, then store a safe string.
_logger.error(
"heirs.save: non-serializable value at {} (type={}); coercing to str. "
"value={!r}".format(_path, type(value).__name__, value)
)
return str(value)
class Heirs(dict, Logger):
def __init__(self, wallet):
@@ -322,7 +358,11 @@ class Heirs(dict, Logger):
invalidate_inheritance_transactions(wallet)
def save(self):
self.db.put("heirs", dict(self))
# Sanitise the heirs mapping before handing it to the wallet DB: this
# guarantees only JSON-serializable values are stored and prevents the
# "cannot pickle '_thread.RLock' object" failure that aborted the
# Build-will task when a runtime object slipped into an heir value.
self.db.put("heirs", _json_safe(dict(self)))
def import_file(self, path):
data = read_json_file(path)