Browse Source

qml: initial crash handler impl

master
Sander van Grieken 3 years ago
parent
commit
fa030b3fa5
  1. 69
      electrum/gui/qml/__init__.py
  2. 157
      electrum/gui/qml/components/ExceptionDialog.qml
  3. 11
      electrum/gui/qml/components/main.qml
  4. 76
      electrum/gui/qml/qeapp.py

69
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)

157
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
}
}
}

11
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 {

76
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:') + '<br/>' +
repr(e)[:120] + '<br/><br/>' +
_("Please report this issue manually") +
f' <a href="{constants.GIT_REPO_ISSUES_URL}">on GitHub</a>.')
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('&#x27;','&apos;')
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

Loading…
Cancel
Save