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