URI: 
       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)