electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

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:
Mgui/kivy/main_window.py | 3++-
Agui/kivy/uix/dialogs/crash_reporter.py | 193+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mgui/qt/exception_window.py | 112++++++++++++++++---------------------------------------------------------------
Alib/base_crash_reporter.py | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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