diff --git a/electrum b/electrum index 9a9f41855..9df3e612a 100755 --- a/electrum +++ b/electrum @@ -87,7 +87,8 @@ if is_local or is_android: imp.load_module('electrum_plugins', *imp.find_module('plugins')) -from electrum import bitcoin + +from electrum import bitcoin, util from electrum import SimpleConfig, Network from electrum.wallet import Wallet, Imported_Wallet from electrum.storage import WalletStorage @@ -296,8 +297,10 @@ def init_plugins(config, gui_name): from electrum.plugins import Plugins return Plugins(config, is_local or is_android, gui_name) -if __name__ == '__main__': +if __name__ == '__main__': + # The hook will only be used in the Qt GUI right now + util.setup_thread_excepthook() # on osx, delete Process Serial Number arg generated for apps launched in Finder sys.argv = list(filter(lambda x: not x.startswith('-psn'), sys.argv)) diff --git a/gui/qt/exception_window.py b/gui/qt/exception_window.py new file mode 100644 index 000000000..a603af5ef --- /dev/null +++ b/gui/qt/exception_window.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +import json +import locale +import platform +import traceback + +import requests +from PyQt5.QtCore import QObject +import PyQt5.QtCore as QtCore +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import * + +from electrum.i18n import _ +import sys +from electrum import ELECTRUM_VERSION + +issue_template = """

Traceback

+
+{traceback}
+
+ +

Additional information

+ +""" +report_server = "https://crashhub.electrum.org/crash" + + +class Exception_Window(QWidget): + _active_window = None + + def __init__(self, main_window, exctype, value, tb): + self.exc_args = (exctype, value, tb) + self.main_window = main_window + QWidget.__init__(self) + self.setWindowTitle('Electrum - ' + _('An Error Occured')) + self.setMinimumSize(600, 300) + + main_box = QVBoxLayout() + + heading = QLabel('

' + _('Sorry!') + '

') + main_box.addWidget(heading) + main_box.addWidget(QLabel(_('Something went wrong while executing Electrum.'))) + + main_box.addWidget(QLabel( + _('To help us diagnose and fix the problem, you can send us a bug report that contains useful debug ' + 'information:'))) + + collapse_info = QPushButton(_("Show report contents")) + collapse_info.clicked.connect(lambda: QMessageBox.about(self, "Report contents", self.get_report_string())) + main_box.addWidget(collapse_info) + + main_box.addWidget(QLabel(_("Please briefly describe what led to the error (optional):"))) + + self.description_textfield = QTextEdit() + self.description_textfield.setFixedHeight(50) + main_box.addWidget(self.description_textfield) + + main_box.addWidget(QLabel(_("Do you want to send this report?"))) + + buttons = QHBoxLayout() + + report_button = QPushButton(_('Send Bug Report')) + report_button.clicked.connect(self.send_report) + report_button.setIcon(QIcon(":icons/tab_send.png")) + buttons.addWidget(report_button) + + never_button = QPushButton(_('Never')) + never_button.clicked.connect(self.show_never) + buttons.addWidget(never_button) + + close_button = QPushButton(_('Not Now')) + close_button.clicked.connect(self.close) + buttons.addWidget(close_button) + + main_box.addLayout(buttons) + + self.setLayout(main_box) + self.show() + + def send_report(self): + report = self.get_traceback_info() + report.update(self.get_additional_info()) + report = json.dumps(report) + response = requests.post(report_server, data=report) + QMessageBox.about(self, "Crash report", response.text) + self.close() + + def on_close(self): + Exception_Window._active_window = None + sys.__excepthook__(*self.exc_args) + self.close() + + def show_never(self): + self.main_window.config.set_key("show_crash_reporter", False) + self.close() + + def closeEvent(self, event): + self.on_close() + event.accept() + + def get_traceback_info(self): + exc_string = str(self.exc_args[1]) + stack = traceback.extract_tb(self.exc_args[2]) + readable_trace = "".join(traceback.format_list(stack)) + id = { + "file": stack[-1].filename, + "name": stack[-1].name, + "type": self.exc_args[0].__name__ + } + return { + "exc_string": exc_string, + "stack": readable_trace, + "id": id + } + + def get_additional_info(self): + args = { + "electrum_version": ELECTRUM_VERSION, + "os": platform.platform(), + "wallet_type": "unknown", + "locale": locale.getdefaultlocale()[0], + "description": self.description_textfield.toPlainText() + } + try: + args["wallet_type"] = self.main_window.wallet.wallet_type + except: + # Maybe the wallet isn't loaded yet + pass + return args + + def get_report_string(self): + info = self.get_additional_info() + info["traceback"] = "".join(traceback.format_exception(*self.exc_args)) + return issue_template.format(**info) + + +def _show_window(*args): + if not Exception_Window._active_window: + Exception_Window._active_window = Exception_Window(*args) + + +class Exception_Hook(QObject): + _report_exception = QtCore.pyqtSignal(object, object, object, object) + + def __init__(self, main_window, *args, **kwargs): + super(Exception_Hook, self).__init__(*args, **kwargs) + if not main_window.config.get("show_crash_reporter", default=True): + return + self.main_window = main_window + sys.excepthook = self.handler + self._report_exception.connect(_show_window) + + def handler(self, *args): + self._report_exception.emit(self.main_window, *args) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 8986e6b2d..e44a182ed 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -32,8 +32,11 @@ from decimal import Decimal import base64 from functools import partial -from PyQt5.QtCore import Qt from PyQt5.QtGui import * +from PyQt5.QtCore import * +import PyQt5.QtCore as QtCore + +from .exception_window import Exception_Hook from PyQt5.QtWidgets import * from electrum.util import bh2u, bfh @@ -103,6 +106,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.gui_object = gui_object self.config = config = gui_object.config + + self.setup_exception_hook() + self.network = gui_object.daemon.network self.fx = gui_object.daemon.fx self.invoices = wallet.invoices @@ -204,6 +210,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): def on_history(self, b): self.new_fx_history_signal.emit() + def setup_exception_hook(self): + Exception_Hook(self) + def on_fx_history(self): self.history_list.refresh_headers() self.history_list.update() diff --git a/lib/util.py b/lib/util.py index db5fe896f..d13b3ff9d 100644 --- a/lib/util.py +++ b/lib/util.py @@ -707,3 +707,29 @@ class QueuePipe: self.send(request) + + +def setup_thread_excepthook(): + """ + Workaround for `sys.excepthook` thread bug from: + http://bugs.python.org/issue1230540 + + Call once from the main thread before creating any threads. + """ + + init_original = threading.Thread.__init__ + + def init(self, *args, **kwargs): + + init_original(self, *args, **kwargs) + run_original = self.run + + def run_with_except_hook(*args2, **kwargs2): + try: + run_original(*args2, **kwargs2) + except Exception: + sys.excepthook(*sys.exc_info()) + + self.run = run_with_except_hook + + threading.Thread.__init__ = init \ No newline at end of file