diff --git a/electrum/gui/qml/__init__.py b/electrum/gui/qml/__init__.py index 8d2434426..436bd48f7 100644 --- a/electrum/gui/qml/__init__.py +++ b/electrum/gui/qml/__init__.py @@ -15,14 +15,15 @@ try: except Exception: sys.exit("Error: Could not import PyQt5.QtQml on Linux systems, you may try 'sudo apt-get install python3-pyqt5.qtquick'") -from PyQt5.QtCore import QLocale, QTimer +from PyQt5.QtCore import (Qt, QCoreApplication, QObject, QLocale, QTimer, pyqtSignal, + QT_VERSION_STR, PYQT_VERSION_STR) from PyQt5.QtGui import QGuiApplication -import PyQt5.QtCore as QtCore from electrum.i18n import set_language, languages from electrum.plugin import run_hook -from electrum.util import (profiler) +from electrum.util import profiler from electrum.logging import Logger +from electrum.base_crash_reporter import BaseCrashReporter, EarlyExceptionsQueue if TYPE_CHECKING: from electrum.daemon import Daemon @@ -40,19 +41,19 @@ class ElectrumGui(Logger): #os.environ['QML_IMPORT_TRACE'] = '1' #os.environ['QT_DEBUG_PLUGINS'] = '1' - self.logger.info(f"Qml GUI starting up... Qt={QtCore.QT_VERSION_STR}, PyQt={QtCore.PYQT_VERSION_STR}") + self.logger.info(f"Qml GUI starting up... Qt={QT_VERSION_STR}, PyQt={PYQT_VERSION_STR}") self.logger.info("CWD=%s" % os.getcwd()) # Uncomment this call to verify objects are being properly # GC-ed when windows are closed #network.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer, # ElectrumWindow], interval=5)]) - QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) - if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"): - QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) + QCoreApplication.setAttribute(Qt.AA_X11InitThreads) + if hasattr(Qt, "AA_ShareOpenGLContexts"): + QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts) if hasattr(QGuiApplication, 'setDesktopFileName'): QGuiApplication.setDesktopFileName('electrum.desktop') - if hasattr(QtCore.Qt, "AA_EnableHighDpiScaling"): - QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling) + if hasattr(Qt, "AA_EnableHighDpiScaling"): + QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling) if "QT_QUICK_CONTROLS_STYLE" not in os.environ: os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material" @@ -60,14 +61,15 @@ class ElectrumGui(Logger): self.gui_thread = threading.current_thread() self.plugins = plugins self.app = ElectrumQmlApplication(sys.argv, config, daemon, plugins) + # timer self.timer = QTimer(self.app) self.timer.setSingleShot(False) self.timer.setInterval(500) # msec self.timer.timeout.connect(lambda: None) # periodically enter python scope - sys.excepthook = self.excepthook - threading.excepthook = self.texcepthook + # hook for crash reporter + Exception_Hook.maybe_setup(config=config, slot=self.app.appController.crash) # Initialize any QML plugins run_hook('init_qml', self) @@ -76,18 +78,6 @@ class ElectrumGui(Logger): def close(self): self.app.quit() - def excepthook(self, exc_type, exc_value, exc_tb): - tb = "".join(traceback.format_exception(exc_type, exc_value, exc_tb)) - self.logger.exception(tb) - self.app._valid = False - self.close() - - def texcepthook(self, arg): - tb = "".join(traceback.format_exception(arg.exc_type, arg.exc_value, arg.exc_tb)) - self.logger.exception(tb) - self.app._valid = False - self.close() - def main(self): if not self.app._valid: return @@ -106,3 +96,36 @@ class ElectrumGui(Logger): name = QLocale.system().name() return name if name in languages else 'en_UK' + + +class Exception_Hook(QObject, Logger): + _report_exception = pyqtSignal(object, object, object, object) + + _INSTANCE = None # type: Optional[Exception_Hook] # singleton + + def __init__(self, *, config: 'SimpleConfig', slot): + QObject.__init__(self) + Logger.__init__(self) + assert self._INSTANCE is None, "Exception_Hook is supposed to be a singleton" + self.config = config + self.wallet_types_seen = set() # type: Set[str] + + sys.excepthook = self.handler + threading.excepthook = self.handler + + self._report_exception.connect(slot) + EarlyExceptionsQueue.set_hook_as_ready() + + @classmethod + def maybe_setup(cls, *, config: 'SimpleConfig', wallet: 'Abstract_Wallet' = None, slot = None) -> None: + if not config.get(BaseCrashReporter.config_key, default=True): + EarlyExceptionsQueue.set_hook_as_ready() # flush already queued exceptions + return + if not cls._INSTANCE: + cls._INSTANCE = Exception_Hook(config=config, slot=slot) + if wallet: + cls._INSTANCE.wallet_types_seen.add(wallet.wallet_type) + + def handler(self, *exc_info): + self.logger.error('exception caught by crash reporter', exc_info=exc_info) + self._report_exception.emit(self.config, *exc_info) diff --git a/electrum/gui/qml/components/ExceptionDialog.qml b/electrum/gui/qml/components/ExceptionDialog.qml new file mode 100644 index 000000000..7e24e234c --- /dev/null +++ b/electrum/gui/qml/components/ExceptionDialog.qml @@ -0,0 +1,157 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.3 +import QtQuick.Controls.Material 2.0 + +import QtQml 2.6 + +import "controls" + +ElDialog +{ + id: root + + property var crashData + + property bool _sending: false + + modal: true + parent: Overlay.overlay + Overlay.modal: Rectangle { + color: "#aa000000" + } + + width: parent.width + height: parent.height + + header: null + + ColumnLayout { + anchors.fill: parent + enabled: !_sending + + Image { + Layout.alignment: Qt.AlignCenter + Layout.preferredWidth: 128 + Layout.preferredHeight: 128 + source: '../../icons/bug.png' + } + Label { + text: qsTr('Sorry!') + font.pixelSize: constants.fontSizeLarge + } + + Label { + Layout.fillWidth: true + text: qsTr('Something went wrong while executing Electrum.') + } + Label { + Layout.fillWidth: true + text: qsTr('To help us diagnose and fix the problem, you can send us a bug report that contains useful debug information:') + wrapMode: Text.Wrap + } + Button { + Layout.alignment: Qt.AlignCenter + text: qsTr('Show report contents') + onClicked: { + console.log('traceback: ' + crashData.traceback.stack) + var dialog = report.createObject(app, { + reportText: crashData.reportstring + }) + dialog.open() + } + } + Label { + Layout.fillWidth: true + text: qsTr('Please briefly describe what led to the error (optional):') + } + TextArea { + Layout.fillWidth: true + Layout.fillHeight: true + background: Rectangle { + color: Qt.darker(Material.background, 1.25) + } + onTextChanged: AppController.setCrashUserText(text) + } + Label { + text: qsTr('Do you want to send this report?') + } + RowLayout { + Button { + Layout.fillWidth: true + Layout.preferredWidth: 3 + text: qsTr('Send Bug Report') + onClicked: AppController.sendReport() + } + Button { + Layout.fillWidth: true + Layout.preferredWidth: 2 + text: qsTr('Never') + onClicked: { + AppController.showNever() + close() + } + } + Button { + Layout.fillWidth: true + Layout.preferredWidth: 2 + text: qsTr('Not Now') + onClicked: close() + } + } + } + + BusyIndicator { + anchors.centerIn: parent + running: _sending + } + + Component { + id: report + ElDialog { + property string reportText + + z: 3000 + modal: true + parent: Overlay.overlay + Overlay.modal: Rectangle { + color: "#aa000000" + } + + width: parent.width + height: parent.height + + header: null + + Label { + text: reportText + wrapMode: Text.Wrap + width: parent.width + } + } + } + + Connections { + target: AppController + function onSendingBugreportSuccess(text) { + _sending = false + var dialog = app.messageDialog.createObject(app, { + text: text, + richText: true + }) + dialog.open() + close() + } + function onSendingBugreportFailure(text) { + _sending = false + var dialog = app.messageDialog.createObject(app, { + text: text, + richText: true + }) + dialog.open() + } + function onSendingBugreport() { + _sending = true + } + } +} diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 5e4158a93..88491456d 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -241,6 +241,11 @@ ApplicationWindow id: notificationPopup } + Component { + id: crashDialog + ExceptionDialog {} + } + Component.onCompleted: { coverTimer.start() @@ -331,6 +336,12 @@ ApplicationWindow function onUserNotify(message) { notificationPopup.show(message) } + function onShowException() { + var dialog = crashDialog.createObject(app, { + crashData: AppController.crashData() + }) + dialog.open() + } } Connections { diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index ec3c537a2..cf915dead 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -2,14 +2,21 @@ import re import queue import time import os +import sys +import html +import threading +import asyncio from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty, QObject, QUrl, QLocale, qInstallMessageHandler, QTimer from PyQt5.QtGui import QGuiApplication, QFontDatabase from PyQt5.QtQml import qmlRegisterType, qmlRegisterUncreatableType, QQmlApplicationEngine -from electrum import version +from electrum import version, constants +from electrum.i18n import _ from electrum.logging import Logger, get_logger from electrum.util import BITCOIN_BIP21_URI_SCHEME, LIGHTNING_URI_SCHEME +from electrum.base_crash_reporter import BaseCrashReporter +from electrum.network import Network from .qeconfig import QEConfig from .qedaemon import QEDaemon @@ -33,15 +40,20 @@ from .qewizard import QENewWalletWizard, QEServerConnectWizard notification = None -class QEAppController(QObject): +class QEAppController(BaseCrashReporter, QObject): + _dummy = pyqtSignal() userNotify = pyqtSignal(str) uriReceived = pyqtSignal(str) + showException = pyqtSignal() + sendingBugreport = pyqtSignal() + sendingBugreportSuccess = pyqtSignal(str) + sendingBugreportFailure = pyqtSignal(str) - _dummy = pyqtSignal() + _crash_user_text = '' def __init__(self, qedaemon, plugins): - super().__init__() - self.logger = get_logger(__name__) + BaseCrashReporter.__init__(self, None, None, None) + QObject.__init__(self) self._qedaemon = qedaemon self._plugins = plugins @@ -192,6 +204,60 @@ class QEAppController(QObject): def isAndroid(self): return 'ANDROID_DATA' in os.environ + @pyqtSlot(result='QVariantMap') + def crashData(self): + return { + 'traceback': self.get_traceback_info(), + 'extra': self.get_additional_info(), + 'reportstring': self.get_report_string() + } + + @pyqtSlot(object,object,object,object) + def crash(self, config, e, text, tb): + self.exc_args = (e, text, tb) # for BaseCrashReporter + self.showException.emit() + + @pyqtSlot() + def sendReport(self): + network = Network.get_instance() + proxy = network.proxy + + def report_task(): + try: + response = BaseCrashReporter.send_report(self, network.asyncio_loop, proxy) + self.sendingBugreportSuccess.emit(response) + except Exception as e: + self.logger.error('There was a problem with the automatic reporting', exc_info=e) + self.sendingBugreportFailure.emit(_('There was a problem with the automatic reporting:') + '
' + + repr(e)[:120] + '

' + + _("Please report this issue manually") + + f' on GitHub.') + + self.sendingBugreport.emit() + threading.Thread(target=report_task).start() + + @pyqtSlot() + def showNever(self): + self.config.set_key(BaseCrashReporter.config_key, False) + + @pyqtSlot(str) + def setCrashUserText(self, text): + self._crash_user_text = text + + def _get_traceback_str_to_display(self) -> str: + # The msg_box that shows the report uses rich_text=True, so + # if traceback contains special HTML characters, e.g. '<', + # they need to be escaped to avoid formatting issues. + traceback_str = super()._get_traceback_str_to_display() + return html.escape(traceback_str).replace(''',''') + + def get_user_description(self): + return self._crash_user_text + + def get_wallet_type(self): + wallet_types = Exception_Hook._INSTANCE.wallet_types_seen + return ",".join(wallet_types) + class ElectrumQmlApplication(QGuiApplication): _valid = True