commit eebabdf20959945181dacb2c8cec9eb154ac57d6
parent d530f8fe84e4d849b9a3334481a047f5d631e682
Author: Neil Booth <kyuupichan@gmail.com>
Date: Tue, 19 Jan 2016 21:01:46 +0900
Merge branch 'client_thread'
Diffstat:
6 files changed, 107 insertions(+), 75 deletions(-)
diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py
@@ -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,
@@ -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))
diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
@@ -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
@@ -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':
@@ -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()
@@ -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())
@@ -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())
@@ -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())
diff --git a/lib/util.py b/lib/util.py
@@ -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
diff --git a/plugins/trezor/clientbase.py b/plugins/trezor/clientbase.py
@@ -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):
@@ -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'])
@@ -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):
@@ -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()
diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py
@@ -1,5 +1,6 @@
import base64
import re
+import threading
import time
from binascii import unhexlify
@@ -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
@@ -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)
@@ -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
diff --git a/plugins/trezor/qt_generic.py b/plugins/trezor/qt_generic.py
@@ -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)
@@ -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,
@@ -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)
@@ -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")
@@ -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
@@ -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()):
@@ -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()
@@ -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)
@@ -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()
@@ -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),
@@ -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)