commit 5eb1cbef928169d86245ca5402b052f4b9439034
parent 17ef023c8cd116c11be706276effec0008465ad7
Author: Johann Bauer <bauerj@bauerj.eu>
Date: Tue, 12 Jun 2018 14:17:34 +0200
[WIP] Crash reports android (#3870)
* Split crash reporter class
In Qt related stuff and basic stuff.
* Crash reports from Android
* Ignore exceptions in crash_reporter (if any)
* Open issue in browser
* Switch back to real server
Diffstat:
4 files changed, 342 insertions(+), 91 deletions(-)
diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py
@@ -36,7 +36,7 @@ from kivy.lang import Builder
#Factory.register('OutputItem', module='electrum_gui.kivy.uix.dialogs')
from .uix.dialogs.installwizard import InstallWizard
-from .uix.dialogs import InfoBubble
+from .uix.dialogs import InfoBubble, crash_reporter
from .uix.dialogs import OutputList, OutputItem
from .uix.dialogs import TopLabel, RefLabel
@@ -450,6 +450,7 @@ class ElectrumWindow(App):
#win.softinput_mode = 'below_target'
self.on_size(win, win.size)
self.init_ui()
+ crash_reporter.ExceptionHook(self)
# init plugins
run_hook('init_kivy', self)
# fiat currency
diff --git a/gui/kivy/uix/dialogs/crash_reporter.py b/gui/kivy/uix/dialogs/crash_reporter.py
@@ -0,0 +1,193 @@
+import sys
+
+import requests
+from kivy import base, utils
+from kivy.clock import Clock
+from kivy.core.window import Window
+from kivy.factory import Factory
+from kivy.lang import Builder
+from kivy.uix.label import Label
+from kivy.utils import platform
+
+
+from electrum.base_crash_reporter import BaseCrashReporter
+from electrum.i18n import _
+
+
+Builder.load_string('''
+<CrashReporter@Popup>
+ BoxLayout:
+ orientation: 'vertical'
+ Label:
+ id: crash_message
+ text_size: root.width, None
+ size: self.texture_size
+ size_hint: None, None
+ Label:
+ id: request_help_message
+ text_size: root.width*.95, None
+ size: self.texture_size
+ size_hint: None, None
+ BoxLayout:
+ size_hint: 1, 0.1
+ Button:
+ text: 'Show report contents'
+ height: '48dp'
+ size_hint: 1, None
+ on_press: root.show_contents()
+ BoxLayout:
+ size_hint: 1, 0.1
+ Label:
+ id: describe_error_message
+ text_size: root.width, None
+ size: self.texture_size
+ size_hint: None, None
+ TextInput:
+ id: user_message
+ size_hint: 1, 0.3
+ BoxLayout:
+ size_hint: 1, 0.7
+ BoxLayout:
+ size_hint: 1, None
+ height: '48dp'
+ orientation: 'horizontal'
+ Button:
+ height: '48dp'
+ text: 'Send'
+ on_release: root.send_report()
+ Button:
+ text: 'Never'
+ on_release: root.show_never()
+ Button:
+ text: 'Not now'
+ on_release: root.dismiss()
+
+<CrashReportDetails@Popup>
+ BoxLayout:
+ orientation: 'vertical'
+ ScrollView:
+ do_scroll_x: False
+ Label:
+ id: contents
+ text_size: root.width*.9, None
+ size: self.texture_size
+ size_hint: None, None
+ Button:
+ text: 'Close'
+ height: '48dp'
+ size_hint: 1, None
+ on_release: root.dismiss()
+''')
+
+
+class CrashReporter(BaseCrashReporter, Factory.Popup):
+ issue_template = """[b]Traceback[/b]
+
+[i]{traceback}[/i]
+
+
+[b]Additional information[/b]
+ * Electrum version: {app_version}
+ * Operating system: {os}
+ * Wallet type: {wallet_type}
+ * Locale: {locale}
+ """
+
+ def __init__(self, main_window, exctype, value, tb):
+ BaseCrashReporter.__init__(self, exctype, value, tb)
+ Factory.Popup.__init__(self)
+ self.main_window = main_window
+ self.title = BaseCrashReporter.CRASH_TITLE
+ self.title_size = "24sp"
+ self.ids.crash_message.text = BaseCrashReporter.CRASH_MESSAGE
+ self.ids.request_help_message.text = BaseCrashReporter.REQUEST_HELP_MESSAGE
+ self.ids.describe_error_message.text = BaseCrashReporter.DESCRIBE_ERROR_MESSAGE
+
+ def show_contents(self):
+ details = CrashReportDetails(self.get_report_string())
+ details.open()
+
+ def show_popup(self, title, content):
+ popup = Factory.Popup(title=title,
+ content=Label(text=content, text_size=(Window.size[0] * 3/4, None)),
+ size_hint=(3/4, 3/4))
+ popup.open()
+
+ def send_report(self):
+ try:
+ response = BaseCrashReporter.send_report(self, "/crash.json").json()
+ except requests.exceptions.RequestException:
+ self.show_popup(_('Unable to send report'), _("Please check your network connection."))
+ else:
+ self.show_popup(_('Report sent'), response["text"])
+ if response["location"]:
+ self.open_url(response["location"])
+ self.dismiss()
+
+ def open_url(self, url):
+ if platform != 'android':
+ return
+ from jnius import autoclass, cast
+ String = autoclass("java.lang.String")
+ url = String(url)
+ PythonActivity = autoclass('org.kivy.android.PythonActivity')
+ activity = PythonActivity.mActivity
+ Intent = autoclass('android.content.Intent')
+ Uri = autoclass('android.net.Uri')
+ browserIntent = Intent()
+ # This line crashes the app:
+ # browserIntent.setAction(Intent.ACTION_VIEW)
+ # Luckily we don't need it because the OS is smart enough to recognize the URL
+ browserIntent.setData(Uri.parse(url))
+ currentActivity = cast('android.app.Activity', activity)
+ currentActivity.startActivity(browserIntent)
+
+ def show_never(self):
+ self.main_window.electrum_config.set_key(BaseCrashReporter.config_key, False)
+ self.dismiss()
+
+ def get_user_description(self):
+ return self.ids.user_message.text
+
+ def get_wallet_type(self):
+ return self.main_window.wallet.wallet_type
+
+ def get_os_version(self):
+ if utils.platform is not "android":
+ return utils.platform
+ import jnius
+ bv = jnius.autoclass('android.os.Build$VERSION')
+ b = jnius.autoclass('android.os.Build')
+ return "Android {} on {} {} ({})".format(bv.RELEASE, b.BRAND, b.DEVICE, b.DISPLAY)
+
+
+class CrashReportDetails(Factory.Popup):
+ def __init__(self, text):
+ Factory.Popup.__init__(self)
+ self.title = "Report Details"
+ self.ids.contents.text = text
+ print(text)
+
+
+class ExceptionHook(base.ExceptionHandler):
+ def __init__(self, main_window):
+ super().__init__()
+ self.main_window = main_window
+ if not main_window.electrum_config.get(BaseCrashReporter.config_key, default=True):
+ return
+ # For exceptions in Kivy:
+ base.ExceptionManager.add_handler(self)
+ # For everything else:
+ sys.excepthook = lambda exctype, value, tb: self.handle_exception(value)
+
+ def handle_exception(self, _inst):
+ exc_info = sys.exc_info()
+ # Check if this is an exception from within the exception handler:
+ import traceback
+ for item in traceback.extract_tb(exc_info[2]):
+ if item.filename.endswith("crash_reporter.py"):
+ return
+ e = CrashReporter(self.main_window, *exc_info)
+ # Open in main thread:
+ Clock.schedule_once(lambda _: e.open(), 0)
+ return base.ExceptionManager.PASS
diff --git a/gui/qt/exception_window.py b/gui/qt/exception_window.py
@@ -21,46 +21,25 @@
# 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 os
import sys
-import subprocess
+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 _
-from electrum import ELECTRUM_VERSION, bitcoin, constants
-
+from electrum.base_crash_reporter import BaseCrashReporter
from .util import MessageBoxMixin
-issue_template = """<h2>Traceback</h2>
-<pre>
-{traceback}
-</pre>
-
-<h2>Additional information</h2>
-<ul>
- <li>Electrum version: {app_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, MessageBoxMixin):
+class Exception_Window(BaseCrashReporter, QWidget, MessageBoxMixin):
_active_window = None
def __init__(self, main_window, exctype, value, tb):
- self.exc_args = (exctype, value, tb)
+ BaseCrashReporter.__init__(self, exctype, value, tb)
self.main_window = main_window
QWidget.__init__(self)
self.setWindowTitle('Electrum - ' + _('An Error Occurred'))
@@ -68,27 +47,26 @@ class Exception_Window(QWidget, MessageBoxMixin):
main_box = QVBoxLayout()
- heading = QLabel('<h2>' + _('Sorry!') + '</h2>')
+ heading = QLabel('<h2>' + BaseCrashReporter.CRASH_TITLE + '</h2>')
main_box.addWidget(heading)
- main_box.addWidget(QLabel(_('Something went wrong while executing Electrum.')))
+ main_box.addWidget(QLabel(BaseCrashReporter.CRASH_MESSAGE))
- main_box.addWidget(QLabel(
- _('To help us diagnose and fix the problem, you can send us a bug report that contains useful debug '
- 'information:')))
+ main_box.addWidget(QLabel(BaseCrashReporter.REQUEST_HELP_MESSAGE))
collapse_info = QPushButton(_("Show report contents"))
collapse_info.clicked.connect(
lambda: self.msg_box(QMessageBox.NoIcon,
- self, "Report contents", self.get_report_string()))
+ 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):")))
+ main_box.addWidget(QLabel(BaseCrashReporter.DESCRIBE_ERROR_MESSAGE))
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?")))
+ main_box.addWidget(QLabel(BaseCrashReporter.ASK_CONFIRM_SEND))
buttons = QHBoxLayout()
@@ -111,24 +89,16 @@ class Exception_Window(QWidget, MessageBoxMixin):
self.show()
def send_report(self):
- if constants.net.GENESIS[-4:] not in ["4943", "e26f"] and ".electrum.org" in report_server:
- # Gah! Some kind of altcoin wants to send us crash reports.
- self.main_window.show_critical(_("Please report this issue manually."))
- return
- report = self.get_traceback_info()
- report.update(self.get_additional_info())
- report = json.dumps(report)
try:
- response = requests.post(report_server, data=report, timeout=20)
+ response = BaseCrashReporter.send_report(self)
except BaseException as e:
traceback.print_exc(file=sys.stderr)
self.main_window.show_critical(_('There was a problem with the automatic reporting:') + '\n' +
str(e) + '\n' +
_("Please report this issue manually."))
return
- else:
- QMessageBox.about(self, "Crash report", response.text)
- self.close()
+ QMessageBox.about(self, _("Crash report"), response.text)
+ self.close()
def on_close(self):
Exception_Window._active_window = None
@@ -136,59 +106,21 @@ class Exception_Window(QWidget, MessageBoxMixin):
self.close()
def show_never(self):
- self.main_window.config.set_key("show_crash_reporter", False)
+ self.main_window.config.set_key(BaseCrashReporter.config_key, 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 = {
- "app_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
- try:
- args["app_version"] = self.get_git_version()
- except:
- # This is probably not running from source
- pass
- return args
+ def get_user_description(self):
+ return self.description_textfield.toPlainText()
- 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 get_wallet_type(self):
+ return self.main_window.wallet.wallet_type
- @staticmethod
- def get_git_version():
- dir = os.path.dirname(os.path.realpath(sys.argv[0]))
- version = subprocess.check_output(
- ['git', 'describe', '--always', '--dirty'], cwd=dir)
- return str(version, "utf8").strip()
+ def get_os_version(self):
+ return platform.platform()
def _show_window(*args):
@@ -201,7 +133,7 @@ class Exception_Hook(QObject):
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):
+ if not main_window.config.get(BaseCrashReporter.config_key, default=True):
return
self.main_window = main_window
sys.excepthook = self.handler
diff --git a/lib/base_crash_reporter.py b/lib/base_crash_reporter.py
@@ -0,0 +1,125 @@
+# 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 traceback
+import subprocess
+import sys
+import os
+
+import requests
+
+from electrum import ELECTRUM_VERSION, constants
+from electrum.i18n import _
+
+
+class BaseCrashReporter(object):
+ report_server = "https://crashhub.electrum.org"
+ config_key = "show_crash_reporter"
+ issue_template = """<h2>Traceback</h2>
+<pre>
+{traceback}
+</pre>
+
+<h2>Additional information</h2>
+<ul>
+ <li>Electrum version: {app_version}</li>
+ <li>Operating system: {os}</li>
+ <li>Wallet type: {wallet_type}</li>
+ <li>Locale: {locale}</li>
+</ul>
+ """
+ CRASH_MESSAGE = _('Something went wrong while executing Electrum.')
+ CRASH_TITLE = _('Sorry!')
+ REQUEST_HELP_MESSAGE = _('To help us diagnose and fix the problem, you can send us a bug report that contains '
+ 'useful debug information:')
+ DESCRIBE_ERROR_MESSAGE = _("Please briefly describe what led to the error (optional):")
+ ASK_CONFIRM_SEND = _("Do you want to send this report?")
+
+ def __init__(self, exctype, value, tb):
+ self.exc_args = (exctype, value, tb)
+
+ def send_report(self, endpoint="/crash"):
+ if constants.net.GENESIS[-4:] not in ["4943", "e26f"] and ".electrum.org" in BaseCrashReporter.report_server:
+ # Gah! Some kind of altcoin wants to send us crash reports.
+ raise BaseException(_("Missing report URL."))
+ report = self.get_traceback_info()
+ report.update(self.get_additional_info())
+ report = json.dumps(report)
+ response = requests.post(BaseCrashReporter.report_server + endpoint, data=report)
+ return response
+
+ 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 = {
+ "app_version": ELECTRUM_VERSION,
+ "os": self.get_os_version(),
+ "wallet_type": "unknown",
+ "locale": locale.getdefaultlocale()[0] or "?",
+ "description": self.get_user_description()
+ }
+ try:
+ args["wallet_type"] = self.get_wallet_type()
+ except:
+ # Maybe the wallet isn't loaded yet
+ pass
+ try:
+ args["app_version"] = self.get_git_version()
+ except:
+ # This is probably not running from source
+ pass
+ return args
+
+ @staticmethod
+ def get_git_version():
+ dir = os.path.dirname(os.path.realpath(sys.argv[0]))
+ version = subprocess.check_output(
+ ['git', 'describe', '--always', '--dirty'], cwd=dir)
+ return str(version, "utf8").strip()
+
+ def get_report_string(self):
+ info = self.get_additional_info()
+ info["traceback"] = "".join(traceback.format_exception(*self.exc_args))
+ return self.issue_template.format(**info)
+
+ def get_user_description(self):
+ raise NotImplementedError
+
+ def get_wallet_type(self):
+ raise NotImplementedError
+
+ def get_os_version(self):
+ raise NotImplementedError