tMerge branch 'client_thread' - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit eebabdf20959945181dacb2c8cec9eb154ac57d6 DIR parent d530f8fe84e4d849b9a3334481a047f5d631e682 HTML Author: Neil Booth <kyuupichan@gmail.com> Date: Tue, 19 Jan 2016 21:01:46 +0900 Merge branch 'client_thread' Diffstat: M gui/qt/installwizard.py | 6 ++++++ M gui/qt/main_window.py | 37 ++++++++++++++++--------------- M lib/util.py | 4 ++++ M plugins/trezor/clientbase.py | 16 ++++++++++++---- M plugins/trezor/plugin.py | 42 ++++++++++++++++++------------- M plugins/trezor/qt_generic.py | 77 ++++++++++++++++--------------- 6 files changed, 107 insertions(+), 75 deletions(-) --- DIR diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py t@@ -14,6 +14,7 @@ from password_dialog import PasswordLayout, PW_NEW, PW_PASSPHRASE from electrum.wallet import Wallet from electrum.mnemonic import prepare_seed +from electrum.util import SilentException from electrum.wizard import (WizardBase, UserCancelled, MSG_ENTER_PASSWORD, MSG_RESTORE_PASSPHRASE, MSG_COSIGNER, MSG_ENTER_SEED_OR_MPK, t@@ -116,6 +117,11 @@ class InstallWizard(WindowModalDialog, WizardBase): self.accept() self.refresh_gui() + def on_error(self, exc_info): + if not isinstance(exc_info[1], SilentException): + traceback.print_exception(*exc_info) + self.show_error(str(exc_info[1])) + def set_icon(self, filename): prior_filename, self.icon_filename = self.icon_filename, filename self.logo.setPixmap(QPixmap(filename).scaledToWidth(60)) DIR diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py t@@ -37,9 +37,10 @@ import icons_rc from electrum.bitcoin import COIN, is_valid, TYPE_ADDRESS from electrum.plugins import run_hook from electrum.i18n import _ -from electrum.util import block_explorer, block_explorer_info, block_explorer_URL -from electrum.util import format_satoshis, format_satoshis_plain, format_time -from electrum.util import PrintError, NotEnoughFunds, StoreDict +from electrum.util import (block_explorer, block_explorer_info, format_time, + block_explorer_URL, format_satoshis, PrintError, + format_satoshis_plain, NotEnoughFunds, StoreDict, + SilentException) from electrum import Transaction, mnemonic from electrum import util, bitcoin, commands from electrum import SimpleConfig, COIN_CHOOSERS, paymentrequest t@@ -198,8 +199,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.raise_() def on_error(self, exc_info): - traceback.print_exception(*exc_info) - self.show_error(str(exc_info[1])) + if not isinstance(exc_info[1], SilentException): + traceback.print_exception(*exc_info) + self.show_error(str(exc_info[1])) def on_network(self, event, *args): if event == 'updated': t@@ -254,6 +256,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): run_hook('close_wallet', self.wallet) def load_wallet(self, wallet): + wallet.thread = TaskThread(self, self.on_error) self.wallet = wallet self.update_recently_visited(wallet.storage.path) self.import_old_contacts() t@@ -2059,14 +2062,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): d.setLayout(vbox) d.exec_() - @protected def do_sign(self, address, message, signature, password): - message = unicode(message.toPlainText()) - message = message.encode('utf-8') - sig = self.wallet.sign_message(str(address.text()), message, password) - sig = base64.b64encode(sig) - signature.setText(sig) + message = unicode(message.toPlainText()).encode('utf-8') + task = partial(self.wallet.sign_message, str(address.text()), + message, password) + def show_signed_message(sig): + signature.setText(base64.b64encode(sig)) + self.wallet.thread.add(task, on_success=show_signed_message) def do_verify(self, address, message, signature): message = unicode(message.toPlainText()) t@@ -2123,13 +2126,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): @protected def do_decrypt(self, message_e, pubkey_e, encrypted_e, password): - try: - decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password) - message_e.setText(decrypted) - except BaseException as e: - traceback.print_exc(file=sys.stdout) - self.show_warning(str(e)) - + cyphertext = str(encrypted_e.toPlainText()) + task = partial(self.wallet.decrypt_message, str(pubkey_e.text()), + cyphertext, password) + self.wallet.thread.add(task, on_success=message_e.setText) def do_encrypt(self, message_e, pubkey_e, encrypted_e): message = unicode(message_e.toPlainText()) t@@ -2856,6 +2856,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): event.accept() def clean_up(self): + self.wallet.thread.stop() if self.network: self.network.unregister_callback(self.on_network) self.config.set_key("is_maximized", self.isMaximized()) DIR diff --git a/lib/util.py b/lib/util.py t@@ -21,6 +21,10 @@ class InvalidPassword(Exception): def __str__(self): return _("Incorrect password") +class SilentException(Exception): + '''An exception that should probably be suppressed from the user''' + pass + class MyEncoder(json.JSONEncoder): def default(self, obj): from transaction import Transaction DIR diff --git a/plugins/trezor/clientbase.py b/plugins/trezor/clientbase.py t@@ -1,7 +1,7 @@ from sys import stderr from electrum.i18n import _ -from electrum.util import PrintError +from electrum.util import PrintError, SilentException class GuiMixin(object): t@@ -20,6 +20,16 @@ class GuiMixin(object): 'passphrase': _("Confirm on %s device to continue"), } + def callback_Failure(self, msg): + # BaseClient's unfortunate call() implementation forces us to + # raise exceptions on failure in order to unwind the stack. + # However, making the user acknowledge they cancelled + # gets old very quickly, so we suppress those. + if msg.code in (self.types.Failure_PinCancelled, + self.types.Failure_ActionCancelled): + raise SilentException() + raise RuntimeError(msg.message) + def callback_ButtonRequest(self, msg): msg_code = self.msg_code_override or msg.code message = self.messages.get(msg_code, self.messages['default']) t@@ -65,6 +75,7 @@ class TrezorClientBase(GuiMixin, PrintError): self.handler = handler self.hid_id_ = hid_id self.tx_api = plugin + self.types = plugin.types self.msg_code_override = None def __str__(self): t@@ -172,9 +183,6 @@ class TrezorClientBase(GuiMixin, PrintError): def wrapped(self, *args, **kwargs): try: return func(self, *args, **kwargs) - except BaseException as e: - self.handler.show_error(str(e)) - raise e finally: self.handler.finished() DIR diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py t@@ -1,5 +1,6 @@ import base64 import re +import threading import time from binascii import unhexlify t@@ -172,6 +173,7 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob): def __init__(self, parent, config, name): BasePlugin.__init__(self, parent, config, name) + self.main_thread = threading.current_thread() self.device = self.wallet_class.device self.wallet_class.plugin = self self.prevent_timeout = time.time() + 3600 * 24 * 365 t@@ -216,6 +218,8 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob): return self.client_class(transport, handler, self, hid_id) def get_client(self, wallet, force_pair=True, check_firmware=True): + assert self.main_thread != threading.current_thread() + '''check_firmware is ignored unless force_pair is True.''' client = self.device_manager().get_client(wallet, force_pair) t@@ -281,26 +285,30 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob): (item, label, pin_protection, passphrase_protection) \ = wallet.handler.request_trezor_init_settings(method, self.device) - client = self.get_client(wallet) language = 'english' - if method == TIM_NEW: - strength = 64 * (item + 2) # 128, 192 or 256 - client.reset_device(True, strength, passphrase_protection, - pin_protection, label, language) - elif method == TIM_RECOVER: - word_count = 6 * (item + 2) # 12, 18 or 24 - client.recovery_device(word_count, passphrase_protection, - pin_protection, label, language) - elif method == TIM_MNEMONIC: - pin = pin_protection # It's the pin, not a boolean - client.load_device_by_mnemonic(str(item), pin, - passphrase_protection, + def initialize_device(): + client = self.get_client(wallet) + + if method == TIM_NEW: + strength = 64 * (item + 2) # 128, 192 or 256 + client.reset_device(True, strength, passphrase_protection, + pin_protection, label, language) + elif method == TIM_RECOVER: + word_count = 6 * (item + 2) # 12, 18 or 24 + client.recovery_device(word_count, passphrase_protection, + pin_protection, label, language) + elif method == TIM_MNEMONIC: + pin = pin_protection # It's the pin, not a boolean + client.load_device_by_mnemonic(str(item), pin, + passphrase_protection, + label, language) + else: + pin = pin_protection # It's the pin, not a boolean + client.load_device_by_xprv(item, pin, passphrase_protection, label, language) - else: - pin = pin_protection # It's the pin, not a boolean - client.load_device_by_xprv(item, pin, passphrase_protection, - label, language) + + wallet.thread.add(initialize_device) def unpaired_clients(self, handler): '''Returns all connected, unpaired devices as a list of clients and a DIR diff --git a/plugins/trezor/qt_generic.py b/plugins/trezor/qt_generic.py t@@ -231,19 +231,25 @@ def qt_plugin_class(base_plugin_class): window.statusBar().addPermanentWidget(window.tzb) wallet.handler = self.create_handler(window) # Trigger a pairing - self.get_client(wallet) + wallet.thread.add(partial(self.get_client, wallet)) def on_create_wallet(self, wallet, wizard): assert type(wallet) == self.wallet_class wallet.handler = self.create_handler(wizard) + wallet.thread = TaskThread(wizard, wizard.on_error) self.select_device(wallet) - wallet.create_hd_account(None) + # Create accounts in separate thread; wait until done + loop = QEventLoop() + wallet.thread.add(partial(wallet.create_hd_account, None), + on_done=loop.quit) + loop.exec_() @hook def receive_menu(self, menu, addrs, wallet): if type(wallet) == self.wallet_class and len(addrs) == 1: - menu.addAction(_("Show on %s") % self.device, - lambda: self.show_address(wallet, addrs[0])) + def show_address(): + wallet.thread.add(partial(self.show_address, wallet, addrs[0])) + menu.addAction(_("Show on %s") % self.device, show_address) def settings_dialog(self, window): hid_id = self.choose_device(window) t@@ -296,23 +302,27 @@ class SettingsDialog(WindowModalDialog): # wallet can be None, needn't be window.wallet wallet = devmgr.wallet_by_hid_id(hid_id) hs_rows, hs_cols = (64, 128) + self.current_label=None - def get_client(): - client = devmgr.client_by_hid_id(hid_id, handler) - if not client: - self.show_error("Device not connected!") - raise RuntimeError("Device not connected") - return client - - def update(): - # self.features for outer scopes - client = get_client() - features = self.features = client.features + def invoke_client(method, *args, **kw_args): + def task(): + client = plugin.get_client(wallet, False) + if not client: + raise RuntimeError("Device not connected") + if method: + getattr(client, method)(*args, **kw_args) + update(client.features) + + wallet.thread.add(task) + + def update(features): + self.current_label = features.label set_label_enabled() bl_hash = features.bootloader_hash.encode('hex') bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]]) noyes = [_("No"), _("Yes")] endis = [_("Enable Passphrases"), _("Disable Passphrases")] + disen = [_("Disabled"), _("Enabled")] setchange = [_("Set a PIN"), _("Change PIN")] version = "%d.%d.%d" % (features.major_version, t@@ -322,10 +332,10 @@ class SettingsDialog(WindowModalDialog): device_label.setText(features.label) pin_set_label.setText(noyes[features.pin_protection]) + passphrases_label.setText(disen[features.passphrase_protection]) bl_hash_label.setText(bl_hash) label_edit.setText(features.label) device_id_label.setText(features.device_id) - serial_number_label.setText(client.hid_id()) initialized_label.setText(noyes[features.initialized]) version_label.setText(version) coins_label.setText(coins) t@@ -337,11 +347,10 @@ class SettingsDialog(WindowModalDialog): language_label.setText(features.language) def set_label_enabled(): - label_apply.setEnabled(label_edit.text() != self.features.label) + label_apply.setEnabled(label_edit.text() != self.current_label) def rename(): - get_client().change_label(unicode(label_edit.text())) - update() + invoke_client('change_label', unicode(label_edit.text())) def toggle_passphrase(): title = _("Confirm Toggle Passphrase Protection") t@@ -354,9 +363,8 @@ class SettingsDialog(WindowModalDialog): "Are you sure you want to proceed?") % plugin.device if not self.question(msg, title=title): return - get_client().toggle_passphrase() + invoke_client('toggle_passphrase') devmgr.unpair(hid_id) - update() def change_homescreen(): from PIL import Image # FIXME t@@ -374,17 +382,16 @@ class SettingsDialog(WindowModalDialog): img += '1' if pix[i, j] else '0' img = ''.join(chr(int(img[i:i + 8], 2)) for i in range(0, len(img), 8)) - get_client().change_homescreen(img) + invoke_client('change_homescreen', img) def clear_homescreen(): - get_client().change_homescreen('\x00') + invoke_client('change_homescreen', '\x00') - def set_pin(remove=False): - get_client().set_pin(remove=remove) - update() + def set_pin(): + invoke_client('set_pin', remove=False) def clear_pin(): - set_pin(remove=True) + invoke_client('set_pin', remove=True) def wipe_device(): if wallet and sum(wallet.get_balance()): t@@ -394,9 +401,8 @@ class SettingsDialog(WindowModalDialog): if not self.question(msg, title=title, icon=QMessageBox.Critical): return - get_client().wipe_device() + invoke_client('wipe_device') devmgr.unpair(hid_id) - update() def slider_moved(): mins = timeout_slider.sliderPosition() t@@ -406,8 +412,6 @@ class SettingsDialog(WindowModalDialog): seconds = timeout_slider.sliderPosition() * 60 wallet.set_session_timeout(seconds) - dialog_vbox = QVBoxLayout(self) - # Information tab info_tab = QWidget() info_layout = QVBoxLayout(info_tab) t@@ -415,9 +419,9 @@ class SettingsDialog(WindowModalDialog): info_glayout.setColumnStretch(2, 1) device_label = QLabel() pin_set_label = QLabel() + passphrases_label = QLabel() version_label = QLabel() device_id_label = QLabel() - serial_number_label = QLabel() bl_hash_label = QLabel() bl_hash_label.setWordWrap(True) coins_label = QLabel() t@@ -427,9 +431,9 @@ class SettingsDialog(WindowModalDialog): rows = [ (_("Device Label"), device_label), (_("PIN set"), pin_set_label), + (_("Passphrases"), passphrases_label), (_("Firmware Version"), version_label), (_("Device ID"), device_id_label), - (_("Serial Number"), serial_number_label), (_("Bootloader Hash"), bl_hash_label), (_("Supported Coins"), coins_label), (_("Language"), language_label), t@@ -580,8 +584,9 @@ class SettingsDialog(WindowModalDialog): tabs.addTab(info_tab, _("Information")) tabs.addTab(settings_tab, _("Settings")) tabs.addTab(advanced_tab, _("Advanced")) - - # Update information - update() + dialog_vbox = QVBoxLayout(self) dialog_vbox.addWidget(tabs) dialog_vbox.addLayout(Buttons(CloseButton(self))) + + # Update information + invoke_client(None)