commit 34080187ffb956ceb28100130c3f70716e7f4979
parent 6faef7efe34393e91e33afdfba0746f9794ddb4d
Author: ThomasV <thomasv@electrum.org>
Date: Tue, 30 Jan 2018 11:16:42 +0100
Merge pull request #2339 from bauerj/error-window
Semi-automated crash reporting
Diffstat:
4 files changed, 223 insertions(+), 3 deletions(-)
diff --git 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
@@ -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 = """<h2>Traceback</h2>
+<pre>
+{traceback}
+</pre>
+
+<h2>Additional information</h2>
+<ul>
+ <li>Electrum version: {electrum_version}</li>
+ <li>Operating system: {os}</li>
+ <li>Wallet type: {wallet_type}</li>
+ <li>Locale: {locale}</li>
+</ul>
+"""
+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('<h2>' + _('Sorry!') + '</h2>')
+ 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
@@ -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
@@ -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